@livestore/webmesh 0.4.0 → 0.5.0-dev.0

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 (56) 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 +23 -18
  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 +20 -21
  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 +26 -22
  12. package/dist/channel/proxy-channel.js.map +1 -1
  13. package/dist/common.d.ts +33 -35
  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/node.d.ts +6 -6
  22. package/dist/node.d.ts.map +1 -1
  23. package/dist/node.js +22 -21
  24. package/dist/node.js.map +1 -1
  25. package/dist/node.test.js +74 -71
  26. package/dist/node.test.js.map +1 -1
  27. package/dist/utils.d.ts +1 -1
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js.map +1 -1
  30. package/dist/websocket-edge.d.ts +18 -16
  31. package/dist/websocket-edge.d.ts.map +1 -1
  32. package/dist/websocket-edge.js +30 -36
  33. package/dist/websocket-edge.js.map +1 -1
  34. package/dist/websocket-edge.test.js +14 -14
  35. package/dist/websocket-edge.test.js.map +1 -1
  36. package/dist/worker/mod.d.ts +24 -0
  37. package/dist/worker/mod.d.ts.map +1 -0
  38. package/dist/worker/mod.js +44 -0
  39. package/dist/worker/mod.js.map +1 -0
  40. package/dist/worker/schema.d.ts +10 -0
  41. package/dist/worker/schema.d.ts.map +1 -0
  42. package/dist/worker/schema.js +7 -0
  43. package/dist/worker/schema.js.map +1 -0
  44. package/package.json +20 -37
  45. package/src/channel/direct-channel-internal.ts +30 -22
  46. package/src/channel/direct-channel.ts +26 -23
  47. package/src/channel/proxy-channel.ts +39 -36
  48. package/src/common.ts +6 -3
  49. package/src/mesh-schema.ts +34 -33
  50. package/src/node.test.ts +99 -86
  51. package/src/node.ts +32 -32
  52. package/src/utils.ts +3 -3
  53. package/src/websocket-edge.test.ts +14 -14
  54. package/src/websocket-edge.ts +47 -41
  55. package/src/worker/mod.ts +89 -0
  56. package/src/worker/schema.ts +8 -0
package/src/node.ts CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  Effect,
7
7
  Exit,
8
8
  Fiber,
9
- Option,
10
9
  PubSub,
11
10
  Queue,
12
11
  Schema,
@@ -61,7 +60,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
61
60
  }): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
62
61
  }
63
62
 
64
- removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
63
+ removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementError>
65
64
 
66
65
  hasChannel: ({
67
66
  target,
@@ -93,10 +92,10 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
93
92
  */
94
93
  channelName: string
95
94
  schema:
96
- | Schema.Schema<MsgListen | MsgSend, any>
95
+ | Schema.Codec<MsgListen | MsgSend, any>
97
96
  | {
98
- listen: Schema.Schema<MsgListen, any>
99
- send: Schema.Schema<MsgSend, any>
97
+ listen: Schema.Codec<MsgListen, any>
98
+ send: Schema.Codec<MsgSend, any>
100
99
  }
101
100
  /**
102
101
  * If possible, prefer using a DirectChannel with transferables (i.e. transferring memory instead of copying it).
@@ -107,7 +106,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
107
106
  *
108
107
  * @default 1 second
109
108
  */
110
- timeout?: Duration.DurationInput
109
+ timeout?: Duration.Input
111
110
  /**
112
111
  * If true, will close an existing channel if it exists.
113
112
  *
@@ -129,7 +128,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
129
128
  */
130
129
  makeBroadcastChannel: <Msg>(args: {
131
130
  channelName: string
132
- schema: Schema.Schema<Msg, any>
131
+ schema: Schema.Codec<Msg, any>
133
132
  }) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
134
133
  }
135
134
 
@@ -137,16 +136,17 @@ export const makeMeshNode = <TName extends MeshNodeName>(
137
136
  nodeName: TName,
138
137
  ): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
139
138
  Effect.gen(function* () {
140
- const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.RuntimeFiber<void> }>()
139
+ const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.Fiber<void> }>()
141
140
 
142
141
  // To avoid unbounded memory growth, we automatically forget about packet ids after a while
143
142
  const handledPacketIds = yield* TimeoutSet.make(Duration.minutes(1))
144
143
 
145
- const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>().pipe(Effect.acquireRelease(PubSub.shutdown))
144
+ const newEdgeAvailablePubSub = yield* Effect.acquireRelease(PubSub.unbounded<MeshNodeName>(), PubSub.shutdown)
146
145
 
147
- // const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
148
- // const messagePacketsToProcess = yield* Queue.unbounded<MessageQueueItem>().pipe(
149
- // Effect.acquireRelease(Queue.shutdown),
146
+ // const proxyPacketsToProcess = yield* Effect.acquireRelease(Queue.unbounded<ProxyQueueItem>(), Queue.shutdown)
147
+ // const messagePacketsToProcess = yield* Effect.acquireRelease(
148
+ // Queue.unbounded<MessageQueueItem>(),
149
+ // Queue.shutdown,
150
150
  // )
151
151
 
152
152
  const channelMap = new Map<
@@ -163,9 +163,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
163
163
  }
