@livestore/adapter-web 0.4.0-dev.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +5 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/in-memory/in-memory-adapter.d.ts +49 -5
  4. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  5. package/dist/in-memory/in-memory-adapter.js +77 -20
  6. package/dist/in-memory/in-memory-adapter.js.map +1 -1
  7. package/dist/index.d.ts +11 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +11 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/single-tab/mod.d.ts +15 -0
  12. package/dist/single-tab/mod.d.ts.map +1 -0
  13. package/dist/single-tab/mod.js +15 -0
  14. package/dist/single-tab/mod.js.map +1 -0
  15. package/dist/single-tab/single-tab-adapter.d.ts +108 -0
  16. package/dist/single-tab/single-tab-adapter.d.ts.map +1 -0
  17. package/dist/single-tab/single-tab-adapter.js +271 -0
  18. package/dist/single-tab/single-tab-adapter.js.map +1 -0
  19. package/dist/web-worker/client-session/client-session-devtools.d.ts +2 -2
  20. package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
  21. package/dist/web-worker/client-session/client-session-devtools.js +20 -9
  22. package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
  23. package/dist/web-worker/client-session/persisted-adapter.d.ts +18 -0
  24. package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
  25. package/dist/web-worker/client-session/persisted-adapter.js +141 -67
  26. package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
  27. package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
  28. package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
  29. package/dist/web-worker/client-session/sqlite-loader.js +16 -0
  30. package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
  31. package/dist/web-worker/common/persisted-sqlite.d.ts +13 -20
  32. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
  33. package/dist/web-worker/common/persisted-sqlite.js +95 -102
  34. package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
  35. package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
  36. package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
  37. package/dist/web-worker/common/shutdown-channel.js +2 -2
  38. package/dist/web-worker/common/shutdown-channel.js.map +1 -1
  39. package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
  40. package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
  41. package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
  42. package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
  43. package/dist/web-worker/common/worker-schema.d.ts +152 -58
  44. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  45. package/dist/web-worker/common/worker-schema.js +55 -37
  46. package/dist/web-worker/common/worker-schema.js.map +1 -1
  47. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +5 -3
  48. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
  49. package/dist/web-worker/leader-worker/make-leader-worker.js +99 -38
  50. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
  51. package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -1
  52. package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
  53. package/dist/web-worker/shared-worker/make-shared-worker.js +62 -52
  54. package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
  55. package/package.json +56 -18
  56. package/src/in-memory/in-memory-adapter.ts +92 -26
  57. package/src/index.ts +15 -1
  58. package/src/single-tab/mod.ts +15 -0
  59. package/src/single-tab/single-tab-adapter.ts +499 -0
  60. package/src/web-worker/ambient.d.ts +7 -24
  61. package/src/web-worker/client-session/client-session-devtools.ts +32 -18
  62. package/src/web-worker/client-session/persisted-adapter.ts +199 -103
  63. package/src/web-worker/client-session/sqlite-loader.ts +19 -0
  64. package/src/web-worker/common/persisted-sqlite.ts +215 -170
  65. package/src/web-worker/common/shutdown-channel.ts +10 -3
  66. package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
  67. package/src/web-worker/common/worker-schema.ts +78 -38
  68. package/src/web-worker/leader-worker/make-leader-worker.ts +149 -71
  69. package/src/web-worker/shared-worker/make-shared-worker.ts +78 -90
  70. package/dist/opfs-utils.d.ts +0 -5
  71. package/dist/opfs-utils.d.ts.map +0 -1
  72. package/dist/opfs-utils.js +0 -43
  73. package/dist/opfs-utils.js.map +0 -1
  74. package/src/opfs-utils.ts +0 -61
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  BootStatus,
3
3
  Devtools,
4
- LeaderAheadError,
4
+ RejectedPushError,
5
5
  liveStoreVersion,
6
6
  MigrationsReport,
7
+ SyncBackend,
7
8
  SyncState,
8
- UnexpectedError,
9
+ UnknownError,
9
10
  } from '@livestore/common'
11
+ import { StreamEventsOptionsFields } from '@livestore/common/leader-thread'
10
12
  import { EventSequenceNumber, LiveStoreEvent } from '@livestore/common/schema'
11
13
  import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
12
14
  import { Schema, Transferable } from '@livestore/utils/effect'
