@livestore/adapter-node 0.4.0-dev.9 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/client-session/adapter.d.ts +69 -3
- package/dist/client-session/adapter.d.ts.map +1 -1
- package/dist/client-session/adapter.js +160 -62
- package/dist/client-session/adapter.js.map +1 -1
- package/dist/devtools/devtools-server.d.ts.map +1 -1
- package/dist/devtools/devtools-server.js +25 -19
- package/dist/devtools/devtools-server.js.map +1 -1
- package/dist/devtools/mod.d.ts +1 -1
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +1 -1
- package/dist/devtools/mod.js.map +1 -1
- package/dist/devtools/vite-dev-server.d.ts +22 -4
- package/dist/devtools/vite-dev-server.d.ts.map +1 -1
- package/dist/devtools/vite-dev-server.js +45 -11
- package/dist/devtools/vite-dev-server.js.map +1 -1
- package/dist/leader-thread-shared.d.ts +8 -7
- package/dist/leader-thread-shared.d.ts.map +1 -1
- package/dist/leader-thread-shared.js +19 -16
- package/dist/leader-thread-shared.js.map +1 -1
- package/dist/make-leader-worker.d.ts +5 -3
- package/dist/make-leader-worker.d.ts.map +1 -1
- package/dist/make-leader-worker.js +75 -59
- package/dist/make-leader-worker.js.map +1 -1
- package/dist/rpc-worker.d.ts +8 -0
- package/dist/rpc-worker.d.ts.map +1 -0
- package/dist/rpc-worker.js +5 -0
- package/dist/rpc-worker.js.map +1 -0
- package/dist/shutdown-channel.d.ts.map +1 -1
- package/dist/shutdown-channel.js.map +1 -1
- package/dist/webchannel.d.ts +1 -1
- package/dist/webchannel.d.ts.map +1 -1
- package/dist/webchannel.js +17 -18
- package/dist/webchannel.js.map +1 -1
- package/dist/worker-schema.d.ts +274 -138
- package/dist/worker-schema.d.ts.map +1 -1
- package/dist/worker-schema.js +79 -45
- package/dist/worker-schema.js.map +1 -1
- package/package.json +51 -20
- package/src/client-session/adapter.ts +228 -124
- package/src/devtools/devtools-server.ts +50 -35
- package/src/devtools/mod.ts +1 -1
- package/src/devtools/vite-dev-server.ts +65 -15
- package/src/leader-thread-shared.ts +37 -34
- package/src/make-leader-worker.ts +164 -115
- package/src/rpc-worker.ts +28 -0
- package/src/shutdown-channel.ts +1 -0
- package/src/webchannel.ts +21 -28
- package/src/worker-schema.ts +132 -117
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import './thread-polyfill.ts'
|
|
2
|
-
|
|
3
2
|
import inspector from 'node:inspector'
|
|
4
3
|
|
|
5
|
-
if (process.execArgv.includes('--inspect')) {
|
|
4
|
+
if (process.execArgv.includes('--inspect') === true) {
|
|
6
5
|
inspector.open()
|
|
7
6
|
inspector.waitForDebugger()
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
import type { SyncOptions } from '@livestore/common'
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { LogConfig, UnknownError } from '@livestore/common'
|
|
11
|
+
import type { StreamEventsOptions } from '@livestore/common/leader-thread'
|
|
12
|
+
import { Eventlog, LeaderThreadCtx, streamEventsWithSyncState } from '@livestore/common/leader-thread'
|
|
13
13
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
14
14
|
import { LiveStoreEvent } from '@livestore/common/schema'
|
|
15
15
|
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
16
16
|
import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
|
|
17
|
+
import { isDevEnv } from '@livestore/utils'
|
|
17
18
|
import {
|
|
18
19
|
Effect,
|
|
19
20
|
FetchHttpClient,
|
|
20
|
-
identity,
|
|
21
21
|
Layer,
|
|
22
|
-
Logger,
|
|
23
|
-
LogLevel,
|
|
24
22
|
OtelTracer,
|
|
23
|
+
Queue,
|
|
24
|
+
References,
|
|
25
|
+
RpcServer,
|
|
26
|
+
RpcWorker,
|
|
25
27
|
Schema,
|
|
28
|
+
Scope,
|
|
26
29
|
Stream,
|
|
27
|
-
WorkerRunner,
|
|
28
30
|
} from '@livestore/utils/effect'
|
|
29
31
|
import { PlatformNode } from '@livestore/utils/node'
|
|
30
32
|
import type * as otel from '@opentelemetry/api'
|
|
@@ -36,132 +38,179 @@ import * as WorkerSchema from './worker-schema.ts'
|
|
|
36
38
|
export type WorkerOptions = {
|
|
37
39
|
schema: LiveStoreSchema
|
|
38
40
|
sync?: SyncOptions
|
|
41
|
+
syncPayloadSchema?: Schema.Schema<any>
|
|
39
42
|
otelOptions?: {
|
|
40
43
|
tracer?: otel.Tracer
|
|
41
44
|
/** @default 'livestore-node-leader-thread' */
|
|
42
45
|
serviceName?: string
|
|
43
46
|
}
|
|
44
47
|
testing?: TestingOverrides
|
|
45
|
-
}
|
|
48
|
+
} & LogConfig.LoggerOptions
|
|
46
49
|
|
|
47
50
|
export const getWorkerArgs = () => Schema.decodeSync(WorkerSchema.WorkerArgv)(process.argv[2]!)
|
|
48
51
|
|
|
49
52
|
export const makeWorker = (options: WorkerOptions) => {
|
|
50
|
-
makeWorkerEffect(options).pipe(
|
|
51
|
-
Effect.provide(Logger.prettyWithThread(options.otelOptions?.serviceName ?? 'livestore-node-leader-thread')),
|
|
52
|
-
PlatformNode.NodeRuntime.runMain,
|
|
53
|
-
)
|
|
53
|
+
makeWorkerEffect(options).pipe(PlatformNode.NodeRuntime.runMain)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export const makeWorkerEffect = (options: WorkerOptions) => {
|
|
57
|
-
const TracingLive =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: undefined
|
|
62
|
-
|
|
63
|
-
return WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
|
|
64
|
-
InitialMessage: (args) =>
|
|
65
|
-
Effect.gen(function* () {
|
|
66
|
-
const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm()).pipe(
|
|
67
|
-
Effect.withSpan('@livestore/adapter-node:leader-thread:loadSqlite3Wasm'),
|
|
57
|
+
const TracingLive =
|
|
58
|
+
options.otelOptions?.tracer !== undefined
|
|
59
|
+
? OtelTracer.layerWithoutOtelTracer.pipe(
|
|
60
|
+
Layer.provideMerge(Layer.succeed(OtelTracer.OtelTracer, options.otelOptions.tracer)),
|
|
68
61
|
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
PushToLeader: ({ batch }) =>
|
|
79
|
-
Effect.andThen(LeaderThreadCtx, (_) =>
|
|
80
|
-
_.syncProcessor.push(
|
|
81
|
-
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
82
|
-
// We'll wait in order to keep back pressure on the client session
|
|
83
|
-
{ waitForProcessing: true },
|
|
84
|
-
),
|
|
85
|
-
).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-node:worker:PushToLeader')),
|
|
86
|
-
BootStatusStream: () =>
|
|
87
|
-
Effect.andThen(LeaderThreadCtx, (_) => Stream.fromQueue(_.bootStatusQueue)).pipe(Stream.unwrap),
|
|
88
|
-
PullStream: ({ cursor }) =>
|
|
89
|
-
Effect.gen(function* () {
|
|
90
|
-
const { syncProcessor } = yield* LeaderThreadCtx
|
|
91
|
-
return syncProcessor.pull({ cursor })
|
|
92
|
-
}).pipe(Stream.unwrapScoped),
|
|
93
|
-
Export: () =>
|
|
94
|
-
Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
|
|
95
|
-
UnexpectedError.mapToUnexpectedError,
|
|
96
|
-
Effect.withSpan('@livestore/adapter-node:worker:Export'),
|
|
97
|
-
),
|
|
98
|
-
ExportEventlog: () =>
|
|
99
|
-
Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
|
|
100
|
-
UnexpectedError.mapToUnexpectedError,
|
|
101
|
-
Effect.withSpan('@livestore/adapter-node:worker:ExportEventlog'),
|
|
102
|
-
),
|
|
103
|
-
GetLeaderHead: () =>
|
|
104
|
-
Effect.gen(function* () {
|
|
105
|
-
const workerCtx = yield* LeaderThreadCtx
|
|
106
|
-
return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
|
|
107
|
-
}).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderHead')),
|
|
108
|
-
GetLeaderSyncState: () =>
|
|
109
|
-
Effect.gen(function* () {
|
|
110
|
-
const workerCtx = yield* LeaderThreadCtx
|
|
111
|
-
return yield* workerCtx.syncProcessor.syncState
|
|
112
|
-
}).pipe(
|
|
113
|
-
UnexpectedError.mapToUnexpectedError,
|
|
114
|
-
Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState'),
|
|
115
|
-
),
|
|
116
|
-
GetRecreateSnapshot: () =>
|
|
117
|
-
Effect.gen(function* () {
|
|
118
|
-
const workerCtx = yield* LeaderThreadCtx
|
|
119
|
-
// const result = yield* Deferred.await(workerCtx.initialSetupDeferred)
|
|
120
|
-
// NOTE we can only return the cached snapshot once as it's transferred (i.e. disposed), so we need to set it to undefined
|
|
121
|
-
// const cachedSnapshot =
|
|
122
|
-
// result._tag === 'Recreate' ? yield* Ref.getAndSet(result.snapshotRef, undefined) : undefined
|
|
123
|
-
// return cachedSnapshot ?? workerCtx.db.export()
|
|
124
|
-
const snapshot = workerCtx.dbState.export()
|
|
125
|
-
return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
|
|
126
|
-
}).pipe(
|
|
127
|
-
UnexpectedError.mapToUnexpectedError,
|
|
128
|
-
Effect.withSpan('@livestore/adapter-node:worker:GetRecreateSnapshot'),
|
|
129
|
-
),
|
|
130
|
-
Shutdown: () =>
|
|
131
|
-
Effect.gen(function* () {
|
|
132
|
-
// const { db, dbEventlog } = yield* LeaderThreadCtx
|
|
133
|
-
yield* Effect.logDebug('[@livestore/adapter-node:worker] Shutdown')
|
|
134
|
-
|
|
135
|
-
// if (devtools.enabled) {
|
|
136
|
-
// yield* FiberSet.clear(devtools.connections)
|
|
137
|
-
// }
|
|
138
|
-
// db.close()
|
|
139
|
-
// dbEventlog.close()
|
|
140
|
-
|
|
141
|
-
// Buy some time for Otel to flush
|
|
142
|
-
// TODO find a cleaner way to do this
|
|
143
|
-
// yield* Effect.sleep(1000)
|
|
144
|
-
}).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:Shutdown')),
|
|
145
|
-
ExtraDevtoolsMessage: ({ message }) =>
|
|
146
|
-
Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
|
|
147
|
-
UnexpectedError.mapToUnexpectedError,
|
|
148
|
-
Effect.withSpan('@livestore/adapter-node:worker:ExtraDevtoolsMessage'),
|
|
149
|
-
),
|
|
150
|
-
}).pipe(
|
|
151
|
-
Layer.provide(PlatformNode.NodeWorkerRunner.layer),
|
|
152
|
-
WorkerRunner.launch,
|
|
62
|
+
: Layer.empty
|
|
63
|
+
|
|
64
|
+
// Merge the runtime dependencies once so we can provide them together without chaining Effect.provide.
|
|
65
|
+
const runtimeLayer = Layer.mergeAll(FetchHttpClient.layer, PlatformNode.NodeFileSystem.layer, TracingLive)
|
|
66
|
+
|
|
67
|
+
return RpcServer.make(WorkerSchema.LeaderWorkerInnerRpcs).pipe(
|
|
68
|
+
Effect.provide(makeWorkerRunnerInner(options)),
|
|
69
|
+
Effect.provide(RpcServer.layerProtocolWorkerRunner),
|
|
70
|
+
Effect.provide(PlatformNode.NodeWorkerRunner.layer),
|
|
153
71
|
Effect.scoped,
|
|
154
72
|
Effect.tapCauseLogPretty,
|
|
155
73
|
Effect.annotateLogs({
|
|
156
74
|
thread: options.otelOptions?.serviceName ?? 'livestore-node-leader-thread',
|
|
157
75
|
processId: process.pid,
|
|
158
76
|
}),
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
77
|
+
Effect.provide(
|
|
78
|
+
Layer.mergeAll(
|
|
79
|
+
options.logger ?? Layer.empty,
|
|
80
|
+
Layer.succeed(References.MinimumLogLevel, options.logLevel ?? (isDevEnv() === true ? 'Debug' : 'Info')),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
Effect.provide(runtimeLayer),
|
|
166
84
|
)
|
|
167
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/shutdown-channel.ts
CHANGED
package/src/webchannel.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { BroadcastChannel as NodeBroadcastChannel } from 'node:worker_threads'
|
|
2
2
|
|
|
3
|
-
import
|
|
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.
|
|
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(() =>
|
|
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.
|
|
35
|
+
const messageEncoded = yield* Schema.encodeEffect(schema)(message)
|
|
29
36
|
channel.postMessage(messageEncoded)
|
|
30
37
|
})
|
|
31
38
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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>()
|
|
49
|
+
const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
|
|
57
50
|
const supportsTransferables = false
|
|
58
51
|
|
|
59
52
|
return {
|