@livestore/adapter-web 0.4.0-dev.2 → 0.4.0-dev.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/in-memory/in-memory-adapter.d.ts +15 -5
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +29 -15
- 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 +67 -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 +114 -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 +103 -58
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +48 -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 +47 -21
- 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 +65 -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 +36 -20
- 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 +112 -59
- package/src/web-worker/client-session/sqlite-loader.ts +19 -0
- package/src/web-worker/common/persisted-sqlite.ts +219 -113
- 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 +62 -35
- package/src/web-worker/leader-worker/make-leader-worker.ts +58 -33
- package/src/web-worker/shared-worker/make-shared-worker.ts +95 -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,249 @@
|
|
|
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)
|
|
37
|
+
|
|
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
|
+
)
|
|
25
58
|
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
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
|
+
})
|
|
28
63
|
}
|
|
29
64
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return fileName ? { fileName, file } : undefined
|
|
34
|
-
}
|
|
65
|
+
const stateDbBuffer = yield* Effect.promise(() =>
|
|
66
|
+
stateDbFileOption.value.file.slice(HEADER_OFFSET_DATA).arrayBuffer(),
|
|
67
|
+
)
|
|
35
68
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
return results
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
)
|
|
85
|
+
|
|
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
|
+
)
|
|
100
|
+
|
|
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
|
+
}
|
|
49
108
|
|
|
50
|
-
|
|
109
|
+
if (directory.includes('/')) {
|
|
110
|
+
return yield* new PersistedSqliteError({
|
|
111
|
+
message: `Nested directories are not yet supported ('${directory}')`,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
51
114
|
|
|
52
|
-
|
|
53
|
-
|
|
115
|
+
return `${directory}@${liveStoreStorageFormatVersion}`
|
|
116
|
+
})
|
|
54
117
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|
|
58
123
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
124
|
+
export const MAX_ARCHIVED_STATE_DBS_IN_DEV = 3
|
|
125
|
+
export const ARCHIVE_DIR_NAME = 'archive'
|
|
126
|
+
|
|
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
|
+
}
|
|
64
170
|
|
|
65
|
-
|
|
66
|
-
|
|
171
|
+
const isDev = isDevEnv()
|
|
172
|
+
const currentDbFileName = getStateDbFileName(currentSchema)
|
|
173
|
+
const currentPath = `/${currentDbFileName}`
|
|
67
174
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
duration: 1000,
|
|
72
|
-
label: '@livestore/adapter-web:readPersistedAppDbFromClientSession',
|
|
73
|
-
}),
|
|
74
|
-
Effect.withPerformanceMeasure('@livestore/adapter-web:readPersistedAppDbFromClientSession'),
|
|
75
|
-
Effect.withSpan('@livestore/adapter-web:readPersistedAppDbFromClientSession'),
|
|
175
|
+
const allPaths = yield* Effect.sync(() => vfs.getTrackedFilePaths())
|
|
176
|
+
const oldStateDbPaths = allPaths.filter(
|
|
177
|
+
(path) => path.startsWith('/state') && path.endsWith('.db') && path !== currentPath,
|
|
76
178
|
)
|
|
77
179
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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'),
|
|
93
|
-
)
|
|
180
|
+
if (oldStateDbPaths.length === 0) {
|
|
181
|
+
yield* Effect.logDebug('No old database files found')
|
|
182
|
+
return
|
|
183
|
+
}
|
|
94
184
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
currentDir = await currentDir.getDirectoryHandle(pathParts[i]!)
|
|
108
|
-
}
|
|
109
|
-
|
|
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
|
|
116
|
-
} else {
|
|
117
|
-
throw error
|
|
118
|
-
}
|
|
185
|
+
const absoluteArchiveDirName = `${opfsDirectory}/${ARCHIVE_DIR_NAME}`
|
|
186
|
+
if (isDev && !(yield* Opfs.exists(absoluteArchiveDirName))) yield* Opfs.makeDirectory(absoluteArchiveDirName)
|
|
187
|
+
|
|
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
|
+
|
|
196
|
+
yield* Opfs.writeFile(`${opfsDirectory}/archive/${archiveFileName}`, new Uint8Array(archiveFileData))
|
|
119
197
|
}
|
|
120
|
-
}).pipe(
|
|
121
|
-
UnexpectedError.mapToUnexpectedError,
|
|
122
|
-
Effect.withSpan('@livestore/adapter-web:worker:opfsDeleteFile', { attributes: { absFilePath: absPath } }),
|
|
123
|
-
)
|
|
124
198
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
199
|
+
const vfsResultCode = yield* Effect.try({
|
|
200
|
+
try: () => vfs.jDelete(fileName, 0),
|
|
201
|
+
catch: (cause) =>
|
|
202
|
+
new PersistedSqliteError({ message: `Failed to delete old state database file: ${fileName}`, cause }),
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// 0 indicates a successful result in SQLite.
|
|
206
|
+
// See https://www.sqlite.org/c3ref/c_abort.html
|
|
207
|
+
if (vfsResultCode !== 0) {
|
|
208
|
+
return yield* new PersistedSqliteError({
|
|
209
|
+
message: `Failed to delete old state database file: ${fileName}, got result code: ${vfsResultCode}`,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
129
212
|
|
|
130
|
-
|
|
131
|
-
throw new Error(
|
|
132
|
-
`@livestore/adapter-web:worker:sanitizeOpfsDir: Nested directories are not yet supported ('${directory}')`,
|
|
133
|
-
)
|
|
213
|
+
yield* Effect.logDebug(`Deleted old state database file: ${fileName}`)
|
|
134
214
|
}
|
|
135
215
|
|
|
136
|
-
|
|
137
|
-
|
|
216
|
+
if (isDev) {
|
|
217
|
+
yield* pruneArchiveDirectory({
|
|
218
|
+
archiveDirectory: absoluteArchiveDirName,
|
|
219
|
+
keep: MAX_ARCHIVED_STATE_DBS_IN_DEV,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
})
|
|
138
223
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
224
|
+
const pruneArchiveDirectory = Effect.fn('@livestore/adapter-web:pruneArchiveDirectory')(function* ({
|
|
225
|
+
archiveDirectory,
|
|
226
|
+
keep,
|
|
227
|
+
}: {
|
|
228
|
+
archiveDirectory: string
|
|
229
|
+
keep: number
|
|
230
|
+
}) {
|
|
231
|
+
const archiveDirHandle = yield* Opfs.getDirectoryHandleByPath(archiveDirectory)
|
|
232
|
+
const handlesStream = yield* Opfs.Opfs.values(archiveDirHandle)
|
|
233
|
+
const filesWithMetadata = yield* handlesStream.pipe(
|
|
234
|
+
Stream.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file'),
|
|
235
|
+
Stream.mapEffect((fileHandle) => Opfs.getMetadata(fileHandle)),
|
|
236
|
+
Stream.runCollect,
|
|
237
|
+
)
|
|
238
|
+
const filesToDelete = filesWithMetadata.pipe(
|
|
239
|
+
Chunk.sort(Order.mapInput(Order.number, (entry: { lastModified: number }) => entry.lastModified)),
|
|
240
|
+
Chunk.drop(keep),
|
|
241
|
+
Chunk.toReadonlyArray,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if (filesToDelete.length === 0) return
|
|
245
|
+
|
|
246
|
+
yield* Effect.forEach(filesToDelete, ({ name }) => Opfs.Opfs.removeEntry(archiveDirHandle, name))
|
|
247
|
+
|
|
248
|
+
yield* Effect.logDebug(`Pruned ${filesToDelete.length} old database file(s) from archive directory`)
|
|
249
|
+
})
|
|
@@ -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,8 +4,9 @@ import {
|
|
|
4
4
|
LeaderAheadError,
|
|
5
5
|
liveStoreVersion,
|
|
6
6
|
MigrationsReport,
|
|
7
|
+
SyncBackend,
|
|
7
8
|
SyncState,
|
|
8
|
-
|
|
9
|
+
UnknownError,
|
|
9
10
|
} from '@livestore/common'
|
|
10
11
|
import { EventSequenceNumber, LiveStoreEvent } from '@livestore/common/schema'
|
|
11
12
|
import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
|
|
@@ -48,7 +49,7 @@ export class LeaderWorkerOuterInitialMessage extends Schema.TaggedRequest<Leader
|
|
|
48
49
|
{
|
|
49
50
|
payload: { port: Transferable.MessagePort, storeId: Schema.String, clientId: Schema.String },
|
|
50
51
|
success: Schema.Void,
|
|
51
|
-
failure:
|
|
52
|
+
failure: UnknownError,
|
|
52
53
|
},
|
|
53
54
|
) {}
|
|
54
55
|
|
|
@@ -64,10 +65,10 @@ export class LeaderWorkerInnerInitialMessage extends Schema.TaggedRequest<Leader
|
|
|
64
65
|
storeId: Schema.String,
|
|
65
66
|
clientId: Schema.String,
|
|
66
67
|
debugInstanceId: Schema.String,
|
|
67
|
-
|
|
68
|
+
syncPayloadEncoded: Schema.UndefinedOr(Schema.JsonValue),
|
|
68
69
|
},
|
|
69
70
|
success: Schema.Void,
|
|
70
|
-
failure:
|
|
71
|
+
failure: UnknownError,
|
|
71
72
|
},
|
|
72
73
|
) {}
|
|
73
74
|
|
|
@@ -76,7 +77,7 @@ export class LeaderWorkerInnerBootStatusStream extends Schema.TaggedRequest<Lead
|
|
|
76
77
|
{
|
|
77
78
|
payload: {},
|
|
78
79
|
success: BootStatus,
|
|
79
|
-
failure:
|
|
80
|
+
failure: UnknownError,
|
|
80
81
|
},
|
|
81
82
|
) {}
|
|
82
83
|
|
|
@@ -84,27 +85,27 @@ export class LeaderWorkerInnerPushToLeader extends Schema.TaggedRequest<LeaderWo
|
|
|
84
85
|
'PushToLeader',
|
|
85
86
|
{
|
|
86
87
|
payload: {
|
|
87
|
-
batch: Schema.Array(LiveStoreEvent.
|
|
88
|
+
batch: Schema.Array(Schema.typeSchema(LiveStoreEvent.Client.Encoded)),
|
|
88
89
|
},
|
|
89
|
-
success: Schema.Void
|
|
90
|
-
failure: Schema.Union(
|
|
90
|
+
success: Schema.Void as Schema.Schema<void>,
|
|
91
|
+
failure: Schema.Union(UnknownError, LeaderAheadError),
|
|
91
92
|
},
|
|
92
93
|
) {}
|
|
93
94
|
|
|
94
95
|
export class LeaderWorkerInnerPullStream extends Schema.TaggedRequest<LeaderWorkerInnerPullStream>()('PullStream', {
|
|
95
96
|
payload: {
|
|
96
|
-
cursor: EventSequenceNumber.
|
|
97
|
+
cursor: Schema.typeSchema(EventSequenceNumber.Client.Composite),
|
|
97
98
|
},
|
|
98
99
|
success: Schema.Struct({
|
|
99
100
|
payload: SyncState.PayloadUpstream,
|
|
100
101
|
}),
|
|
101
|
-
failure:
|
|
102
|
+
failure: UnknownError,
|
|
102
103
|
}) {}
|
|
103
104
|
|
|
104
105
|
export class LeaderWorkerInnerExport extends Schema.TaggedRequest<LeaderWorkerInnerExport>()('Export', {
|
|
105
106
|
payload: {},
|
|
106
107
|
success: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
|
|
107
|
-
failure:
|
|
108
|
+
failure: UnknownError,
|
|
108
109
|
}) {}
|
|
109
110
|
|
|
110
111
|
export class LeaderWorkerInnerExportEventlog extends Schema.TaggedRequest<LeaderWorkerInnerExportEventlog>()(
|
|
@@ -112,7 +113,7 @@ export class LeaderWorkerInnerExportEventlog extends Schema.TaggedRequest<Leader
|
|
|
112
113
|
{
|
|
113
114
|
payload: {},
|
|
114
115
|
success: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
|
|
115
|
-
failure:
|
|
116
|
+
failure: UnknownError,
|
|
116
117
|
},
|
|
117
118
|
) {}
|
|
118
119
|
|
|
@@ -124,7 +125,7 @@ export class LeaderWorkerInnerGetRecreateSnapshot extends Schema.TaggedRequest<L
|
|
|
124
125
|
snapshot: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
|
|
125
126
|
migrationsReport: MigrationsReport,
|
|
126
127
|
}),
|
|
127
|
-
failure:
|
|
128
|
+
failure: UnknownError,
|
|
128
129
|
},
|
|
129
130
|
) {}
|
|
130
131
|
|
|
@@ -132,8 +133,8 @@ export class LeaderWorkerInnerGetLeaderHead extends Schema.TaggedRequest<LeaderW
|
|
|
132
133
|
'GetLeaderHead',
|
|
133
134
|
{
|
|
134
135
|
payload: {},
|
|
135
|
-
success: EventSequenceNumber.
|
|
136
|
-
failure:
|
|
136
|
+
success: Schema.typeSchema(EventSequenceNumber.Client.Composite),
|
|
137
|
+
failure: UnknownError,
|
|
137
138
|
},
|
|
138
139
|
) {}
|
|
139
140
|
|
|
@@ -142,14 +143,41 @@ export class LeaderWorkerInnerGetLeaderSyncState extends Schema.TaggedRequest<Le
|
|
|
142
143
|
{
|
|
143
144
|
payload: {},
|
|
144
145
|
success: SyncState.SyncState,
|
|
145
|
-
failure:
|
|
146
|
+
failure: UnknownError,
|
|
147
|
+
},
|
|
148
|
+
) {}
|
|
149
|
+
|
|
150
|
+
export class LeaderWorkerInnerSyncStateStream extends Schema.TaggedRequest<LeaderWorkerInnerSyncStateStream>()(
|
|
151
|
+
'SyncStateStream',
|
|
152
|
+
{
|
|
153
|
+
payload: {},
|
|
154
|
+
success: SyncState.SyncState,
|
|
155
|
+
failure: UnknownError,
|
|
156
|
+
},
|
|
157
|
+
) {}
|
|
158
|
+
|
|
159
|
+
export class LeaderWorkerInnerGetNetworkStatus extends Schema.TaggedRequest<LeaderWorkerInnerGetNetworkStatus>()(
|
|
160
|
+
'GetNetworkStatus',
|
|
161
|
+
{
|
|
162
|
+
payload: {},
|
|
163
|
+
success: SyncBackend.NetworkStatus,
|
|
164
|
+
failure: UnknownError,
|
|
165
|
+
},
|
|
166
|
+
) {}
|
|
167
|
+
|
|
168
|
+
export class LeaderWorkerInnerNetworkStatusStream extends Schema.TaggedRequest<LeaderWorkerInnerNetworkStatusStream>()(
|
|
169
|
+
'NetworkStatusStream',
|
|
170
|
+
{
|
|
171
|
+
payload: {},
|
|
172
|
+
success: SyncBackend.NetworkStatus,
|
|
173
|
+
failure: UnknownError,
|
|
146
174
|
},
|
|
147
175
|
) {}
|
|
148
176
|
|
|
149
177
|
export class LeaderWorkerInnerShutdown extends Schema.TaggedRequest<LeaderWorkerInnerShutdown>()('Shutdown', {
|
|
150
178
|
payload: {},
|
|
151
179
|
success: Schema.Void,
|
|
152
|
-
failure:
|
|
180
|
+
failure: UnknownError,
|
|
153
181
|
}) {}
|
|
154
182
|
|
|
155
183
|
export class LeaderWorkerInnerExtraDevtoolsMessage extends Schema.TaggedRequest<LeaderWorkerInnerExtraDevtoolsMessage>()(
|
|
@@ -159,7 +187,7 @@ export class LeaderWorkerInnerExtraDevtoolsMessage extends Schema.TaggedRequest<
|
|
|
159
187
|
message: Devtools.Leader.MessageToApp,
|
|
160
188
|
},
|
|
161
189
|
success: Schema.Void,
|
|
162
|
-
failure:
|
|
190
|
+
failure: UnknownError,
|
|
163
191
|
},
|
|
164
192
|
) {}
|
|
165
193
|
|
|
@@ -173,40 +201,36 @@ export const LeaderWorkerInnerRequest = Schema.Union(
|
|
|
173
201
|
LeaderWorkerInnerGetRecreateSnapshot,
|
|
174
202
|
LeaderWorkerInnerGetLeaderHead,
|
|
175
203
|
LeaderWorkerInnerGetLeaderSyncState,
|
|
204
|
+
LeaderWorkerInnerSyncStateStream,
|
|
205
|
+
LeaderWorkerInnerGetNetworkStatus,
|
|
206
|
+
LeaderWorkerInnerNetworkStatusStream,
|
|
176
207
|
LeaderWorkerInnerShutdown,
|
|
177
208
|
LeaderWorkerInnerExtraDevtoolsMessage,
|
|
178
209
|
WebmeshWorker.Schema.CreateConnection,
|
|
179
210
|
)
|
|
180
211
|
export type LeaderWorkerInnerRequest = typeof LeaderWorkerInnerRequest.Type
|
|
181
212
|
|
|
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
213
|
export class SharedWorkerUpdateMessagePort extends Schema.TaggedRequest<SharedWorkerUpdateMessagePort>()(
|
|
198
214
|
'UpdateMessagePort',
|
|
199
215
|
{
|
|
200
216
|
payload: {
|
|
201
217
|
port: Transferable.MessagePort,
|
|
218
|
+
// Version gate to prevent mixed LiveStore builds talking to the same SharedWorker
|
|
219
|
+
liveStoreVersion: Schema.Literal(liveStoreVersion),
|
|
220
|
+
/**
|
|
221
|
+
* Initial configuration for the leader worker. This replaces the previous
|
|
222
|
+
* two-phase SharedWorker handshake and is sent under the tab lock by the
|
|
223
|
+
* elected leader. Subsequent calls can omit changes and will simply rebind
|
|
224
|
+
* the port (join) without reinitializing the store.
|
|
225
|
+
*/
|
|
226
|
+
initial: LeaderWorkerInnerInitialMessage,
|
|
202
227
|
},
|
|
203
228
|
success: Schema.Void,
|
|
204
|
-
failure:
|
|
229
|
+
failure: UnknownError,
|
|
205
230
|
},
|
|
206
231
|
) {}
|
|
207
232
|
|
|
208
233
|
export class SharedWorkerRequest extends Schema.Union(
|
|
209
|
-
SharedWorkerInitialMessage,
|
|
210
234
|
SharedWorkerUpdateMessagePort,
|
|
211
235
|
|
|
212
236
|
// Proxied requests
|
|
@@ -218,6 +242,9 @@ export class SharedWorkerRequest extends Schema.Union(
|
|
|
218
242
|
LeaderWorkerInnerExportEventlog,
|
|
219
243
|
LeaderWorkerInnerGetLeaderHead,
|
|
220
244
|
LeaderWorkerInnerGetLeaderSyncState,
|
|
245
|
+
LeaderWorkerInnerSyncStateStream,
|
|
246
|
+
LeaderWorkerInnerGetNetworkStatus,
|
|
247
|
+
LeaderWorkerInnerNetworkStatusStream,
|
|
221
248
|
LeaderWorkerInnerShutdown,
|
|
222
249
|
LeaderWorkerInnerExtraDevtoolsMessage,
|
|
223
250
|
|