@@ -48,7 +50,7 @@ export class LeaderWorkerOuterInitialMessage extends Schema.TaggedRequest<Leader
48
50
  {
49
51
  payload: { port: Transferable.MessagePort, storeId: Schema.String, clientId: Schema.String },
50
52
  success: Schema.Void,
51
- failure: UnexpectedError,
53
+ failure: Schema.Never,
52
54
  },
53
55
  ) {}
54
56
 
@@ -64,10 +66,10 @@ export class LeaderWorkerInnerInitialMessage extends Schema.TaggedRequest<Leader
64
66
  storeId: Schema.String,
65
67
  clientId: Schema.String,
66
68
  debugInstanceId: Schema.String,
67
- syncPayload: Schema.UndefinedOr(Schema.JsonValue),
69
+ syncPayloadEncoded: Schema.UndefinedOr(Schema.JsonValue),
68
70
  },
69
71
  success: Schema.Void,
70
- failure: UnexpectedError,
72
+ failure: UnknownError,
71
73
  },
72
74
  ) {}
73
75
 
@@ -76,7 +78,7 @@ export class LeaderWorkerInnerBootStatusStream extends Schema.TaggedRequest<Lead
76
78
  {
77
79
  payload: {},
78
80
  success: BootStatus,
79
- failure: UnexpectedError,
81
+ failure: Schema.Never,
80
82
  },
81
83
  ) {}
82
84
 
@@ -84,27 +86,36 @@ export class LeaderWorkerInnerPushToLeader extends Schema.TaggedRequest<LeaderWo
84
86
  'PushToLeader',
85
87
  {
86
88
  payload: {
87
- batch: Schema.Array(LiveStoreEvent.AnyEncoded),
89
+ batch: Schema.Array(Schema.typeSchema(LiveStoreEvent.Client.Encoded)),
88
90
  },
89
- success: Schema.Void,
90
- failure: Schema.Union(UnexpectedError, LeaderAheadError),
91
+ success: Schema.Void as Schema.Schema<void>,
92
+ failure: RejectedPushError,
91
93
  },
92
94
  ) {}
93
95
 
94
96
  export class LeaderWorkerInnerPullStream extends Schema.TaggedRequest<LeaderWorkerInnerPullStream>()('PullStream', {
95
97
  payload: {
96
- cursor: EventSequenceNumber.EventSequenceNumber,
98
+ cursor: Schema.typeSchema(EventSequenceNumber.Client.Composite),
97
99
  },
98
100
  success: Schema.Struct({
99
101
  payload: SyncState.PayloadUpstream,
100
102
  }),
101
- failure: UnexpectedError,
103
+ failure: Schema.Never,
102
104
  }) {}
103
105
 
106
+ export class LeaderWorkerInnerStreamEvents extends Schema.TaggedRequest<LeaderWorkerInnerStreamEvents>()(
107
+ 'StreamEvents',
108
+ {
109
+ payload: StreamEventsOptionsFields,
110
+ success: LiveStoreEvent.Client.Encoded,
111
+ failure: Schema.Never,
112
+ },
113
+ ) {}
114
+
104
115
  export class LeaderWorkerInnerExport extends Schema.TaggedRequest<LeaderWorkerInnerExport>()('Export', {
105
116
  payload: {},
106
117
  success: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
107
- failure: UnexpectedError,
118
+ failure: Schema.Never,
108
119
  }) {}
109
120
 
110
121
  export class LeaderWorkerInnerExportEventlog extends Schema.TaggedRequest<LeaderWorkerInnerExportEventlog>()(
@@ -112,7 +123,7 @@ export class LeaderWorkerInnerExportEventlog extends Schema.TaggedRequest<Leader
112
123
  {
113
124
  payload: {},
114
125
  success: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
115
- failure: UnexpectedError,
126
+ failure: Schema.Never,
116
127
  },
117
128
  ) {}
118
129
 
@@ -124,7 +135,7 @@ export class LeaderWorkerInnerGetRecreateSnapshot extends Schema.TaggedRequest<L
124
135
  snapshot: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
125
136
  migrationsReport: MigrationsReport,
126
137
  }),
127
- failure: UnexpectedError,
138
+ failure: Schema.Never,
128
139
  },
129
140
  ) {}
130
141
 
@@ -132,8 +143,8 @@ export class LeaderWorkerInnerGetLeaderHead extends Schema.TaggedRequest<LeaderW
132
143
  'GetLeaderHead',
