@spider-mesh/core 2.0.41 → 2.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,110 +1,98 @@
1
- import { BehaviorSubject, catchError, EMPTY, filter, finalize, from, lastValueFrom, map, mergeAll, mergeMap, NEVER, Observable, of, retry, Subject, take, tap, throwError, timeout, timer } from "rxjs";
1
+ import { BehaviorSubject, catchError, EMPTY, finalize, firstValueFrom, from, lastValueFrom, map, mergeMap, Observable, of, retry, share, Subject, Subscription, switchMap, take, tap, throwError, timeout, timer } from "rxjs";
2
2
  import { listBeforeMicroserviceOnlineMethods } from "./decorators/BeforeMicroserviceOnline.js";
3
3
  import { LOCAL_SERVICES$ } from "./decorators/Microservice.js";
4
4
  import { SPIDERMESH_NAMESPACE, SPIDERMESH_NODE_HOSTNAME } from "../const.js";
5
5
  const isSubscribable = (value) => {
6
6
  return !!value && typeof value === 'object' && typeof value.subscribe === 'function';
7
7
  };
8
+ const isNamedTransporter = (value) => {
9
+ return !!value && typeof value === 'object';
10
+ };
8
11
  export class SpiderMesh {
9
- options;
10
- node_id = `${Date.now().toString(36).toUpperCase()}`;
12
+ registry;
13
+ node_id = `${Date.now().toString(36).toUpperCase()}|${Math.random().toString(36).slice(10).toUpperCase()}`;
11
14
  namespace = SPIDERMESH_NAMESPACE;
12
- #rpcs = new Map();
13
- #pubsubs = new BehaviorSubject(new Map());
14
- #discovers = new Map();
15
- #metadata$ = new BehaviorSubject({
15
+ #transporters = {
16
+ rpcs: new Map(),
17
+ pubsubs: new Map(),
18
+ discoveries: new Map()
19
+ };
20
+ #localServices = new Map();
21
+ #me$ = new BehaviorSubject({
16
22
  host: SPIDERMESH_NODE_HOSTNAME,
17
23
  namespace: SPIDERMESH_NAMESPACE,
18
24
  node_id: this.node_id,
25
+ topics: [],
19
26
  services: {},
20
27
  transporters: {},
21
28
  nodes: {},
22
29
  version: 0,
23
30
  });
24
- #nodes$ = new BehaviorSubject({
25
- nodes: new Map(),
26
- last_updated_node_id: ''
27
- });
28
- #local_services = new Map();
29
31
  #rpc = {
30
- indexes: new Map(),
31
32
  pending: new Map(),
32
33
  running: new Map()
33
34
  };
34
- constructor(options) {
35
- this.options = options;
36
- if (!options)
37
- throw new Error(`Missing options for SpiderMesh, please using like that new SpiderMeshOptions({ ... options})`);
38
- setTimeout(() => {
39
- LOCAL_SERVICES$.pipe(mergeMap(async (service) => {
40
- const list = listBeforeMicroserviceOnlineMethods(service.instance);
41
- for (const method of list) {
42
- await service.instance[method]();
35
+ constructor(registry) {
36
+ this.registry = registry;
37
+ LOCAL_SERVICES$.pipe(mergeMap(async (service) => {
38
+ const list = listBeforeMicroserviceOnlineMethods(service.instance);
39
+ for (const method of list) {
40
+ await service.instance[method]();
41
+ }
42
+ this.#localServices.set(service.name, service.instance);
43
+ this.#refresh({
44
+ services: {
45
+ [service.name]: service.metadata
43
46
  }
44
- this.#local_services.set(service.name, service.instance);
45
- const metadata = {
46
- ...this.#metadata$.value,
47
- services: {
48
- ...this.#metadata$.value.services,
49
- [service.name]: service.metadata
50
- },
51
- version: this.#metadata$.value.version + 1
52
- };
53
- this.#metadata$.next(metadata);
54
- }, 1), catchError(e => EMPTY)).subscribe();
55
- this.#linkTransporters();
56
- }, 0);
47
+ });
48
+ }, 1), catchError(() => EMPTY)).subscribe();
57
49
  }
