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