@spider-mesh/core 2.0.41 → 2.0.42

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,115 @@
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, filter, finalize, from, lastValueFrom, map, mergeMap, Observable, of, retry, share, Subject, Subscription, 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;
70
+ return (this.registry?.listPeers({ service }) || []).filter(node => {
71
+ return typeof node.transporters?.rpc === 'string';
67
72
  });
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
81
  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);
82
+ const transporterName = typeof filters.transporter === 'string'
83
+ ? filters.transporter
84
+ : filters.transporter?.name;
85
+ if (!this.registry) {
86
+ const transporter = transporterName
87
+ ? this.#transporters.rpcs.get(transporterName)
88
+ : this.#transporters.rpcs.values().next().value;
92
89
  if (!transporter)
93
90
  return null;
94
- return { node, transporter };
91
+ return {
92
+ node_id: filters.node_id,
93
+ transporter
94
+ };
95
95
  }
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
- }
96
+ const nodeId = this.registry.pickRpcNode(filters.service, {
97
+ node_id: filters.node_id
98
+ });
99
+ const resolvedTransporterName = filters.node_id || nodeId
100
+ ? this.registry.getRpcTransporterName(filters.service, {
101
+ node_id: nodeId || filters.node_id
102
+ })
103
+ : transporterName;
104
+ if (!resolvedTransporterName)
101
105
  return null;
102
- }).filter(Boolean).map(node => node);
103
- if (nodes.length === 0)
106
+ const transporter = this.#transporters.rpcs.get(resolvedTransporterName);
107
+ if (!transporter)
104
108
  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];
109
+ return {
110
+ node_id: nodeId || filters.node_id,
111
+ transporter
112
+ };
108
113
  }
109
114
  #normalizeRpcError(error) {
110
115
  if (error && typeof error === 'object') {
@@ -115,24 +120,6 @@ export class SpiderMesh {
115
120
  }
116
121
  return { message: typeof error === 'string' ? error : 'Unknown RPC error' };
117
122
  }
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
123
  #completePendingRpc(request_id) {
137
124
  this.#rpc.pending.delete(request_id);
138
125
  }
@@ -140,7 +127,10 @@ export class SpiderMesh {
140
127
  this.#rpc.running.delete(request_id);
141
128
  }
