@livestore/adapter-web 0.4.0-dev.9 → 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 +11 -29
  32. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
  33. package/dist/web-worker/common/persisted-sqlite.js +87 -188
  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 +98 -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 +200 -298
  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 +148 -71
  69. package/src/web-worker/shared-worker/make-shared-worker.ts +78 -90
  70. package/dist/opfs-utils.d.ts +0 -7
  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
@@ -154,35 +188,33 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
154
188
  dbEventlog,
155
189
  devtoolsOptions,
156
190
  shutdownChannel,
157
- syncPayload,
191
+ syncPayloadEncoded,
192
+ syncPayloadSchema,
193
+ ...(bootWarning !== undefined ? { bootWarning } : {}),
158
194
  })
159
195
  }).pipe(
160
196
  Effect.tapCauseLogPretty,
161
- UnexpectedError.mapToUnexpectedError,
197
+ UnknownError.mapToUnknownError,
162
198
  Effect.withPerformanceMeasure('@livestore/adapter-web:worker:InitialMessage'),
163
199
  Effect.withSpan('@livestore/adapter-web:worker:InitialMessage'),
164
200
  Effect.annotateSpans({ debugInstanceId }),
165
201
  Layer.unwrapScoped,
166
202
  ),
167
- GetRecreateSnapshot: () =>
168
- Effect.gen(function* () {
169
- const workerCtx = yield* LeaderThreadCtx
203
+ GetRecreateSnapshot: Effect.fn('@livestore/adapter-web:worker:GetRecreateSnapshot')(function* () {
204
+ const workerCtx = yield* LeaderThreadCtx
170
205
 
171
- // NOTE we can only return the cached snapshot once as it's transferred (i.e. disposed), so we need to set it to undefined
172
- // const cachedSnapshot =
173
- // 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
174
209
 
175
- // return cachedSnapshot ?? workerCtx.db.export()
210
+ // return cachedSnapshot ?? workerCtx.db.export()
176
211
 
177
- const snapshot = workerCtx.dbState.export()
178
- return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
179
- }).pipe(
180
- UnexpectedError.mapToUnexpectedError,
181
- Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot'),
182
- ),
212
+ const snapshot = workerCtx.dbState.export()
213
+ return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
214
+ }),
183
215
  PullStream: ({ cursor }) =>
184
216
  Effect.gen(function* () {
185
- const { syncProcessor } = yield* LeaderThreadCtx
217
+ const { syncProcessor } = yield* LeaderThreadCtx // <- syncState comes from here
186
218
  return syncProcessor.pull({ cursor })
187
219
  }).pipe(
188
220
  Stream.unwrapScoped,
@@ -192,47 +224,66 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions }: WorkerOptions) =>
192
224
  PushToLeader: ({ batch }) =>
193
225
  Effect.andThen(LeaderThreadCtx, ({ syncProcessor }) =>
194
226
  syncProcessor.push(
195
- batch.map((event) => new LiveStoreEvent.EncodedWithMeta(event)),
227
+ batch.map((event) => new LiveStoreEvent.Client.EncodedWithMeta(event)),
196
228
  // We'll wait in order to keep back pressure on the client session
197
229
  { waitForProcessing: true },
198
230
  ),
199
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
+ ),
200
246
  Export: () =>
201
247
  Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
202
- UnexpectedError.mapToUnexpectedError,
203
248
  Effect.withSpan('@livestore/adapter-web:worker:Export'),
204
249
  ),
205
250
  ExportEventlog: () =>
206
251
  Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
207
- UnexpectedError.mapToUnexpectedError,
208
252
  Effect.withSpan('@livestore/adapter-web:worker:ExportEventlog'),
209
253
  ),
210
254
  BootStatusStream: () =>
211
255
  Effect.andThen(LeaderThreadCtx, (_) => Stream.fromQueue(_.bootStatusQueue)).pipe(Stream.unwrap),
212
- 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: () =>
213
265
  Effect.gen(function* () {
214
266
  const workerCtx = yield* LeaderThreadCtx
215
- return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
216
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-web:worker:GetLeaderHead')),
217
- 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: () =>
218
274
  Effect.gen(function* () {
219
275
  const workerCtx = yield* LeaderThreadCtx
220
- return yield* workerCtx.syncProcessor.syncState
221
- }).pipe(
222
- UnexpectedError.mapToUnexpectedError,
223
- Effect.withSpan('@livestore/adapter-web:worker:GetLeaderSyncState'),
224
- ),
225
- Shutdown: () =>
226
- Effect.gen(function* () {
227
- 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')
228
280
 
229
- // Buy some time for Otel to flush
230
- // TODO find a cleaner way to do this
231
- yield* Effect.sleep(300)
232
- }).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
+ }),
233
285
  ExtraDevtoolsMessage: ({ message }) =>
234
286
  Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
235
- UnexpectedError.mapToUnexpectedError,
236
287
  Effect.withSpan('@livestore/adapter-web:worker:ExtraDevtoolsMessage'),
237
288
  ),
238
289
  'DevtoolsWebCommon.CreateConnection': WebmeshWorker.CreateConnection,
@@ -246,7 +297,7 @@ const makeDevtoolsOptions = ({
246
297
  devtoolsEnabled: boolean
247
298
  dbState: SqliteDb
248
299
  dbEventlog: SqliteDb
249
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope | WebmeshWorker.CacheService> =>
300
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope | WebmeshWorker.CacheService> =>
250
301
  Effect.gen(function* () {
251
302
  if (devtoolsEnabled === false) {
252
303
  return { enabled: false }
@@ -256,13 +307,39 @@ const makeDevtoolsOptions = ({
256
307
 
257
308
  return {
258
309
  enabled: true,
259
- boot: Effect.gen(function* () {
260
- const persistenceInfo = {
310
+ boot: Effect.succeed({
311
+ node,
312
+ persistenceInfo: {
261
313
  state: dbState.metadata.persistenceInfo,
262
314
  eventlog: dbEventlog.metadata.persistenceInfo,
263
- }
264
-
265
- return { node, persistenceInfo, mode: 'direct' }
315
+ },
316
+ mode: 'direct' as const,
266
317
  }),
267
318
  }
268
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
+ })