@livestore/adapter-web 0.4.0-dev.2 → 0.4.0-dev.21

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 (61) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/in-memory/in-memory-adapter.d.ts +49 -5
  3. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  4. package/dist/in-memory/in-memory-adapter.js +69 -16
  5. package/dist/in-memory/in-memory-adapter.js.map +1 -1
  6. package/dist/web-worker/client-session/client-session-devtools.d.ts +1 -1
  7. package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
  8. package/dist/web-worker/client-session/client-session-devtools.js +14 -3
  9. package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
  10. package/dist/web-worker/client-session/persisted-adapter.d.ts +15 -0
  11. package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
  12. package/dist/web-worker/client-session/persisted-adapter.js +68 -46
  13. package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
  14. package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
  15. package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
  16. package/dist/web-worker/client-session/sqlite-loader.js +16 -0
  17. package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
  18. package/dist/web-worker/common/persisted-sqlite.d.ts +23 -7
  19. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
  20. package/dist/web-worker/common/persisted-sqlite.js +125 -76
  21. package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
  22. package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
  23. package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
  24. package/dist/web-worker/common/shutdown-channel.js +2 -2
  25. package/dist/web-worker/common/shutdown-channel.js.map +1 -1
  26. package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
  27. package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
  28. package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
  29. package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
  30. package/dist/web-worker/common/worker-schema.d.ts +147 -56
  31. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  32. package/dist/web-worker/common/worker-schema.js +55 -36
  33. package/dist/web-worker/common/worker-schema.js.map +1 -1
  34. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +4 -2
  35. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
  36. package/dist/web-worker/leader-worker/make-leader-worker.js +63 -27
  37. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
  38. package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -1
  39. package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
  40. package/dist/web-worker/shared-worker/make-shared-worker.js +66 -49
  41. package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
  42. package/dist/web-worker/vite-dev-polyfill.js +1 -0
  43. package/dist/web-worker/vite-dev-polyfill.js.map +1 -1
  44. package/package.json +8 -9
  45. package/src/in-memory/in-memory-adapter.ts +83 -21
  46. package/src/web-worker/ambient.d.ts +7 -24
  47. package/src/web-worker/client-session/client-session-devtools.ts +18 -3
  48. package/src/web-worker/client-session/persisted-adapter.ts +117 -59
  49. package/src/web-worker/client-session/sqlite-loader.ts +19 -0
  50. package/src/web-worker/common/persisted-sqlite.ts +225 -107
  51. package/src/web-worker/common/shutdown-channel.ts +10 -3
  52. package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
  53. package/src/web-worker/common/worker-schema.ts +74 -35
  54. package/src/web-worker/leader-worker/make-leader-worker.ts +86 -41
  55. package/src/web-worker/shared-worker/make-shared-worker.ts +96 -75
  56. package/src/web-worker/vite-dev-polyfill.ts +1 -0
  57. package/dist/opfs-utils.d.ts +0 -5
  58. package/dist/opfs-utils.d.ts.map +0 -1
  59. package/dist/opfs-utils.js +0 -43
  60. package/dist/opfs-utils.js.map +0 -1
  61. package/src/opfs-utils.ts +0 -61
@@ -1,7 +1,13 @@
1
1
  import type { SqliteDb, SyncOptions } from '@livestore/common'
2
- import { Devtools, UnexpectedError } from '@livestore/common'
3
- import type { DevtoolsOptions } from '@livestore/common/leader-thread'
4
- import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
2
+ import { Devtools, LogConfig, UnknownError } from '@livestore/common'
3
+ import type { DevtoolsOptions, StreamEventsOptions } from '@livestore/common/leader-thread'
4
+ import {
5
+ configureConnection,
6
+ Eventlog,
7
+ LeaderThreadCtx,
8
+ makeLeaderThreadLayer,
9
+ streamEventsWithSyncState,
10
+ } from '@livestore/common/leader-thread'
5
11
  import type { LiveStoreSchema } from '@livestore/common/schema'
6
12
  import { LiveStoreEvent } from '@livestore/common/schema'
7
13
  import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
@@ -10,37 +16,36 @@ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
10
16
  import { isDevEnv, LS_DEV } from '@livestore/utils'
11
17
  import type { HttpClient, Scope, WorkerError } from '@livestore/utils/effect'
12
18
  import {
13
- BrowserWorkerRunner,
14
19
  Effect,
15
20
  FetchHttpClient,
16
21
  identity,
17
22
  Layer,
18
- Logger,
19
- LogLevel,
20
23
  OtelTracer,
21
24
  Scheduler,
25
+ type Schema,
22
26
  Stream,
23
27
  TaskTracing,
24
28
  WorkerRunner,
25
29
  } from '@livestore/utils/effect'
30
+ import { BrowserWorkerRunner, Opfs } from '@livestore/utils/effect/browser'
26
31
  import type * as otel from '@opentelemetry/api'
27
32
 