142
129
  callRemoteService(options) {
143
- return this.#nodes$.pipe(map(() => this.#selectRpcTarget(options)), filter(Boolean), take(1), mergeMap(target => {
130
+ const source$ = this.registry
131
+ ? this.registry.watch(options.service)
132
+ : of(undefined);
133
+ return source$.pipe(map(() => this.#selectRpcTransport(options)), filter((target) => !!target), take(1), mergeMap(target => {
144
134
  return new Observable(subscriber => {
145
135
  const request_id = `${this.node_id}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
146
136
  const pending = {
@@ -159,28 +149,28 @@ export class SpiderMesh {
159
149
  subscriber.complete();
160
150
  }
161
151
  });
162
- this.#sendRpcPacket(target.transporter, target.node, {
152
+ target.transporter.send({
163
153
  kind: 'request',
164
154
  request_id,
165
155
  source_node_id: this.node_id,
166
- target_node_id: target.node.node_id,
156
+ target_node_id: target.node_id || '',
167
157
  service: options.service,
168
158
  method: options.method,
169
159
  args: options.args
170
- }).catch(error => {
160
+ }, target.node_id).catch((error) => {
171
161
  pending.finished = true;
172
162
  pending.stream.error(this.#normalizeRpcError(error));
173
163
  this.#completePendingRpc(request_id);
174
164
  });
175
165
  return () => {
176
166
  subscription.unsubscribe();
177
- if (!pending.finished && this.#rpc.pending.has(request_id)) {
178
- void this.#sendRpcPacket(target.transporter, target.node, {
167
+ if (!pending.finished && this.#rpc.pending.has(request_id) && target.node_id) {
168
+ void target.transporter.send({
179
169
  kind: 'cancel',
180
170
  request_id,
181
171
  source_node_id: this.node_id,
182
- target_node_id: target.node.node_id,
183
- }).catch(() => undefined);
172
+ target_node_id: target.node_id,
173
+ }, target.node_id).catch(() => undefined);
184
174
  }
185
175
  this.#completePendingRpc(request_id);
186
176
  };
@@ -203,83 +193,77 @@ export class SpiderMesh {
203
193
  }));
204
194
  }
205
195
  #linkRpcTransporter(name, transporter) {
206
- if (this.#rpcs.has(name))
207
- return EMPTY;
208
- this.#rpcs.set(name, transporter);
196
+ this.#transporters.rpcs.set(name, transporter);
197
+ this.#ensureLocalTransporterPresence(name);
209
198
  const initialEndpoints = transporter.metadata;
210
199
  if (initialEndpoints) {
211
- this.#metadata$.next({
212
- ...this.#metadata$.value,
200
+ this.#refresh({
213
201
  transporters: {
214
- ...this.#metadata$.value.transporters,
215
202
  [name]: initialEndpoints
216
- },
217
- version: this.#metadata$.value.version + 1
203
+ }
218
204
  });
219
205
  }
220
206
  return transporter.pipe(map(({ rpc, offline, endpoints }) => {
221
207
  if (rpc) {
222
208
  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
209
+ if (packet?.kind === 'request' && packet.target_node_id === this.node_id) {
210
+ const reply = async (response) => {
211
+ await transporter.send({
212
+ kind: 'response',
213
+ request_id: packet.request_id,
214
+ source_node_id: this.node_id,
215
+ target_node_id: rpc.node_id,
216
+ ...response
217
+ }, rpc.node_id);
218
+ };
219
+ const handleResponse = (response) => {
220
+ if (isSubscribable(response)) {
221
+ let queue = Promise.resolve();
222
+ const running = response.subscribe({
223
+ next: data => {
224
+ queue = queue.then(() => reply({ data }));
225
+ },
226
+ error: error => {
227
+ queue = queue.then(() => reply({
228
+ error: this.#normalizeRpcError(error),
229
+ completed: true
230
+ })).finally(() => this.#completeRunningRpc(packet.request_id));
231
+ },
232
+ complete: () => {
233
+ queue = queue.then(() => reply({ completed: true })).finally(() => this.#completeRunningRpc(packet.request_id));
234
+ }
232
235
  });
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') {
236
+ this.#rpc.running.set(packet.request_id, running);
237
+ return;
238
+ }
239
+ void reply({ data: response, completed: true });
240
+ };
241
+ const service = this.#localServices.get(packet.service);
242
+ if (!service || typeof service[packet.method] !== 'function') {
243
+ void reply({
244
+ error: {
245
+ code: 'MICROSERVICE_NOT_FOUND',
246
+ message: `Service ${packet.service}.${packet.method} not found`
247
+ },
248
+ completed: true
249
+ });
250
+ }
251
+ else {
252
+ try {
253
+ const response = service[packet.method].apply(service, packet.args);
254
+ Promise.resolve(response)
255
+ .then(handleResponse)
256
+ .catch(error => reply({
257
+ error: this.#normalizeRpcError(error),
258
+ completed: true
259
+ }));
260
+ }
261
+ catch (error) {
258
262
  void reply({
259
- error: {
260
- code: 'MICROSERVICE_NOT_FOUND',
261
- message: `Service ${packet.service}.${packet.method} not found`
262
- },
263
+ error: this.#normalizeRpcError(error),
263
264
  completed: true
264
265
  });
265
266
  }
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
267
  }
284
268
  }
285
269
  if (packet?.kind === 'response') {
@@ -300,50 +284,41 @@ export class SpiderMesh {
300
284
  }
301
285
  }
302
286
  }
303
- 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
- }
287
+ if (packet?.kind === 'cancel' && packet.target_node_id === this.node_id) {
288
+ const stream = this.#rpc.running.get(packet.request_id);
289
+ if (stream) {
290
+ stream.unsubscribe();
291
+ this.#completeRunningRpc(packet.request_id);
310
292
  }
311
293
  }
312
294
  }
313
295
  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
- }
296
+ this.registry?.removePeer(offline);
320
297
  }
321
298
  if (endpoints) {
322
- this.#metadata$.next({
323
- ...this.#metadata$.value,
299
+ this.#refresh({
324
300
  transporters: {
325
- ...this.#metadata$.value.transporters,
326
301
  [name]: endpoints
327
- },
328
- version: this.#metadata$.value.version + 1
302
+ }
329
303
  });
330
304
  }
331
- }), finalize(() => {
332
- this.#rpcs.delete(name);
333
- }));
305
+ }), finalize(() => undefined));
334
306
  }
335
307
  #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
- }));
308
+ this.#transporters.pubsubs.set(name, transporter);
309
+ this.#ensureLocalTransporterPresence(name);
310
+ return transporter.pipe(tap(({ endpoints }) => {
311
+ this.#refresh({
312
+ transporters: {
313
+ [name]: endpoints
314
+ }
315
+ });
316
+ }), finalize(() => undefined));
343
317
  }
344
318
  #linkDiscoveryTransporter(name, transporter) {
345
- this.#discovers.set(name, transporter);
346
- const announce = this.#metadata$.subscribe(metadata => {
319
+ this.#transporters.discoveries.set(name, transporter);
320
+ this.#ensureLocalTransporterPresence(name);
321
+ const announce = this.#me$.subscribe(metadata => {
347
322
  void transporter.broadcast({
348
323
  hi: true,
349
324
  node: metadata,
@@ -353,52 +328,93 @@ export class SpiderMesh {
353
328
  return transporter.pipe(tap(({ discovered: node }) => {
354
329
  if (!node || typeof node !== 'object' || !('node_id' in node) || !('services' in node))
355
330
  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) || {},
331
+ const rpcTransporterName = this.#resolveDiscoveredRpcTransporterName(node);
332
+ const peer = this.registry?.upsertPeer(rpcTransporterName ? {
360
333
  ...node,
361
- rpc
362
- });
363
- this.#nodes$.next({
364
- nodes,
365
- last_updated_node_id: node.node_id
366
- });
334
+ transporters: {
335
+ ...(node.transporters || {}),
336
+ rpc: rpcTransporterName
337
+ }
338
+ } : node);
339
+ if (!this.registry || !peer)
340
+ return;
367
341
  }), finalize(() => {
368
342
  announce.unsubscribe();
369
- this.#discovers.delete(name);
370
343
  }));
371
344
  }
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));
345
+ #isRpcTransporter(transporter) {
346
+ return 'send' in transporter;
347
+ }
348
+ #isPubsubTransporter(transporter) {
349
+ return 'publish' in transporter;
350
+ }
351
+ #isDiscoveryTransporter(transporter) {
352
+ return 'broadcast' in transporter;
353
+ }
354
+ #refresh(patch) {
355
+ const current = this.#me$.value;
356
+ this.#me$.next({
357
+ ...current,
358
+ ...patch,
359
+ version: current.version + 1,
360
+ topics: patch.topics || current.topics,
361
+ services: {
362
+ ...current.services,
363
+ ...(patch.services || {})
364
+ },
365
+ nodes: {
366
+ ...current.nodes,
367
+ ...(patch.nodes || {})
368
+ },
369
+ transporters: {
370
+ ...current.transporters,
371
+ ...(patch.transporters || {})
384
372
  }
385
- if ('broadcast' in transporter) {
386
- links.push(this.#linkDiscoveryTransporter(name, transporter));
373
+ });
374
+ }
375
+ #ensureLocalTransporterPresence(name) {
376
+ if (Object.hasOwn(this.#me$.value.transporters, name))
377
+ return;
378
+ this.#refresh({
379
+ transporters: {
380
+ [name]: true
387
381
  }
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();
382
+ });
383
+ }
384
+ #resolveDiscoveredRpcTransporterName(node) {
385
+ for (const [name] of this.#transporters.rpcs.entries()) {
386
+ if (node.transporters && Object.hasOwn(node.transporters, name))
387
+ return name;
388
+ }
389
+ return null;
390
+ }
391
+ #registerTopic(topic) {
392
+ if (this.#me$.value.topics.includes(topic))
393
+ return;
394
+ this.#refresh({
395
+ topics: [...this.#me$.value.topics, topic]
396
+ });
397
+ }
398
+ #unregisterTopic(topic) {
399
+ if (!this.#me$.value.topics.includes(topic))
400
+ return;
401
+ this.#refresh({
402
+ topics: this.#me$.value.topics.filter(item => item !== topic)
403
+ });
394
404
  }
395
405
  linkEvent(factory) {
396
406
  const topic = factory.name;
407
+ const listen$ = new Observable(subscriber => {
408
+ this.#registerTopic(topic);
409
+ const subscription = from(this.#transporters.pubsubs.values()).pipe(mergeMap(t => t.listen(topic))).subscribe(subscriber);
410
+ return () => {
411
+ subscription.unsubscribe();
412
+ this.#unregisterTopic(topic);
413
+ };
414
+ }).pipe(share());
397
415
  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
- }
416
+ publish: (data) => lastValueFrom(from(this.#transporters.pubsubs.values()).pipe(mergeMap(t => t.publish(topic, data))), { defaultValue: undefined }),
417
+ listen: () => listen$
402
418
  };
403
419
  }
404
420
  }