164
164
  >()
165
165
 
166
- const channelRequestsQueue = yield* Queue.unbounded<ListenForChannelResult>().pipe(
167
- Effect.acquireRelease(Queue.shutdown),
168
- )
166
+ const channelRequestsQueue = yield* Effect.acquireRelease(Queue.unbounded<ListenForChannelResult>(), Queue.shutdown)
169
167
 
170
168
  type RequestId = string
171
169
  const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
@@ -364,10 +362,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
364
362
 
365
363
  // TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
366
364
  const listenFiber = yield* edgeChannel.listen.pipe(
367
- Stream.flatten(),
365
+ Stream.mapEffect(Effect.fromResult),
368
366
  Stream.tap((message) =>
369
367
  Effect.gen(function* () {
370
- const packet = yield* Schema.decodeUnknown(WebmeshSchema.Packet)(message)
368
+ const packet = yield* Schema.decodeUnknownEffect(WebmeshSchema.Packet)(message)
371
369
 
372
370
  // console.debug(nodeName, 'recv', packet._tag, packet.source, packet.target)
373
371
 
@@ -387,8 +385,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
387
385
  const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
388
386
 
389
387
  if (channelMap.has(channelKey) === false) {
390
- const channelQueue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
391
- Effect.acquireRelease(Queue.shutdown),
388
+ const channelQueue = yield* Effect.acquireRelease(
389
+ Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
390
+ Queue.shutdown,
392
391
  )
393
392
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
394
393
  }
@@ -462,7 +461,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
462
461
  const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
463
462
  Effect.gen(function* () {
464
463
  if (edgeChannels.has(targetNodeName) === false) {
465
- return yield* new Cause.NoSuchElementException(`No edge found for ${targetNodeName}`)
464
+ return yield* new Cause.NoSuchElementError(`No edge found for ${targetNodeName}`)
466
465
  }
467
466
 
468
467
  yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
@@ -502,8 +501,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
502
501
  }
503
502
 
504
503
  if (channelMap.has(channelKey) === false) {
505
- const channelQueue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
506
- Effect.acquireRelease(Queue.shutdown),
504
+ const channelQueue = yield* Effect.acquireRelease(
505
+ Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
506
+ Queue.shutdown,
507
507
  )
508
508
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
509
509
  }
@@ -513,7 +513,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
513
513
  yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
514
514
 
515
515
  if (mode === 'direct') {
516
- const incomingPacketsQueue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
516
+ const incomingPacketsQueue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
517
517
 
518
518
  // We're we're draining the queue into another new queue.
519
519
  // It's a bit of a mystery why this is needed, since the unit tests also work without it.
@@ -541,7 +541,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
541
541
 
542
542
  channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel: webChannel, target } })
543
543
 
544
- yield* initialEdgeDeferred
544
+ yield* Deferred.await(initialEdgeDeferred)
545
545
 
546
546
  return webChannel
547
547
  } else {
@@ -605,12 +605,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
605
605
 
606
606
  const debugInfo = {}
607
607
 
608
- const queue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
608
+ const queue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
609
609
  broadcastChannelListenQueueMap.set(channelName, queue)
610
610
 
611
611
  const send = (message: any) =>
612
612
  Effect.gen(function* () {
613
- const payload = yield* Schema.encode(schema)(message)
613
+ const payload = yield* Schema.encodeEffect(schema)(message)
614
614
  const packet = WebmeshSchema.BroadcastChannelPacket.make({
615
615
  channelName,
616
616
  payload,
@@ -624,10 +624,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
624
624
 
625
625
  const listen = Stream.fromQueue(queue).pipe(
626
626
  Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
627
- Stream.map((_) => Schema.decodeEither(schema)(_.payload)),
627
+ Stream.map((_) => Schema.decodeResult(schema)(_.payload)),
628
628
  )
629
629
 
630
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
630
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
631
631
 
632
632
  return {
633
633
  [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
@@ -644,7 +644,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
644
644
 
645
645
  const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
646
646
 
647
- const runtime = yield* Effect.runtime()
647
+ const services = yield* Effect.context()
648
648
 
649
649
  const debug: MeshNode['debug'] = {
650
650
  print: () => {
@@ -670,7 +670,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
670
670
  ' ',
671
671
  value.debugInfo?.channel,
672
672
  '\n',
673
- indent(`Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`, 4),
673
+ indent(`Queue: ${Queue.sizeUnsafe(value.queue)}`, 4),
674
674
  value.queue,
675
675
  )
676
676
  }
@@ -699,7 +699,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
699
699
  yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
700
700
  yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
701
701
  }
702
- }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork)
702
+ }).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runFork)
703
703
  },
704
704
  requestTopology: (timeoutMs = 1000) =>
705
705
  Effect.gen(function* () {
@@ -722,7 +722,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
722
722
  for (const [key, value] of item) {
723
723
  yield* Effect.logDebug(` node '${key}' has edge to: ${Array.from(value.values()).join(', ')}`)
724
724
  }
725
- }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runPromise),
725
+ }).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runPromise),
726
726
  }
