@livestore/utils 0.3.0-dev.3 → 0.3.0-dev.30

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 (144) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/Deferred.d.ts.map +1 -1
  3. package/dist/base64.d.ts.map +1 -1
  4. package/dist/bun/mod.d.ts +5 -0
  5. package/dist/bun/mod.d.ts.map +1 -0
  6. package/dist/bun/mod.js +9 -0
  7. package/dist/bun/mod.js.map +1 -0
  8. package/dist/cuid/cuid.browser.d.ts.map +1 -1
  9. package/dist/cuid/cuid.node.d.ts.map +1 -1
  10. package/dist/effect/BucketQueue.d.ts +15 -2
  11. package/dist/effect/BucketQueue.d.ts.map +1 -1
  12. package/dist/effect/BucketQueue.js +24 -6
  13. package/dist/effect/BucketQueue.js.map +1 -1
  14. package/dist/effect/Effect.d.ts +4 -2
  15. package/dist/effect/Effect.d.ts.map +1 -1
  16. package/dist/effect/Effect.js +21 -18
  17. package/dist/effect/Effect.js.map +1 -1
  18. package/dist/effect/Logger.d.ts +2 -0
  19. package/dist/effect/Logger.d.ts.map +1 -1
  20. package/dist/effect/Logger.js +16 -1
  21. package/dist/effect/Logger.js.map +1 -1
  22. package/dist/effect/Scheduler.d.ts.map +1 -1
  23. package/dist/effect/Schema/debug-diff.d.ts.map +1 -1
  24. package/dist/effect/Schema/index.d.ts +3 -0
  25. package/dist/effect/Schema/index.d.ts.map +1 -1
  26. package/dist/effect/Schema/index.js +19 -0
  27. package/dist/effect/Schema/index.js.map +1 -1
  28. package/dist/effect/Schema/msgpack.d.ts +1 -1
  29. package/dist/effect/Schema/msgpack.d.ts.map +1 -1
  30. package/dist/effect/ServiceContext.d.ts.map +1 -1
  31. package/dist/effect/Stream.d.ts.map +1 -1
  32. package/dist/effect/Subscribable.d.ts +75 -0
  33. package/dist/effect/Subscribable.d.ts.map +1 -0
  34. package/dist/effect/Subscribable.js +76 -0
  35. package/dist/effect/Subscribable.js.map +1 -0
  36. package/dist/effect/TaskTracing.d.ts.map +1 -1
  37. package/dist/effect/{WebChannel.d.ts → WebChannel/WebChannel.d.ts} +32 -14
  38. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -0
  39. package/dist/effect/WebChannel/WebChannel.js +283 -0
  40. package/dist/effect/WebChannel/WebChannel.js.map +1 -0
  41. package/dist/effect/WebChannel/WebChannel.test.d.ts +2 -0
  42. package/dist/effect/WebChannel/WebChannel.test.d.ts.map +1 -0
  43. package/dist/effect/WebChannel/WebChannel.test.js +62 -0
  44. package/dist/effect/WebChannel/WebChannel.test.js.map +1 -0
  45. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts +5 -6
  46. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts.map +1 -1
  47. package/dist/effect/WebChannel/broadcastChannelWithAck.js +12 -9
  48. package/dist/effect/WebChannel/broadcastChannelWithAck.js.map +1 -1
  49. package/dist/effect/WebChannel/common.d.ts +16 -1
  50. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  51. package/dist/effect/WebChannel/common.js +19 -1
  52. package/dist/effect/WebChannel/common.js.map +1 -1
  53. package/dist/effect/WebChannel/mod.d.ts +4 -0
  54. package/dist/effect/WebChannel/mod.d.ts.map +1 -0
  55. package/dist/effect/WebChannel/mod.js +4 -0
  56. package/dist/effect/WebChannel/mod.js.map +1 -0
  57. package/dist/effect/WebLock.d.ts.map +1 -1
  58. package/dist/effect/WebSocket.d.ts +3 -2
  59. package/dist/effect/WebSocket.d.ts.map +1 -1
  60. package/dist/effect/WebSocket.js +45 -19
  61. package/dist/effect/WebSocket.js.map +1 -1
  62. package/dist/effect/WebSocket.test.d.ts +2 -0
  63. package/dist/effect/WebSocket.test.d.ts.map +1 -0
  64. package/dist/effect/WebSocket.test.js +11 -0
  65. package/dist/effect/WebSocket.test.js.map +1 -0
  66. package/dist/effect/index.d.ts +5 -3
  67. package/dist/effect/index.d.ts.map +1 -1
  68. package/dist/effect/index.js +7 -3
  69. package/dist/effect/index.js.map +1 -1
  70. package/dist/env.d.ts +2 -0
  71. package/dist/env.d.ts.map +1 -1
  72. package/dist/env.js +8 -0
  73. package/dist/env.js.map +1 -1
  74. package/dist/fast-deep-equal.d.ts.map +1 -1
  75. package/dist/guards.d.ts.map +1 -1
  76. package/dist/index.d.ts +6 -4
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/misc.d.ts.map +1 -1
  80. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts +0 -1
  81. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -1
  82. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +21 -11
  83. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -1
  84. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.d.ts +2 -0
  85. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.d.ts.map +1 -0
  86. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js +39 -0
  87. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js.map +1 -0
  88. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +75 -0
  89. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts.map +1 -0
  90. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js +62 -0
  91. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js.map +1 -0
  92. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.d.ts +2 -0
  93. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.d.ts.map +1 -0
  94. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js +42 -0
  95. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js.map +1 -0
  96. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +1 -4
  97. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -1
  98. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +1 -4
  99. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
  100. package/dist/node/mod.d.ts +4 -3
  101. package/dist/node/mod.d.ts.map +1 -1
  102. package/dist/node/mod.js +37 -10
  103. package/dist/node/mod.js.map +1 -1
  104. package/dist/object/index.d.ts.map +1 -1
  105. package/dist/object/omit.d.ts.map +1 -1
  106. package/dist/object/pick.d.ts.map +1 -1
  107. package/dist/promise.d.ts.map +1 -1
  108. package/dist/set.d.ts.map +1 -1
  109. package/dist/string.d.ts.map +1 -1
  110. package/dist/time.d.ts.map +1 -1
  111. package/package.json +63 -40
  112. package/src/bun/mod.ts +12 -0
  113. package/src/effect/BucketQueue.ts +33 -6
  114. package/src/effect/Effect.ts +33 -20
  115. package/src/effect/Logger.ts +23 -1
  116. package/src/effect/Schema/index.ts +27 -0
  117. package/src/effect/Subscribable.ts +150 -0
  118. package/src/effect/WebChannel/WebChannel.test.ts +106 -0
  119. package/src/effect/WebChannel/WebChannel.ts +477 -0
  120. package/src/effect/WebChannel/broadcastChannelWithAck.ts +86 -83
  121. package/src/effect/WebChannel/common.ts +49 -2
  122. package/src/effect/WebChannel/mod.ts +3 -0
  123. package/src/effect/WebSocket.test.ts +15 -0
  124. package/src/effect/WebSocket.ts +75 -36
  125. package/src/effect/index.ts +16 -1
  126. package/src/env.ts +11 -0
  127. package/src/index.ts +6 -2
  128. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +40 -29
  129. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.ts +52 -0
  130. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/schema.ts +65 -0
  131. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.ts +53 -0
  132. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +3 -6
  133. package/src/node/mod.ts +49 -11
  134. package/tmp/pack.tgz +0 -0
  135. package/tsconfig.json +1 -1
  136. package/dist/effect/WebChannel.d.ts.map +0 -1
  137. package/dist/effect/WebChannel.js +0 -162
  138. package/dist/effect/WebChannel.js.map +0 -1
  139. package/dist/nanoid/index.browser.d.ts +0 -2
  140. package/dist/nanoid/index.browser.d.ts.map +0 -1
  141. package/dist/nanoid/index.browser.js +0 -3
  142. package/dist/nanoid/index.browser.js.map +0 -1
  143. package/src/effect/WebChannel.ts +0 -290
  144. package/src/nanoid/index.browser.ts +0 -2
