@livestore/webmesh 0.4.0 → 0.5.0-dev.1

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.
Files changed (57) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/channel/direct-channel-internal.d.ts +2 -3
  3. package/dist/channel/direct-channel-internal.d.ts.map +1 -1
  4. package/dist/channel/direct-channel-internal.js +26 -21
  5. package/dist/channel/direct-channel-internal.js.map +1 -1
  6. package/dist/channel/direct-channel.d.ts.map +1 -1
  7. package/dist/channel/direct-channel.js +22 -23
  8. package/dist/channel/direct-channel.js.map +1 -1
  9. package/dist/channel/proxy-channel.d.ts +7 -8
  10. package/dist/channel/proxy-channel.d.ts.map +1 -1
  11. package/dist/channel/proxy-channel.js +28 -24
  12. package/dist/channel/proxy-channel.js.map +1 -1
  13. package/dist/common.d.ts +39 -41
  14. package/dist/common.d.ts.map +1 -1
  15. package/dist/common.js +3 -2
  16. package/dist/common.js.map +1 -1
  17. package/dist/mesh-schema.d.ts +262 -136
  18. package/dist/mesh-schema.d.ts.map +1 -1
  19. package/dist/mesh-schema.js +45 -44
  20. package/dist/mesh-schema.js.map +1 -1
  21. package/dist/mod.js +4 -4
  22. package/dist/node.d.ts +6 -6
  23. package/dist/node.d.ts.map +1 -1
  24. package/dist/node.js +27 -26
  25. package/dist/node.js.map +1 -1
  26. package/dist/node.test.js +80 -77
  27. package/dist/node.test.js.map +1 -1
  28. package/dist/utils.d.ts +1 -1
  29. package/dist/utils.d.ts.map +1 -1
  30. package/dist/utils.js.map +1 -1
  31. package/dist/websocket-edge.d.ts +18 -16
  32. package/dist/websocket-edge.d.ts.map +1 -1
  33. package/dist/websocket-edge.js +32 -43
  34. package/dist/websocket-edge.js.map +1 -1
  35. package/dist/websocket-edge.test.js +17 -17
  36. package/dist/websocket-edge.test.js.map +1 -1
  37. package/dist/worker/mod.d.ts +24 -0
  38. package/dist/worker/mod.d.ts.map +1 -0
  39. package/dist/worker/mod.js +44 -0
  40. package/dist/worker/mod.js.map +1 -0
  41. package/dist/worker/schema.d.ts +10 -0
  42. package/dist/worker/schema.d.ts.map +1 -0
  43. package/dist/worker/schema.js +7 -0
  44. package/dist/worker/schema.js.map +1 -0
  45. package/package.json +35 -52
  46. package/src/channel/direct-channel-internal.ts +30 -22
  47. package/src/channel/direct-channel.ts +26 -23
  48. package/src/channel/proxy-channel.ts +39 -36
  49. package/src/common.ts +6 -3
  50. package/src/mesh-schema.ts +34 -33
  51. package/src/node.test.ts +104 -91
  52. package/src/node.ts +32 -32
  53. package/src/utils.ts +3 -3
  54. package/src/websocket-edge.test.ts +16 -16
  55. package/src/websocket-edge.ts +48 -49
  56. package/src/worker/mod.ts +89 -0
  57. package/src/worker/schema.ts +8 -0
@@ -1,9 +1,9 @@
1
1
  import { casesHandled, shouldNeverHappen } from '@livestore/utils'
2
- import type { PubSub } from '@livestore/utils/effect'
3
2
  import {
3
+ type PubSub,
4
4
  Deferred,
5
5
  Effect,
6
- Either,
6
+ Result,
7
7
  Exit,
8
8
  Fiber,
9
9
  FiberHandle,
@@ -39,11 +39,11 @@ export const ProxyChannelSimulationParams = Schema.Struct({
39
39
  */
40
40
  onPayload: Schema.Struct({
41
41
  /** Delay before sending the ACK response (simulates slow ACK send) */
42
- beforeAckSend: Schema.Int.pipe(Schema.between(0, 500)),
42
+ beforeAckSend: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 500 })),
43
43
  /** Delay after forking the ACK send, before adding message to listen queue */
44
- afterAckFork: Schema.Int.pipe(Schema.between(0, 500)),
44
+ afterAckFork: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 500 })),
45
45
  /** Delay after adding message to listen queue */