133
144
  {
134
145
  payload: {},
135
- success: EventSequenceNumber.EventSequenceNumber,
136
- failure: UnexpectedError,
146
+ success: Schema.typeSchema(EventSequenceNumber.Client.Composite),
147
+ failure: Schema.Never,
137
148
  },
138
149
  ) {}
139
150
 
@@ -142,14 +153,41 @@ export class LeaderWorkerInnerGetLeaderSyncState extends Schema.TaggedRequest<Le
142
153
  {
143
154
  payload: {},
144
155
  success: SyncState.SyncState,
145
- failure: UnexpectedError,
156
+ failure: Schema.Never,
157
+ },
158
+ ) {}
159
+
160
+ export class LeaderWorkerInnerSyncStateStream extends Schema.TaggedRequest<LeaderWorkerInnerSyncStateStream>()(
161
+ 'SyncStateStream',
162
+ {
163
+ payload: {},
164
+ success: SyncState.SyncState,
165
+ failure: Schema.Never,
166
+ },
167
+ ) {}
168
+
169
+ export class LeaderWorkerInnerGetNetworkStatus extends Schema.TaggedRequest<LeaderWorkerInnerGetNetworkStatus>()(
170
+ 'GetNetworkStatus',
171
+ {
172
+ payload: {},
173
+ success: SyncBackend.NetworkStatus,
174
+ failure: Schema.Never,
175
+ },
176
+ ) {}
177
+
178
+ export class LeaderWorkerInnerNetworkStatusStream extends Schema.TaggedRequest<LeaderWorkerInnerNetworkStatusStream>()(
179
+ 'NetworkStatusStream',
180
+ {
181
+ payload: {},
182
+ success: SyncBackend.NetworkStatus,
183
+ failure: Schema.Never,
146
184
  },
147
185
  ) {}
148
186
 
149
187
  export class LeaderWorkerInnerShutdown extends Schema.TaggedRequest<LeaderWorkerInnerShutdown>()('Shutdown', {
150
188
  payload: {},
151
189
  success: Schema.Void,
152
- failure: UnexpectedError,
190
+ failure: Schema.Never,
153
191
  }) {}
154
192
 
155
193
  export class LeaderWorkerInnerExtraDevtoolsMessage extends Schema.TaggedRequest<LeaderWorkerInnerExtraDevtoolsMessage>()(
@@ -159,7 +197,7 @@ export class LeaderWorkerInnerExtraDevtoolsMessage extends Schema.TaggedRequest<
159
197
  message: Devtools.Leader.MessageToApp,
160
198
  },
161
199
  success: Schema.Void,
162
- failure: UnexpectedError,
200
+ failure: Schema.Never,
163
201
  },
164
202
  ) {}
165
203
 
@@ -168,58 +206,60 @@ export const LeaderWorkerInnerRequest = Schema.Union(
168
206
  LeaderWorkerInnerBootStatusStream,
169
207
  LeaderWorkerInnerPushToLeader,
170
208
  LeaderWorkerInnerPullStream,
209
+ LeaderWorkerInnerStreamEvents,
171
210
  LeaderWorkerInnerExport,
172
211
  LeaderWorkerInnerExportEventlog,
173
212
  LeaderWorkerInnerGetRecreateSnapshot,
174
213
  LeaderWorkerInnerGetLeaderHead,
175
214
  LeaderWorkerInnerGetLeaderSyncState,
215
+ LeaderWorkerInnerSyncStateStream,
216
+ LeaderWorkerInnerGetNetworkStatus,
217
+ LeaderWorkerInnerNetworkStatusStream,
176
218
  LeaderWorkerInnerShutdown,
177
219
  LeaderWorkerInnerExtraDevtoolsMessage,
178
220
  WebmeshWorker.Schema.CreateConnection,
179
221
  )
180
222
  export type LeaderWorkerInnerRequest = typeof LeaderWorkerInnerRequest.Type
181
223
 
