@livestore/adapter-node 0.4.0 → 0.5.0-dev.0

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 (41) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/client-session/adapter.d.ts +1 -1
  3. package/dist/client-session/adapter.d.ts.map +1 -1
  4. package/dist/client-session/adapter.js +58 -39
  5. package/dist/client-session/adapter.js.map +1 -1
  6. package/dist/devtools/devtools-server.d.ts.map +1 -1
  7. package/dist/devtools/devtools-server.js +12 -14
  8. package/dist/devtools/devtools-server.js.map +1 -1
  9. package/dist/devtools/vite-dev-server.d.ts +7 -11
  10. package/dist/devtools/vite-dev-server.d.ts.map +1 -1
  11. package/dist/devtools/vite-dev-server.js +5 -6
  12. package/dist/devtools/vite-dev-server.js.map +1 -1
  13. package/dist/leader-thread-shared.d.ts +4 -4
  14. package/dist/leader-thread-shared.d.ts.map +1 -1
  15. package/dist/leader-thread-shared.js +13 -12
  16. package/dist/leader-thread-shared.js.map +1 -1
  17. package/dist/make-leader-worker.d.ts +4 -4
  18. package/dist/make-leader-worker.d.ts.map +1 -1
  19. package/dist/make-leader-worker.js +62 -71
  20. package/dist/make-leader-worker.js.map +1 -1
  21. package/dist/rpc-worker.d.ts +8 -0
  22. package/dist/rpc-worker.d.ts.map +1 -0
  23. package/dist/rpc-worker.js +5 -0
  24. package/dist/rpc-worker.js.map +1 -0
  25. package/dist/webchannel.d.ts +1 -1
  26. package/dist/webchannel.d.ts.map +1 -1
  27. package/dist/webchannel.js +17 -18
  28. package/dist/webchannel.js.map +1 -1
  29. package/dist/worker-schema.d.ts +248 -212
  30. package/dist/worker-schema.d.ts.map +1 -1
  31. package/dist/worker-schema.js +62 -53
  32. package/dist/worker-schema.js.map +1 -1
  33. package/package.json +22 -31
  34. package/src/client-session/adapter.ts +104 -77
  35. package/src/devtools/devtools-server.ts +33 -30
  36. package/src/devtools/vite-dev-server.ts +7 -9
  37. package/src/leader-thread-shared.ts +24 -25
  38. package/src/make-leader-worker.ts +163 -127
  39. package/src/rpc-worker.ts +28 -0
  40. package/src/webchannel.ts +21 -28
  41. package/src/worker-schema.ts +115 -149
@@ -6,8 +6,6 @@ if (process.execArgv.includes('--inspect') === true) {
6
6
  inspector.waitForDebugger()
7
7
  }
8
8
 
9
- import type * as otel from '@opentelemetry/api'
10
-
11
9
  import type { SyncOptions } from '@livestore/common'
12
10
  import { LogConfig, UnknownError } from '@livestore/common'
13
11
  import type { StreamEventsOptions } from '@livestore/common/leader-thread'
@@ -16,8 +14,22 @@ import type { LiveStoreSchema } from '@livestore/common/schema'
16
14
  import { LiveStoreEvent } from '@livestore/common/schema'
17
15
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
18
16
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
19
- import { Effect, FetchHttpClient, Layer, OtelTracer, Schema, Stream, WorkerRunner } from '@livestore/utils/effect'
17
+ import { isDevEnv } from '@livestore/utils'
18
+ import {
19
+ Effect,
20
+ FetchHttpClient,
21
+ Layer,
22
+ OtelTracer,
23
+ Queue,
24
+ References,
25
+ RpcServer,
26
+ RpcWorker,
27
+ Schema,
28
+ Scope,
29
+ Stream,
30
+ } from '@livestore/utils/effect'
20
31
  import { PlatformNode } from '@livestore/utils/node'
32
+ import type * as otel from '@opentelemetry/api'
21
33
 
22
34
  import type { TestingOverrides } from './leader-thread-shared.ts'
23
35
  import { makeLeaderThread } from './leader-thread-shared.ts'
