@livestore/utils 0.3.0-dev.4 → 0.3.0-dev.40

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 (145) 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 +76 -0
  33. package/dist/effect/Subscribable.d.ts.map +1 -0
  34. package/dist/effect/Subscribable.js +80 -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} +29 -16
  38. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -0
  39. package/dist/effect/WebChannel/WebChannel.js +264 -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 +8 -5
  69. package/dist/effect/index.js.map +1 -1
  70. package/dist/env.d.ts +4 -0
  71. package/dist/env.d.ts.map +1 -1
  72. package/dist/env.js +10 -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 -16
  101. package/dist/node/mod.d.ts.map +1 -1
  102. package/dist/node/mod.js +25 -69
  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 +59 -51
  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 +155 -0
  118. package/src/effect/WebChannel/WebChannel.test.ts +106 -0
  119. package/src/effect/WebChannel/WebChannel.ts +438 -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 +18 -2
  126. package/src/env.ts +15 -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 +32 -94
  134. package/dist/effect/WebChannel.d.ts.map +0 -1
  135. package/dist/effect/WebChannel.js +0 -162
  136. package/dist/effect/WebChannel.js.map +0 -1
  137. package/dist/nanoid/index.browser.d.ts +0 -2
  138. package/dist/nanoid/index.browser.d.ts.map +0 -1
  139. package/dist/nanoid/index.browser.js +0 -3
  140. package/dist/nanoid/index.browser.js.map +0 -1
  141. package/src/effect/WebChannel.ts +0 -290
  142. package/src/nanoid/index.browser.ts +0 -2
  143. package/tmp/effect-deferred-repro.ts +0 -29
  144. package/tmp/effect-semaphore-repro.ts +0 -93
  145. package/tsconfig.json +0 -10