182
- export class SharedWorkerInitialMessagePayloadFromClientSession extends Schema.TaggedStruct('FromClientSession', {
183
- initialMessage: LeaderWorkerInnerInitialMessage,
184
- }) {}
185
-
186
- export class SharedWorkerInitialMessage extends Schema.TaggedRequest<SharedWorkerInitialMessage>()('InitialMessage', {
187
- payload: {
188
- payload: Schema.Union(SharedWorkerInitialMessagePayloadFromClientSession, Schema.TaggedStruct('FromWebBridge', {})),
189
- // To guard against scenarios where a client session is already running a newer version of LiveStore
190
- // We should probably find a better way to handle those cases once they become more common.
191
- liveStoreVersion: Schema.Literal(liveStoreVersion),
192
- },
193
- success: Schema.Void,
194
- failure: UnexpectedError,
195
- }) {}
196
-
197
224
  export class SharedWorkerUpdateMessagePort extends Schema.TaggedRequest<SharedWorkerUpdateMessagePort>()(
198
225
  'UpdateMessagePort',
199
226
  {
200
227
  payload: {
201
228
  port: Transferable.MessagePort,
229
+ // Version gate to prevent mixed LiveStore builds talking to the same SharedWorker
230
+ liveStoreVersion: Schema.Literal(liveStoreVersion),
231
+ /**
232
+ * Initial configuration for the leader worker. This replaces the previous
233
+ * two-phase SharedWorker handshake and is sent under the tab lock by the
234
+ * elected leader. Subsequent calls can omit changes and will simply rebind
235
+ * the port (join) without reinitializing the store.
236
+ */
237
+ initial: LeaderWorkerInnerInitialMessage,
202
238
  },
203
239
  success: Schema.Void,
204
- failure: UnexpectedError,
240
+ failure: UnknownError,
205
241
  },
206
242
  ) {}
207
243
 
208
- export class SharedWorkerRequest extends Schema.Union(
209
- SharedWorkerInitialMessage,
244
+ export const SharedWorkerRequest = Schema.Union(
210
245
  SharedWorkerUpdateMessagePort,
211
246
 
212
247
  // Proxied requests
213
248
  LeaderWorkerInnerBootStatusStream,
214
249
  LeaderWorkerInnerPushToLeader,
215
250
  LeaderWorkerInnerPullStream,
251
+ LeaderWorkerInnerStreamEvents,
216
252
  LeaderWorkerInnerExport,
217
253
  LeaderWorkerInnerGetRecreateSnapshot,
218
254
  LeaderWorkerInnerExportEventlog,
219
255
  LeaderWorkerInnerGetLeaderHead,
220
256
  LeaderWorkerInnerGetLeaderSyncState,
257
+ LeaderWorkerInnerSyncStateStream,
258
+ LeaderWorkerInnerGetNetworkStatus,
259
+ LeaderWorkerInnerNetworkStatusStream,
221
260
  LeaderWorkerInnerShutdown,
222
261
  LeaderWorkerInnerExtraDevtoolsMessage,
223
262
 
224
263
  WebmeshWorker.Schema.CreateConnection,
225
- ) {}
264
+ )
265
+ export type SharedWorkerRequest = typeof SharedWorkerRequest.Type
@@ -1,7 +1,15 @@
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'
1
+ import type * as otel from '@opentelemetry/api'
2
+
3
+ import type { BootStatus, BootWarningReason, SqliteDb, SyncOptions } from '@livestore/common'
4
+ import { Devtools, LogConfig, UnknownError } from '@livestore/common'
5
+ import type { DevtoolsOptions, StreamEventsOptions } from '@livestore/common/leader-thread'
6
+ import {
7
+ configureConnection,
8
+ Eventlog,
9
+ LeaderThreadCtx,
10
+ makeLeaderThreadLayer,
11
+ streamEventsWithSyncState,
12
+ } from '@livestore/common/leader-thread'
5
13
  import type { LiveStoreSchema } from '@livestore/common/schema'
6
14
  import { LiveStoreEvent } from '@livestore/common/schema'
7
15
  import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
@@ -10,22 +18,19 @@ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
10
18
  import { isDevEnv, LS_DEV } from '@livestore/utils'
11
19
  import type { HttpClient, Scope, WorkerError } from '@livestore/utils/effect'
12
20
  import {
13
- BrowserWorkerRunner,
14
21
  Effect,
15
22
  FetchHttpClient,
16
23
  identity,
17
24
  Layer,
18
- Logger,
19
- LogLevel,
20
25
  OtelTracer,
21
26
  Scheduler,
27
+ Schema,
22
28
  Stream,
23
29
  TaskTracing,
24
30
  WorkerRunner,
25
31
  } from '@livestore/utils/effect'
