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

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 +15 -5
  3. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  4. package/dist/in-memory/in-memory-adapter.js +29 -15
  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 +67 -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 +114 -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 +103 -58
  31. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  32. package/dist/web-worker/common/worker-schema.js +48 -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 +47 -21
  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 +65 -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 +36 -20
  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 +112 -59
  49. package/src/web-worker/client-session/sqlite-loader.ts +19 -0
  50. package/src/web-worker/common/persisted-sqlite.ts +219 -113
  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 +62 -35
  54. package/src/web-worker/leader-worker/make-leader-worker.ts +58 -33
  55. package/src/web-worker/shared-worker/make-shared-worker.ts +95 -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,5 +1,5 @@
1
1
  import type { SqliteDb, SyncOptions } from '@livestore/common'
2
- import { Devtools, UnexpectedError } from '@livestore/common'
2
+ import { Devtools, LogConfig, UnknownError } from '@livestore/common'
3
3
  import type { DevtoolsOptions } from '@livestore/common/leader-thread'
4
4
  import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
5
5
  import type { LiveStoreSchema } from '@livestore/common/schema'
@@ -10,37 +10,36 @@ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
10
10
  import { isDevEnv, LS_DEV } from '@livestore/utils'
11
11
  import type { HttpClient, Scope, WorkerError } from '@livestore/utils/effect'
12
12
  import {
13
- BrowserWorkerRunner,
14
13
  Effect,
15
14
  FetchHttpClient,
16
15
  identity,
17
16
  Layer,
18
- Logger,
19
- LogLevel,
20
17
  OtelTracer,
21
18
  Scheduler,
19
+ type Schema,
22
20
  Stream,
23
21
  TaskTracing,
24
22
  WorkerRunner,
25
23
  } from '@livestore/utils/effect'
24
+ import { BrowserWorkerRunner, Opfs } from '@livestore/utils/effect/browser'
26
25
  import type * as otel from '@opentelemetry/api'
27
26
 
28
- import * as OpfsUtils from '../../opfs-utils.ts'
29
- import { getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
27
+ import { cleanupOldStateDbFiles, getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
30
28
  import { makeShutdownChannel } from '../common/shutdown-channel.ts'
31
29
  import * as WorkerSchema from '../common/worker-schema.ts'
32
30
 
33
31
  export type WorkerOptions = {
34
32
  schema: LiveStoreSchema
35
33
  sync?: SyncOptions
34
+ syncPayloadSchema?: Schema.Schema<any>
36
35
  otelOptions?: {
37
36
  tracer?: otel.Tracer
38
37
  }
39
- }
38
+ } & LogConfig.WithLoggerOptions
40
39
 
41
40
  if (isDevEnv()) {
42
41
  globalThis.__debugLiveStoreUtils = {
43
- opfs: OpfsUtils,
42
+ opfs: Opfs.debugUtils,
44
43
  blobUrl: (buffer: Uint8Array<ArrayBuffer>) =>
45
44
  URL.createObjectURL(new Blob([buffer], { type: 'application/octet-stream' })),
46
45
  runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
@@ -59,23 +58,23 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
59
58
  )
60
59
  : undefined
61
60
 
61
+ const runtimeLayer = Layer.mergeAll(FetchHttpClient.layer, TracingLive ?? Layer.empty)
62
+
62
63
  return makeWorkerRunnerOuter(options).pipe(
63
64
  Layer.provide(BrowserWorkerRunner.layer),
64
65
  WorkerRunner.launch,
65
66
  Effect.scoped,
66
67
  Effect.tapCauseLogPretty,
67
68
  Effect.annotateLogs({ thread: self.name }),
68
- Effect.provide(Logger.prettyWithThread(self.name)),
69
- Effect.provide(FetchHttpClient.layer),
69
+ Effect.provide(runtimeLayer),
70
70
  LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
71
- TracingLive ? Effect.provide(TracingLive) : identity,
72
71
  // We're using this custom scheduler to improve op batching behaviour and reduce the overhead
73
72
  // of the Effect fiber runtime given we have different tradeoffs on a worker thread.
74
73
  // Despite the "message channel" name, is has nothing to do with the `incomingRequestsPort` above.
75
74
  Effect.withScheduler(Scheduler.messageChannel()),
76
75
  // We're increasing the Effect ops limit here to allow for larger chunks of operations at a time
77
76
  Effect.withMaxOpsBeforeYield(4096),
78
- Logger.withMinimumLogLevel(LogLevel.Debug),
77
+ LogConfig.withLoggerConfig({ logger: options.logger, logLevel: options.logLevel }, { threadName: self.name }),
79
78
  )
80
79
  }
81
80
 
@@ -93,7 +92,12 @@ const makeWorkerRunnerOuter = (
93
92
  Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage:innerFiber'),
94
93
  Effect.tapCauseLogPretty,
95
94
  Effect.provide(
96
- WebmeshWorker.CacheService.layer({ nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }) }),
95
+ Layer.mergeAll(
96
+ Opfs.Opfs.Default,
97
+ WebmeshWorker.CacheService.layer({
98
+ nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }),
99
+ }),
100
+ ),
97
101
  ),
