@livestore/webmesh 0.4.0-dev.9 → 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 (59) hide show
  1. package/README.md +5 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/direct-channel-internal.d.ts +2 -3
  4. package/dist/channel/direct-channel-internal.d.ts.map +1 -1
  5. package/dist/channel/direct-channel-internal.js +35 -38
  6. package/dist/channel/direct-channel-internal.js.map +1 -1
  7. package/dist/channel/direct-channel.d.ts.map +1 -1
  8. package/dist/channel/direct-channel.js +22 -23
  9. package/dist/channel/direct-channel.js.map +1 -1
  10. package/dist/channel/proxy-channel.d.ts +29 -5
  11. package/dist/channel/proxy-channel.d.ts.map +1 -1
  12. package/dist/channel/proxy-channel.js +87 -37
  13. package/dist/channel/proxy-channel.js.map +1 -1
  14. package/dist/common.d.ts +33 -35
  15. package/dist/common.d.ts.map +1 -1
  16. package/dist/common.js +7 -7
  17. package/dist/common.js.map +1 -1
  18. package/dist/mesh-schema.d.ts +262 -136
  19. package/dist/mesh-schema.d.ts.map +1 -1
  20. package/dist/mesh-schema.js +45 -44
  21. package/dist/mesh-schema.js.map +1 -1
  22. package/dist/node.d.ts +12 -6
  23. package/dist/node.d.ts.map +1 -1
  24. package/dist/node.js +47 -44
  25. package/dist/node.js.map +1 -1
  26. package/dist/node.test.js +253 -63
  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 +20 -18
  32. package/dist/websocket-edge.d.ts.map +1 -1
  33. package/dist/websocket-edge.js +33 -41
  34. package/dist/websocket-edge.js.map +1 -1
  35. package/dist/websocket-edge.test.d.ts +7 -0
  36. package/dist/websocket-edge.test.d.ts.map +1 -0
  37. package/dist/websocket-edge.test.js +74 -0
  38. package/dist/websocket-edge.test.js.map +1 -0
  39. package/dist/worker/mod.d.ts +24 -0
  40. package/dist/worker/mod.d.ts.map +1 -0
  41. package/dist/worker/mod.js +44 -0
  42. package/dist/worker/mod.js.map +1 -0
  43. package/dist/worker/schema.d.ts +10 -0
  44. package/dist/worker/schema.d.ts.map +1 -0
  45. package/dist/worker/schema.js +7 -0
  46. package/dist/worker/schema.js.map +1 -0
  47. package/package.json +48 -12
  48. package/src/channel/direct-channel-internal.ts +41 -47
  49. package/src/channel/direct-channel.ts +28 -25
  50. package/src/channel/proxy-channel.ts +120 -57
  51. package/src/common.ts +9 -7
  52. package/src/mesh-schema.ts +34 -33
  53. package/src/node.test.ts +356 -80
  54. package/src/node.ts +63 -55
  55. package/src/utils.ts +3 -3
  56. package/src/websocket-edge.test.ts +98 -0
  57. package/src/websocket-edge.ts +52 -48
  58. package/src/worker/mod.ts +89 -0
  59. package/src/worker/schema.ts +8 -0
