@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
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.decodeEffect(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 { MessageBinary, 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 MessageBinary.
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
- // Encode to msgpack
24
- const encoded = yield* Schema.encode(MessageMsgPack)(initMessage)
23
+ // Encode to binary
24
+ const encoded = yield* Schema.encodeEffect(MessageBinary)(initMessage)
25
25
 
26
26
  // Decode back
27
- const decoded = yield* Schema.decode(MessageMsgPack)(encoded)
27
+ const decoded = yield* Schema.decodeEffect(MessageBinary)(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,
@@ -46,11 +46,11 @@ Vitest.describe('websocket-edge', () => {
46
46
  }
47
47
  const wsMessage = WSEdgePayload.make({ from: 'test-node', payload: packet })
48
48
 
49
- // Encode to msgpack
50
- const encoded = yield* Schema.encode(MessageMsgPack)(wsMessage)
49
+ // Encode to binary
50
+ const encoded = yield* Schema.encodeEffect(MessageBinary)(wsMessage)
51
51
 
52
52
  // Decode back
53
- const decoded = yield* Schema.decode(MessageMsgPack)(encoded)
53
+ const decoded = yield* Schema.decodeEffect(MessageBinary)(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.decodeEffect(schema.listen)(packet)
95
95
  expect(result._tag).toBe('NetworkEdgeAdded')
96
96
  }).pipe(Vitest.withTestCtx(test)),
97
97
  )
@@ -1,14 +1,15 @@
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,
7
- Layer,
8
- MsgPack,
8
+ Latch,
9
9
  Queue,
10
10
  Schedule,
11
11
  Schema,
12
+ SchemaBinary,
12
13
  Scope,
13
14
  Socket,
14
15
  Stream,
@@ -18,18 +19,18 @@ import {
18
19
  import * as WebmeshSchema from './mesh-schema.ts'
19
20
  import type { MeshNode } from './node.ts'
20
21
 
21
- export class WSEdgeInit extends Schema.TaggedStruct('WSEdgeInit', {
22
+ export const WSEdgeInit = Schema.TaggedStruct('WSEdgeInit', {
22
23
  from: Schema.String,
23
- }) {}
24
+ })
24
25
 
25
- export class WSEdgePayload extends Schema.TaggedStruct('WSEdgePayload', {
26
+ export const WSEdgePayload = Schema.TaggedStruct('WSEdgePayload', {
26
27
  from: Schema.String,
27
28
  payload: Schema.Any,
28
- }) {}
29
+ })
29
30
 
30
- export class WSEdgeMessage extends Schema.Union(WSEdgeInit, WSEdgePayload) {}
31
+ export const WSEdgeMessage = Schema.Union([WSEdgeInit, WSEdgePayload])
31
32
 
32
- export const MessageMsgPack = MsgPack.schema(WSEdgeMessage)
33
+ export const MessageBinary = SchemaBinary.toCodec(WSEdgeMessage)
33
34
 
34
35
  export type SocketType =
35
36
  | {
@@ -58,18 +59,13 @@ export const connectViaWebSocket = ({
58
59
  debug: { id: `node:${node.nodeName}` },
59
60
  })
60
61
 
61
- yield* node
62
- .addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true })
63
- .pipe(Effect.acquireRelease(() => node.removeEdge('ws').pipe(Effect.orDie)))
62
+ yield* Effect.acquireRelease(
63
+ node.addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true }),
64
+ () => node.removeEdge('ws').pipe(Effect.orDie),
65
+ )
64
66
 
65
- yield* edgeChannel.webChannel.closedDeferred
66
- }).pipe(Effect.scoped, Effect.forever, Effect.interruptible, Effect.provide(binaryWebSocketConstructorLayer))
67
-
68
- const binaryWebSocketConstructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url, protocols) => {
69
- const socket = new globalThis.WebSocket(url, protocols)
70
- socket.binaryType = 'arraybuffer'
71
- return socket
72
- })
67
+ yield* Deferred.await(edgeChannel.webChannel.closedDeferred)
68
+ }).pipe(Effect.scoped, Effect.forever, Effect.interruptible, Effect.provide(Socket.layerWebSocketConstructorGlobal))
73
69
 