@@ -0,0 +1,477 @@
1
+ import { Deferred, Either, Exit, GlobalValue, Option, PubSub, Queue, Scope } from 'effect'
2
+
3
+ import * as Effect from '../Effect.js'
4
+ import * as Schema from '../Schema/index.js'
5
+ import * as Stream from '../Stream.js'
6
+ import { DebugPingMessage, type InputSchema, type WebChannel, WebChannelSymbol } from './common.js'
7
+ import { listenToDebugPing, mapSchema } from './common.js'
8
+
9
+ export const shutdown = <MsgListen, MsgSend>(webChannel: WebChannel<MsgListen, MsgSend>): Effect.Effect<void> =>
10
+ Deferred.done(webChannel.closedDeferred, Exit.void)
11
+
12
+ export const noopChannel = <MsgListen, MsgSend>(): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
13
+ Effect.scopeWithCloseable((scope) =>
14
+ Effect.gen(function* () {
15
+ return {
16
+ [WebChannelSymbol]: WebChannelSymbol,
17
+ send: () => Effect.void,
18
+ listen: Stream.never,
19
+ closedDeferred: yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void))),
20
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
21
+ schema: {
22
+ listen: Schema.Any,
23
+ send: Schema.Any,
24
+ } as any,
25
+ supportsTransferables: false,
26
+ }
27
+ }).pipe(Effect.withSpan(`WebChannel:noopChannel`)),
28
+ )
29
+
30
+ /** Only works in browser environments */
31
+ export const broadcastChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
32
+ channelName,
33
+ schema: inputSchema,
34
+ }: {
35
+ channelName: string
36
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
37
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
38
+ Effect.scopeWithCloseable((scope) =>
39
+ Effect.gen(function* () {
40
+ const schema = mapSchema(inputSchema)
41
+
42
+ const channel = new BroadcastChannel(channelName)
43
+
44
+ yield* Effect.addFinalizer(() => Effect.try(() => channel.close()).pipe(Effect.ignoreLogged))
45
+
46
+ const send = (message: MsgSend) =>
47
+ Effect.gen(function* () {
48
+ const messageEncoded = yield* Schema.encode(schema.send)(message)
49
+ channel.postMessage(messageEncoded)
50
+ })
51
+
52
+ // TODO also listen to `messageerror` in parallel
53
+ const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
54
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
55
+ listenToDebugPing(channelName),
56
+ )
57
+
58
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
59
+ const supportsTransferables = false
60
+
61
+ return {
62
+ [WebChannelSymbol]: WebChannelSymbol,
63
+ send,
64
+ listen,
65
+ closedDeferred,
66
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
67
+ schema,
68
+ supportsTransferables,
69
+ }
70
+ }).pipe(Effect.withSpan(`WebChannel:broadcastChannel(${channelName})`)),
71
+ )
72
+
73
+ /**
74
+ * NOTE the `listenName` and `sendName` is needed for cases where both sides are using the same window
75
+ * e.g. for a browser extension, so we need a way to know for which side a message is intended for.
76
+ */
77
+ export const windowChannel2 = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
78
+ listenWindow,
79
+ sendWindow,
80
+ targetOrigin = '*',
81
+ ids,
82
+ schema: inputSchema,
83
+ }: {
84
+ listenWindow: Window
85
+ sendWindow: Window
86
+ targetOrigin?: string
87
+ ids: { own: string; other: string }
88
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
89
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
90
+ Effect.scopeWithCloseable((scope) =>
91
+ Effect.gen(function* () {
92
+ const schema = mapSchema(inputSchema)
93
+
94
+ const debugInfo = {
95
+ sendTotal: 0,
96
+ listenTotal: 0,
97
+ targetOrigin,
98
+ ids,
99
+ }
100
+
101
+ const WindowMessageListen = Schema.Struct({
102
+ message: schema.listen,
103
+ from: Schema.Literal(ids.other),
104
+ to: Schema.Literal(ids.own),
105
+ }).annotations({ title: 'webmesh.WindowMessageListen' })
106
+
107
+ const WindowMessageSend = Schema.Struct({
108
+ message: schema.send,
109
+ from: Schema.Literal(ids.own),
110
+ to: Schema.Literal(ids.other),
111
+ }).annotations({ title: 'webmesh.WindowMessageSend' })
112
+
113
+ const send = (message: MsgSend) =>
114
+ Effect.gen(function* () {
115
+ debugInfo.sendTotal++
116
+
117
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(WindowMessageSend)({
118
+ message,
119
+ from: ids.own,
120
+ to: ids.other,
121
+ })
122
+ sendWindow.postMessage(messageEncoded, targetOrigin, transferables)
123
+ })
124
+
125
+ const listen = Stream.fromEventListener<MessageEvent>(listenWindow, 'message').pipe(
126
+ // Stream.tap((_) => Effect.log(`${ids.other}→${ids.own}:message`, _.data)),
127
+ Stream.filter((_) => Schema.is(Schema.encodedSchema(WindowMessageListen))(_.data)),
128
+ Stream.map((_) => {
129
+ debugInfo.listenTotal++
130
+ return Schema.decodeEither(schema.listen)(_.data.message)
131
+ }),
132
+ listenToDebugPing('window'),
133
+ )
134
+
135
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
136
+ const supportsTransferables = true
137
+
138
+ return {
139
+ [WebChannelSymbol]: WebChannelSymbol,
140
+ send,
141
+ listen,
142
+ closedDeferred,
143
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
144
+ schema,
145
+ supportsTransferables,
146
+ debugInfo,
147
+ }
148
+ }).pipe(Effect.withSpan(`WebChannel:windowChannel`)),
149
+ )
150
+
151
+ export const windowChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
152
+ window,
153
+ targetOrigin = '*',
154
+ schema: inputSchema,
155
+ }: {
156
+ window: Window
157
+ targetOrigin?: string
158
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
159
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
160
+ Effect.scopeWithCloseable((scope) =>
161
+ Effect.gen(function* () {
162
+ const schema = mapSchema(inputSchema)
163
+
164
+ const send = (message: MsgSend) =>
165
+ Effect.gen(function* () {
166
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
167
+ window.postMessage(messageEncoded, targetOrigin, transferables)
168
+ })
169
+
170
+ const listen = Stream.fromEventListener<MessageEvent>(window, 'message').pipe(
171
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
172
+ listenToDebugPing('window'),
173
+ )
174
+
175
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
176
+ const supportsTransferables = true
177
+
178
+ return {
179
+ [WebChannelSymbol]: WebChannelSymbol,
180
+ send,
181
+ listen,
182
+ closedDeferred,
183
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
184
+ schema,
185
+ supportsTransferables,
186
+ }
187
+ }).pipe(Effect.withSpan(`WebChannel:windowChannel`)),
188
+ )
189
+
190
+ export const messagePortChannel: {
191
+ <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
192
+ port: MessagePort
193
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
194
+ debugId?: string | number
195
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
196
+ } = ({ port, schema: inputSchema, debugId }) =>
197
+ Effect.scopeWithCloseable((scope) =>
198
+ Effect.gen(function* () {
199
+ const schema = mapSchema(inputSchema)
200
+
201
+ const label = debugId === undefined ? 'messagePort' : `messagePort:${debugId}`
202
+
203
+ const send = (message: any) =>
204
+ Effect.gen(function* () {
205
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
206
+ port.postMessage(messageEncoded, transferables)
207
+ })
208
+
209
+ const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
210
+ // Stream.tap((_) => Effect.log(`${label}:message`, _.data)),
211
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
212
+ listenToDebugPing(label),
213
+ )
214
+
215
+ // NOTE unfortunately MessagePorts don't emit a `close` event when the other end is closed
216
+
217
+ port.start()
218
+
219
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
220
+ const supportsTransferables = true
221
+
222
+ yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
223
+
224
+ return {
225
+ [WebChannelSymbol]: WebChannelSymbol,
226
+ send,
227
+ listen,
228
+ closedDeferred,
229
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
230
+ schema,
231
+ supportsTransferables,
232
+ }
233
+ }).pipe(Effect.withSpan(`WebChannel:messagePortChannel`)),
234
+ )
235
+
236
+ const sameThreadChannels = GlobalValue.globalValue(
237
+ 'livestore:sameThreadChannels',
238
+ () => new Map<string, PubSub.PubSub<any>>(),
239
+ )
240
+
241
+ export const sameThreadChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
242
+ schema: inputSchema,
243
+ channelName,
244
+ }: {
245
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
246
+ channelName: string
247
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
248
+ Effect.scopeWithCloseable((scope) =>
249
+ Effect.gen(function* () {
250
+ let pubSub = sameThreadChannels.get(channelName)
251
+ if (pubSub === undefined) {
252
+ pubSub = yield* PubSub.unbounded<any>().pipe(Effect.acquireRelease(PubSub.shutdown))
253
+ sameThreadChannels.set(channelName, pubSub)
254
+ }
255
+
256
+ const schema = mapSchema(inputSchema)
257
+
258
+ const send = (message: MsgSend) =>
259
+ Effect.gen(function* () {
260
+ yield* PubSub.publish(pubSub, message)
261
+ })
262
+
263
+ const listen = Stream.fromPubSub(pubSub).pipe(Stream.map(Either.right), listenToDebugPing(channelName))
264
+
265
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
266
+
267
+ return {
268
+ [WebChannelSymbol]: WebChannelSymbol,
269
+ send,
270
+ listen,
271
+ closedDeferred,
272
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
273
+ schema,
274
+ supportsTransferables: false,
275
+ }
276
+ }),
277
+ )
278
+
279
+ export const messagePortChannelWithAck: {
280
+ <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
281
+ port: MessagePort
282
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
283
+ debugId?: string | number
284
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
285
+ } = ({ port, schema: inputSchema, debugId }) =>
286
+ Effect.scopeWithCloseable((scope) =>
287
+ Effect.gen(function* () {
288
+ const schema = mapSchema(inputSchema)
289
+
290
+ const label = debugId === undefined ? 'messagePort' : `messagePort:${debugId}`
291
+
292
+ type RequestId = string
293
+ const requestAckMap = new Map<RequestId, Deferred.Deferred<void>>()
294
+
295
+ const ChannelRequest = Schema.TaggedStruct('ChannelRequest', {
296
+ id: Schema.String,
297
+ payload: Schema.Union(schema.listen, schema.send),
298
+ }).annotations({ title: 'webmesh.ChannelRequest' })
299
+ const ChannelRequestAck = Schema.TaggedStruct('ChannelRequestAck', {
300
+ reqId: Schema.String,
301
+ }).annotations({ title: 'webmesh.ChannelRequestAck' })
302
+ const ChannelMessage = Schema.Union(ChannelRequest, ChannelRequestAck).annotations({
303
+ title: 'webmesh.ChannelMessage',
304
+ })
305
+ type ChannelMessage = typeof ChannelMessage.Type
306
+
307
+ const debugInfo = {
308
+ sendTotal: 0,
309
+ sendPending: 0,
310
+ listenTotal: 0,
311
+ id: debugId,
312
+ }
313
+
314
+ const send = (message: any) =>
315
+ Effect.gen(function* () {
316
+ debugInfo.sendTotal++
317
+ debugInfo.sendPending++
318
+
319
+ const id = crypto.randomUUID()
320
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(ChannelMessage)({
321
+ _tag: 'ChannelRequest',
322
+ id,
323
+ payload: message,
324
+ })
325
+
326
+ const ack = yield* Deferred.make<void>()
327
+ requestAckMap.set(id, ack)
328
+
329
+ port.postMessage(messageEncoded, transferables)
330
+
331
+ yield* ack
332
+
333
+ requestAckMap.delete(id)
334
+
335
+ debugInfo.sendPending--
336
+ })
337
+
338
+ // TODO re-implement this via `port.onmessage`
339
+ // https://github.com/livestorejs/livestore/issues/262
340
+ const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
341
+ // Stream.onStart(Effect.log(`${label}:listen:start`)),
342
+ // Stream.tap((_) => Effect.log(`${label}:message`, _.data)),
343
+ Stream.map((_) => Schema.decodeEither(ChannelMessage)(_.data)),
344
+ Stream.tap((msg) =>
345
+ Effect.gen(function* () {
346
+ if (msg._tag === 'Right') {
347
+ if (msg.right._tag === 'ChannelRequestAck') {
348
+ yield* Deferred.succeed(requestAckMap.get(msg.right.reqId)!, void 0)
349
+ } else if (msg.right._tag === 'ChannelRequest') {
350
+ debugInfo.listenTotal++
351
+ port.postMessage(Schema.encodeSync(ChannelMessage)({ _tag: 'ChannelRequestAck', reqId: msg.right.id }))
352
+ }
353
+ }
354
+ }),
355
+ ),
356
+ Stream.filterMap((msg) =>
357
+ msg._tag === 'Left'
358
+ ? Option.some(msg as any)
359
+ : msg.right._tag === 'ChannelRequest'
360
+ ? Option.some(Either.right(msg.right.payload))
361
+ : Option.none(),
362
+ ),
363
+ (_) => _ as Stream.Stream<Either.Either<any, any>>,
364
+ listenToDebugPing(label),
365
+ )
366
+
367
+ port.start()
368
+
369
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
370
+ const supportsTransferables = true
371
+
372
+ yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
373
+
374
+ return {
375
+ [WebChannelSymbol]: WebChannelSymbol,
376
+ send,
377
+ listen,
378
+ closedDeferred,
379
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
380
+ schema,
381
+ supportsTransferables,
382
+ debugInfo,
383
+ }
384
+ }).pipe(Effect.withSpan(`WebChannel:messagePortChannelWithAck`)),
385
+ )
386
+
387
+ export type QueueChannelProxy<MsgListen, MsgSend> = {
388
+ /** Only meant to be used externally */
389
+ webChannel: WebChannel<MsgListen, MsgSend>
390
+ /**
391
+ * Meant to be listened to (e.g. via `Stream.fromQueue`) for messages that have been sent
392
+ * via `webChannel.send()`.
393
+ */
394
+ sendQueue: Queue.Dequeue<MsgSend>
395
+ /**
396
+ * Meant to be pushed to (e.g. via `Queue.offer`) for messages that will be received
397
+ * via `webChannel.listen()`.
398
+ */
399
+ listenQueue: Queue.Enqueue<MsgListen>
400
+ }
401
+
402
+ /**
403
+ * From the outside the `sendQueue` is only accessible read-only,
404
+ * and the `listenQueue` is only accessible write-only.
405
+ */
406
+ export const queueChannelProxy = <MsgListen, MsgSend>({
407
+ schema: inputSchema,
408
+ }: {
409
+ schema:
410
+ | Schema.Schema<MsgListen | MsgSend, any>
411
+ | { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
412
+ }): Effect.Effect<QueueChannelProxy<MsgListen, MsgSend>, never, Scope.Scope> =>
413
+ Effect.scopeWithCloseable((scope) =>
414
+ Effect.gen(function* () {
415
+ const sendQueue = yield* Queue.unbounded<MsgSend>().pipe(Effect.acquireRelease(Queue.shutdown))
416
+ const listenQueue = yield* Queue.unbounded<MsgListen>().pipe(Effect.acquireRelease(Queue.shutdown))
417
+
418
+ const send = (message: MsgSend) => Queue.offer(sendQueue, message)
419
+
420
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right), listenToDebugPing('queueChannel'))
421
+
422
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
423
+ const supportsTransferables = true
424
+
425
+ const schema = mapSchema(inputSchema)
426
+
427
+ const webChannel = {
428
+ [WebChannelSymbol]: WebChannelSymbol,
429
+ send,
430
+ listen,
431
+ closedDeferred,
432
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
433
+ schema,
434
+ supportsTransferables,
435
+ }
436
+
437
+ return { webChannel, sendQueue, listenQueue }
438
+ }).pipe(Effect.withSpan(`WebChannel:queueChannelProxy`)),
439
+ )
440
+
441
+ /**
442
+ * Eagerly starts listening to a channel by buffering incoming messages in a queue.
443
+ */
444
+ export const toOpenChannel = (channel: WebChannel<any, any>): Effect.Effect<WebChannel<any, any>, never, Scope.Scope> =>
445
+ Effect.gen(function* () {
446
+ const queue = yield* Queue.unbounded<Either.Either<any, any>>().pipe(Effect.acquireRelease(Queue.shutdown))
447
+
448
+ yield* channel.listen.pipe(
449
+ Stream.tapChunk((chunk) => Queue.offerAll(queue, chunk)),
450
+ Stream.runDrain,
451
+ Effect.forkScoped,
452
+ )
453
+
454
+ // We're currently limiting the chunk size to 1 to not drop messages in scearnios where
455
+ // the listen stream get subscribed to, only take N messages and then unsubscribe.
456
+ // Without this limit, messages would be dropped.
457
+ const listen = Stream.fromQueue(queue, { maxChunkSize: 1 })
458
+
459
+ return {
460
+ [WebChannelSymbol]: WebChannelSymbol,
461
+ send: channel.send,
462
+ listen,
463
+ closedDeferred: channel.closedDeferred,
464
+ shutdown: channel.shutdown,
465
+ schema: channel.schema,
466
+ supportsTransferables: channel.supportsTransferables,
467
+ debugInfo: {
468
+ innerDebugInfo: channel.debugInfo,
469
+ listenQueueSize: queue,
470
+ },
471
+ }
472
+ })
473
+
474
+ export const sendDebugPing = (channel: WebChannel<any, any>) =>
475
+ Effect.gen(function* () {
476
+ yield* channel.send(DebugPingMessage.make({ message: 'ping' }))
477
+ })
@@ -1,8 +1,8 @@
1
- import type { Scope } from 'effect'
2
- import { Deferred, Effect, Predicate, Queue, Schema, Stream } from 'effect'
1
+ import { Deferred, Exit, Predicate, Queue, Schema, Scope, Stream } from 'effect'
3
2
 
