@livestore/utils 0.3.0-dev.11 → 0.3.0-dev.12

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 (37) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/effect/Effect.d.ts +4 -2
  3. package/dist/effect/Effect.d.ts.map +1 -1
  4. package/dist/effect/Effect.js +9 -1
  5. package/dist/effect/Effect.js.map +1 -1
  6. package/dist/effect/WebChannel/WebChannel.d.ts +61 -0
  7. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -0
  8. package/dist/effect/WebChannel/WebChannel.js +209 -0
  9. package/dist/effect/WebChannel/WebChannel.js.map +1 -0
  10. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts +5 -6
  11. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts.map +1 -1
  12. package/dist/effect/WebChannel/broadcastChannelWithAck.js +12 -8
  13. package/dist/effect/WebChannel/broadcastChannelWithAck.js.map +1 -1
  14. package/dist/effect/WebChannel/common.d.ts +16 -1
  15. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  16. package/dist/effect/WebChannel/common.js +30 -1
  17. package/dist/effect/WebChannel/common.js.map +1 -1
  18. package/dist/effect/WebChannel.d.ts +5 -12
  19. package/dist/effect/WebChannel.d.ts.map +1 -1
  20. package/dist/effect/WebChannel.js +16 -31
  21. package/dist/effect/WebChannel.js.map +1 -1
  22. package/dist/effect/index.d.ts +2 -2
  23. package/dist/effect/index.d.ts.map +1 -1
  24. package/dist/effect/index.js +2 -2
  25. package/dist/effect/index.js.map +1 -1
  26. package/dist/env.d.ts +1 -0
  27. package/dist/env.d.ts.map +1 -1
  28. package/dist/env.js +2 -0
  29. package/dist/env.js.map +1 -1
  30. package/package.json +4 -3
  31. package/src/effect/Effect.ts +14 -2
  32. package/src/effect/WebChannel/WebChannel.ts +357 -0
  33. package/src/effect/WebChannel/broadcastChannelWithAck.ts +86 -82
  34. package/src/effect/WebChannel/common.ts +56 -2
  35. package/src/effect/index.ts +4 -1
  36. package/src/env.ts +4 -0
  37. package/src/effect/WebChannel.ts +0 -305