26
- import type * as otel from '@opentelemetry/api'
32
+ import { BrowserWorkerRunner, Opfs, WebError } from '@livestore/utils/effect/browser'
27
33
 
28
- import * as OpfsUtils from '../../opfs-utils.ts'
29
34
  import { cleanupOldStateDbFiles, getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
30
35
  import { makeShutdownChannel } from '../common/shutdown-channel.ts'
31
36
  import * as WorkerSchema from '../common/worker-schema.ts'
@@ -33,18 +38,19 @@ import * as WorkerSchema from '../common/worker-schema.ts'
33
38
  export type WorkerOptions = {
34
39
  schema: LiveStoreSchema
35
40
  sync?: SyncOptions
41
+ syncPayloadSchema?: Schema.Schema<any>
36
42
  otelOptions?: {
37
43
  tracer?: otel.Tracer
38
44
  }
39
- }
45
+ } & LogConfig.WithLoggerOptions
40
46
 
41
- if (isDevEnv()) {
47
+ if (isDevEnv() === true) {
42
48
  globalThis.__debugLiveStoreUtils = {
43
- opfs: OpfsUtils,
49
+ opfs: Opfs.debugUtils,
44
50
  blobUrl: (buffer: Uint8Array<ArrayBuffer>) =>
45
51
  URL.createObjectURL(new Blob([buffer], { type: 'application/octet-stream' })),
46
- runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
47
- runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
52
+ runSync: <A, E>(effect: Effect.Effect<A, E>) => Effect.runSync(effect),
53
+ runFork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runFork(effect),
48
54
  }
49
55
  }
50
56
 
@@ -53,13 +59,13 @@ export const makeWorker = (options: WorkerOptions) => {
53
59
  }
54
60
 
55
61
  export const makeWorkerEffect = (options: WorkerOptions) => {
56
- const TracingLive = options.otelOptions?.tracer
62
+ const TracingLive = options.otelOptions?.tracer !== undefined
57
63
  ? Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
58
64
  Layer.provideMerge(Layer.succeed(OtelTracer.OtelTracer, options.otelOptions.tracer)),
59
65
  )
60
66
  : undefined
61
67
 
62
- const layer = Layer.mergeAll(Logger.prettyWithThread(self.name), FetchHttpClient.layer, TracingLive ?? Layer.empty)
68
+ const runtimeLayer = Layer.mergeAll(FetchHttpClient.layer, TracingLive ?? Layer.empty)
63
69
 
64
70
  return makeWorkerRunnerOuter(options).pipe(
65
71
  Layer.provide(BrowserWorkerRunner.layer),
@@ -67,15 +73,15 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
67
73
  Effect.scoped,
68
74
  Effect.tapCauseLogPretty,
69
75
  Effect.annotateLogs({ thread: self.name }),
70
- Effect.provide(layer),
71
- LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
76
+ Effect.provide(runtimeLayer),
77
+ LS_DEV === true ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
72
78
  // We're using this custom scheduler to improve op batching behaviour and reduce the overhead
73
79
  // of the Effect fiber runtime given we have different tradeoffs on a worker thread.
74
80
  // Despite the "message channel" name, is has nothing to do with the `incomingRequestsPort` above.
75
81
  Effect.withScheduler(Scheduler.messageChannel()),
76
82
  // We're increasing the Effect ops limit here to allow for larger chunks of operations at a time
77
83
  Effect.withMaxOpsBeforeYield(4096),
78
- Logger.withMinimumLogLevel(LogLevel.Debug),
84
+ LogConfig.withLoggerConfig({ logger: options.logger, logLevel: options.logLevel }, { threadName: self.name }),
79
85
  )
80
86
  }
81
87
 
@@ -93,7 +99,12 @@ const makeWorkerRunnerOuter = (
93
99
  Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage:innerFiber'),
94
100
  Effect.tapCauseLogPretty,
95
101
  Effect.provide(
96
- WebmeshWorker.CacheService.layer({ nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }) }),
102
+ Layer.mergeAll(
103
+ Opfs.Opfs.Default,
104
+ WebmeshWorker.CacheService.layer({
105
+ nodeName: Devtools.makeNodeName.client.leader({ storeId, clientId }),
106
+ }),
107
+ ),
97
108
  ),
98
109
  Effect.forkScoped,
99
110
  )