727
727
 
728
728
  return {
package/src/utils.ts CHANGED
@@ -6,14 +6,14 @@ import { Duration, Effect } from '@livestore/utils/effect'
6
6
  */
7
7
  export class TimeoutSet<V> {
8
8
  private values = new Map<V, number>()
9
- private timeoutHandle: NodeJS.Timeout | undefined
9
+ private timeoutHandle: ReturnType<typeof setTimeout> | undefined
10
10
  private readonly timeoutMs: number
11
11
 
12
- private constructor({ timeout }: { timeout: Duration.DurationInput }) {
12
+ private constructor({ timeout }: { timeout: Duration.Input }) {
13
13
  this.timeoutMs = Duration.toMillis(timeout)
14
14
  }
15
15
 
16
- static make = (timeout: Duration.DurationInput) =>
16
+ static make = (timeout: Duration.Input) =>
17
17
  Effect.gen(function* () {
18
18
  const timeoutSet = new TimeoutSet({ timeout })
19
19
 
@@ -7,24 +7,24 @@
7
7
  import { expect } from 'vitest'
8
8
 
9
9
  import { Vitest } from '@livestore/utils-dev/node-vitest'
10
- import { Effect, Either, Schema, WebChannel } from '@livestore/utils/effect'
10
+ import { Effect, Result, Schema, WebChannel } from '@livestore/utils/effect'
11
11
 
12
12
  import * as MeshSchema from './mesh-schema.ts'
13
- import { MessageMsgPack, WSEdgeInit, WSEdgePayload } from './websocket-edge.ts'
13
+ import { MessageMsgpack, WSEdgeInit, WSEdgePayload } from './websocket-edge.ts'
14
14
 
15
15
  Vitest.describe('websocket-edge', () => {
16
16
  /**
17
- * Test that WSEdgeInit messages can be encoded/decoded via MessageMsgPack.
17
+ * Test that WSEdgeInit messages can be encoded/decoded via MessageMsgpack.
18
18
  */
19
- Vitest.scopedLive('should encode/decode WSEdgeInit', (test) =>
19
+ Vitest.live('should encode/decode WSEdgeInit', (test) =>
20
20
  Effect.gen(function* () {
21
21
  const initMessage = WSEdgeInit.make({ from: 'test-node' })
22
22
 
23
23
  // Encode to msgpack
24
- const encoded = yield* Schema.encode(MessageMsgPack)(initMessage)
24
+ const encoded = yield* Schema.encodeEffect(MessageMsgpack)(initMessage)
25
25
 
26
26
  // Decode back
27
- const decoded = yield* Schema.decode(MessageMsgPack)(encoded)
27
+ const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
28
28
 
29
29
  expect(decoded._tag).toBe('WSEdgeInit')
30
30
  if (decoded._tag === 'WSEdgeInit') {
@@ -36,7 +36,7 @@ Vitest.describe('websocket-edge', () => {
36
36
  /**
37
37
  * Test that WSEdgePayload messages with valid Packet payloads work correctly.
38
38
  */
39
- Vitest.scopedLive('should encode/decode WSEdgePayload with Packet', (test) =>
39
+ Vitest.live('should encode/decode WSEdgePayload with Packet', (test) =>
40
40
  Effect.gen(function* () {
41
41
  const packet = {
42
42
  _tag: 'NetworkEdgeAdded' as const,
@@ -47,10 +47,10 @@ Vitest.describe('websocket-edge', () => {
47
47
  const wsMessage = WSEdgePayload.make({ from: 'test-node', payload: packet })
48
48
 
49
49
  // Encode to msgpack
50
- const encoded = yield* Schema.encode(MessageMsgPack)(wsMessage)
50
+ const encoded = yield* Schema.encodeEffect(MessageMsgpack)(wsMessage)
51
51
 
52
52
  // Decode back
53
- const decoded = yield* Schema.decode(MessageMsgPack)(encoded)
53
+ const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
54
54
 
55
55
  expect(decoded._tag).toBe('WSEdgePayload')
56
56
  if (decoded._tag === 'WSEdgePayload') {
@@ -64,23 +64,23 @@ Vitest.describe('websocket-edge', () => {
64
64
  * Test that mapSchema(Packet) includes WebChannel internal messages.
65
65
  * This is important because channels can send/receive WebChannel.Ping/Pong/DebugPing.
66
66
  */
67
- Vitest.scopedLive('mapSchema should include WebChannel messages in schema', (test) =>
67
+ Vitest.live('mapSchema should include WebChannel messages in schema', (test) =>
68
68
  Effect.gen(function* () {
69
69
  const schema = WebChannel.mapSchema(MeshSchema.Packet)
70
70
 
71
71
  // WebChannel.Ping should be decodable via the wrapped listen schema
72
72
  const pingPayload = { _tag: 'WebChannel.Ping' as const, requestId: 'test-123' }
73
- const result = Schema.decodeUnknownEither(schema.listen)(pingPayload)
73
+ const result = Schema.decodeUnknownResult(schema.listen)(pingPayload)
74
74
 
75
75
  // mapSchema adds WebChannel messages to the schema
76
- expect(Either.isRight(result)).toBe(true)
76
+ expect(Result.isSuccess(result)).toBe(true)
77
77
  }).pipe(Vitest.withTestCtx(test)),
78
78
  )
79
79
 
80
80
  /**
81
81
  * Test that valid webmesh packets can be decoded.
82
82
  */
83
- Vitest.scopedLive('should decode valid webmesh packets', (test) =>
83
+ Vitest.live('should decode valid webmesh packets', (test) =>
84
84
  Effect.gen(function* () {
85
85
  const schema = WebChannel.mapSchema(MeshSchema.Packet)
86
86
 
@@ -91,7 +91,7 @@ Vitest.describe('websocket-edge', () => {
91
91
  target: 'node-b',
92
92
  }
93
93
 
94
- const result = yield* Schema.decodeUnknown(schema.listen)(packet)
94
+ const result = yield* Schema.decodeUnknownEffect(schema.listen)(packet)
95
95
  expect(result._tag).toBe('NetworkEdgeAdded')
96
96
  }).pipe(Vitest.withTestCtx(test)),
97
97
  )
@@ -1,11 +1,13 @@
1
- import type { HttpClient } from '@livestore/utils/effect'
2
1
  import {
2
+ type HttpClient,
3
3
  Deferred,
4
+ Duration,
4
5
  Effect,
5
- Either,
6
+ Result,
6
7
  Exit,
8
+ Latch,
7
9
  Layer,
8
- MsgPack,
10
+ Msgpack,
9
11
  Queue,
10
12
  Schedule,
11
13
  Schema,
@@ -18,18 +20,18 @@ import {
18
20
  import * as WebmeshSchema from './mesh-schema.ts'
19
21
  import type { MeshNode } from './node.ts'
20
22
 
21
- export class WSEdgeInit extends Schema.TaggedStruct('WSEdgeInit', {
23
+ export const WSEdgeInit = Schema.TaggedStruct('WSEdgeInit', {
22
24
  from: Schema.String,
23
- }) {}
25
+ })
24
26
 
25
- export class WSEdgePayload extends Schema.TaggedStruct('WSEdgePayload', {
27
+ export const WSEdgePayload = Schema.TaggedStruct('WSEdgePayload', {
26
28
  from: Schema.String,
27
29
  payload: Schema.Any,
28
- }) {}
30
+ })
29
31
 
30
- export class WSEdgeMessage extends Schema.Union(WSEdgeInit, WSEdgePayload) {}
32
+ export const WSEdgeMessage = Schema.Union([WSEdgeInit, WSEdgePayload])
31
33
 
32
- export const MessageMsgPack = MsgPack.schema(WSEdgeMessage)
34
+ export const MessageMsgpack = Msgpack.schema(WSEdgeMessage)
33
35
 
34
36
  export type SocketType =
35
37
  | {
@@ -58,11 +60,12 @@ export const connectViaWebSocket = ({
58
60
  debug: { id: `node:${node.nodeName}` },
59
61
  })
60
62
 
61
- yield* node
62
- .addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true })
63
- .pipe(Effect.acquireRelease(() => node.removeEdge('ws').pipe(Effect.orDie)))
63
+ yield* Effect.acquireRelease(
64
+ node.addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true }),
65
+ () => node.removeEdge('ws').pipe(Effect.orDie),
66
+ )
64
67
 
65
- yield* edgeChannel.webChannel.closedDeferred
68
+ yield* Deferred.await(edgeChannel.webChannel.closedDeferred)
66
69
  }).pipe(Effect.scoped, Effect.forever, Effect.interruptible, Effect.provide(binaryWebSocketConstructorLayer))
67
70
 
68
71
  const binaryWebSocketConstructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url, protocols) => {
@@ -91,46 +94,49 @@ export const makeWebSocketEdge = ({
91
94
  Effect.gen(function* () {
92
95
  const fromDeferred = yield* Deferred.make<string>()
93
96
 
94
- const listenQueue = yield* Queue.unbounded<typeof WebmeshSchema.Packet.Type>().pipe(
95
- Effect.acquireRelease(Queue.shutdown),
97
+ const listenQueue = yield* Effect.acquireRelease(
98
+ Queue.unbounded<typeof WebmeshSchema.Packet.Type>(),
99
+ Queue.shutdown,
96
100
  )
97
101
 
98
102
  const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
99
103
 
100
- const isConnectedLatch = yield* Effect.makeLatch(true)
104
+ const isConnectedLatch = yield* Latch.make(true)
101
105
 
102
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
106
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
103
107
 
104
- const retryOpenTimeoutSchedule = Schedule.union(Schedule.exponential(100), Schedule.spaced(5000)).pipe(
105
- Schedule.whileInput((_: Socket.SocketError) => _.reason === 'OpenTimeout' || _.reason === 'Open'),
108
+ const retryOpenErrorSchedule = Schedule.exponential(100).pipe(
109
+ Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.millis(5000)))),
110
+ Schedule.setInputType<Socket.SocketError>(),
111
+ Schedule.while(({ input }) => input.reason._tag === 'SocketOpenError'),
106
112
  )
107
113
 
108
114
  const sendToSocket = yield* socket.writer
109
115
 
110
116
  yield* Stream.never.pipe(
111
117
  Stream.pipeThroughChannel(Socket.toChannel(socket)),
112
- Stream.catchTag(
113
- 'SocketError',
114
- Effect.fnUntraced(function* (error) {
115
- // In the case of the socket being closed, we're interrupting the stream
116
- // and close the WebChannel (which can be observed from the outside)
117
- if (error.reason === 'Close') {
118
- yield* Deferred.succeed(closedDeferred, undefined)
119
- yield* isConnectedLatch.close
120
- return yield* Effect.interrupt
121
- } else {
122
- return yield* error
123
- }
124
- }),
125
- ),
126
- Stream.retry(retryOpenTimeoutSchedule),
118
+ Stream.catchTag('SocketError', (error) => {
119
+ // In the case of the socket being closed, close the WebChannel
120
+ // which can be observed from the outside.
121
+ if (error.reason._tag === 'SocketCloseError') {
122
+ return Stream.fromEffectDrain(
123
+ Effect.gen(function* () {
124
+ yield* Deferred.succeed(closedDeferred, undefined)
125
+ yield* isConnectedLatch.close
126
+ }),
127
+ )
128
+ }
129
+
130
+ return Stream.fail(error)
131
+ }),
132
+ Stream.retry(retryOpenErrorSchedule),
127
133
  Stream.tap(
128
134
  Effect.fn(function* (bytes) {
129
- const msg = yield* Schema.decode(MessageMsgPack)(new Uint8Array(bytes))
135
+ const msg = yield* Schema.decodeEffect(MessageMsgpack)(new Uint8Array(bytes))
130
136
  if (msg._tag === 'WSEdgeInit') {
131
137
  yield* Deferred.succeed(fromDeferred, msg.from)
132
138
  } else {
133
- const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
139
+ const decodedPayload = yield* Schema.decodeEffect(schema.listen)(msg.payload)
134
140
  yield* Queue.offer(listenQueue, decodedPayload)
135
141
  }
136
142
  }),
@@ -149,13 +155,13 @@ export const makeWebSocketEdge = ({
149
155
  )
150
156
 
151
157
  const initHandshake = (from: string) =>
152
- sendToSocket(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgeInit', from }))
158
+ sendToSocket(Schema.encodeSync(MessageMsgpack)({ _tag: 'WSEdgeInit', from }))
153
159
 
154
160
  if (socketType._tag === 'leaf') {
155
161
  yield* initHandshake(socketType.from)
156
162
  }
157
163
 
158
- const deferredResult = yield* fromDeferred
164
+ const deferredResult = yield* Deferred.await(fromDeferred)
159
165
  const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
160
166
 
161
167
  if (socketType._tag === 'relay') {
@@ -165,12 +171,12 @@ export const makeWebSocketEdge = ({
165
171
  const send = (message: typeof WebmeshSchema.Packet.Type) =>
166
172
  Effect.gen(function* () {
167
173
  yield* isConnectedLatch.await
168
- const payload = yield* Schema.encode(schema.send)(message)
169
- yield* sendToSocket(yield* Schema.encode(MessageMsgPack)({ _tag: 'WSEdgePayload', payload, from }))
174
+ const payload = yield* Schema.encodeEffect(schema.send)(message)
175
+ yield* sendToSocket(yield* Schema.encodeEffect(MessageMsgpack)({ _tag: 'WSEdgePayload', payload, from }))
170
176
  }).pipe(Effect.orDie)
171
177
 
172
178
  const listen = Stream.fromQueue(listenQueue).pipe(
173
- Stream.map(Either.right),
179
+ Stream.map(Result.succeed),
174
180
  WebChannel.listenToDebugPing('websocket-edge'),
175
181
  )
176
182
 
@@ -0,0 +1,89 @@
1
+ import { LS_DEV } from '@livestore/utils'
2
+ import { Context, Deferred, Effect, Layer, Queue, Stream, WebChannel } from '@livestore/utils/effect'
3
+
4
+ import * as WebmeshSchema from '../mesh-schema.ts'
5
+ import type { MeshNode } from '../node.ts'
6
+ import { makeMeshNode } from '../node.ts'
7
+ import * as WorkerSchema from './schema.ts'
8
+
9
+ export * as Schema from './schema.ts'
10
+
11
+ declare global {
12
+ var __debugWebmeshNode: any
13
+ }
14
+
15
+ export class CacheService extends Context.Service<CacheService, { node: MeshNode }>()(
16
+ '@livestore/webmesh:worker:CacheService',
17
+ ) {
18
+ static layer = ({ nodeName }: { nodeName: string }) =>
19
+ Effect.gen(function* () {
20
+ const node = yield* makeMeshNode(nodeName)
21
+
22
+ globalThis.__debugWebmeshNode = node
23
+
24
+ return CacheService.of({ node })
25
+ }).pipe(Layer.effect(CacheService))
26
+ }
27
+
28
+ export const CreateConnection = ({ from, port }: typeof WorkerSchema.CreateConnection.Type) =>
29
+ Stream.callback<{}, never, CacheService>((emit) =>
30
+ Effect.gen(function* () {
31
+ const { node } = yield* CacheService
32
+
33
+ const messagePortChannel = yield* WebChannel.messagePortChannel({ port, schema: WebmeshSchema.Packet })
34
+
35
+ yield* node.addEdge({ target: from, edgeChannel: messagePortChannel, replaceIfExists: true })
36
+
37
+ if (LS_DEV === true) {
38
+ yield* Effect.logDebug(`@livestore/webmesh:worker: accepted edge: ${node.nodeName} <- ${from}`)
39
+ }
40
+
41
+ // `Stream.callback` exposes a queue-like emitter in Effect v4. Emit the single success value
42
+ // and then end the stream to preserve the old one-response worker command behavior.
43
+ yield* Queue.offer(emit, {})
44
+ yield* Queue.end(emit)
45
+
46
+ yield* Effect.spanEvent({ connectedTo: [...node.edgeKeys] })
47
+ }).pipe(Effect.orDie),
48
+ ).pipe(Stream.withSpan(`@livestore/webmesh:worker:create-connection:${from}`))
49
+
50
+ export const connectViaWorker = ({
51
+ node,
52
+ target,
53
+ worker,
54
+ }: {
55
+ node: MeshNode
56
+ target: string
57
+ // Effect v4 removed the serialized worker pool type this package used to reference. Webmesh only
58
+ // needs this structural `execute` contract; adapter code can provide any compatible pool.
59
+ worker: { execute: (request: typeof WorkerSchema.Request.Type) => Stream.Stream<{}> }
60
+ }) =>
61
+ Effect.gen(function* () {
62
+ const mc = new MessageChannel()
63
+
64
+ const isConnected = yield* Deferred.make<boolean>()
65
+
66
+ if (LS_DEV === true) {
67
+ yield* Effect.addFinalizerLog(`@livestore/webmesh:worker: closing message channel ${node.nodeName} -> ${target}`)
68
+ }
69
+
70
+ yield* worker.execute(WorkerSchema.CreateConnection.make({ from: node.nodeName, port: mc.port1 })).pipe(
71
+ Stream.tap(() => Deferred.succeed(isConnected, true)),
72
+ Stream.runDrain,
73
+ Effect.tapCauseLogPretty,
74
+ Effect.forkScoped,
75
+ )
76
+
77
+ yield* Deferred.await(isConnected)
78
+
79
+ const workerConnection = yield* WebChannel.messagePortChannel({
80
+ port: mc.port2,
81
+ schema: WebmeshSchema.Packet,
82
+ })
83
+
84
+ yield* node.addEdge({ target, edgeChannel: workerConnection, replaceIfExists: true })
85
+
86
+ if (LS_DEV === true) {
87
+ yield* Effect.logDebug(`@livestore/webmesh:worker: initiated connection: ${node.nodeName} -> ${target}`)
88
+ }
89
+ })
@@ -0,0 +1,8 @@
1
+ import { Schema, Transferable } from '@livestore/utils/effect'
2
+
3
+ export const CreateConnection = Schema.TaggedStruct('WebmeshWorker.CreateConnection', {
4
+ from: Schema.String,
5
+ port: Transferable.MessagePort,
6
+ })
7
+
8
+ export const Request = Schema.Union([CreateConnection])