@@ -33,7 +45,7 @@ export type WorkerOptions = {
33
45
  serviceName?: string
34
46
  }
35
47
  testing?: TestingOverrides
36
- } & LogConfig.WithLoggerOptions
48
+ } & LogConfig.LoggerOptions
37
49
 
38
50
  export const getWorkerArgs = () => Schema.decodeSync(WorkerSchema.WorkerArgv)(process.argv[2]!)
39
51
 
@@ -42,139 +54,163 @@ export const makeWorker = (options: WorkerOptions) => {
42
54
  }
43
55
 
44
56
  export const makeWorkerEffect = (options: WorkerOptions) => {
45
- const TracingLive = options.otelOptions?.tracer !== undefined
46
- ? Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
47
- Layer.provideMerge(Layer.succeed(OtelTracer.OtelTracer, options.otelOptions.tracer)),
48
- )
49
- : undefined
57
+ const TracingLive =
58
+ options.otelOptions?.tracer !== undefined
59
+ ? OtelTracer.layerWithoutOtelTracer.pipe(
60
+ Layer.provideMerge(Layer.succeed(OtelTracer.OtelTracer, options.otelOptions.tracer)),
61
+ )
62
+ : Layer.empty
50
63
 
51
64
  // Merge the runtime dependencies once so we can provide them together without chaining Effect.provide.
52
- const runtimeLayer = Layer.mergeAll(
53
- FetchHttpClient.layer,
54
- PlatformNode.NodeFileSystem.layer,
55
- TracingLive ?? Layer.empty,
56
- )
65
+ const runtimeLayer = Layer.mergeAll(FetchHttpClient.layer, PlatformNode.NodeFileSystem.layer, TracingLive)
57
66
 