@@ -102,18 +113,34 @@ const makeWorkerRunnerOuter = (
102
113
  }).pipe(Effect.withSpan('@livestore/adapter-web:worker:wrapper:InitialMessage'), Layer.unwrapScoped),
103
114
  })
104
115
 
105
- const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
116
+ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }: WorkerOptions) =>
106
117
  WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
107
- InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayload }) =>
118
+ InitialMessage: ({ storageOptions, storeId, clientId, devtoolsEnabled, debugInstanceId, syncPayloadEncoded }) =>
108
119
  Effect.gen(function* () {
109
120
  const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
110
121
  const makeSqliteDb = sqliteDbFactory({ sqlite3 })
111
- const runtime = yield* Effect.runtime<never>()
122
+ const runtime = yield* Effect.runtime()
123
+
124
+ // Check OPFS availability and determine storage mode
125
+ const opfsCheck = yield* checkOpfsAvailability
126
+ const useOpfs = opfsCheck === undefined
127
+
128
+ // Track boot warning to emit later
129
+ let bootWarning: BootStatus | undefined
130
+ if (useOpfs === false) {
131
+ yield* Effect.logWarning(
132
+ '[@livestore/adapter-web:worker] OPFS unavailable, using in-memory storage',
133
+ opfsCheck,
134
+ )
135
+ bootWarning = { stage: 'warning', ...opfsCheck }
136
+ }
137
+
138
+ const opfsDirectory = useOpfs === true ? yield* sanitizeOpfsDir(storageOptions.directory, storeId) : undefined
112
139
 
113
- const makeDb = (kind: 'state' | 'eventlog') =>
140
+ const makeOpfsDb = (kind: 'state' | 'eventlog') =>
114
141
  makeSqliteDb({
115
142
  _tag: 'opfs',
116
- opfsDirectory: sanitizeOpfsDir(storageOptions.directory, storeId),
143
+ opfsDirectory: opfsDirectory!,
117
144
  fileName: kind === 'state' ? getStateDbFileName(schema) : 'eventlog.db',
118
145
  configureDb: (db) =>
119
146
  configureConnection(db, {
@@ -126,10 +153,17 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
126
153
  }).pipe(Effect.provide(runtime), Effect.runSync),
127
154
  }).pipe(Effect.acquireRelease((db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged)))
128
155
 
129
- // Might involve some async work, so we're running them concurrently
130
- const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], {
131
- concurrency: 2,
132
- })
156
+ const makeInMemoryDb = () =>
157
+ makeSqliteDb({
158
+ _tag: 'in-memory',
159
+ configureDb: (db) =>
160
+ configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
161
+ }).pipe(Effect.acquireRelease((db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged)))
162
+
163
+ // Use OPFS if available, otherwise fall back to in-memory
164
+ const [dbState, dbEventlog] = useOpfs === true
165
+ ? yield* Effect.all([makeOpfsDb('state'), makeOpfsDb('eventlog')], { concurrency: 2 })
166
+ : yield* Effect.all([makeInMemoryDb(), makeInMemoryDb()], { concurrency: 2 })
133
167
 
134
168
  // Clean up old state database files after successful database creation
135
169
  // This prevents OPFS file pool capacity exhaustion from accumulated state db files after schema changes/migrations
@@ -137,6 +171,7 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
137
171
  yield* cleanupOldStateDbFiles({
138
172
  vfs: dbState.metadata.vfs,
139
173
  currentSchema: schema,
174
+ opfsDirectory: dbState.metadata.persistenceInfo.opfsDirectory,
140
175
  })
141
176
  }
142
177
 
@@ -153,35 +188,33 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
153
188
  dbEventlog,
154
189
  devtoolsOptions,
155
190
  shutdownChannel,
156
- syncPayload,
191
+ syncPayloadEncoded,
192
+ syncPayloadSchema,
193
+ ...(bootWarning !== undefined ? { bootWarning } : {}),
157
194
  })
158
195
  }).pipe(
159
196
  Effect.tapCauseLogPretty,
160
- UnexpectedError.mapToUnexpectedError,
197
+ UnknownError.mapToUnknownError,
161
198
  Effect.withPerformanceMeasure('@livestore/adapter-web:worker:InitialMessage'),
162
199
  Effect.withSpan('@livestore/adapter-web:worker:InitialMessage'),
163
200
  Effect.annotateSpans({ debugInstanceId }),
164
201
  Layer.unwrapScoped,
165
202
  ),