@@ -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
  | {
@@ -50,7 +52,7 @@ export const connectViaWebSocket = ({
50
52
  openTimeout?: number
51
53
  }): Effect.Effect<void, never, Scope.Scope | HttpClient.HttpClient> =>
52
54
  Effect.gen(function* () {
53
- const socket = yield* Socket.makeWebSocket(url, { openTimeout })
55
+ const socket = yield* Socket.makeWebSocket(url, openTimeout !== undefined ? { openTimeout } : undefined)
54
56
 
55
57
  const edgeChannel = yield* makeWebSocketEdge({
56
58
  socket,
@@ -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) => {
@@ -78,7 +81,7 @@ export const makeWebSocketEdge = ({
78
81
  }: {
79
82
  socket: Socket.Socket
80
83
  socketType: SocketType
81
- debug?: { id?: string }
84
+ debug?: { id?: string | undefined } | undefined
82
85
  }): Effect.Effect<
83
86
  {
84
87
  webChannel: WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
@@ -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
 
114
+ const sendToSocket = yield* socket.writer
115
+
108
116
  yield* Stream.never.pipe(
109
117
  Stream.pipeThroughChannel(Socket.toChannel(socket)),
110
- Stream.catchTag(
111
- 'SocketError',
112
- Effect.fnUntraced(function* (error) {
113
- // yield* Effect.logError(`[websocket-edge] Socket error`, error, { socketType, debugId: debugInfo?.id })
114
- // In the case of the socket being closed, we're interrupting the stream
115
- // and close the WebChannel (which can be observed from the outside)
116
- if (error.reason === 'Close') {
117
- yield* Deferred.succeed(closedDeferred, undefined)
118
- yield* isConnectedLatch.close
119
- return yield* Effect.interrupt
120
- } else {
121
- return yield* Effect.fail(error)
122
- }
123
- }),
124
- ),
125
- 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),
126
133
  Stream.tap(
127
134
  Effect.fn(function* (bytes) {
128
- const msg = yield* Schema.decode(MessageMsgPack)(new Uint8Array(bytes))
135
+ const msg = yield* Schema.decodeEffect(MessageMsgpack)(new Uint8Array(bytes))
129
136
  if (msg._tag === 'WSEdgeInit') {
130
137
  yield* Deferred.succeed(fromDeferred, msg.from)
131
138
  } else {
132
- const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
133
- // yield* Effect.logDebug(`[websocket-edge] recv from ${msg.from}: ${decodedPayload._tag}`, decodedPayload)
139
+ const decodedPayload = yield* Schema.decodeEffect(schema.listen)(msg.payload)
134
140
  yield* Queue.offer(listenQueue, decodedPayload)
135
141
  }
136
142
  }),
@@ -148,16 +154,14 @@ export const makeWebSocketEdge = ({
148
154
  Effect.forkScoped,
149
155
  )
150
156
 
151
- const sendToSocket = yield* socket.writer
152
-
153
157
  const initHandshake = (from: string) =>
154
- sendToSocket(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgeInit', from }))
158
+ sendToSocket(Schema.encodeSync(MessageMsgpack)({ _tag: 'WSEdgeInit', from }))
155
159
 
156
160
  if (socketType._tag === 'leaf') {
157
161
  yield* initHandshake(socketType.from)
158
162
  }
159
163
 
160
- const deferredResult = yield* fromDeferred
164
+ const deferredResult = yield* Deferred.await(fromDeferred)
161
165
  const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
162
166
 
163
167
  if (socketType._tag === 'relay') {
@@ -167,12 +171,12 @@ export const makeWebSocketEdge = ({
167
171
  const send = (message: typeof WebmeshSchema.Packet.Type) =>
168
172
  Effect.gen(function* () {
169
173
  yield* isConnectedLatch.await
170
- const payload = yield* Schema.encode(schema.send)(message)
171
- yield* sendToSocket(Schema.encodeSync(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 }))
172
176
  }).pipe(Effect.orDie)
173
177
 
174
178
  const listen = Stream.fromQueue(listenQueue).pipe(
175
- Stream.map(Either.right),
179
+ Stream.map(Result.succeed),
176
180
  WebChannel.listenToDebugPing('websocket-edge'),
177
181
  )
178
182
 
@@ -184,7 +188,7 @@ export const makeWebSocketEdge = ({
184
188
  schema,
185
189
  supportsTransferables: false,
186
190
  shutdown: Scope.close(scope, Exit.void),
187
- debugInfo,
191
+ ...(debugInfo !== undefined ? { debugInfo } : {}),
188
192
  } satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
189
193
 
190
194
  return { webChannel, from }
@@ -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])