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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/in-memory/in-memory-adapter.d.ts +49 -5
  3. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  4. package/dist/in-memory/in-memory-adapter.js +69 -16
  5. package/dist/in-memory/in-memory-adapter.js.map +1 -1
  6. package/dist/web-worker/client-session/client-session-devtools.d.ts +1 -1
  7. package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
  8. package/dist/web-worker/client-session/client-session-devtools.js +14 -3
  9. package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
  10. package/dist/web-worker/client-session/persisted-adapter.d.ts +15 -0
  11. package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
  12. package/dist/web-worker/client-session/persisted-adapter.js +68 -46
  13. package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
  14. package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
  15. package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
  16. package/dist/web-worker/client-session/sqlite-loader.js +16 -0
  17. package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
  18. package/dist/web-worker/common/persisted-sqlite.d.ts +23 -7
  19. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
  20. package/dist/web-worker/common/persisted-sqlite.js +125 -76
  21. package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
  22. package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
  23. package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
  24. package/dist/web-worker/common/shutdown-channel.js +2 -2
  25. package/dist/web-worker/common/shutdown-channel.js.map +1 -1
  26. package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
  27. package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
  28. package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
  29. package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
  30. package/dist/web-worker/common/worker-schema.d.ts +147 -56
  31. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  32. package/dist/web-worker/common/worker-schema.js +55 -36
  33. package/dist/web-worker/common/worker-schema.js.map +1 -1
  34. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +4 -2
  35. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
  36. package/dist/web-worker/leader-worker/make-leader-worker.js +63 -27
  37. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
  38. package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -1
  39. package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
  40. package/dist/web-worker/shared-worker/make-shared-worker.js +66 -49
  41. package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
  42. package/dist/web-worker/vite-dev-polyfill.js +1 -0
  43. package/dist/web-worker/vite-dev-polyfill.js.map +1 -1
  44. package/package.json +8 -9
  45. package/src/in-memory/in-memory-adapter.ts +83 -21
  46. package/src/web-worker/ambient.d.ts +7 -24
  47. package/src/web-worker/client-session/client-session-devtools.ts +18 -3
  48. package/src/web-worker/client-session/persisted-adapter.ts +117 -59
  49. package/src/web-worker/client-session/sqlite-loader.ts +19 -0
  50. package/src/web-worker/common/persisted-sqlite.ts +225 -107
  51. package/src/web-worker/common/shutdown-channel.ts +10 -3
  52. package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
  53. package/src/web-worker/common/worker-schema.ts +74 -35
  54. package/src/web-worker/leader-worker/make-leader-worker.ts +86 -41
  55. package/src/web-worker/shared-worker/make-shared-worker.ts +96 -75
  56. package/src/web-worker/vite-dev-polyfill.ts +1 -0
  57. package/dist/opfs-utils.d.ts +0 -5
  58. package/dist/opfs-utils.d.ts.map +0 -1
  59. package/dist/opfs-utils.js +0 -43
  60. package/dist/opfs-utils.js.map +0 -1
  61. package/src/opfs-utils.ts +0 -61