@@ -0,0 +1,438 @@
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 windowChannel = <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 messagePortChannel: {
152
+ <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
153
+ port: MessagePort
154
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
155
+ debugId?: string | number
156
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
157
+ } = ({ port, schema: inputSchema, debugId }) =>
158
+ Effect.scopeWithCloseable((scope) =>
159
+ Effect.gen(function* () {
160
+ const schema = mapSchema(inputSchema)
161
+
162
+ const label = debugId === undefined ? 'messagePort' : `messagePort:${debugId}`
163
+
164
+ const send = (message: any) =>
165
+ Effect.gen(function* () {
166
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
167
+ port.postMessage(messageEncoded, transferables)
168
+ })
169
+
170
+ const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
171
+ // Stream.tap((_) => Effect.log(`${label}:message`, _.data)),
172
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
173
+ listenToDebugPing(label),
174
+ )
175
+
176
+ // NOTE unfortunately MessagePorts don't emit a `close` event when the other end is closed
177
+
178
+ port.start()
179
+
180
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
181
+ const supportsTransferables = true
182
+
183
+ yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
184
+
185
+ return {
186
+ [WebChannelSymbol]: WebChannelSymbol,
187
+ send,
188
+ listen,
189
+ closedDeferred,
190
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
191
+ schema,
192
+ supportsTransferables,
193
+ }
194
+ }).pipe(Effect.withSpan(`WebChannel:messagePortChannel`)),
195
+ )
196
+
197
+ const sameThreadChannels = GlobalValue.globalValue(
198
+ 'livestore:sameThreadChannels',
199
+ () => new Map<string, PubSub.PubSub<any>>(),
200
+ )
201
+
202
+ export const sameThreadChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
203
+ schema: inputSchema,
204
+ channelName,
205
+ }: {
206
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
207
+ channelName: string
208
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
209
+ Effect.scopeWithCloseable((scope) =>
210
+ Effect.gen(function* () {
211
+ let pubSub = sameThreadChannels.get(channelName)
212
+ if (pubSub === undefined) {
213
+ pubSub = yield* PubSub.unbounded<any>().pipe(Effect.acquireRelease(PubSub.shutdown))
214
+ sameThreadChannels.set(channelName, pubSub)
215
+ }
216
+
217
+ const schema = mapSchema(inputSchema)
218
+
219
+ const send = (message: MsgSend) =>
220
+ Effect.gen(function* () {
221
+ yield* PubSub.publish(pubSub, message)
222
+ })
223
+
224
+ const listen = Stream.fromPubSub(pubSub).pipe(Stream.map(Either.right), listenToDebugPing(channelName))
225
+
226
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
227
+
228
+ return {
229
+ [WebChannelSymbol]: WebChannelSymbol,
230
+ send,
231
+ listen,
232
+ closedDeferred,
233
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
234
+ schema,
235
+ supportsTransferables: false,
236
+ }
237
+ }),
238
+ )
239
+
240
+ export const messagePortChannelWithAck: {
241
+ <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
242
+ port: MessagePort
243
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
244
+ debugId?: string | number
245
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
246
+ } = ({ port, schema: inputSchema, debugId }) =>
247
+ Effect.scopeWithCloseable((scope) =>
248
+ Effect.gen(function* () {
249
+ const schema = mapSchema(inputSchema)
250
+
251
+ const label = debugId === undefined ? 'messagePort' : `messagePort:${debugId}`
252
+
253
+ type RequestId = string
254
+ const requestAckMap = new Map<RequestId, Deferred.Deferred<void>>()
255
+
256
+ const ChannelRequest = Schema.TaggedStruct('ChannelRequest', {
257
+ id: Schema.String,
258
+ payload: Schema.Union(schema.listen, schema.send),
259
+ }).annotations({ title: 'webmesh.ChannelRequest' })
260
+ const ChannelRequestAck = Schema.TaggedStruct('ChannelRequestAck', {
261
+ reqId: Schema.String,
262
+ }).annotations({ title: 'webmesh.ChannelRequestAck' })
263
+ const ChannelMessage = Schema.Union(ChannelRequest, ChannelRequestAck).annotations({
264
+ title: 'webmesh.ChannelMessage',
265
+ })
266
+ type ChannelMessage = typeof ChannelMessage.Type
267
+
268
+ const debugInfo = {
269
+ sendTotal: 0,
270
+ sendPending: 0,
271
+ listenTotal: 0,
272
+ id: debugId,
273
+ }
274
+
275
+ const send = (message: any) =>
276
+ Effect.gen(function* () {
277
+ debugInfo.sendTotal++
278
+ debugInfo.sendPending++
279
+
280
+ const id = crypto.randomUUID()
281
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(ChannelMessage)({
282
+ _tag: 'ChannelRequest',
283
+ id,
284
+ payload: message,
285
+ })
286
+
287
+ const ack = yield* Deferred.make<void>()
288
+ requestAckMap.set(id, ack)
289
+
290
+ port.postMessage(messageEncoded, transferables)
291
+
292
+ yield* ack
293
+
294
+ requestAckMap.delete(id)
295
+
296
+ debugInfo.sendPending--
297
+ })
298
+
299
+ // TODO re-implement this via `port.onmessage`
300
+ // https://github.com/livestorejs/livestore/issues/262
301
+ const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
302
+ // Stream.onStart(Effect.log(`${label}:listen:start`)),
303
+ // Stream.tap((_) => Effect.log(`${label}:message`, _.data)),
304
+ Stream.map((_) => Schema.decodeEither(ChannelMessage)(_.data)),
305
+ Stream.tap((msg) =>
306
+ Effect.gen(function* () {
307
+ if (msg._tag === 'Right') {
308
+ if (msg.right._tag === 'ChannelRequestAck') {
309
+ yield* Deferred.succeed(requestAckMap.get(msg.right.reqId)!, void 0)
310
+ } else if (msg.right._tag === 'ChannelRequest') {
311
+ debugInfo.listenTotal++
312
+ port.postMessage(Schema.encodeSync(ChannelMessage)({ _tag: 'ChannelRequestAck', reqId: msg.right.id }))
313
+ }
314
+ }
315
+ }),
316
+ ),
317
+ Stream.filterMap((msg) =>
318
+ msg._tag === 'Left'
319
+ ? Option.some(msg as any)
320
+ : msg.right._tag === 'ChannelRequest'
321
+ ? Option.some(Either.right(msg.right.payload))
322
+ : Option.none(),
323
+ ),
324
+ (_) => _ as Stream.Stream<Either.Either<any, any>>,
325
+ listenToDebugPing(label),
326
+ )
327
+
328
+ port.start()
329
+
330
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
331
+ const supportsTransferables = true
332
+
333
+ yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
334
+
335
+ return {
336
+ [WebChannelSymbol]: WebChannelSymbol,
337
+ send,
338
+ listen,
339
+ closedDeferred,
340
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
341
+ schema,
342
+ supportsTransferables,
343
+ debugInfo,
344
+ }
345
+ }).pipe(Effect.withSpan(`WebChannel:messagePortChannelWithAck`)),
346
+ )
347
+
348
+ export type QueueChannelProxy<MsgListen, MsgSend> = {
349
+ /** Only meant to be used externally */
350
+ webChannel: WebChannel<MsgListen, MsgSend>
351
+ /**
352
+ * Meant to be listened to (e.g. via `Stream.fromQueue`) for messages that have been sent
353
+ * via `webChannel.send()`.
354
+ */
355
+ sendQueue: Queue.Dequeue<MsgSend>
356
+ /**
357
+ * Meant to be pushed to (e.g. via `Queue.offer`) for messages that will be received
358
+ * via `webChannel.listen()`.
359
+ */
360
+ listenQueue: Queue.Enqueue<MsgListen>
361
+ }
362
+
363
+ /**
364
+ * From the outside the `sendQueue` is only accessible read-only,
365
+ * and the `listenQueue` is only accessible write-only.
366
+ */
367
+ export const queueChannelProxy = <MsgListen, MsgSend>({
368
+ schema: inputSchema,
369
+ }: {
370
+ schema:
371
+ | Schema.Schema<MsgListen | MsgSend, any>
372
+ | { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
373
+ }): Effect.Effect<QueueChannelProxy<MsgListen, MsgSend>, never, Scope.Scope> =>
374
+ Effect.scopeWithCloseable((scope) =>
375
+ Effect.gen(function* () {
376
+ const sendQueue = yield* Queue.unbounded<MsgSend>().pipe(Effect.acquireRelease(Queue.shutdown))
377
+ const listenQueue = yield* Queue.unbounded<MsgListen>().pipe(Effect.acquireRelease(Queue.shutdown))
378
+
379
+ const send = (message: MsgSend) => Queue.offer(sendQueue, message)
380
+
381
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right), listenToDebugPing('queueChannel'))
382
+
383
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
384
+ const supportsTransferables = true
385
+
386
+ const schema = mapSchema(inputSchema)
387
+
388
+ const webChannel = {
389
+ [WebChannelSymbol]: WebChannelSymbol,
390
+ send,
391
+ listen,
392
+ closedDeferred,
393
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
394
+ schema,
395
+ supportsTransferables,
396
+ }
397
+
398
+ return { webChannel, sendQueue, listenQueue }
399
+ }).pipe(Effect.withSpan(`WebChannel:queueChannelProxy`)),
400
+ )
401
+
402
+ /**
403
+ * Eagerly starts listening to a channel by buffering incoming messages in a queue.
404
+ */
405
+ export const toOpenChannel = (channel: WebChannel<any, any>): Effect.Effect<WebChannel<any, any>, never, Scope.Scope> =>
406
+ Effect.gen(function* () {
407
+ const queue = yield* Queue.unbounded<Either.Either<any, any>>().pipe(Effect.acquireRelease(Queue.shutdown))
408
+
409
+ yield* channel.listen.pipe(
410
+ Stream.tapChunk((chunk) => Queue.offerAll(queue, chunk)),
411
+ Stream.runDrain,
412
+ Effect.forkScoped,
413
+ )
414
+
415
+ // We're currently limiting the chunk size to 1 to not drop messages in scearnios where
416
+ // the listen stream get subscribed to, only take N messages and then unsubscribe.
417
+ // Without this limit, messages would be dropped.
418
+ const listen = Stream.fromQueue(queue, { maxChunkSize: 1 })
419
+
420
+ return {
421
+ [WebChannelSymbol]: WebChannelSymbol,
422
+ send: channel.send,
423
+ listen,
424
+ closedDeferred: channel.closedDeferred,
425
+ shutdown: channel.shutdown,
426
+ schema: channel.schema,
427
+ supportsTransferables: channel.supportsTransferables,
428
+ debugInfo: {
429
+ innerDebugInfo: channel.debugInfo,
430
+ listenQueueSize: queue,
431
+ },
432
+ }
433
+ })
434
+
435
+ export const sendDebugPing = (channel: WebChannel<any, any>) =>
436
+ Effect.gen(function* () {
437
+ yield* channel.send(DebugPingMessage.make({ message: 'ping' }))
438
+ })
@@ -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
+ )
@@ -1,5 +1,5 @@
1
- import type { Deferred, Effect, Either, ParseResult, Schema, Stream } from 'effect'
2
- import { Predicate } from 'effect'
1
+ import type { Deferred, Either, ParseResult } from 'effect'
2
+ import { Effect, Predicate, Schema, Stream } from 'effect'
3
3
 
