@livestore/utils 0.3.0-dev.5 → 0.3.0-dev.50

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