@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/client-session/adapter.d.ts +1 -1
- package/dist/client-session/adapter.d.ts.map +1 -1
- package/dist/client-session/adapter.js +58 -39
- 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 +12 -14
- package/dist/devtools/devtools-server.js.map +1 -1
- package/dist/devtools/vite-dev-server.d.ts +7 -11
- package/dist/devtools/vite-dev-server.d.ts.map +1 -1
- package/dist/devtools/vite-dev-server.js +5 -6
- package/dist/devtools/vite-dev-server.js.map +1 -1
- package/dist/leader-thread-shared.d.ts +4 -4
- package/dist/leader-thread-shared.d.ts.map +1 -1
- package/dist/leader-thread-shared.js +13 -12
- package/dist/leader-thread-shared.js.map +1 -1
- package/dist/make-leader-worker.d.ts +4 -4
- package/dist/make-leader-worker.d.ts.map +1 -1
- package/dist/make-leader-worker.js +62 -71
- 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/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 +248 -212
- package/dist/worker-schema.d.ts.map +1 -1
- package/dist/worker-schema.js +62 -53
- package/dist/worker-schema.js.map +1 -1
- package/package.json +22 -31
- package/src/client-session/adapter.ts +104 -77
- package/src/devtools/devtools-server.ts +33 -30
- package/src/devtools/vite-dev-server.ts +7 -9
- package/src/leader-thread-shared.ts +24 -25
- package/src/make-leader-worker.ts +163 -127
- package/src/rpc-worker.ts +28 -0
- package/src/webchannel.ts +21 -28
- 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 {
|
|
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.
|
|
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 =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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 {
|