28
- import * as OpfsUtils from '../../opfs-utils.ts'
29
- import { getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
33
+ import { cleanupOldStateDbFiles, getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
30
34
  import { makeShutdownChannel } from '../common/shutdown-channel.ts'
31
35
  import * as WorkerSchema from '../common/worker-schema.ts'
32
36
 
33
37
  export type WorkerOptions = {
34
38
  schema: LiveStoreSchema
35
39
  sync?: SyncOptions
40
+ syncPayloadSchema?: Schema.Schema<any>
36
41
  otelOptions?: {
37
42
  tracer?: otel.Tracer
38
43
  }
39
- }
44
+ } & LogConfig.WithLoggerOptions
40
45
 
41
46
  if (isDevEnv()) {
42
47
  globalThis.__debugLiveStoreUtils = {
43
- opfs: OpfsUtils,
48
+ opfs: Opfs.debugUtils,
44
49
  blobUrl: (buffer: Uint8Array<ArrayBuffer>) =>
45
50
  URL.createObjectURL(new Blob([buffer], { type: 'application/octet-stream' })),
46
51
  runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
@@ -59,23 +64,23 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
59
64
  )
60
65
  : undefined
61
66
 
67
+ const runtimeLayer = Layer.mergeAll(FetchHttpClient.layer, TracingLive ?? Layer.empty)
68
+
62
69
  return makeWorkerRunnerOuter(options).pipe(
63
70
  Layer.provide(BrowserWorkerRunner.layer),
64
71
  WorkerRunner.launch,
65
72
  Effect.scoped,
66
73
  Effect.tapCauseLogPretty,
67
74
  Effect.annotateLogs({ thread: self.name }),
68
- Effect.provide(Logger.prettyWithThread(self.name)),
69
- Effect.provide(FetchHttpClient.layer),
75
+ Effect.provide(runtimeLayer),
70
76
  LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
71
- TracingLive ? Effect.provide(TracingLive) : identity,
72
77
  // We're using this custom scheduler to improve op batching behaviour and reduce the overhead
73
78
  // of the Effect fiber runtime given we have different tradeoffs on a worker thread.
74
79
  // Despite the "message channel" name, is has nothing to do with the `incomingRequestsPort` above.
75
80
  Effect.withScheduler(Scheduler.messageChannel()),
76
81
  // We're increasing the Effect ops limit here to allow for larger chunks of operations at a time
77
82
  Effect.withMaxOpsBeforeYield(4096),
78
- Logger.withMinimumLogLevel(LogLevel.Debug),
83
+ LogConfig.withLoggerConfig({ logger: options.logger, logLevel: options.logLevel }, { threadName: self.name }),
79
84
  )
80
85
  }
81
86
 
@@ -93,7 +98,12 @@ const makeWorkerRunnerOuter = (
93
98
  Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage:innerFiber'),
94
99
  Effect.tapCauseLogPretty,
95
100
  Effect.provide(
96
- WebmeshWorker.CacheService.layer({ nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }) }),
101
+ Layer.mergeAll(
102
+ Opfs.Opfs.Default,
103
+ WebmeshWorker.CacheService.layer({
104
+ nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }),
105
+ }),
106
+ ),
97
107
  ),
98
108
  Effect.forkScoped,
99
109
  )
@@ -102,18 +112,19 @@ const makeWorkerRunnerOuter = (
102
112
  }).pipe(Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage'), Layer.unwrapScoped),
103
113
  })
104
114
 
105
- const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
115
+ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }: WorkerOptions) =>
106
116
  WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
107
- InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayload }) =>
117
+ InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayloadEncoded }) =>
108
118
  Effect.gen(function* () {
109
119
  const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
110
120
  const makeSqliteDb = sqliteDbFactory({ sqlite3 })
121
+ const opfsDirectory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
111
122
  const runtime = yield* Effect.runtime<never>()
112
123
 
113
124
  const makeDb = (kind: 'state' | 'eventlog') =>
114
125
  makeSqliteDb({
115
126
  _tag: 'opfs',
116
- opfsDirectory: sanitizeOpfsDir(storageOptions.directory, storeId),
127
+ opfsDirectory,
117
128
  fileName: kind === 'state' ? getStateDbFileName(schema) : 'eventlog.db',
118
129
  configureDb: (db) =>
119
130
  configureConnection(db, {
@@ -131,6 +142,16 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
131
142
  concurrency: 2,
132
143
  })
133
144
 
145
+ // Clean up old state database files after successful database creation
146
+ // This prevents OPFS file pool capacity exhaustion from accumulated state db files after schema changes/migrations
147
+ if (dbState.metadata._tag === 'opfs') {
148
+ yield* cleanupOldStateDbFiles({
149
+ vfs: dbState.metadata.vfs,
150
+ currentSchema: schema,
151
+ opfsDirectory: dbState.metadata.persistenceInfo.opfsDirectory,
152
+ })
153
+ }
154
+
134
155
  const devtoolsOptions = yield* makeDevtoolsOptions({ devtoolsEnabled, dbState, dbEventlog })
135
156
  const shutdownChannel = yield* makeShutdownChannel(storeId)
136
157
 
@@ -144,11 +165,12 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
144
165
  dbEventlog,
145
166
  devtoolsOptions,
146
167
  shutdownChannel,
147
- syncPayload,
168
+ syncPayloadEncoded,
169
+ syncPayloadSchema,
148
170
  })
149
171
  }).pipe(
150
172
  Effect.tapCauseLogPretty,
151
- UnexpectedError.mapToUnexpectedError,
173
+ UnknownError.mapToUnknownError,
152
174
  Effect.withPerformanceMeasure('@livestore/adapter-web:worker:InitialMessage'),
153
175
  Effect.withSpan('@livestore/adapter-web:worker:InitialMessage'),
154
176
  Effect.annotateSpans({ debugInstanceId }),
@@ -166,13 +188,10 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
166
188
 
167
189
  const snapshot = workerCtx.dbState.export()
168
190
  return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
169
- }).pipe(
170
- UnexpectedError.mapToUnexpectedError,
171
- Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot'),
172
- ),
191
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot')),
173
192
  PullStream: ({ cursor }) =>