46
- afterListenQueueOffer: Schema.Int.pipe(Schema.between(0, 500)),
46
+ afterListenQueueOffer: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 500 })),
47
47
  }),
48
48
  })
49
49
 
@@ -66,8 +66,8 @@ interface MakeProxyChannelArgs {
66
66
  channelName: ChannelName
67
67
  target: MeshNodeName
68
68
  schema: {
69
- send: Schema.Schema<any, any>
70
- listen: Schema.Schema<any, any>
69
+ send: Schema.Codec<any, any>
70
+ listen: Schema.Codec<any, any>
71
71
  }
72
72
  /** Optional simulation parameters for testing timing-sensitive behavior */
73
73
  simulation?: ProxyChannelSimulationParams
@@ -105,7 +105,7 @@ export const makeProxyChannel = ({
105
105
 
106
106
  type ProxiedChannelStateEstablished = {
107
107
  _tag: 'Established'
108
- listenSchema: Schema.Schema<any, any>
108
+ listenSchema: Schema.Codec<any, any>
109
109
  listenQueue: Queue.Queue<any>
110
110
  ackMap: Map<string, Deferred.Deferred<void>>
111
111
  combinedChannelId: string
@@ -131,6 +131,10 @@ export const makeProxyChannel = ({
131
131
  const channelSpan = yield* Effect.currentSpan.pipe(Effect.orDie)
132
132
 
133
133
  const connectedStateRef = yield* SubscriptionRef.make<ProxiedChannelStateEstablished | false>(false)
134
+ // Incoming packets can be processed as soon as the queue-draining fiber starts, so state
135
+ // referenced by established channels must exist before that processor is forked.
136
+ const listenQueue = yield* Queue.unbounded<any>()
137
+ const ackMap = new Map<string, Deferred.Deferred<void>>()
134
138
 
135
139
  const waitForEstablished = Effect.gen(function* () {
136
140
  const state = yield* SubscriptionRef.waitUntil(connectedStateRef, (state) => state !== false)
@@ -162,8 +166,9 @@ export const makeProxyChannel = ({
162
166
  const getCombinedChannelId = (otherSideChannelIdCandidate: string) =>
163
167
  [channelIdCandidate, otherSideChannelIdCandidate].toSorted().join('_')
164
168
 
165
- const earlyPayloadBuffer = yield* Queue.unbounded<typeof MeshSchema.ProxyChannelPayload.Type>().pipe(
166
- Effect.acquireRelease(Queue.shutdown),
169
+ const earlyPayloadBuffer = yield* Effect.acquireRelease(
170
+ Queue.unbounded<typeof MeshSchema.ProxyChannelPayload.Type>(),
171
+ Queue.shutdown,
167
172
  )
168
173
 
169
174
  const processProxyPacket = ({ packet, respondToSender }: ProxyQueueItem) =>
@@ -267,7 +272,7 @@ export const makeProxyChannel = ({
267
272
  const establishedState = channelStateRef.current
268
273
  if (establishedState._tag === 'Established') {
269
274
  //
270
- const bufferedPackets = yield* Queue.takeAll(earlyPayloadBuffer)
275
+ const bufferedPackets = yield* Queue.clear(earlyPayloadBuffer)
271
276
  // yield* Effect.logDebug(
272
277
  // `[${nodeName}] Draining early payload buffer (${bufferedPackets.length}) after ResponseSuccess`,
273
278
  // )
@@ -278,10 +283,10 @@ export const makeProxyChannel = ({
278
283
  )
279
284
  continue
280
285
  }
281
- const decodedMessage = yield* Schema.decodeUnknown(establishedState.listenSchema)(
286
+ const decodedMessage = yield* Schema.decodeUnknownEffect(establishedState.listenSchema)(
282
287
  bufferedPacket.payload,
283
288
  )
284
- yield* establishedState.listenQueue.pipe(Queue.offer(decodedMessage))
289
+ yield* Queue.offer(establishedState.listenQueue, decodedMessage)
285
290
  }
286
291
  } else {
287
292
  yield* Effect.logError(
@@ -327,8 +332,8 @@ export const makeProxyChannel = ({
327
332
  yield* simSleep('onPayload', 'afterAckFork')
328
333
 
329
334
  if (channelState._tag === 'Established') {
330
- const decodedMessage = yield* Schema.decodeUnknown(channelState.listenSchema)(packet.payload)
331
- yield* channelState.listenQueue.pipe(Queue.offer(decodedMessage))
335
+ const decodedMessage = yield* Schema.decodeUnknownEffect(channelState.listenSchema)(packet.payload)
336
+ yield* Queue.offer(channelState.listenQueue, decodedMessage)
332
337
 
333
338
  // Simulation point: delay after adding to listen queue
334
339
  yield* simSleep('onPayload', 'afterListenQueueOffer')
@@ -370,19 +375,8 @@ export const makeProxyChannel = ({
370
375
  }),
371
376
  )
372
377
 
373
- yield* Stream.fromQueue(queue).pipe(
374
- Stream.tap(processProxyPacket),
375
- Stream.runDrain,
376
- Effect.tapCauseLogPretty,
377
- Effect.forkScoped,
378
- )
379
-
380
- const listenQueue = yield* Queue.unbounded<any>()
381
-
382
378
  yield* Effect.spanEvent(`Connecting`)
383
379
 
384
- const ackMap = new Map<string, Deferred.Deferred<void>>()
385
-
386
380
  // check if already established via incoming `ProxyChannelRequest` from other side
387
381
  // which indicates we already have a edge to the target node
388
382
  // const channelState = channelStateRef.current
@@ -401,6 +395,15 @@ export const makeProxyChannel = ({
401
395
  Effect.forkScoped,
402
396
  )
403
397
 
398
+ // Drain queued proxy packets only after the local side entered `Pending` and sent its
399
+ // request. Otherwise an already-buffered response can race initialization.
400
+ yield* Stream.fromQueue(queue).pipe(
401
+ Stream.tap(processProxyPacket),
402
+ Stream.runDrain,
403
+ Effect.tapCauseLogPretty,
404
+ Effect.forkScoped,
405
+ )
406
+
404
407
  const { combinedChannelId: channelId } = yield* waitForEstablished
405
408
 
406
409
  yield* Fiber.interrupt(retryOnNewEdgeFiber)
@@ -410,7 +413,7 @@ export const makeProxyChannel = ({
410
413
 
411
414
  const send = (message: any) =>
412
415
  Effect.gen(function* () {
413
- const payload = yield* Schema.encodeUnknown(schema.send)(message)
416
+ const payload = yield* Schema.encodeUnknownEffect(schema.send)(message)
414
417
  const sendFiberHandle = yield* FiberHandle.make<void, never>()
415
418
 
416
419
  const sentDeferred = yield* Deferred.make<void>()
@@ -441,7 +444,7 @@ export const makeProxyChannel = ({
441
444
 
442
445
  yield* sendPacket(packet)
443
446
 
444
- yield* ack
447
+ yield* Deferred.await(ack)
445
448
  yield* Deferred.succeed(sentDeferred, void 0)
446
449
 
447
450
  debugInfo.pendingSends--
@@ -450,18 +453,18 @@ export const makeProxyChannel = ({
450
453
  // TODO make this configurable
451
454
  // Schedule.exponential(10): 10, 20, 40, 80, 160, 320, ...
452
455
  yield* innerSend.pipe(Effect.timeout(100), Effect.retry(Schedule.exponential(10)), Effect.orDie)
453
- }).pipe(Effect.tapErrorCause(Effect.logError))
456
+ }).pipe(Effect.tapCause(Effect.logError))
454
457
 
455
- const rerunOnNewChannelFiber = yield* connectedStateRef.changes.pipe(
458
+ const rerunOnNewChannelFiber = yield* SubscriptionRef.changes(connectedStateRef).pipe(
456
459
  Stream.filter((_) => _ === false),
457
460
  Stream.tap(() => FiberHandle.run(sendFiberHandle, trySend)),
458
461
  Stream.runDrain,
459
- Effect.fork,
462
+ Effect.forkChild,
460
463
  )
461
464
 
462
465
  yield* FiberHandle.run(sendFiberHandle, trySend)
463
466
 
464
- yield* sentDeferred
467
+ yield* Deferred.await(sentDeferred)
465
468
 
466
469
  yield* Fiber.interrupt(rerunOnNewChannelFiber)
467
470
  }).pipe(
@@ -470,11 +473,11 @@ export const makeProxyChannel = ({
470
473
  Effect.withParentSpan(channelSpan),
471
474
  )
472
475
 
473
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
476
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Result.succeed))
474
477
 
475
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
478
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
476
479
 
477
- const runtime = yield* Effect.runtime()
480
+ const services = yield* Effect.context()
478
481
 
479
482
  const webChannel = {
480
483
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
@@ -489,7 +492,7 @@ export const makeProxyChannel = ({
489
492
  debug: {
490
493
  ping: (message = 'ping') =>
491
494
  send(WebChannel.DebugPingMessage.make({ message })).pipe(
492
- Effect.provide(runtime),
495
+ Effect.provide(services),
493
496
  Effect.tapCauseLogPretty,
494
497
  Effect.runFork,
495
498
  ),
package/src/common.ts CHANGED
@@ -23,14 +23,17 @@ export type ChannelKey = `target:${MeshNodeName}, channelName:${ChannelName}`
23
23
  // '~@livestore/webmesh/NoConnectionRouteSignal',
24
24
  // )('NoConnectionRouteSignal', {}) {}
25
25
 
26
- export class EdgeAlreadyExistsError extends Schema.TaggedError<EdgeAlreadyExistsError>('~@livestore/webmesh/EdgeAlreadyExistsError')('EdgeAlreadyExistsError', {
26
+ export class EdgeAlreadyExistsError extends Schema.TaggedError<EdgeAlreadyExistsError>(
27
+ '~@livestore/webmesh/EdgeAlreadyExistsError',
28
+ )('EdgeAlreadyExistsError', {
27
29
  target: Schema.String,
28
30
  }) {}
29
31
 
30
32
  export const packetAsOtelAttributes = (packet: typeof Packet.Type) => ({
31
33
  packetId: packet.id,
32
34
  'span.label':
33
- packet.id + (Predicate.hasProperty(packet, 'reqId') === true && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''),
35
+ packet.id +
36
+ (Predicate.hasProperty(packet, 'reqId') === true && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''),
34
37
  ...omitUndefineds({
35
38
  packet:
36
39
  packet._tag !== 'DirectChannelResponseSuccess' && packet._tag !== 'ProxyChannelPayload' ? packet : undefined,
@@ -40,7 +43,7 @@ export const packetAsOtelAttributes = (packet: typeof Packet.Type) => ({
40
43
  export const ListenForChannelResult = Schema.Struct({
41
44
  channelName: Schema.String,
42
45
  source: Schema.String,
43
- mode: Schema.Literal('proxy', 'direct'),
46
+ mode: Schema.Literals(['proxy', 'direct']),
44
47
  })
45
48
 
46
49
  export type ListenForChannelResult = typeof ListenForChannelResult.Type
@@ -1,9 +1,9 @@
1
- import { Schema, Transferable } from '@livestore/utils/effect'
1
+ import { Effect, Schema, Transferable } from '@livestore/utils/effect'
2
2
  import { nanoid } from '@livestore/utils/nanoid'
3
3
 
4
4
  const id = Schema.String.pipe(
5
- Schema.optional,
6
- Schema.withDefaults({ constructor: () => nanoid(10), decoding: () => nanoid(10) }),
5
+ Schema.withDecodingDefaultType(Effect.sync(() => nanoid(10))),
6
+ Schema.withConstructorDefault(Effect.sync(() => nanoid(10))),
7
7
  )
8
8
 
9
9
  const defaultPacketFields = {
@@ -23,10 +23,10 @@ const remainingHopsUndefined = Schema.Undefined.pipe(Schema.optional)
23
23
  * We need a clear path back to the sender to avoid this, thus we respond with a separate
24
24
  * `DirectChannelResponseSuccess` which contains the `port`.
25
25
  */
26
- export class DirectChannelRequest extends Schema.TaggedStruct('DirectChannelRequest', {
26
+ export const DirectChannelRequest = Schema.TaggedStruct('DirectChannelRequest', {
27
27
  ...defaultPacketFields,
28
28
  remainingHops: Schema.Array(Schema.String).pipe(Schema.optional),
29
- channelVersion: Schema.Number,
29
+ channelVersion: Schema.Finite,
30
30
  /** Only set if the request is in response to an incoming request */
31
31
  reqId: Schema.UndefinedOr(Schema.String),
32
32
  /**
@@ -34,70 +34,70 @@ export class DirectChannelRequest extends Schema.TaggedStruct('DirectChannelRequ
34
34
  * source has changed.
35
35
  */
36
36
  sourceId: Schema.String,
37
- }) {}
37
+ })
38
38
 
39
- export class DirectChannelResponseSuccess extends Schema.TaggedStruct('DirectChannelResponseSuccess', {
39
+ export const DirectChannelResponseSuccess = Schema.TaggedStruct('DirectChannelResponseSuccess', {
40
40
  ...defaultPacketFields,
41
41
  reqId: Schema.String,
42
42
  port: Transferable.MessagePort,
43
43
  // Since we can't copy this message, we need to follow the exact route back to the sender
44
44
  remainingHops: Schema.Array(Schema.String),
45
- channelVersion: Schema.Number,
46
- }) {}
45
+ channelVersion: Schema.Finite,
46
+ })
47
47
 
48
- export class DirectChannelResponseNoTransferables extends Schema.TaggedStruct('DirectChannelResponseNoTransferables', {
48
+ export const DirectChannelResponseNoTransferables = Schema.TaggedStruct('DirectChannelResponseNoTransferables', {
49
49
  ...defaultPacketFields,
50
50
  reqId: Schema.String,
51
51
  remainingHops: Schema.Array(Schema.String),
52
- }) {}
52
+ })
53
53
 
54
- export class ProxyChannelRequest extends Schema.TaggedStruct('ProxyChannelRequest', {
54
+ export const ProxyChannelRequest = Schema.TaggedStruct('ProxyChannelRequest', {
55
55
  ...defaultPacketFields,
56
56
  remainingHops: remainingHopsUndefined,
57
57
  channelIdCandidate: Schema.String,
58
- }) {}
58
+ })
59
59
 
60
- export class ProxyChannelResponseSuccess extends Schema.TaggedStruct('ProxyChannelResponseSuccess', {
60
+ export const ProxyChannelResponseSuccess = Schema.TaggedStruct('ProxyChannelResponseSuccess', {
61
61
  ...defaultPacketFields,
62
62
  reqId: Schema.String,
63
63
  remainingHops: Schema.Array(Schema.String),
64
64
  combinedChannelId: Schema.String,
65
65
  channelIdCandidate: Schema.String,
66
- }) {}
66
+ })
67
67
 
68
- export class ProxyChannelPayload extends Schema.TaggedStruct('ProxyChannelPayload', {
68
+ export const ProxyChannelPayload = Schema.TaggedStruct('ProxyChannelPayload', {
69
69
  ...defaultPacketFields,
70
70
  remainingHops: remainingHopsUndefined,
71
71
  payload: Schema.Any,
72
72
  combinedChannelId: Schema.String,
73
- }) {}
73
+ })
74
74
 
75
- export class ProxyChannelPayloadAck extends Schema.TaggedStruct('ProxyChannelPayloadAck', {
75
+ export const ProxyChannelPayloadAck = Schema.TaggedStruct('ProxyChannelPayloadAck', {
76
76
  ...defaultPacketFields,
77
77
  reqId: Schema.String,
78
78
  remainingHops: Schema.Array(Schema.String),
79
79
  combinedChannelId: Schema.String,
80
- }) {}
80
+ })
81
81
 
82
82
  /**
83
83
  * Broadcast to all nodes when a new edge is added.
84
84
  * Mostly used for auto-reconnect purposes.
85
85
  */
86
- export class NetworkEdgeAdded extends Schema.TaggedStruct('NetworkEdgeAdded', {
86
+ export const NetworkEdgeAdded = Schema.TaggedStruct('NetworkEdgeAdded', {
87
87
  id,
88
88
  source: Schema.String,
89
89
  target: Schema.String,
90
- }) {}
90
+ })
91
91
 
92
- export class NetworkTopologyRequest extends Schema.TaggedStruct('NetworkTopologyRequest', {
92
+ export const NetworkTopologyRequest = Schema.TaggedStruct('NetworkTopologyRequest', {
93
93
  id,
94
94
  hops: Schema.Array(Schema.String),
95
95
  /** Always fixed to who requested the topology */
96
96
  source: Schema.String,
97
97
  target: Schema.Literal('-'),
98
- }) {}
98
+ })
99
99
 
100
- export class NetworkTopologyResponse extends Schema.TaggedStruct('NetworkTopologyResponse', {
100
+ export const NetworkTopologyResponse = Schema.TaggedStruct('NetworkTopologyResponse', {
101
101
  id,
102
102
  reqId: Schema.String,
103
103
  remainingHops: Schema.Array(Schema.String),
@@ -106,7 +106,7 @@ export class NetworkTopologyResponse extends Schema.TaggedStruct('NetworkTopolog
106
106
  /** Always fixed to who requested the topology */
107
107
  source: Schema.String,
108
108
  target: Schema.Literal('-'),
109
- }) {}
109
+ })
110
110
 
111
111
  export const BroadcastChannelPacket = Schema.TaggedStruct('BroadcastChannelPacket', {
112
112
  id,
@@ -121,27 +121,28 @@ export const BroadcastChannelPacket = Schema.TaggedStruct('BroadcastChannelPacke
121
121
  target: Schema.Literal('-'),
122
122
  })
123
123
 
124
- export class DirectChannelPacket extends Schema.Union(
124
+ export const DirectChannelPacket = Schema.Union([
125
125
  DirectChannelRequest,
126
126
  DirectChannelResponseSuccess,
127
127
  DirectChannelResponseNoTransferables,
128
- ) {}
128
+ ])
129
129
 
130
- export class ProxyChannelPacket extends Schema.Union(
130
+ export const ProxyChannelPacket = Schema.Union([
131
131
  ProxyChannelRequest,
132
132
  ProxyChannelResponseSuccess,
133
133
  ProxyChannelPayload,
134
134
  ProxyChannelPayloadAck,
135
- ) {}
135
+ ])
136
136
 
137
- export class Packet extends Schema.Union(
137
+ export const Packet = Schema.Union([
138
138
  DirectChannelPacket,
139
139
  ProxyChannelPacket,
140
140
  NetworkEdgeAdded,
141
141
  NetworkTopologyRequest,
142
142
  NetworkTopologyResponse,
143
143
  BroadcastChannelPacket,
144
- ) {}
144
+ ])
145
+
146
+ export const DirectChannelPing = Schema.TaggedStruct('DirectChannelPing', {})
145
147
 
146
- export class DirectChannelPing extends Schema.TaggedStruct('DirectChannelPing', {}) {}
147
- export class DirectChannelPong extends Schema.TaggedStruct('DirectChannelPong', {}) {}
148
+ export const DirectChannelPong = Schema.TaggedStruct('DirectChannelPong', {})