@@ -0,0 +1,357 @@
1
+ import { Deferred, Either, Exit, Option, 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 * from './broadcastChannelWithAck.js'
10
+
11
+ export * from './common.js'
12
+
13
+ export const shutdown = <MsgListen, MsgSend>(webChannel: WebChannel<MsgListen, MsgSend>): Effect.Effect<void> =>
14
+ Deferred.done(webChannel.closedDeferred, Exit.void)
15
+
16
+ export const noopChannel = <MsgListen, MsgSend>(): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
17
+ Effect.scopeWithCloseable((scope) =>
18
+ Effect.gen(function* () {
19
+ return {
20
+ [WebChannelSymbol]: WebChannelSymbol,
21
+ send: () => Effect.void,
22
+ listen: Stream.never,
23
+ closedDeferred: yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void))),
24
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
25
+ schema: {
26
+ listen: Schema.Any,
27
+ send: Schema.Any,
28
+ } as any,
29
+ supportsTransferables: false,
30
+ }
31
+ }).pipe(Effect.withSpan(`WebChannel:noopChannel`)),
32
+ )
33
+
34
+ export const broadcastChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
35
+ channelName,
36
+ schema: inputSchema,
37
+ }: {
38
+ channelName: string
39
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
40
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
41
+ Effect.scopeWithCloseable((scope) =>
42
+ Effect.gen(function* () {
43
+ const schema = mapSchema(inputSchema)
44
+
45
+ const channel = new BroadcastChannel(channelName)
46
+
47
+ yield* Effect.addFinalizer(() => Effect.try(() => channel.close()).pipe(Effect.ignoreLogged))
48
+
49
+ const send = (message: MsgSend) =>
50
+ Effect.gen(function* () {
51
+ const messageEncoded = yield* Schema.encode(schema.send)(message)
52
+ channel.postMessage(messageEncoded)
53
+ })
54
+
55
+ // TODO also listen to `messageerror` in parallel
56
+ const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
57
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
58
+ listenToDebugPing(channelName),
59
+ )
60
+
61
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
62
+ const supportsTransferables = false
63
+
64
+ return {
65
+ [WebChannelSymbol]: WebChannelSymbol,
66
+ send,
67
+ listen,
68
+ closedDeferred,
69
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
70
+ schema,
71
+ supportsTransferables,
72
+ }
73
+ }).pipe(Effect.withSpan(`WebChannel:broadcastChannel(${channelName})`)),
74
+ )
75
+
76
+ export const windowChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
77
+ window,
78
+ targetOrigin = '*',
79
+ schema: inputSchema,
80
+ }: {
81
+ window: Window
82
+ targetOrigin?: string
83
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
84
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
85
+ Effect.scopeWithCloseable((scope) =>
86
+ Effect.gen(function* () {
87
+ const schema = mapSchema(inputSchema)
88
+
89
+ const send = (message: MsgSend) =>
90
+ Effect.gen(function* () {
91
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
92
+ window.postMessage(messageEncoded, targetOrigin, transferables)
93
+ })
94
+
95
+ const listen = Stream.fromEventListener<MessageEvent>(window, 'message').pipe(
96
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
97
+ listenToDebugPing('window'),
98
+ )
99
+
100
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
101
+ const supportsTransferables = true
102
+
103
+ return {
104
+ [WebChannelSymbol]: WebChannelSymbol,
105
+ send,
106
+ listen,
107
+ closedDeferred,
108
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
109
+ schema,
110
+ supportsTransferables,
111
+ }
112
+ }).pipe(Effect.withSpan(`WebChannel:windowChannel`)),
113
+ )
114
+
115
+ export const messagePortChannel: {
116
+ <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
117
+ port: MessagePort
118
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
119
+ debugId?: string | number
120
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
121
+ } = ({ port, schema: inputSchema, debugId }) =>
122
+ Effect.scopeWithCloseable((scope) =>
123
+ Effect.gen(function* () {
124
+ const schema = mapSchema(inputSchema)
125
+
126
+ const label = debugId === undefined ? 'messagePort' : `messagePort:${debugId}`
127
+
128
+ const send = (message: any) =>
129
+ Effect.gen(function* () {
130
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
131
+ port.postMessage(messageEncoded, transferables)
132
+ })
133
+
134
+ const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
135
+ // Stream.tap((_) => Effect.log(`${label}:message`, _.data)),
136
+ Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
137
+ listenToDebugPing(label),
138
+ )
139
+
140
+ // NOTE unfortunately MessagePorts don't emit a `close` event when the other end is closed
141
+
142
+ port.start()
143
+
144
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
145
+ const supportsTransferables = true
146
+
147
+ yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
148
+
149
+ return {
150
+ [WebChannelSymbol]: WebChannelSymbol,
151
+ send,
152
+ listen,
153
+ closedDeferred,
154
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
155
+ schema,
156
+ supportsTransferables,
157
+ }
158
+ }).pipe(Effect.withSpan(`WebChannel:messagePortChannel`)),
159
+ )
160
+
161
+ export const messagePortChannelWithAck: {
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
+ type RequestId = string
175
+ const requestAckMap = new Map<RequestId, Deferred.Deferred<void>>()
176
+
177
+ const ChannelRequest = Schema.TaggedStruct('ChannelRequest', {
178
+ id: Schema.String,
179
+ payload: Schema.Union(schema.listen, schema.send),
180
+ })
181
+ const ChannelRequestAck = Schema.TaggedStruct('ChannelRequestAck', {
182
+ reqId: Schema.String,
183
+ })
184
+ const ChannelMessage = Schema.Union(ChannelRequest, ChannelRequestAck)
185
+ type ChannelMessage = typeof ChannelMessage.Type
186
+
187
+ const debugInfo = {
188
+ sendTotal: 0,
189
+ sendPending: 0,
190
+ listenTotal: 0,
191
+ id: debugId,
192
+ }
193
+
194
+ const send = (message: any) =>
195
+ Effect.gen(function* () {
196
+ debugInfo.sendTotal++
197
+ debugInfo.sendPending++
198
+
199
+ const id = crypto.randomUUID()
200
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(ChannelMessage)({
201
+ _tag: 'ChannelRequest',
202
+ id,
203
+ payload: message,
204
+ })
205
+
206
+ const ack = yield* Deferred.make<void>()
207
+ requestAckMap.set(id, ack)
208
+
209
+ port.postMessage(messageEncoded, transferables)
210
+
211
+ yield* ack
212
+
213
+ requestAckMap.delete(id)
214
+
215
+ debugInfo.sendPending--
216
+ })
217
+
218
+ // TODO re-implement this via `port.onmessage`
219
+ // https://github.com/livestorejs/livestore/issues/262
220
+ const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
221
+ // Stream.onStart(Effect.log(`${label}:listen:start`)),
222
+ // Stream.tap((_) => Effect.log(`${label}:message`, _.data)),
223
+ Stream.map((_) => Schema.decodeEither(ChannelMessage)(_.data)),
224
+ Stream.tap((msg) =>
225
+ Effect.gen(function* () {
226
+ if (msg._tag === 'Right') {
227
+ if (msg.right._tag === 'ChannelRequestAck') {
228
+ yield* Deferred.succeed(requestAckMap.get(msg.right.reqId)!, void 0)
229
+ } else if (msg.right._tag === 'ChannelRequest') {
230
+ debugInfo.listenTotal++
231
+ port.postMessage(Schema.encodeSync(ChannelMessage)({ _tag: 'ChannelRequestAck', reqId: msg.right.id }))
232
+ }
233
+ }
234
+ }),
235
+ ),
236
+ Stream.filterMap((msg) =>
237
+ msg._tag === 'Left'
238
+ ? Option.some(msg as any)
239
+ : msg.right._tag === 'ChannelRequest'
240
+ ? Option.some(Either.right(msg.right.payload))
241
+ : Option.none(),
242
+ ),
243
+ (_) => _ as Stream.Stream<Either.Either<any, any>>,
244
+ listenToDebugPing(label),
245
+ )
246
+
247
+ port.start()
248
+
249
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
250
+ const supportsTransferables = true
251
+
252
+ yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
253
+
254
+ return {
255
+ [WebChannelSymbol]: WebChannelSymbol,
256
+ send,
257
+ listen,
258
+ closedDeferred,
259
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
260
+ schema,
261
+ supportsTransferables,
262
+ debugInfo,
263
+ }
264
+ }).pipe(Effect.withSpan(`WebChannel:messagePortChannelWithAck`)),
265
+ )
266
+
267
+ export type QueueChannelProxy<MsgListen, MsgSend> = {
268
+ /** Only meant to be used externally */
269
+ webChannel: WebChannel<MsgListen, MsgSend>
270
+ /**
271
+ * Meant to be listened to (e.g. via `Stream.fromQueue`) for messages that have been sent
272
+ * via `webChannel.send()`.
273
+ */
274
+ sendQueue: Queue.Dequeue<MsgSend>
275
+ /**
276
+ * Meant to be pushed to (e.g. via `Queue.offer`) for messages that will be received
277
+ * via `webChannel.listen()`.
278
+ */
279
+ listenQueue: Queue.Enqueue<MsgListen>
280
+ }
281
+
282
+ /**
283
+ * From the outside the `sendQueue` is only accessible read-only,
284
+ * and the `listenQueue` is only accessible write-only.
285
+ */
286
+ export const queueChannelProxy = <MsgListen, MsgSend>({
287
+ schema: inputSchema,
288
+ }: {
289
+ schema:
290
+ | Schema.Schema<MsgListen | MsgSend, any>
291
+ | { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
292
+ }): Effect.Effect<QueueChannelProxy<MsgListen, MsgSend>, never, Scope.Scope> =>
293
+ Effect.scopeWithCloseable((scope) =>
294
+ Effect.gen(function* () {
295
+ const sendQueue = yield* Queue.unbounded<MsgSend>().pipe(Effect.acquireRelease(Queue.shutdown))
296
+ const listenQueue = yield* Queue.unbounded<MsgListen>().pipe(Effect.acquireRelease(Queue.shutdown))
297
+
298
+ const send = (message: MsgSend) => Queue.offer(sendQueue, message)
299
+
300
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right), listenToDebugPing('queueChannel'))
301
+
302
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
303
+ const supportsTransferables = true
304
+
305
+ const schema = mapSchema(inputSchema)
306
+
307
+ const webChannel = {
308
+ [WebChannelSymbol]: WebChannelSymbol,
309
+ send,
310
+ listen,
311
+ closedDeferred,
312
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
313
+ schema,
314
+ supportsTransferables,
315
+ }
316
+
317
+ return { webChannel, sendQueue, listenQueue }
318
+ }).pipe(Effect.withSpan(`WebChannel:queueChannelProxy`)),
319
+ )
320
+
321
+ /**
322
+ * Eagerly starts listening to a channel by buffering incoming messages in a queue.
323
+ */
324
+ export const toOpenChannel = (channel: WebChannel<any, any>): Effect.Effect<WebChannel<any, any>, never, Scope.Scope> =>
325
+ Effect.gen(function* () {
326
+ const queue = yield* Queue.unbounded<Either.Either<any, any>>().pipe(Effect.acquireRelease(Queue.shutdown))
327
+
328
+ yield* channel.listen.pipe(
329
+ Stream.tapChunk((chunk) => Queue.offerAll(queue, chunk)),
330
+ Stream.runDrain,
331
+ Effect.forkScoped,
332
+ )
333
+
334
+ // We're currently limiting the chunk size to 1 to not drop messages in scearnios where
335
+ // the listen stream get subscribed to, only take N messages and then unsubscribe.
336
+ // Without this limit, messages would be dropped.
337
+ const listen = Stream.fromQueue(queue, { maxChunkSize: 1 })
338
+
339
+ return {
340
+ [WebChannelSymbol]: WebChannelSymbol,
341
+ send: channel.send,
342
+ listen,
343
+ closedDeferred: channel.closedDeferred,
344
+ shutdown: channel.shutdown,
345
+ schema: channel.schema,
346
+ supportsTransferables: channel.supportsTransferables,
347
+ debugInfo: {
348
+ innerDebugInfo: channel.debugInfo,
349
+ listenQueueSize: queue,
350
+ },
351
+ }
352
+ })
353
+
354
+ export const sendDebugPing = (channel: WebChannel<any, any>) =>
355
+ Effect.gen(function* () {
356
+ yield* channel.send(DebugPingMessage.make({ message: 'ping' }))
357
+ })
@@ -1,8 +1,9 @@
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'
3
+ import * as Effect from '../Effect.js'
4
+ import type { InputSchema, WebChannel } from './common.js'
5
5
  import { WebChannelSymbol } from './common.js'