174
193
  Effect.gen(function* () {
175
- const { syncProcessor } = yield* LeaderThreadCtx
194
+ const { syncProcessor } = yield* LeaderThreadCtx // <- syncState comes from here
176
195
  return syncProcessor.pull({ cursor })
177
196
  }).pipe(
178
197
  Stream.unwrapScoped,
@@ -182,19 +201,33 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
182
201
  PushToLeader: ({ batch }) =>
183
202
  Effect.andThen(LeaderThreadCtx, ({ syncProcessor }) =>
184
203
  syncProcessor.push(
185
- batch.map((event) => new LiveStoreEvent.EncodedWithMeta(event)),
204
+ batch.map((event) => new LiveStoreEvent.Client.EncodedWithMeta(event)),
186
205
  // We'll wait in order to keep back pressure on the client session
187
206
  { waitForProcessing: true },
188
207
  ),
189
208
  ).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-web:worker:PushToLeader')),
209
+ StreamEvents: (options) =>
210
+ LeaderThreadCtx.pipe(
211
+ Effect.map(({ dbEventlog, syncProcessor }) => {
212
+ const { _tag: _ignored, ...payload } = options as any
213
+ const streamOptions = payload as StreamEventsOptions
214
+ return streamEventsWithSyncState({
215
+ dbEventlog,
216
+ syncState: syncProcessor.syncState,
217
+ options: streamOptions,
218
+ })
219
+ }),
220
+ Stream.unwrapScoped,
221
+ Stream.withSpan('@livestore/adapter-web:worker:StreamEvents'),
222
+ ),
190
223
  Export: () =>
191
224
  Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
192
- UnexpectedError.mapToUnexpectedError,
225
+ UnknownError.mapToUnknownError,
193
226
  Effect.withSpan('@livestore/adapter-web:worker:Export'),
194
227
  ),
195
228
  ExportEventlog: () =>
196
229
  Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
197
- UnexpectedError.mapToUnexpectedError,
230
+ UnknownError.mapToUnknownError,
198
231
  Effect.withSpan('@livestore/adapter-web:worker:ExportEventlog'),
199
232
  ),
200
233
  BootStatusStream: () =>