166
- GetRecreateSnapshot: () =>
167
- Effect.gen(function* () {
168
- const workerCtx = yield* LeaderThreadCtx
203
+ GetRecreateSnapshot: Effect.fn('@livestore/adapter-web:worker:GetRecreateSnapshot')(function* () {
204
+ const workerCtx = yield* LeaderThreadCtx
169
205
 
170
- // NOTE we can only return the cached snapshot once as it's transferred (i.e. disposed), so we need to set it to undefined
171
- // const cachedSnapshot =
172
- // result._tag === 'Recreate' ? yield* Ref.getAndSet(result.snapshotRef, undefined) : undefined
206
+ // NOTE we can only return the cached snapshot once as it's transferred (i.e. disposed), so we need to set it to undefined
207
+ // const cachedSnapshot =
208
+ // result._tag === 'Recreate' ? yield* Ref.getAndSet(result.snapshotRef, undefined) : undefined
173
209
 
174
- // return cachedSnapshot ?? workerCtx.db.export()
210
+ // return cachedSnapshot ?? workerCtx.db.export()
175
211
 
176
- const snapshot = workerCtx.dbState.export()
177
- return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
178
- }).pipe(
179
- UnexpectedError.mapToUnexpectedError,
180
- Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot'),
181
- ),
212
+ const snapshot = workerCtx.dbState.export()
213
+ return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
214
+ }),
182
215
  PullStream: ({ cursor }) =>
183
216
  Effect.gen(function* () {
184
- const { syncProcessor } = yield* LeaderThreadCtx
217
+ const { syncProcessor } = yield* LeaderThreadCtx // <- syncState comes from here
185
218
  return syncProcessor.pull({ cursor })
186
219
  }).pipe(
187
220
  Stream.unwrapScoped,
@@ -191,47 +224,66 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
191
224
  PushToLeader: ({ batch }) =>
192
225
  Effect.andThen(LeaderThreadCtx, ({ syncProcessor }) =>
193
226
  syncProcessor.push(
194
- batch.map((event) => new LiveStoreEvent.EncodedWithMeta(event)),
227
+ batch.map((event) => new LiveStoreEvent.Client.EncodedWithMeta(event)),
195
228
  // We'll wait in order to keep back pressure on the client session
196
229
  { waitForProcessing: true },
197
230
  ),
198
231
  ).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-web:worker:PushToLeader')),
232
+ StreamEvents: (options) =>
233
+ LeaderThreadCtx.pipe(
234
+ Effect.map(({ dbEventlog, syncProcessor }) => {
235
+ const { _tag: _ignored, ...payload } = options as any
236
+ const streamOptions = payload as StreamEventsOptions
237
+ return streamEventsWithSyncState({
238
+ dbEventlog,
239
+ syncState: syncProcessor.syncState,
240
+ options: streamOptions,
241
+ })
242
+ }),
243
+ Stream.unwrapScoped,
244
+ Stream.withSpan('@livestore/adapter-web:worker:StreamEvents'),
245
+ ),
199
246
  Export: () =>
200
247
  Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
201
- UnexpectedError.mapToUnexpectedError,
202
248
  Effect.withSpan('@livestore/adapter-web:worker:Export'),
203
249
  ),
204
250
  ExportEventlog: () =>
205
251
  Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
206
- UnexpectedError.mapToUnexpectedError,
207
252
  Effect.withSpan('@livestore/adapter-web:worker:ExportEventlog'),
208
253
  ),
209
254
  BootStatusStream: () =>
210
255
  Effect.andThen(LeaderThreadCtx, (_) => Stream.fromQueue(_.bootStatusQueue)).pipe(Stream.unwrap),