4
4
  export const WebChannelSymbol = Symbol('WebChannel')
5
5
  export type WebChannelSymbol = typeof WebChannelSymbol
@@ -13,5 +13,52 @@ export interface WebChannel<MsgListen, MsgSend, E = never> {
13
13
  listen: Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, E>
14
14
  supportsTransferables: boolean
15
15
  closedDeferred: Deferred.Deferred<void>
16
+ shutdown: Effect.Effect<void>
16
17
  schema: { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
18
+ debugInfo?: Record<string, any>
17
19
  }
20
+
21
+ export const DebugPingMessage = Schema.TaggedStruct('WebChannel.DebugPing', {
22
+ message: Schema.String,
23
+ payload: Schema.optional(Schema.String),
24
+ })
25
+
26
+ export const schemaWithDebugPing = <MsgListen, MsgSend>(
27
+ schema: OutputSchema<MsgListen, MsgSend, any, any>,
28
+ ): OutputSchema<MsgListen | typeof DebugPingMessage.Type, MsgSend | typeof DebugPingMessage.Type, any, any> => ({
29
+ send: Schema.Union(schema.send, DebugPingMessage),
30
+ listen: Schema.Union(schema.listen, DebugPingMessage),
31
+ })
32
+
33
+ export type InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded> =
34
+ | Schema.Schema<MsgListen | MsgSend, MsgListenEncoded | MsgSendEncoded>
35
+ | OutputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
36
+
37
+ export type OutputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded> = {
38
+ listen: Schema.Schema<MsgListen, MsgListenEncoded>
39
+ send: Schema.Schema<MsgSend, MsgSendEncoded>
40
+ }
41
+
42
+ export const mapSchema = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(
43
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>,
44
+ ): OutputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded> =>
45
+ Predicate.hasProperty(schema, 'send') && Predicate.hasProperty(schema, 'listen')
46
+ ? (schemaWithDebugPing(schema) as any)
47
+ : (schemaWithDebugPing({ send: schema, listen: schema }) as any)
48
+
49
+ export const listenToDebugPing =
50
+ (channelName: string) =>
51
+ <MsgListen>(
52
+ stream: Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, never>,
53
+ ): Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, never> =>
54
+ stream.pipe(
55
+ Stream.filterEffect(
56
+ Effect.fn(function* (msg) {
57
+ if (msg._tag === 'Right' && Schema.is(DebugPingMessage)(msg.right)) {
58
+ yield* Effect.logDebug(`WebChannel:ping [${channelName}] ${msg.right.message}`, msg.right.payload)
59
+ return false
60
+ }
61
+ return true
62
+ }),
63
+ ),
64
+ )