@@ -203,15 +236,27 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
203
236
  Effect.gen(function* () {
204
237
  const workerCtx = yield* LeaderThreadCtx
205
238
  return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
206
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
239
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
207
240
  GetLeaderSyncState: () =>
208
241
  Effect.gen(function* () {
209
242
  const workerCtx = yield* LeaderThreadCtx
210
243
  return yield* workerCtx.syncProcessor.syncState
211
- }).pipe(
212
- UnexpectedError.mapToUnexpectedError,
213
- Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState'),
214
- ),
244
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState')),
245
+ SyncStateStream: () =>
246
+ Effect.gen(function* () {
247
+ const workerCtx = yield* LeaderThreadCtx
248
+ return workerCtx.syncProcessor.syncState.changes
249
+ }).pipe(Stream.unwrapScoped),
250
+ GetNetworkStatus: () =>
251
+ Effect.gen(function* () {
252
+ const workerCtx = yield* LeaderThreadCtx
253
+ return yield* workerCtx.networkStatus
254
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetNetworkStatus')),
255
+ NetworkStatusStream: () =>
256
+ Effect.gen(function* () {
257
+ const workerCtx = yield* LeaderThreadCtx
258
+ return workerCtx.networkStatus.changes
259
+ }).pipe(Stream.unwrapScoped),
215
260
  Shutdown: () =>
216
261
  Effect.gen(function* () {
217
262
  yield* Effect.logDebug('[@livestore/adapter-web:worker] Shutdown')
@@ -219,10 +264,10 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
219
264
  // Buy some time for Otel to flush
220
265
  // TODO find a cleaner way to do this
221
266
  yield* Effect.sleep(300)
222
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:Shutdown')),
267
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:Shutdown')),
223
268
  ExtraDevtoolsMessage: ({ message }) =>
224
269
  Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
225
- UnexpectedError.mapToUnexpectedError,
270
+ UnknownError.mapToUnknownError,
226
271
  Effect.withSpan('@livestore/adapter-web:worker:ExtraDevtoolsMessage'),
227
272
  ),
228
273
  'DevtoolsWebCommon.CreateConnection': WebmeshWorker.CreateConnection,
@@ -236,7 +281,7 @@ const makeDevtoolsOptions = ({
236
281
  devtoolsEnabled: boolean
237
282
  dbState: SqliteDb
238
283
  dbEventlog: SqliteDb
239
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope | WebmeshWorker.CacheService> =>
284
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope | WebmeshWorker.CacheService> =>
240
285
  Effect.gen(function* () {
241
286
  if (devtoolsEnabled === false) {
242
287
  return { enabled: false }
@@ -246,13 +291,13 @@ const makeDevtoolsOptions = ({
246
291
 
247
292
  return {
248
293
  enabled: true,
249
- boot: Effect.gen(function* () {
250
- const persistenceInfo = {
294
+ boot: Effect.succeed({
295
+ node,
296
+ persistenceInfo: {
251
297
  state: dbState.metadata.persistenceInfo,
252
298
  eventlog: dbEventlog.metadata.persistenceInfo,
253
- }
254
-
255
- return { node, persistenceInfo, mode: 'direct' }
299
+ },
300
+ mode: 'direct' as const,
256
301
  }),
257
302
  }
258
303
  })
@@ -1,18 +1,14 @@
1
- import { Devtools, UnexpectedError } from '@livestore/common'
1
+ import { Devtools, LogConfig, liveStoreVersion, UnknownError } from '@livestore/common'
2
2
  import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
3
3
  import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
4
4
  import { isDevEnv, isNotUndefined, LS_DEV } from '@livestore/utils'
5
5
  import {
6
- BrowserWorker,
7
- BrowserWorkerRunner,
8
6
  Deferred,
9
7
  Effect,
10
8
  Exit,
11
9
  FetchHttpClient,
12
10
  identity,
13
11
  Layer,
14
- Logger,
15
- LogLevel,
16
12
  ParseResult,
17
13
  Ref,
18
14
  Schema,
@@ -24,10 +20,25 @@ import {
24
20
  WorkerError,
25
21
  WorkerRunner,
26
22
  } from '@livestore/utils/effect'
23
+ import { BrowserWorker, BrowserWorkerRunner } from '@livestore/utils/effect/browser'
27
24
 
28
25
  import { makeShutdownChannel } from '../common/shutdown-channel.ts'
29
26
  import * as WorkerSchema from '../common/worker-schema.ts'
30
27
 
28
+ // Extract from `livestore-shared-worker-${storeId}`
29
+ const storeId = self.name.replace('livestore-shared-worker-', '')
30
+
31
+ // We acquire a lock that is held as long as this shared worker is alive.
32
+ // This way, when the shared worker is terminated (e.g. by the browser when the page is closed),
33
+ // the lock is released and any thread waiting for the lock can be notified.
34
+ const LIVESTORE_SHARED_WORKER_TERMINATION_LOCK = `livestore-shared-worker-termination-lock-${storeId}`
35
+ navigator.locks.request(
36
+ LIVESTORE_SHARED_WORKER_TERMINATION_LOCK,
37
+ { steal: true },
38
+ // We use a never-resolving promise to hold the lock
39
+ async () => new Promise(() => {}),
40
+ )
41
+
31
42
  if (isDevEnv()) {
32
43
  globalThis.__debugLiveStoreUtils = {
33
44
  blobUrl: (buffer: Uint8Array<ArrayBuffer>) =>
@@ -46,22 +57,21 @@ const makeWorkerRunner = Effect.gen(function* () {
46
57
  | undefined
47
58
  >(undefined)
48
59
 
49
- const initialMessagePayloadDeferredRef = yield* Deferred.make<
50
- typeof WorkerSchema.SharedWorkerInitialMessagePayloadFromClientSession.Type
51
- >().pipe(Effect.andThen(Ref.make))
52
-
53
60
  const waitForWorker = SubscriptionRef.waitUntil(leaderWorkerContextSubRef, isNotUndefined).pipe(
54
61
  Effect.map((_) => _.worker),
55
62
  )
56
63
 
57
64
  const forwardRequest = <TReq extends WorkerSchema.LeaderWorkerInnerRequest>(
58
65
  req: TReq,
59
- ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer _R>
60
- ? Effect.Effect<A, UnexpectedError, never>
61
- : never =>
66
+ ): Effect.Effect<
67
+ Schema.WithResult.Success<TReq>,
68
+ UnknownError | Schema.WithResult.Failure<TReq>,
69
+ Schema.WithResult.Context<TReq>
70
+ > =>
71
+ // Forward the request to the active worker and normalize platform errors into UnknownError.
62
72
  waitForWorker.pipe(
63
73
  // Effect.logBefore(`forwardRequest: ${req._tag}`),
64
- Effect.andThen((worker) => worker.executeEffect(req) as Effect.Effect<unknown, unknown, never>),
74
+ Effect.andThen((worker) => worker.executeEffect(req) as Effect.Effect<unknown, unknown, unknown>),
65
75
  // Effect.tap((_) => Effect.log(`forwardRequest: ${req._tag}`, _)),
66
76
  // Effect.tapError((cause) => Effect.logError(`forwardRequest err: ${req._tag}`, cause)),
67
77
  Effect.interruptible,
@@ -70,25 +80,31 @@ const makeWorkerRunner = Effect.gen(function* () {
70
80
  duration: 500,
71
81
  }),
72
82
  Effect.mapError((cause) =>
73
- Schema.is(UnexpectedError)(cause)
83
+ Schema.is(UnknownError)(cause)
74
84
  ? cause
75
85
  : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
76
- ? new UnexpectedError({ cause })
86
+ ? new UnknownError({ cause })
77
87
  : cause,
78
88
  ),
79
- Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
89
+ Effect.catchAllDefect((cause) => new UnknownError({ cause })),
80
90
  Effect.tapCauseLogPretty,
81
- ) as any
91
+ ) as Effect.Effect<
92
+ Schema.WithResult.Success<TReq>,
93
+ UnknownError | Schema.WithResult.Failure<TReq>,
94
+ Schema.WithResult.Context<TReq>
95
+ >
82
96
 
83
97
  const forwardRequestStream = <TReq extends WorkerSchema.LeaderWorkerInnerRequest>(
84
98
  req: TReq,
85
- ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer _R>
86
- ? Stream.Stream<A, UnexpectedError, never>
87
- : never =>
99
+ ): Stream.Stream<
100
+ Schema.WithResult.Success<TReq>,
101
+ UnknownError | Schema.WithResult.Failure<TReq>,
102
+ Schema.WithResult.Context<TReq>
103
+ > =>
88
104
  Effect.gen(function* () {
89
105
  yield* Effect.logDebug(`forwardRequestStream: ${req._tag}`)
90
106
  const { worker, scope } = yield* SubscriptionRef.waitUntil(leaderWorkerContextSubRef, isNotUndefined)
91
- const stream = worker.execute(req) as Stream.Stream<unknown, unknown, never>
107
+ const stream = worker.execute(req) as Stream.Stream<unknown, unknown, unknown>
92
108
 
93
109
  // It seems the request stream is not automatically interrupted when the scope shuts down
94
110
  // so we need to manually interrupt it when the scope shuts down
@@ -104,12 +120,16 @@ const makeWorkerRunner = Effect.gen(function* () {
104
120
  return Stream.merge(stream, scopeShutdownStream, { haltStrategy: 'either' })
105
121
  }).pipe(
106
122
  Effect.interruptible,
107
- UnexpectedError.mapToUnexpectedError,
123
+ UnknownError.mapToUnknownError,
108
124
  Effect.tapCauseLogPretty,
109
125
  Stream.unwrap,
110
126
  Stream.ensuring(Effect.logDebug(`shutting down stream for ${req._tag}`)),
111
- UnexpectedError.mapToUnexpectedErrorStream,
112
- ) as any
127
+ UnknownError.mapToUnknownErrorStream,
128
+ ) as Stream.Stream<
129
+ Schema.WithResult.Success<TReq>,
130
+ UnknownError | Schema.WithResult.Failure<TReq>,
131
+ Schema.WithResult.Context<TReq>
132
+ >
113
133
 
114
134
  const resetCurrentWorkerCtx = Effect.gen(function* () {
115
135
  const prevWorker = yield* SubscriptionRef.get(leaderWorkerContextSubRef)
@@ -128,62 +148,59 @@ const makeWorkerRunner = Effect.gen(function* () {
128
148
  }
129
149
  }).pipe(Effect.withSpan('@livestore/adapter-web:shared-worker:resetCurrentWorkerCtx'))
130
150
 
131
- // const devtoolsWebBridge = yield* makeDevtoolsWebBridge
132
-
133
151
  const reset = Effect.gen(function* () {
134
152
  yield* Effect.logDebug('reset')
135
-
136
- const initialMessagePayloadDeferred =
137
- yield* Deferred.make<typeof WorkerSchema.SharedWorkerInitialMessagePayloadFromClientSession.Type>()
138
- yield* Ref.set(initialMessagePayloadDeferredRef, initialMessagePayloadDeferred)
139
-
153
+ // Clear cached invariants so a fresh configuration can be accepted after shutdown
154
+ yield* Ref.set(invariantsRef, undefined)
155
+ // Tear down current leader worker context
140
156
  yield* resetCurrentWorkerCtx
141
- // yield* devtoolsWebBridge.reset
142
157
  })
143
158
 
144
- return WorkerRunner.layerSerialized(WorkerSchema.SharedWorkerRequest, {
145
- InitialMessage: (message) =>
146
- Effect.gen(function* () {
147
- if (message.payload._tag === 'FromWebBridge') return
148
-
149
- const initialMessagePayloadDeferred = yield* Ref.get(initialMessagePayloadDeferredRef)
150
- const deferredAlreadyDone = yield* Deferred.isDone(initialMessagePayloadDeferred)
151
- const initialMessage = message.payload.initialMessage
152
-
153
- if (deferredAlreadyDone) {
154
- const previousInitialMessage = yield* Deferred.await(initialMessagePayloadDeferred)
155
- const messageSchema = WorkerSchema.LeaderWorkerInnerInitialMessage.pipe(
156
- Schema.omit('devtoolsEnabled', 'debugInstanceId'),
157
- )
158
- const isEqual = Schema.equivalence(messageSchema)
159
- if (isEqual(initialMessage, previousInitialMessage.initialMessage) === false) {
160
- const diff = Schema.debugDiff(messageSchema)(previousInitialMessage.initialMessage, initialMessage)
159
+ // Cache first-applied invariants to enforce stability across leader transitions
160
+ const InvariantsSchema = Schema.Struct({
161
+ storeId: Schema.String,
162
+ storageOptions: WorkerSchema.StorageType,
163
+ syncPayloadEncoded: Schema.UndefinedOr(Schema.JsonValue),
164
+ liveStoreVersion: Schema.Literal(liveStoreVersion),
165
+ devtoolsEnabled: Schema.Boolean,
166
+ })
167
+ type Invariants = typeof InvariantsSchema.Type
168
+ const invariantsRef = yield* Ref.make<Invariants | undefined>(undefined)
169
+ const sameInvariants = Schema.equivalence(InvariantsSchema)
161
170
 
162
- return yield* new UnexpectedError({
163
- cause: 'Initial message already sent and was different now',
164
- payload: {
165
- diff,
166
- previousInitialMessage: previousInitialMessage.initialMessage,
167
- newInitialMessage: initialMessage,
168
- },
169
- })
170
- }
171
- } else {
172
- yield* Deferred.succeed(initialMessagePayloadDeferred, message.payload)
173
- }
174
- }),
171
+ return WorkerRunner.layerSerialized(WorkerSchema.SharedWorkerRequest, {
175
172
  // Whenever the client session leader changes (and thus creates a new leader thread), the new client session leader
176
173
  // sends a new MessagePort to the shared worker which proxies messages to the new leader thread.
177
- UpdateMessagePort: ({ port }) =>
174
+ UpdateMessagePort: ({ port, initial, liveStoreVersion: clientLiveStoreVersion }) =>
178
175
  Effect.gen(function* () {
179
- const initialMessagePayload = yield* initialMessagePayloadDeferredRef.get.pipe(Effect.andThen(Deferred.await))
176
+ // Enforce invariants: storeId, storageOptions, syncPayloadEncoded, liveStoreVersion must remain stable
177
+ const invariants: Invariants = {
178
+ storeId: initial.storeId,
179
+ storageOptions: initial.storageOptions,
180
+ syncPayloadEncoded: initial.syncPayloadEncoded,
181
+ liveStoreVersion: clientLiveStoreVersion,
182
+ devtoolsEnabled: initial.devtoolsEnabled,
183
+ }
184
+ const prev = yield* Ref.get(invariantsRef)
185
+ // Early return on mismatch to keep happy path linear
186
+ if (prev !== undefined && !sameInvariants(prev, invariants)) {
187
+ const diff = Schema.debugDiff(InvariantsSchema)(prev, invariants)
188
+ return yield* new UnknownError({
189
+ cause: 'Store invariants changed across leader transitions',
190
+ payload: { diff, previous: prev, next: invariants },
191
+ })
192
+ }
193
+ // First writer records invariants
194
+ if (prev === undefined) {
195
+ yield* Ref.set(invariantsRef, invariants)
196
+ }
180
197
 
181
198
  yield* resetCurrentWorkerCtx
182
199
 
183
200
  const scope = yield* Scope.make()
184
201
 
185
202
  yield* Effect.gen(function* () {
186
- const shutdownChannel = yield* makeShutdownChannel(initialMessagePayload.initialMessage.storeId)
203
+ const shutdownChannel = yield* makeShutdownChannel(initial.storeId)
187
204
 
188
205
  yield* shutdownChannel.listen.pipe(
189
206
  Stream.flatten(),
@@ -198,7 +215,7 @@ const makeWorkerRunner = Effect.gen(function* () {
198
215
  const worker = yield* Worker.makePoolSerialized<WorkerSchema.LeaderWorkerInnerRequest>({
199
216
  size: 1,
200
217
  concurrency: 100,
201
- initialMessage: () => initialMessagePayload.initialMessage,
218
+ initialMessage: () => initial,
202
219
  }).pipe(
203
220
  Effect.provide(workerLayer),
204
221
  Effect.withSpan('@livestore/adapter-web:shared-worker:makeWorkerProxyFromPort'),
@@ -206,7 +223,7 @@ const makeWorkerRunner = Effect.gen(function* () {
206
223
 
207
224
  // Prepare the web mesh connection for leader worker to be able to connect to the devtools
208
225
  const { node } = yield* WebmeshWorker.CacheService
209
- const { storeId, clientId } = initialMessagePayload.initialMessage
226
+ const { storeId, clientId } = initial
210
227
 
211
228
  yield* DevtoolsWeb.connectViaWorker({
212
229
  node,
@@ -218,7 +235,7 @@ const makeWorkerRunner = Effect.gen(function* () {
218
235
  }).pipe(Effect.tapCauseLogPretty, Scope.extend(scope), Effect.forkIn(scope))
219
236
  }).pipe(
220
237
  Effect.withSpan('@livestore/adapter-web:shared-worker:updateMessagePort'),
221
- UnexpectedError.mapToUnexpectedError,
238
+ UnknownError.mapToUnknownError,
222
239
  Effect.tapCauseLogPretty,
223
240
  ),
224
241
 
@@ -226,12 +243,16 @@ const makeWorkerRunner = Effect.gen(function* () {
226
243
  BootStatusStream: forwardRequestStream,
227
244
  PushToLeader: forwardRequest,
228
245
  PullStream: forwardRequestStream,
246
+ StreamEvents: forwardRequestStream,
229
247
  Export: forwardRequest,
230
248
  GetRecreateSnapshot: forwardRequest,
231
249
  ExportEventlog: forwardRequest,
232
250
  Setup: forwardRequest,
233
251
  GetLeaderSyncState: forwardRequest,
252
+ SyncStateStream: forwardRequestStream,
234
253
  GetLeaderHead: forwardRequest,
254
+ GetNetworkStatus: forwardRequest,
255
+ NetworkStatusStream: forwardRequestStream,
235
256
  Shutdown: forwardRequest,
236
257
  ExtraDevtoolsMessage: forwardRequest,
237
258
 
@@ -240,9 +261,11 @@ const makeWorkerRunner = Effect.gen(function* () {
240
261
  })
241
262
  }).pipe(Layer.unwrapScoped)
242
263
 
243
- export const makeWorker = () => {
244
- // Extract from `livestore-shared-worker-${storeId}`
245
- const storeId = self.name.replace('livestore-shared-worker-', '')
264
+ export const makeWorker = (options?: LogConfig.WithLoggerOptions): void => {
265
+ const runtimeLayer = Layer.mergeAll(
266
+ FetchHttpClient.layer,
267
+ WebmeshWorker.CacheService.layer({ nodeName: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }) }),
268
+ )
246
269
 
247
270
  makeWorkerRunner.pipe(
248
271
  Layer.provide(BrowserWorkerRunner.layer),
@@ -251,13 +274,11 @@ export const makeWorker = () => {
251
274
  Effect.scoped,
252
275
  Effect.tapCauseLogPretty,
253
276
  Effect.annotateLogs({ thread: self.name }),
254
- Effect.provide(Logger.prettyWithThread(self.name)),
255
- Effect.provide(FetchHttpClient.layer),
256
- Effect.provide(WebmeshWorker.CacheService.layer({ nodeName: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }) })),
277
+ Effect.provide(runtimeLayer),
257
278
  LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
258
279
  // TODO remove type-cast (currently needed to silence a tsc bug)
259
280
  (_) => _ as any as Effect.Effect<void, any>,
260
- Logger.withMinimumLogLevel(LogLevel.Debug),
281
+ LogConfig.withLoggerConfig(options, { threadName: self.name }),
261
282
  Effect.runFork,
262
283
  )
263
284
  }
@@ -5,6 +5,7 @@ globalThis.$RefreshReg$ = () => {}
5
5
  // @ts-expect-error TODO remove when Vite does proper treeshaking during dev
6
6
  globalThis.$RefreshSig$ = () => (type: any) => type
7
7
 
8
+ // biome-ignore lint/suspicious/noTsIgnore: sometimes @types/node is there, sometimes not.
8
9
  // @ts-ignore
9
10
  globalThis.process = globalThis.process ?? { env: {} }
10
11
 
@@ -1,5 +0,0 @@
1
- export declare const rootHandlePromise: Promise<FileSystemDirectoryHandle>;
2
- export declare const getDirHandle: (absDirPath: string | undefined) => Promise<FileSystemDirectoryHandle>;
3
- export declare const printTree: (directoryHandle_?: FileSystemDirectoryHandle | Promise<FileSystemDirectoryHandle>, depth?: number, prefix?: string) => Promise<void>;
4
- export declare const deleteAll: (directoryHandle: FileSystemDirectoryHandle) => Promise<void>;
5
- //# sourceMappingURL=opfs-utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"opfs-utils.d.ts","sourceRoot":"","sources":["../src/opfs-utils.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB,oCAYQ,CAAA;AAEtC,eAAO,MAAM,YAAY,GAAU,YAAY,MAAM,GAAG,SAAS,uCAWhE,CAAA;AAED,eAAO,MAAM,SAAS,GACpB,mBAAkB,yBAAyB,GAAG,OAAO,CAAC,yBAAyB,CAAqB,EACpG,QAAO,MAAiC,EACxC,eAAW,KACV,OAAO,CAAC,IAAI,CAgBd,CAAA;AAED,eAAO,MAAM,SAAS,GAAU,iBAAiB,yBAAyB,kBAMzE,CAAA"}
@@ -1,43 +0,0 @@
1
- // NOTE we're already firing off this promise call here since we'll need it anyway and need it cached
2
- import { prettyBytes } from '@livestore/utils';
3
- // To improve LiveStore compatibility with e.g. Node.js we're guarding for `navigator` / `navigator.storage` to be defined.
4
- export const rootHandlePromise = typeof navigator === 'undefined' || navigator.storage === undefined
5
- ? // We're using a proxy here to make the promise reject lazy
6
- new Proxy({}, {
7
- get: () => Promise.reject(new Error(`Can't get OPFS root handle in this environment as navigator.storage is undefined`)),
8
- })
9
- : navigator.storage.getDirectory();
10
- export const getDirHandle = async (absDirPath) => {
11
- const rootHandle = await rootHandlePromise;
12
- if (absDirPath === undefined)
13
- return rootHandle;
14
- let dirHandle = rootHandle;
15
- const directoryStack = absDirPath?.split('/').filter(Boolean);
16
- while (directoryStack.length > 0) {
17
- dirHandle = await dirHandle.getDirectoryHandle(directoryStack.shift());
18
- }
19
- return dirHandle;
20
- };
21
- export const printTree = async (directoryHandle_ = rootHandlePromise, depth = Number.POSITIVE_INFINITY, prefix = '') => {
22
- if (depth < 0)
23
- return;
24
- const directoryHandle = await directoryHandle_;
25
- const entries = directoryHandle.values();
26
- for await (const entry of entries) {
27
- const isDirectory = entry.kind === 'directory';
28
- const size = entry.kind === 'file' ? await entry.getFile().then((file) => prettyBytes(file.size)) : undefined;
29
- console.log(`${prefix}${isDirectory ? '📁' : '📄'} ${entry.name} ${size ? `(${size})` : ''}`);
30
- if (isDirectory) {
31
- const nestedDirectoryHandle = await directoryHandle.getDirectoryHandle(entry.name);
32
- await printTree(nestedDirectoryHandle, depth - 1, `${prefix} `);
33
- }
34
- }
35
- };
36
- export const deleteAll = async (directoryHandle) => {
37
- if (directoryHandle.kind !== 'directory')
38
- return;
39
- for await (const entryName of directoryHandle.keys()) {
40
- await directoryHandle.removeEntry(entryName, { recursive: true });
41
- }
42
- };
43
- //# sourceMappingURL=opfs-utils.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"opfs-utils.js","sourceRoot":"","sources":["../src/opfs-utils.ts"],"names":[],"mappings":"AAAA,qGAAqG;AAErG,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,2HAA2H;AAC3H,MAAM,CAAC,MAAM,iBAAiB,GAC5B,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,OAAO,KAAK,SAAS;IACjE,CAAC,CAAC,2DAA2D;QAC1D,IAAI,KAAK,CACR,EAAE,EACF;YACE,GAAG,EAAE,GAAG,EAAE,CACR,OAAO,CAAC,MAAM,CACZ,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAC9F;SACJ,CACQ;IACb,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;AAEtC,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,UAA8B,EAAE,EAAE;IACnE,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAA;IAC1C,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAA;IAE/C,IAAI,SAAS,GAAG,UAAU,CAAA;IAC1B,MAAM,cAAc,GAAG,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC7D,OAAO,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,SAAS,GAAG,MAAM,SAAS,CAAC,kBAAkB,CAAC,cAAc,CAAC,KAAK,EAAG,CAAC,CAAA;IACzE,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,mBAAmF,iBAAiB,EACpG,QAAgB,MAAM,CAAC,iBAAiB,EACxC,MAAM,GAAG,EAAE,EACI,EAAE;IACjB,IAAI,KAAK,GAAG,CAAC;QAAE,OAAM;IAErB,MAAM,eAAe,GAAG,MAAM,gBAAgB,CAAA;IAC9C,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,EAAE,CAAA;IAExC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAClC,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,KAAK,WAAW,CAAA;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7G,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;QAE7F,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,qBAAqB,GAAG,MAAM,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAClF,MAAM,SAAS,CAAC,qBAAqB,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,MAAM,IAAI,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,eAA0C,EAAE,EAAE;IAC5E,IAAI,eAAe,CAAC,IAAI,KAAK,WAAW;QAAE,OAAM;IAEhD,IAAI,KAAK,EAAE,MAAM,SAAS,IAAI,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC;QACrD,MAAM,eAAe,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnE,CAAC;AACH,CAAC,CAAA"}