211
- GetLeaderHead: () =>
256
+ GetLeaderHead: Effect.fn('@livestore/adapter-web:worker:GetLeaderHead')(function* () {
257
+ const workerCtx = yield* LeaderThreadCtx
258
+ return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
259
+ }),
260
+ GetLeaderSyncState: Effect.fn('@livestore/adapter-web:worker:GetLeaderSyncState')(function* () {
261
+ const workerCtx = yield* LeaderThreadCtx
262
+ return yield* workerCtx.syncProcessor.syncState
263
+ }),
264
+ SyncStateStream: () =>
212
265
  Effect.gen(function* () {
213
266
  const workerCtx = yield* LeaderThreadCtx
214
- return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
215
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
216
- GetLeaderSyncState: () =>
267
+ return workerCtx.syncProcessor.syncState.changes
268
+ }).pipe(Stream.unwrapScoped),
269
+ GetNetworkStatus: Effect.fn('@livestore/adapter-web:worker:GetNetworkStatus')(function* () {
270
+ const workerCtx = yield* LeaderThreadCtx
271
+ return yield* workerCtx.networkStatus
272
+ }),
273
+ NetworkStatusStream: () =>
217
274
  Effect.gen(function* () {
218
275
  const workerCtx = yield* LeaderThreadCtx
219
- return yield* workerCtx.syncProcessor.syncState
220
- }).pipe(
221
- UnexpectedError.mapToUnexpectedError,
222
- Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState'),
223
- ),
224
- Shutdown: () =>
225
- Effect.gen(function* () {
226
- yield* Effect.logDebug('[@livestore/adapter-web:worker] Shutdown')
276
+ return workerCtx.networkStatus.changes
277
+ }).pipe(Stream.unwrapScoped),
278
+ Shutdown: Effect.fn('@livestore/adapter-web:worker:Shutdown')(function* () {
279
+ yield* Effect.logDebug('[@livestore/adapter-web:worker] Shutdown')
227
280
 
228
- // Buy some time for Otel to flush
229
- // TODO find a cleaner way to do this
230
- yield* Effect.sleep(300)
231
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:Shutdown')),
281
+ // Buy some time for Otel to flush
282
+ // TODO find a cleaner way to do this
283
+ yield* Effect.sleep(300)
284
+ }),
232
285
  ExtraDevtoolsMessage: ({ message }) =>
233
286
  Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
234
- UnexpectedError.mapToUnexpectedError,
235
287
  Effect.withSpan('@livestore/adapter-web:worker:ExtraDevtoolsMessage'),
236
288
  ),
237
289
  'DevtoolsWebCommon.CreateConnection': WebmeshWorker.CreateConnection,
@@ -245,7 +297,7 @@ const makeDevtoolsOptions = ({
245
297
  devtoolsEnabled: boolean
246
298
  dbState: SqliteDb
247
299
  dbEventlog: SqliteDb
248
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope | WebmeshWorker.CacheService> =>
300
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope | WebmeshWorker.CacheService> =>
249
301
  Effect.gen(function* () {
250
302
  if (devtoolsEnabled === false) {
251
303
  return { enabled: false }
@@ -255,13 +307,39 @@ const makeDevtoolsOptions = ({
255
307
 
256
308
  return {
257
309
  enabled: true,
258
- boot: Effect.gen(function* () {
259
- const persistenceInfo = {
310
+ boot: Effect.succeed({
311
+ node,
312
+ persistenceInfo: {
260
313
  state: dbState.metadata.persistenceInfo,
261
314
  eventlog: dbEventlog.metadata.persistenceInfo,
262
- }
263
-
264
- return { node, persistenceInfo, mode: 'direct' }
315
+ },
316
+ mode: 'direct' as const,
265
317
  }),
266
318
  }
267
319
  })
320
+
321
+ /**
322
+ * Attempts to access OPFS and returns a warning if unavailable.
323
+ *
324
+ * Common failure scenarios:
325
+ * - Safari/Firefox private browsing: SecurityError or NotAllowedError
326
+ * - Permission denied: NotAllowedError
327
+ * - Quota exceeded: QuotaExceededError
328
+ */
329
+ const checkOpfsAvailability = Effect.gen(function* () {
330
+ const opfs = yield* Opfs.Opfs
331
+ return yield* opfs.getRootDirectoryHandle.pipe(
332
+ Effect.as(undefined),
333
+ Effect.catchAll((error) => {
334
+ const reason: BootWarningReason =
335
+ Schema.is(WebError.SecurityError)(error) === true || Schema.is(WebError.NotAllowedError)(error) === true
336
+ ? 'private-browsing'
337
+ : 'storage-unavailable'
338
+ const message =
339
+ reason === 'private-browsing'
340
+ ? 'Storage unavailable in private browsing mode. LiveStore will continue without persistence.'
341
+ : 'Storage access denied. LiveStore will continue without persistence.'
342
+ return Effect.succeed({ reason, message } as const)
343
+ }),
344
+ )
345
+ })