74
70
  export const makeWebSocketEdge = ({
75
71
  socket,
@@ -91,46 +87,49 @@ export const makeWebSocketEdge = ({
91
87
  Effect.gen(function* () {
92
88
  const fromDeferred = yield* Deferred.make<string>()
93
89
 
94
- const listenQueue = yield* Queue.unbounded<typeof WebmeshSchema.Packet.Type>().pipe(
95
- Effect.acquireRelease(Queue.shutdown),
90
+ const listenQueue = yield* Effect.acquireRelease(
91
+ Queue.unbounded<typeof WebmeshSchema.Packet.Type>(),
92
+ Queue.shutdown,
96
93
  )
97
94
 
98
95
  const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
99
96
 
100
- const isConnectedLatch = yield* Effect.makeLatch(true)
97
+ const isConnectedLatch = yield* Latch.make(true)
101
98
 
102
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
99
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
103
100
 
104
- const retryOpenTimeoutSchedule = Schedule.union(Schedule.exponential(100), Schedule.spaced(5000)).pipe(
105
- Schedule.whileInput((_: Socket.SocketError) => _.reason === 'OpenTimeout' || _.reason === 'Open'),
101
+ const retryOpenErrorSchedule = Schedule.exponential(100).pipe(
102
+ Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.millis(5000)))),
103
+ Schedule.setInputType<Socket.SocketError>(),
104
+ Schedule.while(({ input }) => input.reason._tag === 'SocketOpenError'),
106
105
  )
107
106
 
108
107
  const sendToSocket = yield* socket.writer
109
108
 
110
109
  yield* Stream.never.pipe(
111
110
  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),
111
+ Stream.catchTag('SocketError', (error) => {
112
+ // In the case of the socket being closed, close the WebChannel
113
+ // which can be observed from the outside.
114
+ if (error.reason._tag === 'SocketCloseError') {
115
+ return Stream.fromEffectDrain(
116
+ Effect.gen(function* () {
117
+ yield* Deferred.succeed(closedDeferred, undefined)
118
+ yield* isConnectedLatch.close
119
+ }),
120
+ )
121
+ }
122
+
123
+ return Stream.fail(error)
124
+ }),
125
+ Stream.retry(retryOpenErrorSchedule),
127
126
  Stream.tap(
128
127
  Effect.fn(function* (bytes) {
129
- const msg = yield* Schema.decode(MessageMsgPack)(new Uint8Array(bytes))
128
+ const msg = yield* Schema.decodeEffect(MessageBinary)(new Uint8Array(bytes))
130
129
  if (msg._tag === 'WSEdgeInit') {
131
130
  yield* Deferred.succeed(fromDeferred, msg.from)
132
131
  } else {
133
- const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
132
+ const decodedPayload = yield* Schema.decodeEffect(schema.listen)(msg.payload)
134
133
  yield* Queue.offer(listenQueue, decodedPayload)
135
134
  }
136
135
  }),
@@ -149,13 +148,13 @@ export const makeWebSocketEdge = ({
149
148
  )
150
149
 
151
150
  const initHandshake = (from: string) =>
152
- sendToSocket(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgeInit', from }))
151
+ sendToSocket.write(Schema.encodeSync(MessageBinary)({ _tag: 'WSEdgeInit', from }))
153
152
 
154
153
  if (socketType._tag === 'leaf') {
155
154
  yield* initHandshake(socketType.from)
156
155
  }
157
156
 
158
- const deferredResult = yield* fromDeferred
157
+ const deferredResult = yield* Deferred.await(fromDeferred)
159
158
  const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
160
159
 
161
160
  if (socketType._tag === 'relay') {
@@ -165,12 +164,12 @@ export const makeWebSocketEdge = ({
165
164
  const send = (message: typeof WebmeshSchema.Packet.Type) =>
166
165
  Effect.gen(function* () {
167
166
  yield* isConnectedLatch.await
168
- const payload = yield* Schema.encode(schema.send)(message)
169
- yield* sendToSocket(yield* Schema.encode(MessageMsgPack)({ _tag: 'WSEdgePayload', payload, from }))
167
+ const payload = yield* Schema.encodeEffect(schema.send)(message)
168
+ yield* sendToSocket.write(yield* Schema.encodeEffect(MessageBinary)({ _tag: 'WSEdgePayload', payload, from }))
170
169
  }).pipe(Effect.orDie)
171
170
 
172
171
  const listen = Stream.fromQueue(listenQueue).pipe(
173
- Stream.map(Either.right),
172
+ Stream.map(Result.succeed),
174
173
  WebChannel.listenToDebugPing('websocket-edge'),
175
174
  )
176
175
 
@@ -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])