58
- static asProvider(options) {
59
- return {
60
- provide: this,
61
- useFactory: async () => new this(options)
62
- };
50
+ registerTransporter(meshTransporter, name) {
51
+ const resolvedName = name
52
+ || (isNamedTransporter(meshTransporter) ? meshTransporter.constructor?.name : undefined)
53
+ || 'AnonymousTransporter';
54
+ const subscription = new Subscription();
55
+ if (this.registry && typeof meshTransporter.linkRegistry === 'function') {
56
+ meshTransporter.linkRegistry(this.registry);
57
+ }
58
+ if (this.#isRpcTransporter(meshTransporter)) {
59
+ subscription.add(this.#linkRpcTransporter(resolvedName, meshTransporter).subscribe());
60
+ }
61
+ if (this.#isPubsubTransporter(meshTransporter)) {
62
+ subscription.add(this.#linkPubsubTransporter(resolvedName, meshTransporter).subscribe());
63
+ }
64
+ if (this.#isDiscoveryTransporter(meshTransporter)) {
65
+ subscription.add(this.#linkDiscoveryTransporter(resolvedName, meshTransporter).subscribe());
66
+ }
67
+ return subscription;
63
68
  }
64
69
  listRpcNodes(service) {
65
- return [...this.#nodes$.value.nodes.values()].filter(node => {
66
- return !!node.rpc && node.services[service] != undefined;
67
- });
70
+ if (!this.registry)
71
+ return [];
72
+ return this.registry.listPeers(service).filter(node => !!node.transporters.rpc);
68
73
  }
69
74
  watchService(service) {
70
- return this.#nodes$.pipe(filter((e, i) => {
71
- if (i == 0 || !e.last_updated_node_id)
72
- return true;
73
- if (e.last_updated_node_id.startsWith('offline:')) {
74
- return e.last_updated_node_id.split(':')[1].includes(service);
75
- }
76
- const node = e.nodes.get(e.last_updated_node_id);
77
- return node ? node.services[service] != undefined : false;
78
- }), map(e => [...e.nodes.values()].filter(node => node.services[service] != undefined)));
75
+ if (!this.registry)
76
+ return EMPTY;
77
+ return this.registry.watch(service).pipe(map(() => this.listRpcNodes(service)));
79
78
  }
80
- #selectRpcTarget(filters = {}) {
79
+ #selectRpcTransport(filters = {}) {
81
80
  if (!filters.service)
82
- return null;
83
- if (filters.node_id) {
84
- const node = this.#nodes$.value.nodes.get(filters.node_id);
85
- if (!node)
86
- return null;
87
- if (!node.services[filters.service])
88
- return null;
89
- if (!node.rpc)
90
- return null;
91
- const transporter = this.#rpcs.get(node.rpc);
92
- if (!transporter)
93
- return null;
94
- return { node, transporter };
81
+ return undefined;
82
+ if (filters.transporter) {
83
+ const name = typeof filters.transporter === 'string' ? filters.transporter : filters.transporter.name;
84
+ if (!name)
85
+ return;
86
+ return this.#transporters.rpcs.get(name);
95
87
  }
96
- const nodes = this.listRpcNodes(filters.service).map(node => {
97
- const transporter = node.rpc && this.#rpcs.get(node.rpc);
98
- if (transporter) {
99
- return { node, transporter };
100
- }
101
- return null;
102
- }).filter(Boolean).map(node => node);
103
- if (nodes.length === 0)
104
- return null;
105
- const index = this.#rpc.indexes.get(filters.service) || 0;
106
- this.#rpc.indexes.set(filters.service, (index + 1) % nodes.length);
107
- return nodes[index % nodes.length];
88
+ if (!this.registry) {
89
+ return this.#transporters.rpcs.values().next().value;
90
+ }
91
+ const name = this.registry.getRpcTransporterName(filters.service);
92
+ if (!name)
93
+ return;
94
+ const transporter = this.#transporters.rpcs.get(name);
95
+ return transporter;
108
96
  }