58
- return WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
59
- InitialMessage: (args) =>
60
- Effect.gen(function* () {
61
- const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm()).pipe(
62
- Effect.withSpan('@livestore/adapter-node:leader-thread:loadSqlite3Wasm'),
63
- )
64
- const makeSqliteDb = yield* sqliteDbFactory({ sqlite3 })
65
- return yield* makeLeaderThread({
66
- ...args,
67
- syncOptions: options.sync,
68
- schema: options.schema,
69
- testing: options.testing,
70
- makeSqliteDb,
71
- syncPayloadEncoded: args.syncPayloadEncoded,
72
- syncPayloadSchema: options.syncPayloadSchema,
73
- })
74
- }).pipe(Layer.unwrapScoped),
75
- PushToLeader: ({ batch }) =>
76
- Effect.andThen(LeaderThreadCtx, (_) =>
77
- _.syncProcessor.push(
78
- batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
79
- // We'll wait in order to keep back pressure on the client session
80
- { waitForProcessing: true },
81
- ),
82
- ).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-node:worker:PushToLeader')),
83
- BootStatusStream: () =>
84
- Effect.andThen(LeaderThreadCtx, (_) => Stream.fromQueue(_.bootStatusQueue)).pipe(Stream.unwrap),
85
- PullStream: ({ cursor }) =>
86
- Effect.gen(function* () {
87
- const { syncProcessor } = yield* LeaderThreadCtx
88
- return syncProcessor.pull({ cursor })
89
- }).pipe(Stream.unwrapScoped),
90
- StreamEvents: (options: WorkerSchema.LeaderWorkerInnerStreamEvents) =>
91
- LeaderThreadCtx.pipe(
92
- Effect.map(({ dbEventlog, syncProcessor }) => {
93
- const { _tag: _ignored, ...payload } = options
94
- const streamOptions = payload as StreamEventsOptions
95
- return streamEventsWithSyncState({
96
- dbEventlog,
97
- syncState: syncProcessor.syncState,
98
- options: streamOptions,
99
- })
100
- }),
101
- Stream.unwrapScoped,
102
- Stream.withSpan('@livestore/adapter-node:worker:StreamEvents'),
103
- ),
104
- Export: () =>
105
- Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
106
- Effect.withSpan('@livestore/adapter-node:worker:Export'),
107
- ),
108
- ExportEventlog: () =>
109
- Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
110
- Effect.withSpan('@livestore/adapter-node:worker:ExportEventlog'),
111
- ),
112
- GetLeaderHead: Effect.fn('@livestore/adapter-node:worker:GetLeaderHead')(function* () {
113
- const workerCtx = yield* LeaderThreadCtx
114
- return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
115
- }),
116
- GetLeaderSyncState: Effect.fn('@livestore/adapter-node:worker:GetLeaderSyncState')(function* () {
117
- const workerCtx = yield* LeaderThreadCtx
118
- return yield* workerCtx.syncProcessor.syncState
119
- }),
120
- SyncStateStream: () =>
121
- Effect.gen(function* () {
122
- const workerCtx = yield* LeaderThreadCtx
123
- return workerCtx.syncProcessor.syncState.changes
124
- }).pipe(Stream.unwrapScoped),
125
- GetNetworkStatus: Effect.fn('@livestore/adapter-node:worker:GetNetworkStatus')(function* () {
126
- const workerCtx = yield* LeaderThreadCtx
127
- return yield* workerCtx.networkStatus
128
- }),
129
- NetworkStatusStream: () =>
130
- Effect.gen(function* () {
131
- const workerCtx = yield* LeaderThreadCtx
132
- return workerCtx.networkStatus.changes
133
- }).pipe(Stream.unwrapScoped),
134
- GetRecreateSnapshot: Effect.fn('@livestore/adapter-node:worker:GetRecreateSnapshot')(function* () {
135
- const workerCtx = yield* LeaderThreadCtx
136
- // const result = yield* Deferred.await(workerCtx.initialSetupDeferred)
137
- // NOTE we can only return the cached snapshot once as it's transferred (i.e. disposed), so we need to set it to undefined
138
- // const cachedSnapshot =
139
- // result._tag === 'Recreate' ? yield* Ref.getAndSet(result.snapshotRef, undefined) : undefined
140
- // return cachedSnapshot ?? workerCtx.db.export()
141
- const snapshot = workerCtx.dbState.export()
142
- return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
143
- }),
144
- Shutdown: Effect.fn('@livestore/adapter-node:worker:Shutdown')(function* () {
145
- // const { db, dbEventlog } = yield* LeaderThreadCtx
146
- yield* Effect.logDebug('[@livestore/adapter-node:worker] Shutdown')
147
-
148
- // if (devtools.enabled) {
149
- // yield* FiberSet.clear(devtools.connections)
150
- // }
151
- // db.close()
152
- // dbEventlog.close()
153
-
154
- // Buy some time for Otel to flush
155
- // TODO find a cleaner way to do this
156
- // yield* Effect.sleep(1000)
157
- }),
158
- ExtraDevtoolsMessage: ({ message }) =>
159
- Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
160
- Effect.withSpan('@livestore/adapter-node:worker:ExtraDevtoolsMessage'),
161
- ),
162
- }).pipe(
163
- Layer.provide(PlatformNode.NodeWorkerRunner.layer),
164
- WorkerRunner.launch,
67
+ return RpcServer.make(WorkerSchema.LeaderWorkerInnerRpcs).pipe(
68
+ Effect.provide(makeWorkerRunnerInner(options)),
69
+ Effect.provide(RpcServer.layerProtocolWorkerRunner),
70
+ Effect.provide(PlatformNode.NodeWorkerRunner.layer),
165
71
  Effect.scoped,
166
72
  Effect.tapCauseLogPretty,
167
73
  Effect.annotateLogs({
168
74
  thread: options.otelOptions?.serviceName ?? 'livestore-node-leader-thread',
169
75
  processId: process.pid,
170
76
  }),
171
- LogConfig.withLoggerConfig(
172
- { logger: options.logger, logLevel: options.logLevel },
173
- { threadName: options.otelOptions?.serviceName ?? 'livestore-node-leader-thread' },
77
+ Effect.provide(
78
+ Layer.mergeAll(
79
+ options.logger ?? Layer.empty,
80
+ Layer.succeed(References.MinimumLogLevel, options.logLevel ?? (isDevEnv() === true ? 'Debug' : 'Info')),
81
+ ),
174
82
  ),
175
- // TODO bring back with Effect 4 once it's easier to work with replacing loggers.
176
- // We basically only want to provide this logger if it's replacing the default logger, not if there's a custom logger already provided.
177
- // Effect.provide(Logger.prettyWithThread(options.otelOptions?.serviceName ?? 'livestore-node-leader-thread')),
178
83
  Effect.provide(runtimeLayer),
179
84
  )
180
85
  }
86
+
87
+ const makeWorkerRunnerInner = (options: WorkerOptions) =>
88
+ WorkerSchema.LeaderWorkerInnerRpcs.toLayer(
89
+ Effect.gen(function* () {
90
+ const leaderThreadScope = yield* Scope.make()
91
+ yield* Effect.addFinalizer((exit) => Scope.close(leaderThreadScope, exit))
92
+
93
+ const leaderThreadContextOnce = yield* Effect.cached(
94
+ Effect.gen(function* () {
95
+ const { storeId, clientId, storage, devtools, syncPayloadEncoded } = yield* RpcWorker.initialMessage(
96
+ WorkerSchema.LeaderWorkerInnerInitialMessage.payloadSchema,
97
+ )
98
+
99
+ const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm()).pipe(
100
+ Effect.withSpan('@livestore/adapter-node:leader-thread:loadSqlite3Wasm'),
101
+ )
102
+ const makeSqliteDb = yield* sqliteDbFactory({ sqlite3 })
103
+
104
+ const layer = yield* makeLeaderThread({
105
+ storeId,
106
+ clientId,
107
+ storage,
108
+ devtools,
109
+ syncOptions: options.sync,
110
+ schema: options.schema,
111
+ testing: options.testing,
112
+ makeSqliteDb,
113
+ syncPayloadEncoded,
114
+ syncPayloadSchema: options.syncPayloadSchema,
115
+ })
116
+
117
+ return yield* Layer.buildWithScope(layer, leaderThreadScope)
118
+ }).pipe(
119
+ Scope.provide(leaderThreadScope),
120
+ Effect.tapCauseLogPretty,
121
+ UnknownError.mapToUnknownError,
122
+ Effect.withSpan('@livestore/adapter-node:worker:InitialMessage'),
123
+ Effect.orDie,
124
+ ),
125
+ )
126
+
127
+ const provideLeaderThread = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
128
+ Effect.gen(function* () {
129
+ const leaderThreadContext = yield* leaderThreadContextOnce
130
+ return yield* effect.pipe(Effect.provide(leaderThreadContext))
131
+ })
132
+
133
+ return WorkerSchema.LeaderWorkerInnerRpcs.of({
134
+ GetRecreateSnapshot: () =>
135
+ Effect.gen(function* () {
136
+ const workerCtx = yield* LeaderThreadCtx
137
+ const snapshot = workerCtx.dbState.export()
138
+ return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
139
+ }).pipe(provideLeaderThread, Effect.withSpan('@livestore/adapter-node:worker:GetRecreateSnapshot')),
140
+ PullStream: ({ cursor }) =>
141
+ Effect.gen(function* () {
142
+ const { syncProcessor } = yield* LeaderThreadCtx
143
+ return syncProcessor.pull({ cursor })
144
+ }).pipe(provideLeaderThread, Stream.unwrap),
145
+ PushToLeader: ({ batch }) =>
146
+ Effect.andThen(LeaderThreadCtx, ({ syncProcessor }) =>
147
+ syncProcessor.push(batch.map((event) => new LiveStoreEvent.Client.EncodedWithMeta(event))),
148
+ ).pipe(provideLeaderThread, Effect.uninterruptible, Effect.withSpan('@livestore/adapter-node:worker:PushToLeader')),
149
+ StreamEvents: (options) =>
150
+ LeaderThreadCtx.pipe(
151
+ Effect.map(({ dbEventlog, syncProcessor }) =>
152
+ streamEventsWithSyncState({
153
+ dbEventlog,
154
+ syncState: syncProcessor.syncState,
155
+ options: options as StreamEventsOptions,
156
+ }),
157
+ ),
158
+ provideLeaderThread,
159
+ Stream.unwrap,
160
+ Stream.withSpan('@livestore/adapter-node:worker:StreamEvents'),
161
+ ),
162
+ Export: () =>
163
+ LeaderThreadCtx.pipe(
164
+ Effect.flatMap((_) => Effect.sync(() => _.dbState.export())),
165
+ provideLeaderThread,
166
+ Effect.withSpan('@livestore/adapter-node:worker:Export'),
167
+ ),
168
+ ExportEventlog: () =>
169
+ LeaderThreadCtx.pipe(
170
+ Effect.flatMap((_) => Effect.sync(() => _.dbEventlog.export())),
171
+ provideLeaderThread,
172
+ Effect.withSpan('@livestore/adapter-node:worker:ExportEventlog'),
173
+ ),
174
+ BootStatusStream: () =>
175
+ LeaderThreadCtx.pipe(
176
+ Effect.map((_) => Stream.fromQueue(_.bootStatusQueue)),
177
+ provideLeaderThread,
178
+ Stream.unwrap,
179
+ ),
180
+ GetLeaderHead: () =>
181
+ Effect.gen(function* () {
182
+ const workerCtx = yield* LeaderThreadCtx
183
+ return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
184
+ }).pipe(provideLeaderThread, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderHead')),
185
+ GetLeaderSyncState: () =>
186
+ Effect.gen(function* () {
187
+ const workerCtx = yield* LeaderThreadCtx
188
+ return yield* workerCtx.syncProcessor.syncState
189
+ }).pipe(provideLeaderThread, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState')),
190
+ SyncStateStream: () =>
191
+ Effect.gen(function* () {
192
+ const workerCtx = yield* LeaderThreadCtx
193
+ return workerCtx.syncProcessor.syncState.changes
194
+ }).pipe(provideLeaderThread, Stream.unwrap),
195
+ GetNetworkStatus: () =>
196
+ Effect.gen(function* () {
197
+ const workerCtx = yield* LeaderThreadCtx
198
+ return yield* workerCtx.networkStatus
199
+ }).pipe(provideLeaderThread, Effect.withSpan('@livestore/adapter-node:worker:GetNetworkStatus')),
200
+ NetworkStatusStream: () =>
201
+ Effect.gen(function* () {
202
+ const workerCtx = yield* LeaderThreadCtx
203
+ return workerCtx.networkStatus.changes
204
+ }).pipe(provideLeaderThread, Stream.unwrap),
205
+ Shutdown: Effect.fn('@livestore/adapter-node:worker:Shutdown')(function* () {
206
+ yield* Effect.logDebug('[@livestore/adapter-node:worker] Shutdown')
207
+ }),
208
+ ExtraDevtoolsMessage: ({ message }) =>
209
+ Effect.andThen(LeaderThreadCtx, (_) => Queue.offer(_.extraIncomingMessagesQueue, message)).pipe(
210
+ provideLeaderThread,
211
+ Effect.asVoid,
212
+ Effect.withSpan('@livestore/adapter-node:worker:ExtraDevtoolsMessage'),
213
+ ),
214
+ })
215
+ }),
216
+ )
@@ -0,0 +1,28 @@
1
+ import { Effect, RpcClientError, Schema, Stream } from '@livestore/utils/effect'
2
+
3
+ export type WithoutRpcClientError<E> = E extends { readonly _tag: infer Tag }
4
+ ? 'RpcClientError' extends Tag
5
+ ? never
6
+ : E
7
+ : E
8
+
9
+ export const isRpcClientError = (error: unknown): error is RpcClientError.RpcClientError =>
10
+ Schema.is(RpcClientError.RpcClientError)(error)
11
+
12
+ export const dieOnRpcClientError = <A, E, R>(
13
+ effect: Effect.Effect<A, E, R>,
14
+ ): Effect.Effect<A, WithoutRpcClientError<E>, R> =>
15
+ (effect as Effect.Effect<A, RpcClientError.RpcClientError | WithoutRpcClientError<E>, R>).pipe(
16
+ Effect.catchIf(isRpcClientError, (error) => Effect.die(error)),
17
+ ) as Effect.Effect<A, WithoutRpcClientError<E>, R>
18
+
19
+ export const dieOnRpcClientErrorStream = <A, E, R>(
20
+ stream: Stream.Stream<A, E, R>,
21
+ ): Stream.Stream<A, WithoutRpcClientError<E>, R> =>
22
+ (stream as Stream.Stream<A, RpcClientError.RpcClientError | WithoutRpcClientError<E>, R>).pipe(
23
+ Stream.catchIf(
24
+ isRpcClientError,
25
+ (error) => Stream.die(error),
26
+ (error) => Stream.fail(error),
27
+ ),
28
+ ) as Stream.Stream<A, WithoutRpcClientError<E>, R>
package/src/webchannel.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import type { BroadcastChannel as NodeBroadcastChannel } from 'node:worker_threads'
2
2
 
3
- import type { Either, ParseResult } from '@livestore/utils/effect'
4
- import { Deferred, Effect, Exit, Schema, Scope, Stream, WebChannel } from '@livestore/utils/effect'
3
+ import { Deferred, Effect, Exit, Queue, Schema, Scope, Stream, WebChannel } from '@livestore/utils/effect'
5
4
 
6
5
  export const makeBroadcastChannel = <Msg, MsgEncoded>({
7
6
  channelName,
8
7
  schema,
9
8
  }: {
10
9
  channelName: string
11
- schema: Schema.Schema<Msg, MsgEncoded>
10
+ schema: Schema.Codec<Msg, MsgEncoded>
12
11
  }): Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope> =>
13
12
  Effect.scopeWithCloseable((scope) =>
14
13
  Effect.gen(function* () {
@@ -21,39 +20,33 @@ export const makeBroadcastChannel = <Msg, MsgEncoded>({
21
20
  // from `node:worker_threads` is not yet stable in Deno
22
21
  const channel = new globalThis.BroadcastChannel(channelName) as any as NodeBroadcastChannel
23
22
 
24
- yield* Effect.addFinalizer(() => Effect.try(() => channel.close()).pipe(Effect.ignoreLogged))
23
+ yield* Effect.addFinalizer(() =>
24
+ Effect.try(() => {
25
+ // NOTE not all BroadcastChannel implementations have the `unref` method
26
+ if (typeof channel.unref === 'function') {
27
+ channel.unref()
28
+ }
29
+ channel.close()
30
+ }).pipe(Effect.ignore),
31
+ )
25
32
 
26
33
  const send = (message: Msg) =>
27
34
  Effect.gen(function* () {
28
- const messageEncoded = yield* Schema.encode(schema)(message)
35
+ const messageEncoded = yield* Schema.encodeEffect(schema)(message)
29
36
  channel.postMessage(messageEncoded)
30
37
  })
31
38
 
32
- // TODO also listen to `messageerror` in parallel
33
- // const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
34
- // Stream.map((_) => Schema.decodeEither(listenSchema)(_.data)),
35
- // )
36
-
37
- const listen = Stream.asyncPush<Either.Either<Msg, ParseResult.ParseError>>((emit) =>
38
- Effect.acquireRelease(
39
- Effect.gen(function* () {
40
- channel.onmessage = (event: any) => {
41
- return emit.single(Schema.decodeEither(schema)(event.data))
42
- }
39
+ // In Effect v4, forking a consumer is not a readiness handshake. Install the message
40
+ // listener while constructing the channel and buffer into a scoped queue so early messages
41
+ // are not lost before `listen` is pulled.
42
+ const messageQueue = yield* Effect.acquireRelease(Queue.unbounded<unknown>(), Queue.shutdown)
43
+ channel.onmessage = (event: any) => {
44
+ Queue.offerUnsafe(messageQueue, event.data)
45
+ }
43
46
 
44
- return channel
45
- }),
46
- (channel) =>
47
- Effect.sync(() => {
48
- // NOTE not all BroadcastChannel implementations have the `unref` method
49
- if (typeof channel.unref === 'function') {
50
- channel.unref()
51
- }
52
- }),
53
- ),
54
- )
47
+ const listen = Stream.fromQueue(messageQueue).pipe(Stream.map((data) => Schema.decodeUnknownResult(schema)(data)))
55
48
 
56
- const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
49
+ const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
57
50
  const supportsTransferables = false
58
51
 
59
52
  return {