@@ -1,143 +1,261 @@
1
- import { liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
1
+ import { liveStoreStorageFormatVersion } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
- import { decodeSAHPoolFilename, HEADER_OFFSET_DATA } from '@livestore/sqlite-wasm/browser'
4
- import { Effect, Schedule, Schema } from '@livestore/utils/effect'
5
-
6
- import * as OpfsUtils from '../../opfs-utils.ts'
3
+ import {
4
+ decodeAccessHandlePoolFilename,
5
+ HEADER_OFFSET_DATA,
6
+ type WebDatabaseMetadataOpfs,
7
+ } from '@livestore/sqlite-wasm/browser'
8
+ import { isDevEnv } from '@livestore/utils'
9
+ import { Chunk, Effect, Option, Order, Schedule, Schema, Stream } from '@livestore/utils/effect'
10
+ import { Opfs, type WebError } from '@livestore/utils/effect/browser'
7
11
  import type * as WorkerSchema from './worker-schema.ts'
8
12
 
9
13
  export class PersistedSqliteError extends Schema.TaggedError<PersistedSqliteError>()('PersistedSqliteError', {
10
- cause: Schema.Defect,
14
+ message: Schema.String,
15
+ cause: Schema.optional(Schema.Defect),
11
16
  }) {}
12
17
 
13
- export const readPersistedAppDbFromClientSession = ({
14
- storageOptions,
15
- storeId,
16
- schema,
17
- }: {
18
+ export const readPersistedStateDbFromClientSession: (args: {
18
19
  storageOptions: WorkerSchema.StorageType
19
20
  storeId: string
20
21
  schema: LiveStoreSchema
21
- }) =>
22
- Effect.promise(async () => {
23
- const directory = sanitizeOpfsDir(storageOptions.directory, storeId)
24
- const sahPoolOpaqueDir = await OpfsUtils.getDirHandle(directory).catch(() => undefined)
22
+ }) => Effect.Effect<
23
+ Uint8Array<ArrayBuffer>,
24
+ // All the following errors could actually happen:
25
+ | PersistedSqliteError
26
+ | WebError.UnknownError
27
+ | WebError.TypeError
28
+ | WebError.NotFoundError
29
+ | WebError.NotAllowedError
30
+ | WebError.TypeMismatchError
31
+ | WebError.SecurityError
32
+ | Opfs.OpfsError,
33
+ Opfs.Opfs
34
+ > = Effect.fn('@livestore/adapter-web:readPersistedStateDbFromClientSession')(
35
+ function* ({ storageOptions, storeId, schema }) {
36
+ const accessHandlePoolDirString = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
25
37
 
26
- if (sahPoolOpaqueDir === undefined) {
27
- return undefined
28
- }
38
+ const accessHandlePoolDirHandle = yield* Opfs.getDirectoryHandleByPath(accessHandlePoolDirString)
39
+
40
+ const stateDbFileName = `/${getStateDbFileName(schema)}`
41
+
42
+ const handlesStream = yield* Opfs.Opfs.values(accessHandlePoolDirHandle)
43
+
44
+ const stateDbFileOption = yield* handlesStream.pipe(
45
+ Stream.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file'),
46
+ Stream.mapEffect(
47
+ (fileHandle) =>
48
+ Effect.gen(function* () {
49
+ const file = yield* Opfs.Opfs.getFile(fileHandle)
50
+ const fileName = yield* Effect.promise(() => decodeAccessHandlePoolFilename(file))
51
+ return { file, fileName }
52
+ }),
53
+ { concurrency: 'unbounded' },
54
+ ),
55
+ Stream.find(({ fileName }) => fileName === stateDbFileName),
56
+ Stream.runHead,
57
+ )
29
58
 
30
- const tryGetDbFile = async (fileHandle: FileSystemFileHandle) => {
31
- const file = await fileHandle.getFile()
32
- const fileName = await decodeSAHPoolFilename(file)
33
- return fileName ? { fileName, file } : undefined
59
+ if (Option.isNone(stateDbFileOption)) {
60
+ return yield* new PersistedSqliteError({
61
+ message: `State database file not found in client session (expected '${stateDbFileName}' in '${accessHandlePoolDirString}')`,
62
+ })
34
63
  }
35
64
 
36
- const getAllFiles = async (asyncIterator: AsyncIterable<FileSystemHandle>): Promise<FileSystemFileHandle[]> => {
37
- const results: FileSystemFileHandle[] = []
38
- for await (const value of asyncIterator) {
39
- if (value.kind === 'file') {
40
- results.push(value as FileSystemFileHandle)
41
- }
42
- }
43
- return results
65
+ const stateDbBuffer = yield* Effect.promise(() =>
66
+ stateDbFileOption.value.file.slice(HEADER_OFFSET_DATA).arrayBuffer(),
67
+ )
68
+
69
+ // Given the access handle pool always eagerly creates files with empty non-header data,
70
+ // we want to return undefined if the file exists but is empty
71
+ if (stateDbBuffer.byteLength === 0) {
72
+ return yield* new PersistedSqliteError({
73
+ message: `State database file is empty in client session (expected '${stateDbFileName}' in '${accessHandlePoolDirString}')`,
74
+ })
44
75
  }
45
76
 
46
- const files = await getAllFiles(sahPoolOpaqueDir.values())
77
+ return new Uint8Array(stateDbBuffer)
78
+ },
79
+ Effect.logWarnIfTakesLongerThan({
80
+ duration: 1000,
81
+ label: '@livestore/adapter-web:readPersistedStateDbFromClientSession',
82
+ }),
83
+ Effect.withPerformanceMeasure('@livestore/adapter-web:readPersistedStateDbFromClientSession'),
84
+ )
47
85
 
48
- const fileResults = await Promise.all(files.map(tryGetDbFile))
86
+ export const resetPersistedDataFromClientSession = Effect.fn(
87
+ '@livestore/adapter-web:resetPersistedDataFromClientSession',
88
+ )(
89
+ function* ({ storageOptions, storeId }: { storageOptions: WorkerSchema.StorageType; storeId: string }) {
90
+ const directory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
91
+ yield* Opfs.remove(directory).pipe(
92
+ // We ignore NotFoundError here as it may not exist or have already been deleted
93
+ Effect.catchTag('@livestore/utils/Web/NotFoundError', () => Effect.void),
94
+ )
95
+ },
96
+ Effect.retry({
97
+ schedule: Schedule.exponentialBackoff10Sec,
98
+ }),
99
+ )
49
100
 
50
- const appDbFileName = `/${getStateDbFileName(schema)}`
101
+ export const sanitizeOpfsDir = Effect.fn('@livestore/adapter-web:sanitizeOpfsDir')(function* (
102
+ directory: string | undefined,
103
+ storeId: string,
104
+ ) {
105
+ if (directory === undefined || directory === '' || directory === '/') {
106
+ return `livestore-${storeId}@${liveStoreStorageFormatVersion}`
107
+ }
51
108
 
52
- const dbFileRes = fileResults.find((_) => _?.fileName === appDbFileName)
53
- // console.debug('fileResults', fileResults, 'dbFileRes', dbFileRes)
109
+ if (directory.includes('/')) {
110
+ return yield* new PersistedSqliteError({
111
+ message: `Nested directories are not yet supported ('${directory}')`,
112
+ })
113
+ }
54
114
 
55
- if (dbFileRes !== undefined) {
56
- const data = await dbFileRes.file.slice(HEADER_OFFSET_DATA).arrayBuffer()
57
- // console.debug('readPersistedAppDbFromClientSession', data.byteLength, data)
115
+ return `${directory}@${liveStoreStorageFormatVersion}`
116
+ })
58
117
 
59
- // Given the SAH pool always eagerly creates files with empty non-header data,
60
- // we want to return undefined if the file exists but is empty
61
- if (data.byteLength === 0) {
62
- return undefined
63
- }
118
+ export const getStateDbFileName = (schema: LiveStoreSchema) => {
119
+ const schemaHashSuffix =
120
+ schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
121
+ return `state${schemaHashSuffix}.db`
122
+ }
64
123
 
65
- return new Uint8Array(data)
66
- }
124
+ export const MAX_ARCHIVED_STATE_DBS_IN_DEV = 3
125
+ export const ARCHIVE_DIR_NAME = 'archive'
67
126
 
68
- return undefined
69
- }).pipe(
70
- Effect.logWarnIfTakesLongerThan({
71
- duration: 1000,
72
- label: '@livestore/adapter-web:readPersistedAppDbFromClientSession',
73
- }),
74
- Effect.withPerformanceMeasure('@livestore/adapter-web:readPersistedAppDbFromClientSession'),
75
- Effect.withSpan('@livestore/adapter-web:readPersistedAppDbFromClientSession'),
76
- )
127
+ /**
128
+ * Cleanup old state database files after successful migration.
129
+ * This prevents OPFS file pool capacity from being exhausted by accumulated schema files.
130
+ *
131
+ * @param vfs - The AccessHandlePoolVFS instance for safe file operations
132
+ * @param currentSchema - Current schema (to avoid deleting the active database)
133
+ */
134
+ export const cleanupOldStateDbFiles: (options: {
135
+ vfs: WebDatabaseMetadataOpfs['vfs']
136
+ currentSchema: LiveStoreSchema
137
+ opfsDirectory: string
138
+ }) => Effect.Effect<
139
+ void,
140
+ // All the following errors could actually happen:
141
+ | WebError.AbortError
142
+ | WebError.DataCloneError
143
+ | WebError.EvalError
144
+ | WebError.InvalidModificationError
145
+ | WebError.InvalidStateError
146
+ | WebError.NoModificationAllowedError
147
+ | WebError.NotAllowedError
148
+ | WebError.NotFoundError
149
+ | WebError.QuotaExceededError
150
+ | WebError.RangeError
151
+ | WebError.ReferenceError
152
+ | WebError.SecurityError
153
+ | WebError.TypeError
154
+ | WebError.TypeMismatchError
155
+ | WebError.URIError
156
+ | WebError.UnknownError
157
+ | Opfs.OpfsError
158
+ | PersistedSqliteError,
159
+ Opfs.Opfs
160
+ > = Effect.fn('@livestore/adapter-web:cleanupOldStateDbFiles')(function* ({ vfs, currentSchema, opfsDirectory }) {
161
+ // Only cleanup for auto migration strategy because:
162
+ // - Auto strategy: Creates new database files per schema change (e.g., state123.db, state456.db)
163
+ // which accumulate over time and can exhaust OPFS file pool capacity
164
+ // - Manual strategy: Always reuses the same database file (statefixed.db) across schema changes,
165
+ // so there are never multiple old files to clean up
166
+ if (currentSchema.state.sqlite.migrations.strategy === 'manual') {
167
+ yield* Effect.logDebug('Skipping state db cleanup - manual migration strategy uses fixed filename')
168
+ return
169
+ }
77
170
 
78
- export const resetPersistedDataFromClientSession = ({
79
- storageOptions,
80
- storeId,
81
- }: {
82
- storageOptions: WorkerSchema.StorageType
83
- storeId: string
84
- }) =>
85
- Effect.gen(function* () {
86
- const directory = sanitizeOpfsDir(storageOptions.directory, storeId)
87
- yield* opfsDeleteAbs(directory)
88
- }).pipe(
89
- Effect.retry({
90
- schedule: Schedule.exponentialBackoff10Sec,
91
- }),
92
- Effect.withSpan('@livestore/adapter-web:resetPersistedDataFromClientSession'),
171
+ const isDev = isDevEnv()
172
+ const currentDbFileName = getStateDbFileName(currentSchema)
173
+ const currentPath = `/${currentDbFileName}`
174
+
175
+ const allPaths = yield* Effect.sync(() => vfs.getTrackedFilePaths())
176
+ const oldStateDbPaths = allPaths.filter(
177
+ (path) => path.startsWith('/state') && path.endsWith('.db') && path !== currentPath,
93
178
  )
94
179
 
95
- const opfsDeleteAbs = (absPath: string) =>
96
- Effect.promise(async () => {
97
- // Get the root directory handle
98
- const root = await OpfsUtils.rootHandlePromise
180
+ if (oldStateDbPaths.length === 0) {
181
+ yield* Effect.logDebug('No old database files found')
182
+ return
183
+ }
99
184
 
100
- // Split the absolute path to traverse directories
101
- const pathParts = absPath.split('/').filter((part) => part.length)
185
+ const absoluteArchiveDirName = `${opfsDirectory}/${ARCHIVE_DIR_NAME}`
186
+ if (isDev && !(yield* Opfs.exists(absoluteArchiveDirName))) yield* Opfs.makeDirectory(absoluteArchiveDirName)
102
187
 
103
- try {
104
- // Traverse to the target file handle
105
- let currentDir = root
106
- for (let i = 0; i < pathParts.length - 1; i++) {
107
- currentDir = await currentDir.getDirectoryHandle(pathParts[i]!)
108
- }
188
+ for (const path of oldStateDbPaths) {
189
+ const fileName = path.startsWith('/') ? path.slice(1) : path
190
+
191
+ if (isDev) {
192
+ const archiveFileData = yield* vfs.readFilePayload(fileName)
193
+
194
+ const archiveFileName = `${Date.now()}-${fileName}`
195
+ const archivePath = `${opfsDirectory}/archive/${archiveFileName}`
196
+ const archiveData = new Uint8Array(archiveFileData)
197
+
198
+ // Prefer writeFile (atomic) when createWritable is available (Chrome, Firefox, Safari 26+),
199
+ // fall back to syncWriteFile (non-atomic) for Safari 18.x compatibility.
200
+ // TODO: Remove feature detection and use writeFile directly when Safari >= 26 is widely available.
201
+ const supportsCreateWritable =
202
+ typeof FileSystemFileHandle !== 'undefined' && 'createWritable' in FileSystemFileHandle.prototype
109
203
 
110
- // Delete the file
111
- await currentDir.removeEntry(pathParts.at(-1)!, { recursive: true })
112
- } catch (error) {
113
- if (error instanceof DOMException && error.name === 'NotFoundError') {
114
- // Can ignore as it's already been deleted or not there in the first place
115
- return
204
+ if (supportsCreateWritable) {
205
+ yield* Opfs.writeFile(archivePath, archiveData)
116
206
  } else {
117
- throw error
207
+ yield* Opfs.syncWriteFile(archivePath, archiveData)
118
208
  }
119
209
  }
120
- }).pipe(
121
- UnexpectedError.mapToUnexpectedError,
122
- Effect.withSpan('@livestore/adapter-web:worker:opfsDeleteFile', { attributes: { absFilePath: absPath } }),
123
- )
124
210
 
125
- export const sanitizeOpfsDir = (directory: string | undefined, storeId: string) => {
126
- // Root dir should be `''` not `/`
127
- if (directory === undefined || directory === '' || directory === '/')
128
- return `livestore-${storeId}@${liveStoreStorageFormatVersion}`
211
+ const vfsResultCode = yield* Effect.try({
212
+ try: () => vfs.jDelete(fileName, 0),
213
+ catch: (cause) =>
214
+ new PersistedSqliteError({ message: `Failed to delete old state database file: ${fileName}`, cause }),
215
+ })
129
216
 
130
- if (directory.includes('/')) {
131
- throw new Error(
132
- `@livestore/adapter-web:worker:sanitizeOpfsDir: Nested directories are not yet supported ('${directory}')`,
133
- )
217
+ // 0 indicates a successful result in SQLite.
218
+ // See https://www.sqlite.org/c3ref/c_abort.html
219
+ if (vfsResultCode !== 0) {
220
+ return yield* new PersistedSqliteError({
221
+ message: `Failed to delete old state database file: ${fileName}, got result code: ${vfsResultCode}`,
222
+ })
223
+ }
224
+
225
+ yield* Effect.logDebug(`Deleted old state database file: ${fileName}`)
134
226
  }
135
227
 
136
- return `${directory}@${liveStoreStorageFormatVersion}`
137
- }
228
+ if (isDev) {
229
+ yield* pruneArchiveDirectory({
230
+ archiveDirectory: absoluteArchiveDirName,
231
+ keep: MAX_ARCHIVED_STATE_DBS_IN_DEV,
232
+ })
233
+ }
234
+ })
138
235
 
139
- export const getStateDbFileName = (schema: LiveStoreSchema) => {
140
- const schemaHashSuffix =
141
- schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
142
- return `state${schemaHashSuffix}.db`
143
- }
236
+ const pruneArchiveDirectory = Effect.fn('@livestore/adapter-web:pruneArchiveDirectory')(function* ({
237
+ archiveDirectory,
238
+ keep,
239
+ }: {
240
+ archiveDirectory: string
241
+ keep: number
242
+ }) {
243
+ const archiveDirHandle = yield* Opfs.getDirectoryHandleByPath(archiveDirectory)
244
+ const handlesStream = yield* Opfs.Opfs.values(archiveDirHandle)
245
+ const filesWithMetadata = yield* handlesStream.pipe(
246
+ Stream.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file'),
247
+ Stream.mapEffect((fileHandle) => Opfs.getMetadata(fileHandle)),
248
+ Stream.runCollect,
249
+ )
250
+ const filesToDelete = filesWithMetadata.pipe(
251
+ Chunk.sort(Order.mapInput(Order.number, (entry: { lastModified: number }) => entry.lastModified)),
252
+ Chunk.drop(keep),
253
+ Chunk.toReadonlyArray,
254
+ )
255
+
256
+ if (filesToDelete.length === 0) return
257
+
258
+ yield* Effect.forEach(filesToDelete, ({ name }) => Opfs.Opfs.removeEntry(archiveDirHandle, name))
259
+
260
+ yield* Effect.logDebug(`Pruned ${filesToDelete.length} old database file(s) from archive directory`)
261
+ })
@@ -1,8 +1,15 @@
1
1
  import { ShutdownChannel } from '@livestore/common/leader-thread'
2
- import { WebChannel } from '@livestore/utils/effect'
2
+ import type { Effect, Scope, WebChannel } from '@livestore/utils/effect'
3
+ import { WebChannelBrowser } from '@livestore/utils/effect/browser'
3
4
 
4
- export const makeShutdownChannel = (storeId: string) =>
5
- WebChannel.broadcastChannel({
5
+ export const makeShutdownChannel = (
6
+ storeId: string,
7
+ ): Effect.Effect<
8
+ WebChannel.WebChannel<typeof ShutdownChannel.All.Type, typeof ShutdownChannel.All.Type>,
9
+ never,
10
+ Scope.Scope
11
+ > =>
12
+ WebChannelBrowser.broadcastChannel({
6
13
  channelName: `livestore.shutdown.${storeId}`,
7
14
  schema: ShutdownChannel.All,
8
15
  })
@@ -1,10 +1,17 @@
1
- import { Schema, WebChannel } from '@livestore/utils/effect'
1
+ import { type Effect, Schema, type Scope, type WebChannel } from '@livestore/utils/effect'
2
+ import { WebChannelBrowser } from '@livestore/utils/effect/browser'
2
3
 
3
4
  export class DedicatedWorkerDisconnectBroadcast extends Schema.TaggedStruct('DedicatedWorkerDisconnectBroadcast', {}) {}
4
5
 
5
6
  /** Used across workers for leader election purposes */
6
- export const makeWorkerDisconnectChannel = (storeId: string) =>
7
- WebChannel.broadcastChannel({
7
+ export const makeWorkerDisconnectChannel = (
8
+ storeId: string,
9
+ ): Effect.Effect<
10
+ WebChannel.WebChannel<typeof DedicatedWorkerDisconnectBroadcast.Type, typeof DedicatedWorkerDisconnectBroadcast.Type>,
11
+ never,
12
+ Scope.Scope
13
+ > =>
14
+ WebChannelBrowser.broadcastChannel({
8
15
  channelName: `livestore.worker-disconnect.${storeId}`,
9
16
  schema: DedicatedWorkerDisconnectBroadcast,
10
17
  })
@@ -4,9 +4,11 @@ import {
4
4
  LeaderAheadError,
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: UnknownError,
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: UnknownError,
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: Schema.Union(UnknownError, LeaderAheadError),
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: UnknownError,
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: UnknownError,
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: UnknownError,
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: UnknownError,
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: UnknownError,
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: UnknownError,
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: UnknownError,
157
+ },
158
+ ) {}
159
+
160
+ export class LeaderWorkerInnerSyncStateStream extends Schema.TaggedRequest<LeaderWorkerInnerSyncStateStream>()(
161
+ 'SyncStateStream',
162
+ {
163
+ payload: {},
164
+ success: SyncState.SyncState,
165
+ failure: UnknownError,
166
+ },
167
+ ) {}
168
+
169
+ export class LeaderWorkerInnerGetNetworkStatus extends Schema.TaggedRequest<LeaderWorkerInnerGetNetworkStatus>()(
170
+ 'GetNetworkStatus',
171
+ {
172
+ payload: {},
173
+ success: SyncBackend.NetworkStatus,
174
+ failure: UnknownError,
175
+ },
176
+ ) {}
177
+
178
+ export class LeaderWorkerInnerNetworkStatusStream extends Schema.TaggedRequest<LeaderWorkerInnerNetworkStatusStream>()(
179
+ 'NetworkStatusStream',
180
+ {
181
+ payload: {},
182
+ success: SyncBackend.NetworkStatus,
183
+ failure: UnknownError,
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: UnknownError,
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: UnknownError,
163
201
  },
164
202
  ) {}
165
203
 
@@ -168,56 +206,57 @@ 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
244
  export class SharedWorkerRequest extends Schema.Union(
209
- SharedWorkerInitialMessage,
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