6
+ import { listenToDebugPing, mapSchema } from './WebChannel.js'
6
7
 
7
8
  const ConnectMessage = Schema.TaggedStruct('ConnectMessage', {
8
9
  from: Schema.String,
@@ -31,96 +32,99 @@ const Message = Schema.Union(ConnectMessage, ConnectAckMessage, DisconnectMessag
31
32
  */
32
33
  export const broadcastChannelWithAck = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
33
34
  channelName,
34
- listenSchema,
35
- sendSchema,
35
+ schema: inputSchema,
36
36
  }: {
37
37
  channelName: string
38
- listenSchema: Schema.Schema<MsgListen, MsgListenEncoded>
39
- sendSchema: Schema.Schema<MsgSend, MsgSendEncoded>
38
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
40
39
  }): 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()
40
+ Effect.scopeWithCloseable((scope) =>
41
+ Effect.gen(function* () {
42
+ const channel = new BroadcastChannel(channelName)
43
+ const messageQueue = yield* Queue.unbounded<MsgSend>()
44
+ const connectionId = crypto.randomUUID()
45
+ const schema = mapSchema(inputSchema)
45
46
 
46
- const peerIdRef = { current: undefined as undefined | string }
47
- const connectedLatch = yield* Effect.makeLatch(false)
48
- const supportsTransferables = false
47
+ const peerIdRef = { current: undefined as undefined | string }
48
+ const connectedLatch = yield* Effect.makeLatch(false)
49
+ const supportsTransferables = false
49
50
 
50
- const postMessage = (msg: typeof Message.Type) => channel.postMessage(Schema.encodeSync(Message)(msg))
51
+ const postMessage = (msg: typeof Message.Type) => channel.postMessage(Schema.encodeSync(Message)(msg))
51
52
 
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) =>
53
+ const send = (message: MsgSend) =>
65
54
  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) {
55
+ yield* connectedLatch.await
56
+
57
+ const payload = yield* Schema.encode(schema.send)(message)
58
+ postMessage(PayloadMessage.make({ from: connectionId, to: peerIdRef.current!, payload }))
59
+ })
60
+
61
+ const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
62
+ Stream.map(({ data }) => data),
63
+ Stream.map(Schema.decodeOption(Message)),
64
+ Stream.filterMap((_) => _),
65
+ Stream.mapEffect((data) =>
66
+ Effect.gen(function* () {
67
+ switch (data._tag) {
68
+ // Case: other side sends connect message (because otherside wasn't yet online when this side send their connect message)
69
+ case 'ConnectMessage': {
77
70
  peerIdRef.current = data.from
71
+ postMessage(ConnectAckMessage.make({ from: connectionId, to: data.from }))
78
72
  yield* connectedLatch.open
73
+ break
79
74
  }
80
- break
81
- }
82
- case 'DisconnectMessage': {
83
- if (data.from === peerIdRef.current) {
84
- peerIdRef.current = undefined
85
- yield* connectedLatch.close
86
- yield* establishConnection
75
+ // Case: other side sends connect-ack message (because otherside was already online when this side connected)
76
+ case 'ConnectAckMessage': {
77
+ if (data.to === connectionId) {
78
+ peerIdRef.current = data.from
79
+ yield* connectedLatch.open
80
+ }
81
+ break
87
82
  }
88
- break
89
- }
90
- case 'PayloadMessage': {
91
- if (data.to === connectionId) {
92
- return Schema.decodeEither(listenSchema)(data.payload)
83
+ case 'DisconnectMessage': {
84
+ if (data.from === peerIdRef.current) {
85
+ peerIdRef.current = undefined
86
+ yield* connectedLatch.close
87
+ yield* establishConnection
88
+ }
89
+ break
90
+ }
91
+ case 'PayloadMessage': {
92
+ if (data.to === connectionId) {
93
+ return Schema.decodeEither(schema.listen)(data.payload)
94
+ }
95
+ break
93
96
  }
94
- break
95
97
  }
96
- }
98
+ }),
99
+ ),
100
+ Stream.filter(Predicate.isNotUndefined),
101
+ listenToDebugPing(channelName),
102
+ )
103
+
104
+ const establishConnection = Effect.gen(function* () {
105
+ postMessage(ConnectMessage.make({ from: connectionId }))
106
+ })
107
+
108
+ yield* establishConnection
109
+
110
+ yield* Effect.addFinalizer(() =>
111
+ Effect.gen(function* () {
112
+ postMessage(DisconnectMessage.make({ from: connectionId }))
113
+ channel.close()
114
+ yield* Queue.shutdown(messageQueue)
97
115
  }),
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})`))
116
+ )
117
+
118
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
119
+
120
+ return {
121
+ [WebChannelSymbol]: WebChannelSymbol,
122
+ send,
123
+ listen,
124
+ closedDeferred,
125
+ shutdown: Scope.close(scope, Exit.void),
126
+ schema,
127
+ supportsTransferables,
128
+ }
129
+ }).pipe(Effect.withSpan(`WebChannel:broadcastChannelWithAck(${channelName})`)),
130
+ )
@@ -1,5 +1,5 @@
1
- import type { Deferred, Effect, Either, ParseResult, Schema, Stream } from 'effect'
2
- import { Predicate } from 'effect'
1
+ import type { Deferred, Effect, Either, ParseResult } from 'effect'
2
+ import { Predicate, Schema, Stream } from 'effect'
3
3
 
4
4
  export const WebChannelSymbol = Symbol('WebChannel')
5
5
  export type WebChannelSymbol = typeof WebChannelSymbol
@@ -13,5 +13,59 @@ 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>
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)
47
+ : (schemaWithDebugPing({ send: schema, listen: schema }) as any)
48
+
49
+ export const listenToDebugPing = (channelName: string) => {
50
+ const threadName = (() => {
51
+ if (typeof globalThis !== 'undefined' && Predicate.hasProperty(globalThis, 'name') && self.name !== '') {
52
+ return self.name
53
+ } else if (typeof globalThis !== 'undefined') {
54
+ return 'window'
55
+ }
56
+ return 'unknown thread'
57
+ })()
58
+
59
+ return <MsgListen>(
60
+ stream: Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, never>,
61
+ ): Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, never> =>
62
+ stream.pipe(
63
+ Stream.filter((msg) => {
64
+ if (msg._tag === 'Right' && Schema.is(DebugPingMessage)(msg.right)) {
65
+ console.log(`[${threadName}] WebChannel:ping [${channelName}]`, msg.right.message, msg.right.payload)
66
+ return false
67
+ }
68
+ return true
69
+ }),
70
+ )
17
71
  }
@@ -56,6 +56,9 @@ export {
56
56
  Match,
57
57
  TestServices,
58
58
  Mailbox,
59
+ ExecutionStrategy,
60
+ PrimaryKey,
61
+ Types,
59
62
  } from 'effect'
60
63
 
61
64
  export { dual } from 'effect/Function'
@@ -69,7 +72,7 @@ export * as Subscribable from './Subscribable.js'
69
72
 
70
73
  export * as Logger from './Logger.js'
71
74
 
72
- export * as WebChannel from './WebChannel.js'
75
+ export * as WebChannel from './WebChannel/WebChannel.js'
73
76
  export * as WebSocket from './WebSocket.js'
74
77
 
75
78
  export * as SchemaAST from 'effect/SchemaAST'
package/src/env.ts CHANGED
@@ -29,3 +29,7 @@ export const isDevEnv = () => {
29
29
  export const TRACE_VERBOSE = env('LS_TRACE_VERBOSE') !== undefined || env('VITE_LS_TRACE_VERBOSE') !== undefined
30
30
 
31
31
  export const LS_DEV = env('LS_DEV') !== undefined || env('VITE_LS_DEV') !== undefined
32
+
33
+ const envTruish = (env: string | undefined) => env !== undefined && env !== 'false' && env !== '0'
34
+
35
+ export const IS_CI = envTruish(env('CI'))