4
- import type { WebChannel } from './common.js'
5
- import { WebChannelSymbol } from './common.js'
3
+ import * as Effect from '../Effect.js'
4
+ import type { InputSchema, WebChannel } from './common.js'
5
+ import { listenToDebugPing, mapSchema, WebChannelSymbol } from './common.js'
6
6
 
7
7
  const ConnectMessage = Schema.TaggedStruct('ConnectMessage', {
8
8
  from: Schema.String,
@@ -31,96 +31,99 @@ const Message = Schema.Union(ConnectMessage, ConnectAckMessage, DisconnectMessag
31
31
  */
32
32
  export const broadcastChannelWithAck = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
33
33
  channelName,
34
- listenSchema,
35
- sendSchema,
34
+ schema: inputSchema,
36
35
  }: {
37
36
  channelName: string
38
- listenSchema: Schema.Schema<MsgListen, MsgListenEncoded>
39
- sendSchema: Schema.Schema<MsgSend, MsgSendEncoded>
37
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
40
38
  }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
41
- Effect.gen(function* () {
42
- const channel = new BroadcastChannel(channelName)
43
- const messageQueue = yield* Queue.unbounded<MsgSend>()
44
- const connectionId = crypto.randomUUID()
39
+ Effect.scopeWithCloseable((scope) =>
40
+ Effect.gen(function* () {
41
+ const channel = new BroadcastChannel(channelName)
42
+ const messageQueue = yield* Queue.unbounded<MsgSend>()
43
+ const connectionId = crypto.randomUUID()
44
+ const schema = mapSchema(inputSchema)
45
45
 
46
- const peerIdRef = { current: undefined as undefined | string }
47
- const connectedLatch = yield* Effect.makeLatch(false)
48
- const supportsTransferables = false
46
+ const peerIdRef = { current: undefined as undefined | string }
47
+ const connectedLatch = yield* Effect.makeLatch(false)
48
+ const supportsTransferables = false
49
49
 
50
- const postMessage = (msg: typeof Message.Type) => channel.postMessage(Schema.encodeSync(Message)(msg))
50
+ const postMessage = (msg: typeof Message.Type) => channel.postMessage(Schema.encodeSync(Message)(msg))
51
51
 
52
- const send = (message: MsgSend) =>
53
- Effect.gen(function* () {
54
- yield* connectedLatch.await
55
-
56
- const payload = yield* Schema.encode(sendSchema)(message)
57
- postMessage(PayloadMessage.make({ from: connectionId, to: peerIdRef.current!, payload }))
58
- })
59
-
60
- const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
61
- Stream.map(({ data }) => data),
62
- Stream.map(Schema.decodeOption(Message)),
63
- Stream.filterMap((_) => _),
64
- Stream.mapEffect((data) =>
52
+ const send = (message: MsgSend) =>
65
53
  Effect.gen(function* () {
66
- switch (data._tag) {
67
- // Case: other side sends connect message (because otherside wasn't yet online when this side send their connect message)
68
- case 'ConnectMessage': {
69
- peerIdRef.current = data.from
70
- postMessage(ConnectAckMessage.make({ from: connectionId, to: data.from }))
71
- yield* connectedLatch.open
72
- break
73
- }
74
- // Case: other side sends connect-ack message (because otherside was already online when this side connected)
75
- case 'ConnectAckMessage': {
76
- if (data.to === connectionId) {
54
+ yield* connectedLatch.await
55
+
56
+ const payload = yield* Schema.encode(schema.send)(message)
57
+ postMessage(PayloadMessage.make({ from: connectionId, to: peerIdRef.current!, payload }))
58
+ })
59
+
60
+ const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
61
+ Stream.map(({ data }) => data),
62
+ Stream.map(Schema.decodeOption(Message)),
63
+ Stream.filterMap((_) => _),
64
+ Stream.mapEffect((data) =>
65
+ Effect.gen(function* () {
66
+ switch (data._tag) {
67
+ // Case: other side sends connect message (because otherside wasn't yet online when this side send their connect message)
68
+ case 'ConnectMessage': {
77
69
  peerIdRef.current = data.from
70
+ postMessage(ConnectAckMessage.make({ from: connectionId, to: data.from }))
78
71
  yield* connectedLatch.open
72
+ break
79
73
  }
80
- break
81
- }
82
- case 'DisconnectMessage': {
83
- if (data.from === peerIdRef.current) {
84
- peerIdRef.current = undefined
85
- yield* connectedLatch.close
86
- yield* establishConnection
74
+ // Case: other side sends connect-ack message (because otherside was already online when this side connected)
75
+ case 'ConnectAckMessage': {
76
+ if (data.to === connectionId) {
77
+ peerIdRef.current = data.from
78
+ yield* connectedLatch.open
79
+ }
80
+ break
87
81
  }
88
- break
89
- }
90
- case 'PayloadMessage': {
91
- if (data.to === connectionId) {
92
- return Schema.decodeEither(listenSchema)(data.payload)
82
+ case 'DisconnectMessage': {
83
+ if (data.from === peerIdRef.current) {
84
+ peerIdRef.current = undefined
85
+ yield* connectedLatch.close
86
+ yield* establishConnection
87
+ }
88
+ break
89
+ }
90
+ case 'PayloadMessage': {
91
+ if (data.to === connectionId) {
92
+ return Schema.decodeEither(schema.listen)(data.payload)
93
+ }
94
+ break
93
95
  }
94
- break
95
96
  }
96
- }
97
+ }),
98
+ ),
99
+ Stream.filter(Predicate.isNotUndefined),
100
+ listenToDebugPing(channelName),
101
+ )
102
+
103
+ const establishConnection = Effect.gen(function* () {
104
+ postMessage(ConnectMessage.make({ from: connectionId }))
105
+ })
106
+
107
+ yield* establishConnection
108
+
109
+ yield* Effect.addFinalizer(() =>
110
+ Effect.gen(function* () {
111
+ postMessage(DisconnectMessage.make({ from: connectionId }))
112
+ channel.close()
113
+ yield* Queue.shutdown(messageQueue)
97
114
  }),
98
- ),
99
- Stream.filter(Predicate.isNotUndefined),
100
- )
101
-
102
- const establishConnection = Effect.gen(function* () {
103
- postMessage(ConnectMessage.make({ from: connectionId }))
104
- })
105
-
106
- yield* establishConnection
107
-
108
- yield* Effect.addFinalizer(() =>
109
- Effect.gen(function* () {
110
- postMessage(DisconnectMessage.make({ from: connectionId }))
111
- channel.close()
112
- yield* Queue.shutdown(messageQueue)
113
- }),
114
- )
115
-
116
- const closedDeferred = yield* Deferred.make<void>()
117
-
118
- return {
119
- [WebChannelSymbol]: WebChannelSymbol,
120
- send,
121
- listen,
122
- closedDeferred,
123
- schema: { listen: listenSchema, send: sendSchema },
124
- supportsTransferables,
125
- }
126
- }).pipe(Effect.withSpan(`WebChannel:broadcastChannelWithAck(${channelName})`))
115
+ )
116
+
117
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
118
+
119
+ return {
120
+ [WebChannelSymbol]: WebChannelSymbol,
121
+ send,
122
+ listen,
123
+ closedDeferred,
124
+ shutdown: Scope.close(scope, Exit.void),
125
+ schema,
126
+ supportsTransferables,
127
+ }
128
+ }).pipe(Effect.withSpan(`WebChannel:broadcastChannelWithAck(${channelName})`)),
129
+ )