@livestore/utils 0.3.0-dev.10 → 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.
- package/dist/.tsbuildinfo.json +1 -1
- package/dist/effect/Effect.d.ts +4 -2
- package/dist/effect/Effect.d.ts.map +1 -1
- package/dist/effect/Effect.js +9 -1
- package/dist/effect/Effect.js.map +1 -1
- package/dist/effect/WebChannel/WebChannel.d.ts +61 -0
- package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -0
- package/dist/effect/WebChannel/WebChannel.js +209 -0
- package/dist/effect/WebChannel/WebChannel.js.map +1 -0
- package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts +5 -6
- package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts.map +1 -1
- package/dist/effect/WebChannel/broadcastChannelWithAck.js +12 -8
- package/dist/effect/WebChannel/broadcastChannelWithAck.js.map +1 -1
- package/dist/effect/WebChannel/common.d.ts +16 -1
- package/dist/effect/WebChannel/common.d.ts.map +1 -1
- package/dist/effect/WebChannel/common.js +30 -1
- package/dist/effect/WebChannel/common.js.map +1 -1
- package/dist/effect/WebChannel.d.ts +5 -12
- package/dist/effect/WebChannel.d.ts.map +1 -1
- package/dist/effect/WebChannel.js +16 -31
- package/dist/effect/WebChannel.js.map +1 -1
- package/dist/effect/index.d.ts +2 -2
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +2 -2
- package/dist/effect/index.js.map +1 -1
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +2 -0
- package/dist/env.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts +0 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunner.js +0 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +0 -3
- package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessWorker.js +0 -3
- package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
- package/package.json +4 -3
- package/src/effect/Effect.ts +14 -2
- package/src/effect/WebChannel/WebChannel.ts +357 -0
- package/src/effect/WebChannel/broadcastChannelWithAck.ts +86 -82
- package/src/effect/WebChannel/common.ts +56 -2
- package/src/effect/index.ts +4 -1
- package/src/env.ts +4 -0
- package/src/node/ChildProcessRunner/ChildProcessRunner.ts +0 -1
- package/src/node/ChildProcessRunner/ChildProcessWorker.ts +0 -3
- 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
|
|
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
|
|
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
|
-
|
|
35
|
-
sendSchema,
|
|
35
|
+
schema: inputSchema,
|
|
36
36
|
}: {
|
|
37
37
|
channelName: string
|
|
38
|
-
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const peerIdRef = { current: undefined as undefined | string }
|
|
48
|
+
const connectedLatch = yield* Effect.makeLatch(false)
|
|
49
|
+
const supportsTransferables = false
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
const postMessage = (msg: typeof Message.Type) => channel.postMessage(Schema.encodeSync(Message)(msg))
|
|
51
52
|
|
|
52
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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
|
}
|
package/src/effect/index.ts
CHANGED
|
@@ -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'))
|
|
@@ -55,12 +55,9 @@ const platformWorkerImpl = Worker.makePlatform<ChildProcess.ChildProcess>()({
|
|
|
55
55
|
},
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
/** @internal */
|
|
59
58
|
export const layerWorker = Layer.succeed(Worker.PlatformWorker, platformWorkerImpl)
|
|
60
59
|
|
|
61
|
-
/** @internal */
|
|
62
60
|
export const layerManager = Layer.provide(Worker.layerManager, layerWorker)
|
|
63
61
|
|
|
64
|
-
/** @internal */
|
|
65
62
|
export const layer = (spawn: (id: number) => ChildProcess.ChildProcess) =>
|
|
66
63
|
Layer.merge(layerManager, Worker.layerSpawner(spawn))
|