-
Notifications
You must be signed in to change notification settings - Fork 451
/
SERVICES.md
351 lines (270 loc) · 10.4 KB
/
SERVICES.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# Services
Libp2p ships with very little functionality by default, this is to allow the greatest amount of flexibility and to ensure, for example, if you are deploying to web browsers you only pull in the code that your application needs.
The functionality of your Libp2p node can be extended by configuring additional services.
```ts
import { createLibp2p } from 'libp2p'
import { identify } from '@libp2p/identify'
const node = await createLibp2p({
//.. other config here
services: {
identify: identify()
}
})
```
You can extend the capabilities of your node to suit your needs by writing custom services.
## Writing custom services
At it's simplest a service might look like this:
```ts
import { createLibp2p } from 'libp2p'
// the service implementation
class MyService {
saySomething (): string {
return 'Hello'
}
}
// a function that returns a factory function
function myService () {
return () => {
return new MyService()
}
}
// create the libp2p node
const node = await createLibp2p({
//.. other config here
services: {
myService: myService()
}
})
// invoke the service function
console.info(node.services.myService.saySomething()) // 'Hello'
```
### Accessing libp2p components
Services can access internal libp2p components such as the address manger and connection manager by accepting an argument to the returned function.
> [!IMPORTANT]
> The key names of the `components` argument must match the field names of the internal [Components](https://github.com/libp2p/js-libp2p/blob/d1f1c2be78bd195f404e62627c2c9f545845e5f5/packages/libp2p/src/components.ts#L8-L28) class
```ts
import { createLibp2p } from 'libp2p'
import type { ConnectionManager } from '@libp2p/interface-internal'
// an interface that defines the minimal set of components the service requires
interface MyServiceComponents {
connectionManager: ConnectionManager
}
// the service implementation
class MyService {
private readonly components: MyServiceComponents
constructor (components: MyServiceComponents) {
this.components = components
}
saySomething (): string {
return `There are ${this.components.connectionManager.getDialQueue().length} pending dials`
}
}
// a function that returns a factory function
function myService () {
return (components: MyServiceComponents) => {
return new MyService(components)
}
}
// create the libp2p node
const node = await createLibp2p({
//.. other config here
services: {
myService: myService()
}
})
// invoke the service function
console.info(node.services.myService.saySomething()) // 'There are 0 pending dials'
```
### Init args
Your service can take arguments that allow for custom config.
> [!TIP]
> Make all arguments optional with sensible defaults
```ts
import { createLibp2p } from 'libp2p'
import type { ConnectionManager } from '@libp2p/interface-internal'
// an interface that defines the minimal set of components the service requires
interface MyServiceComponents {
connectionManager: ConnectionManager
}
// this interface defines the options this service supports
interface MyServiceInit {
message?: string
}
// the service implementation
class MyService {
private readonly components: MyServiceComponents
private readonly message: string
constructor (components: MyServiceComponents, init: MyServiceInit = {}) {
this.components = components
this.message = init.message ?? 'There are {} pending dials'
}
saySomething (): string {
return this.message.replace('{}', `${this.components.connectionManager.getDialQueue().length}`)
}
}
// a function that returns a factory function
function myService (init: MyServiceInit) {
return (components: MyServiceComponents) => {
return new MyService(components, init)
}
}
// create the libp2p node
const node = await createLibp2p({
//.. other config here
services: {
myService: myService({
message: 'The queue is {} dials long'
})
}
})
// invoke the service function
console.info(node.services.myService.saySomething()) // 'The queue is 0 dials long'
```
## Service lifecycle
Services that need to do async work during startup/shutdown can implement the [Startable](https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface.Startable.html) interface.
It defines several methods that if defined, will be invoked when starting/stopping the node.
All methods may return either `void` or `Promise<void>`.
> [!WARNING]
> If your functions are async, libp2p will wait for the returned promise to resolve before continuing which can increase startup/shutdown duration
```ts
import type { Startable } from '@libp2p/interface'
class MyService implements Startable {
async beforeStart (): Promise<void> {
// optional, can be sync or async
}
async start (): Promise<void> {
// can be sync or async
}
async afterStart (): Promise<void> {
// optional, can be sync or async
}
async beforeStop (): Promise<void> {
// optional, can be sync or async
}
async stop (): Promise<void> {
// can be sync or async
}
async afterStop (): Promise<void> {
// optional, can be sync or async
}
}
```
### Depending on other services
All configured services will be added to the `components` object, so you are able to access other custom services as well as libp2p internals.
Defining it as part of your service components interface will cause TypeScript compilation errors if an instance is not present at the expected key in the service map. This should prevent misconfigurations if you are using TypeScript.
If you do not depend on another service directly but still require it to be configured, see the next section on expressing service capabilities and dependencies.
```ts
import { createLibp2p } from 'libp2p'
// first service
class MyService {
saySomething (): string {
return 'Hello from myService'
}
}
function myService () {
return () => {
return new MyService()
}
}
// second service
interface MyOtherServiceComponents {
myService: MyService
}
class MyOtherService {
private readonly components: MyOtherServiceComponents
constructor (components: MyOtherServiceComponents) {
this.components = components
}
speakToMyService (): string {
return this.components.myService.saySomething()
}
}
function myOtherService () {
return (components: MyOtherServiceComponents) => {
return new MyOtherService(components)
}
}
// configure the node with both services
const node = await createLibp2p({
// .. other config here
services: {
myService: myService(),
myOtherService: myOtherService()
}
})
console.info(node.services.myOtherService.speakToMyService()) // 'Hello from myService'
```
## Expressing service capabilities and dependencies
If you have a dependency on the capabilities provided by another service without needing to directly invoke methods on it, you can inform libp2p by using symbol properties.
libp2p will throw on construction if the dependencies of your service cannot be satisfied.
This is useful if, for example, you configure a service that reacts to peer discovery in some way - you can define a requirement to have at least one peer discovery method configured.
Similarly, if your service registers a network topology, these work by notifying topologies after [Identify](https://github.com/libp2p/specs/blob/master/identify/README.md) has run, so any service using topologies has an indirect dependency on `@libp2p/identify`.
```ts
import { createLibp2p } from 'libp2p'
import { serviceCapabilities, serviceDependencies } from '@libp2p/interface'
import type { Startable } from '@libp2p/interface'
import type { Registrar } from '@libp2p/interface-internal'
interface MyServiceComponents {
registrar: Registrar
}
// This service registers a network topology. This functionality will not work
// without the Identify protocol present, so it's defined as a dependency
class MyService implements Startable {
private readonly components: MyServiceComponents
private topologyId?: string
constructor (components: MyServiceComponents) {
this.components = components
}
// this property is used as a human-friendly name for the service
readonly [Symbol.toStringTag] = 'ServiceA'
// this service provides these capabilities to the node
readonly [serviceCapabilities]: string[] = [
'@my-org/my-capability'
]
// this service requires Identify to be configured on the current node
readonly [serviceDependencies]: string[] = [
'@libp2p/identify'
]
async start (): Promise<void> {
this.topologyId = await this.components.registrar.register('/my/protocol', {
onConnect (peer, connection) {
// handle connect
}
})
}
stop (): void {
if (this.topologyId != null) {
this.components.registrar.unregister(this.topologyId)
}
}
}
function myService () {
return (components: MyServiceComponents) => {
return new MyService(components)
}
}
// configure the node but omit identify
const node = await createLibp2p({
// .. other config here
services: {
myService: myService()
}
}) // throws error because identify is not present
```
### Frequently used dependencies
These capabilities are provided by commonly used libp2p modules such as `@libp2p/identify`, `@chainsafe/libp2p-noise`, `@libp2p/webrtc` etc.
Adding these strings to your service dependencies will cause starting libp2p to throw unless a service is configured to provide these capabilities.
| Dependency | Implementations | Notes |
| -------- | ------- | ------- |
| `@libp2p/identify` | `@libp2p/identify` | You should declare this a as a dependency if your service uses the [Registrar](https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface_internal.Registrar.html) to register a network topology. |
| `@libp2p/identify-push` | `@libp2p/identify` | |
| `@libp2p/connection-encryption` | `@chainsafe/libp2p-noise`, `@libp2p/tls`, `@libp2p/plaintext` | |
| `@libp2p/stream-multiplexing` | `@chainsafe/libp2p-yamux` | |
| `@libp2p/content-routing` | `@libp2p/kad-dht` | |
| `@libp2p/peer-routing` | `@libp2p/kad-dht` | |
| `@libp2p/peer-discovery` | `@libp2p/kad-dht`, `@libp2p/bootstrap`, `@libp2p/mdns` | |
| `@libp2p/keychain` | `@libp2p/keychain` | |
| `@libp2p/metrics` | `@libp2p/prometheus-metrics`, `@libp2p/simple-metrics`, `@libp2p/devtool-metrics` | |
| `@libp2p/transport` | `@libp2p/tcp`, `@libp2p/websockets`, `@libp2p/webrtc`, `@libp2p/webtransport`, `@libp2p/circuit-relay-v2` | |
| `@libp2p/circuit-relay-v2-transport` | `@libp2p/circuit-relay-v2` | |
| `@libp2p/nat-traversal` | `@libp2p/upnp-nat` | |