@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/in-memory/in-memory-adapter.d.ts +49 -5
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +69 -16
- package/dist/in-memory/in-memory-adapter.js.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.d.ts +1 -1
- package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.js +14 -3
- package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.d.ts +15 -0
- package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.js +68 -46
- package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
- package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
- package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
- package/dist/web-worker/client-session/sqlite-loader.js +16 -0
- package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
- package/dist/web-worker/common/persisted-sqlite.d.ts +23 -7
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.js +125 -76
- package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
- package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
- package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
- package/dist/web-worker/common/shutdown-channel.js +2 -2
- package/dist/web-worker/common/shutdown-channel.js.map +1 -1
- package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
- package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
- package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
- package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
- package/dist/web-worker/common/worker-schema.d.ts +147 -56
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +55 -36
- package/dist/web-worker/common/worker-schema.js.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts +4 -2
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.js +63 -27
- package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -1
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.js +66 -49
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
- package/dist/web-worker/vite-dev-polyfill.js +1 -0
- package/dist/web-worker/vite-dev-polyfill.js.map +1 -1
- package/package.json +8 -9
- package/src/in-memory/in-memory-adapter.ts +83 -21
- package/src/web-worker/ambient.d.ts +7 -24
- package/src/web-worker/client-session/client-session-devtools.ts +18 -3
- package/src/web-worker/client-session/persisted-adapter.ts +117 -59
- package/src/web-worker/client-session/sqlite-loader.ts +19 -0
- package/src/web-worker/common/persisted-sqlite.ts +225 -107
- package/src/web-worker/common/shutdown-channel.ts +10 -3
- package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
- package/src/web-worker/common/worker-schema.ts +74 -35
- package/src/web-worker/leader-worker/make-leader-worker.ts +86 -41
- package/src/web-worker/shared-worker/make-shared-worker.ts +96 -75
- package/src/web-worker/vite-dev-polyfill.ts +1 -0
- package/dist/opfs-utils.d.ts +0 -5
- package/dist/opfs-utils.d.ts.map +0 -1
- package/dist/opfs-utils.js +0 -43
- package/dist/opfs-utils.js.map +0 -1
- package/src/opfs-utils.ts +0 -61
|
@@ -1,143 +1,261 @@
|
|
|
1
|
-
import { liveStoreStorageFormatVersion
|
|
1
|
+
import { liveStoreStorageFormatVersion } from '@livestore/common'
|
|
2
2
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
14
|
+
message: Schema.String,
|
|
15
|
+
cause: Schema.optional(Schema.Defect),
|
|
11
16
|
}) {}
|
|
12
17
|
|
|
13
|
-
export const
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
109
|
+
if (directory.includes('/')) {
|
|
110
|
+
return yield* new PersistedSqliteError({
|
|
111
|
+
message: `Nested directories are not yet supported ('${directory}')`,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
54
114
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// console.debug('readPersistedAppDbFromClientSession', data.byteLength, data)
|
|
115
|
+
return `${directory}@${liveStoreStorageFormatVersion}`
|
|
116
|
+
})
|
|
58
117
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
124
|
+
export const MAX_ARCHIVED_STATE_DBS_IN_DEV = 3
|
|
125
|
+
export const ARCHIVE_DIR_NAME = 'archive'
|
|
67
126
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
180
|
+
if (oldStateDbPaths.length === 0) {
|
|
181
|
+
yield* Effect.logDebug('No old database files found')
|
|
182
|
+
return
|
|
183
|
+
}
|
|
99
184
|
|
|
100
|
-
|
|
101
|
-
|
|
185
|
+
const absoluteArchiveDirName = `${opfsDirectory}/${ARCHIVE_DIR_NAME}`
|
|
186
|
+
if (isDev && !(yield* Opfs.exists(absoluteArchiveDirName))) yield* Opfs.makeDirectory(absoluteArchiveDirName)
|
|
102
187
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
228
|
+
if (isDev) {
|
|
229
|
+
yield* pruneArchiveDirectory({
|
|
230
|
+
archiveDirectory: absoluteArchiveDirName,
|
|
231
|
+
keep: MAX_ARCHIVED_STATE_DBS_IN_DEV,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
})
|
|
138
235
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 = (
|
|
5
|
-
|
|
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 = (
|
|
7
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
69
|
+
syncPayloadEncoded: Schema.UndefinedOr(Schema.JsonValue),
|
|
68
70
|
},
|
|
69
71
|
success: Schema.Void,
|
|
70
|
-
failure:
|
|
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:
|
|
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.
|
|
89
|
+
batch: Schema.Array(Schema.typeSchema(LiveStoreEvent.Client.Encoded)),
|
|
88
90
|
},
|
|
89
|
-
success: Schema.Void
|
|
90
|
-
failure: Schema.Union(
|
|
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.
|
|
98
|
+
cursor: Schema.typeSchema(EventSequenceNumber.Client.Composite),
|
|
97
99
|
},
|
|
98
100
|
success: Schema.Struct({
|
|
99
101
|
payload: SyncState.PayloadUpstream,
|
|
100
102
|
}),
|
|
101
|
-
failure:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
136
|
-
failure:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|