109
97
  #normalizeRpcError(error) {
110
98
  if (error && typeof error === 'object') {
@@ -115,24 +103,6 @@ export class SpiderMesh {
115
103
  }
116
104
  return { message: typeof error === 'string' ? error : 'Unknown RPC error' };
117
105
  }
118
- #sendRpcPacket(transporter, node, packet) {
119
- return transporter.send(packet, node);
120
- }
121
- #resolveRpcNode(node_id) {
122
- const known = this.#nodes$.value.nodes.get(node_id);
123
- if (known) {
124
- return known;
125
- }
126
- return {
127
- node_id,
128
- namespace: this.namespace,
129
- host: '',
130
- version: 0,
131
- services: {},
132
- nodes: {},
133
- transporters: {}
134
- };
135
- }
136
106
  #completePendingRpc(request_id) {
137
107
  this.#rpc.pending.delete(request_id);
138
108
  }
@@ -140,7 +110,10 @@ export class SpiderMesh {
140
110
  this.#rpc.running.delete(request_id);
141
111
  }
142
112
  callRemoteService(options) {
143
- return this.#nodes$.pipe(map(() => this.#selectRpcTarget(options)), filter(Boolean), take(1), mergeMap(target => {
113
+ return of(1).pipe(switchMap(() => this.registry ? firstValueFrom(this.registry.watch(options.service)) : of(1)), take(1), mergeMap(() => {
114
+ const transporter = this.#selectRpcTransport(options);
115
+ if (!transporter)
116
+ throw { code: 'MICROSERVICE_OFFLINE', message: `No transporter available for service ${options.service}` };
144
117
  return new Observable(subscriber => {
145
118
  const request_id = `${this.node_id}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
146
119
  const pending = {
@@ -159,29 +132,20 @@ export class SpiderMesh {
159
132
  subscriber.complete();
160
133
  }
161
134
  });
162
- this.#sendRpcPacket(target.transporter, target.node, {
135
+ transporter.send({
163
136
  kind: 'request',
164
137
  request_id,
165
- source_node_id: this.node_id,
166
- target_node_id: target.node.node_id,
138
+ sender_node_id: this.node_id,
167
139
  service: options.service,
168
140
  method: options.method,
169
141
  args: options.args
170
- }).catch(error => {
142
+ }, options.node_id).catch((error) => {
171
143
  pending.finished = true;
172
144
  pending.stream.error(this.#normalizeRpcError(error));
173
145
  this.#completePendingRpc(request_id);
174
146
  });
175
147
  return () => {
176
148
  subscription.unsubscribe();
177
- if (!pending.finished && this.#rpc.pending.has(request_id)) {
178
- void this.#sendRpcPacket(target.transporter, target.node, {
179
- kind: 'cancel',
180
- request_id,
181
- source_node_id: this.node_id,
182
- target_node_id: target.node.node_id,
183
- }).catch(() => undefined);
184
- }
185
149
  this.#completePendingRpc(request_id);
186
150
  };
187
151
  });
@@ -203,83 +167,75 @@ export class SpiderMesh {
203
167
  }));
204
168
  }
205
169
  #linkRpcTransporter(name, transporter) {
206
- if (this.#rpcs.has(name))
207
- return EMPTY;
208
- this.#rpcs.set(name, transporter);
170
+ this.#transporters.rpcs.set(name, transporter);
171
+ this.#ensureLocalTransporterPresence(name);
209
172
  const initialEndpoints = transporter.metadata;
210
173
  if (initialEndpoints) {
211
- this.#metadata$.next({
212
- ...this.#metadata$.value,
174
+ this.#refresh({
213
175
  transporters: {
214
- ...this.#metadata$.value.transporters,
215
176
  [name]: initialEndpoints
216
- },
217
- version: this.#metadata$.value.version + 1
177
+ }
218
178
  });
219
179
  }
220
180
  return transporter.pipe(map(({ rpc, offline, endpoints }) => {
221
181
  if (rpc) {
222
- const packet = rpc.packet;
223
- if (packet?.kind === 'request') {
224
- if (packet.target_node_id === this.node_id) {
225
- const reply = async (response) => {
226
- await this.#sendRpcPacket(transporter, this.#resolveRpcNode(rpc.node_id), {
227
- kind: 'response',
228
- request_id: packet.request_id,
229
- source_node_id: this.node_id,
230
- target_node_id: rpc.node_id,
231
- ...response
182
+ const packet = rpc;
183
+ if (packet.kind == 'request') {
184
+ const reply = async (response) => {
185
+ await transporter.send({
186
+ kind: 'response',
187
+ request_id: packet.request_id,
188
+ ...response
189
+ }, packet.sender_node_id);
190
+ };
191
+ const handleResponse = (response) => {
192
+ if (isSubscribable(response)) {
193
+ let queue = Promise.resolve();
194
+ const running = response.subscribe({
195
+ next: data => {
196
+ queue = queue.then(() => reply({ data }));
197
+ },
198
+ error: error => {
199
+ queue = queue.then(() => reply({
200
+ error: this.#normalizeRpcError(error),
201
+ completed: true
202
+ })).finally(() => this.#completeRunningRpc(packet.request_id));
203
+ },
204
+ complete: () => {
205
+ queue = queue.then(() => reply({ completed: true })).finally(() => this.#completeRunningRpc(packet.request_id));
206
+ }
232
207
  });
233
- };
234
- const handleResponse = (response) => {
235
- if (isSubscribable(response)) {
236
- let queue = Promise.resolve();
237
- const running = response.subscribe({
238
- next: data => {
239
- queue = queue.then(() => reply({ data }));
240
- },
241
- error: error => {
242
- queue = queue.then(() => reply({
243
- error: this.#normalizeRpcError(error),
244
- completed: true
245
- })).finally(() => this.#completeRunningRpc(packet.request_id));
246
- },
247
- complete: () => {
248
- queue = queue.then(() => reply({ completed: true })).finally(() => this.#completeRunningRpc(packet.request_id));
249
- }
250
- });
251
- this.#rpc.running.set(packet.request_id, running);
252
- return;
253
- }
254
- void reply({ data: response, completed: true });
255
- };
256
- const service = this.#local_services.get(packet.service);
257
- if (!service || typeof service[packet.method] !== 'function') {
208
+ this.#rpc.running.set(packet.request_id, running);
209
+ return;
210
+ }
211
+ void reply({ data: response, completed: true });
212
+ };
213
+ const service = this.#localServices.get(packet.service);
214
+ if (!service || typeof service[packet.method] !== 'function') {
215
+ void reply({
216
+ error: {
217
+ code: 'MICROSERVICE_NOT_FOUND',
218
+ message: `Service ${packet.service}.${packet.method} not found`
219
+ },
220
+ completed: true
221
+ });
222
+ }
223
+ else {
224
+ try {
225
+ const response = service[packet.method].apply(service, packet.args);
226
+ Promise.resolve(response)
227
+ .then(handleResponse)
228
+ .catch(error => reply({
229
+ error: this.#normalizeRpcError(error),
230
+ completed: true
231
+ }));
232
+ }
233
+ catch (error) {
258
234
  void reply({
259
- error: {
260
- code: 'MICROSERVICE_NOT_FOUND',
261
- message: `Service ${packet.service}.${packet.method} not found`
262
- },
235
+ error: this.#normalizeRpcError(error),
263
236
  completed: true
264
237
  });
265
238
  }
266
- else {
267
- try {
268
- const response = service[packet.method].apply(service, packet.args);
269
- Promise.resolve(response)
270
- .then(handleResponse)
271
- .catch(error => reply({
272
- error: this.#normalizeRpcError(error),
273
- completed: true
274
- }));
275
- }
276
- catch (error) {
277
- void reply({
278
- error: this.#normalizeRpcError(error),
279
- completed: true
280
- });
281
- }
282
- }
283
239
  }
284
240
  }
285
241
  if (packet?.kind === 'response') {
@@ -301,49 +257,40 @@ export class SpiderMesh {
301
257
  }
302
258
  }
303
259
  if (packet?.kind === 'cancel') {
304
- if (packet.target_node_id === this.node_id) {
305
- const stream = this.#rpc.running.get(packet.request_id);
306
- if (stream) {
307
- stream.unsubscribe();
308
- this.#completeRunningRpc(packet.request_id);
309
- }
260
+ const stream = this.#rpc.running.get(packet.request_id);
261
+ if (stream) {
262
+ stream.unsubscribe();
263
+ this.#completeRunningRpc(packet.request_id);
310
264
  }
311
265
  }
312
266
  }
313
267
  if (offline) {
314
- const nodes = this.#nodes$.value.nodes;
315
- const node = nodes.get(offline);
316
- if (node) {
317
- nodes.delete(offline);
318
- this.#nodes$.next({ nodes, last_updated_node_id: `offline:${Object.keys(node.services).join(',')}` });
319
- }
268
+ this.registry?.removePeer(offline);
320
269
  }
321
270
  if (endpoints) {
322
- this.#metadata$.next({
323
- ...this.#metadata$.value,
271
+ this.#refresh({
324
272
  transporters: {
325
- ...this.#metadata$.value.transporters,
326
273
  [name]: endpoints
327
- },
328
- version: this.#metadata$.value.version + 1
274
+ }
329
275
  });
330
276
  }
331
- }), finalize(() => {
332
- this.#rpcs.delete(name);
333
- }));
277
+ }), finalize(() => undefined));
334
278
  }
335
279
  #linkPubsubTransporter(name, transporter) {
336
- const transporters = this.#pubsubs.getValue();
337
- transporters.set(name, transporter);
338
- this.#pubsubs.next(transporters);
339
- return NEVER.pipe(finalize(() => {
340
- transporters.delete(name);
341
- this.#pubsubs.next(transporters);
342
- }));
280
+ this.#transporters.pubsubs.set(name, transporter);
281
+ this.#ensureLocalTransporterPresence(name);
282
+ return transporter.pipe(tap(({ endpoints }) => {
283
+ this.#refresh({
284
+ transporters: {
285
+ [name]: endpoints
286
+ }
287
+ });
288
+ }), finalize(() => undefined));
343
289
  }
344
290
  #linkDiscoveryTransporter(name, transporter) {
345
- this.#discovers.set(name, transporter);
346
- const announce = this.#metadata$.subscribe(metadata => {
291
+ this.#transporters.discoveries.set(name, transporter);
292
+ this.#ensureLocalTransporterPresence(name);
293
+ const announce = this.#me$.subscribe(metadata => {
347
294
  void transporter.broadcast({
348
295
  hi: true,
349
296
  node: metadata,
@@ -353,52 +300,93 @@ export class SpiderMesh {
353
300
  return transporter.pipe(tap(({ discovered: node }) => {
354
301
  if (!node || typeof node !== 'object' || !('node_id' in node) || !('services' in node))
355
302
  return;
356
- const nodes = this.#nodes$.value.nodes;
357
- const rpc = Object.keys(node.transporters || {}).find(key => this.#rpcs.has(key));
358
- nodes.set(node.node_id, {
359
- ...nodes.get(node.node_id) || {},
303
+ const rpcTransporterName = this.#resolveDiscoveredRpcTransporterName(node);
304
+ const peer = this.registry?.upsertPeer(rpcTransporterName ? {
360
305
  ...node,
361
- rpc
362
- });
363
- this.#nodes$.next({
364
- nodes,
365
- last_updated_node_id: node.node_id
366
- });
306
+ transporters: {
307
+ ...(node.transporters || {}),
308
+ rpc: rpcTransporterName
309
+ }
310
+ } : node);
311
+ if (!this.registry || !peer)
312
+ return;
367
313
  }), finalize(() => {
368
314
  announce.unsubscribe();
369
- this.#discovers.delete(name);
370
315
  }));
371
316
  }
372
- #linkTransporters() {
373
- return from(this.options.transporters).pipe(mergeMap(entry => {
374
- const transporter = typeof entry === 'function' ? new entry() : entry;
375
- const name = typeof entry === 'function'
376
- ? entry.name
377
- : transporter.constructor?.name || 'AnonymousTransporter';
378
- const links = [];
379
- if ('send' in transporter) {
380
- links.push(this.#linkRpcTransporter(name, transporter));
381
- }
382
- if ('publish' in transporter) {
383
- links.push(this.#linkPubsubTransporter(name, transporter));
317
+ #isRpcTransporter(transporter) {
318
+ return 'send' in transporter;
319
+ }
320
+ #isPubsubTransporter(transporter) {
321
+ return 'publish' in transporter;
322
+ }
323
+ #isDiscoveryTransporter(transporter) {
324
+ return 'broadcast' in transporter;
325
+ }
326
+ #refresh(patch) {
327
+ const current = this.#me$.value;
328
+ this.#me$.next({
329
+ ...current,
330
+ ...patch,
331
+ version: current.version + 1,
332
+ topics: patch.topics || current.topics,
333
+ services: {
334
+ ...current.services,
335
+ ...(patch.services || {})
336
+ },
337
+ nodes: {
338
+ ...current.nodes,
339
+ ...(patch.nodes || {})
340
+ },
341
+ transporters: {
342
+ ...current.transporters,
343
+ ...(patch.transporters || {})
384
344
  }
385
- if ('broadcast' in transporter) {
386
- links.push(this.#linkDiscoveryTransporter(name, transporter));
345
+ });
346
+ }
347
+ #ensureLocalTransporterPresence(name) {
348
+ if (Object.hasOwn(this.#me$.value.transporters, name))
349
+ return;
350
+ this.#refresh({
351
+ transporters: {
352
+ [name]: true
387
353
  }
388
- if (links.length === 0)
389
- return EMPTY;
390
- if (links.length === 1)
391
- return links[0];
392
- return from(links).pipe(mergeAll());
393
- })).subscribe();
354
+ });
355
+ }
356
+ #resolveDiscoveredRpcTransporterName(node) {
357
+ for (const [name] of this.#transporters.rpcs.entries()) {
358
+ if (node.transporters && Object.hasOwn(node.transporters, name))
359
+ return name;
360
+ }
361
+ return null;
362
+ }
363
+ #registerTopic(topic) {
364
+ if (this.#me$.value.topics.includes(topic))
365
+ return;
366
+ this.#refresh({
367
+ topics: [...this.#me$.value.topics, topic]
368
+ });
369
+ }
370
+ #unregisterTopic(topic) {
371
+ if (!this.#me$.value.topics.includes(topic))
372
+ return;
373
+ this.#refresh({
374
+ topics: this.#me$.value.topics.filter(item => item !== topic)
375
+ });
394
376
  }
395
377
  linkEvent(factory) {
396
378
  const topic = factory.name;
379
+ const listen$ = new Observable(subscriber => {
380
+ this.#registerTopic(topic);
381
+ const subscription = from(this.#transporters.pubsubs.values()).pipe(mergeMap(t => t.listen(topic))).subscribe(subscriber);
382
+ return () => {
383
+ subscription.unsubscribe();
384
+ this.#unregisterTopic(topic);
385
+ };
386
+ }).pipe(share());
397
387
  return {
398
- publish: (data) => lastValueFrom(from(this.#pubsubs.getValue().values()).pipe(mergeMap(t => t.publish(topic, data))), { defaultValue: undefined }),
399
- listen: () => {
400
- return this.#pubsubs.pipe(map(list => [...list.values()]), mergeAll(), mergeMap(t => t.listen(topic)));
401
- }
388
+ publish: (data) => lastValueFrom(from(this.#transporters.pubsubs.values()).pipe(mergeMap(t => t.publish(topic, data))), { defaultValue: undefined }),
389
+ listen: () => listen$
402
390
  };
403
391
  }
404
392
  }