98
102
  Effect.forkScoped,
99
103
  )
@@ -102,18 +106,19 @@ const makeWorkerRunnerOuter = (
102
106
  }).pipe(Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage'), Layer.unwrapScoped),
103
107
  })
104
108
 
105
- const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
109
+ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }: WorkerOptions) =>
106
110
  WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
107
- InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayload }) =>
111
+ InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayloadEncoded }) =>
108
112
  Effect.gen(function* () {
109
113
  const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
110
114
  const makeSqliteDb = sqliteDbFactory({ sqlite3 })
115
+ const opfsDirectory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
111
116
  const runtime = yield* Effect.runtime<never>()
112
117
 
113
118
  const makeDb = (kind: 'state' | 'eventlog') =>
114
119
  makeSqliteDb({
115
120
  _tag: 'opfs',
116
- opfsDirectory: sanitizeOpfsDir(storageOptions.directory, storeId),
121
+ opfsDirectory,
117
122
  fileName: kind === 'state' ? getStateDbFileName(schema) : 'eventlog.db',
118
123
  configureDb: (db) =>
119
124
  configureConnection(db, {
@@ -131,6 +136,16 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
131
136
  concurrency: 2,
132
137
  })
133
138
 
139
+ // Clean up old state database files after successful database creation
140
+ // This prevents OPFS file pool capacity exhaustion from accumulated state db files after schema changes/migrations
141
+ if (dbState.metadata._tag === 'opfs') {
142
+ yield* cleanupOldStateDbFiles({
143
+ vfs: dbState.metadata.vfs,
144
+ currentSchema: schema,
145
+ opfsDirectory: dbState.metadata.persistenceInfo.opfsDirectory,
146
+ })
147
+ }
148
+
134
149
  const devtoolsOptions = yield* makeDevtoolsOptions({ devtoolsEnabled, dbState, dbEventlog })
135
150
  const shutdownChannel = yield* makeShutdownChannel(storeId)
136
151
 
@@ -144,11 +159,12 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
144
159
  dbEventlog,
145
160
  devtoolsOptions,
146
161
  shutdownChannel,
147
- syncPayload,
162
+ syncPayloadEncoded,
163
+ syncPayloadSchema,
148
164
  })
149
165
  }).pipe(
150
166
  Effect.tapCauseLogPretty,
151
- UnexpectedError.mapToUnexpectedError,
167
+ UnknownError.mapToUnknownError,
152
168
  Effect.withPerformanceMeasure('@livestore/adapter-web:worker:InitialMessage'),
153
169
  Effect.withSpan('@livestore/adapter-web:worker:InitialMessage'),
154
170
  Effect.annotateSpans({ debugInstanceId }),
@@ -166,10 +182,7 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
166
182
 
167
183
  const snapshot = workerCtx.dbState.export()
168
184
  return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
169
- }).pipe(
170
- UnexpectedError.mapToUnexpectedError,
171
- Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot'),
172
- ),
185
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot')),
173
186
  PullStream: ({ cursor }) =>
174
187
  Effect.gen(function* () {
175
188
  const { syncProcessor } = yield* LeaderThreadCtx
@@ -182,19 +195,19 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
182
195
  PushToLeader: ({ batch }) =>
183
196
  Effect.andThen(LeaderThreadCtx, ({ syncProcessor }) =>
184
197
  syncProcessor.push(
185
- batch.map((event) => new LiveStoreEvent.EncodedWithMeta(event)),
198
+ batch.map((event) => new LiveStoreEvent.Client.EncodedWithMeta(event)),
186
199
  // We'll wait in order to keep back pressure on the client session
187
200
  { waitForProcessing: true },
188
201
  ),
189
202
  ).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-web:worker:PushToLeader')),
190
203
  Export: () =>
191
204
  Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
192
- UnexpectedError.mapToUnexpectedError,
205
+ UnknownError.mapToUnknownError,
193
206
  Effect.withSpan('@livestore/adapter-web:worker:Export'),
194
207
  ),
195
208
  ExportEventlog: () =>
196
209
  Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
197
- UnexpectedError.mapToUnexpectedError,
210
+ UnknownError.mapToUnknownError,
198
211
  Effect.withSpan('@livestore/adapter-web:worker:ExportEventlog'),
199
212
  ),
200
213
  BootStatusStream: () =>
@@ -203,15 +216,27 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
203
216
  Effect.gen(function* () {
204
217
  const workerCtx = yield* LeaderThreadCtx
205
218
  return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
206
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
219
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
207
220
  GetLeaderSyncState: () =>
208
221
  Effect.gen(function* () {
209
222
  const workerCtx = yield* LeaderThreadCtx
210
223
  return yield* workerCtx.syncProcessor.syncState
211
- }).pipe(
212
- UnexpectedError.mapToUnexpectedError,
213
- Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState'),
214
- ),
224
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState')),
225
+ SyncStateStream: () =>
226
+ Effect.gen(function* () {
227
+ const workerCtx = yield* LeaderThreadCtx
228
+ return workerCtx.syncProcessor.syncState.changes
229
+ }).pipe(Stream.unwrapScoped),
230
+ GetNetworkStatus: () =>
231
+ Effect.gen(function* () {
232
+ const workerCtx = yield* LeaderThreadCtx
233
+ return yield* workerCtx.networkStatus
234
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetNetworkStatus')),
235
+ NetworkStatusStream: () =>
236
+ Effect.gen(function* () {
237
+ const workerCtx = yield* LeaderThreadCtx
238
+ return workerCtx.networkStatus.changes
239
+ }).pipe(Stream.unwrapScoped),
215
240
  Shutdown: () =>
216
241
  Effect.gen(function* () {
217
242
  yield* Effect.logDebug('[@livestore/adapter-web:worker] Shutdown')
@@ -219,10 +244,10 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
219
244
  // Buy some time for Otel to flush
220
245
  // TODO find a cleaner way to do this
221
246
  yield* Effect.sleep(300)
222
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:Shutdown')),
247
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:Shutdown')),
223
248
  ExtraDevtoolsMessage: ({ message }) =>
224
249
  Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
225
- UnexpectedError.mapToUnexpectedError,
250
+ UnknownError.mapToUnknownError,
226
251
  Effect.withSpan('@livestore/adapter-web:worker:ExtraDevtoolsMessage'),
227
252
  ),
228
253
  'DevtoolsWebCommon.CreateConnection': WebmeshWorker.CreateConnection,
@@ -236,7 +261,7 @@ const makeDevtoolsOptions = ({
236
261
  devtoolsEnabled: boolean
237
262
  dbState: SqliteDb
238
263
  dbEventlog: SqliteDb
239
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope | WebmeshWorker.CacheService> =>
264
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope | WebmeshWorker.CacheService> =>
240
265
  Effect.gen(function* () {
241
266
  if (devtoolsEnabled === false) {
242
267
  return { enabled: false }
@@ -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
 
@@ -231,7 +248,10 @@ const makeWorkerRunner = Effect.gen(function* () {
231
248
  ExportEventlog: forwardRequest,
232
249
  Setup: forwardRequest,
233
250
  GetLeaderSyncState: forwardRequest,
251
+ SyncStateStream: forwardRequestStream,
234
252
  GetLeaderHead: forwardRequest,
253
+ GetNetworkStatus: forwardRequest,
254
+ NetworkStatusStream: forwardRequestStream,
235
255
  Shutdown: forwardRequest,
236
256
  ExtraDevtoolsMessage: forwardRequest,
237
257
 
@@ -240,9 +260,11 @@ const makeWorkerRunner = Effect.gen(function* () {
240
260
  })
241
261
  }).pipe(Layer.unwrapScoped)
242
262
 
243
- export const makeWorker = () => {
244
- // Extract from `livestore-shared-worker-${storeId}`
245
- const storeId = self.name.replace('livestore-shared-worker-', '')
263
+ export const makeWorker = (options?: LogConfig.WithLoggerOptions): void => {
264
+ const runtimeLayer = Layer.mergeAll(
265
+ FetchHttpClient.layer,
266
+ WebmeshWorker.CacheService.layer({ nodeName: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }) }),
267
+ )
246
268
 
247
269
  makeWorkerRunner.pipe(
248
270
  Layer.provide(BrowserWorkerRunner.layer),
@@ -251,13 +273,11 @@ export const makeWorker = () => {
251
273
  Effect.scoped,
252
274
  Effect.tapCauseLogPretty,
253
275
  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 }) })),
276
+ Effect.provide(runtimeLayer),
257
277
  LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
258
278
  // TODO remove type-cast (currently needed to silence a tsc bug)
259
279
  (_) => _ as any as Effect.Effect<void, any>,
260
- Logger.withMinimumLogLevel(LogLevel.Debug),
280
+ LogConfig.withLoggerConfig(options, { threadName: self.name }),
261
281
  Effect.runFork,
262
282
  )
263
283
  }
@@ -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"}