@syncular/client 0.30.10 → 0.30.11
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 +8 -0
- package/dist/apply.js +17 -25
- package/dist/blob.js +13 -39
- package/dist/client.js +118 -117
- package/dist/failed-overlay.d.ts +6 -2
- package/dist/failed-overlay.js +82 -15
- package/dist/outbox.js +11 -21
- package/dist/outcomes.d.ts +8 -0
- package/dist/outcomes.js +18 -27
- package/dist/previous-version.js +14 -28
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +50 -85
- package/dist/state.d.ts +6 -9
- package/dist/state.js +66 -32
- package/dist/wasm-database.js +40 -24
- package/dist/worker-host.js +37 -11
- package/package.json +3 -3
- package/src/apply.ts +17 -25
- package/src/blob.ts +17 -39
- package/src/client.ts +222 -163
- package/src/failed-overlay.ts +155 -17
- package/src/outbox.ts +11 -21
- package/src/outcomes.ts +35 -31
- package/src/previous-version.ts +14 -28
- package/src/schema.ts +91 -96
- package/src/state.ts +90 -33
- package/src/wasm-database.ts +56 -29
- package/src/worker-host.ts +34 -12
package/dist/worker-host.js
CHANGED
|
@@ -437,7 +437,29 @@ async function startWorkerCore(options) {
|
|
|
437
437
|
const worker = config.worker();
|
|
438
438
|
const pending = new Map();
|
|
439
439
|
const nextId = { value: 1 };
|
|
440
|
+
let terminated = false;
|
|
441
|
+
let rejectReady;
|
|
442
|
+
const closedError = new ClientSyncError(WORKER_FAILED_CODE, 'the handle was closed');
|
|
443
|
+
const terminate = () => {
|
|
444
|
+
if (terminated)
|
|
445
|
+
return;
|
|
446
|
+
terminated = true;
|
|
447
|
+
globalThis.removeEventListener?.('pagehide', terminate);
|
|
448
|
+
bridge?.close();
|
|
449
|
+
worker.terminate();
|
|
450
|
+
rejectReady?.(closedError);
|
|
451
|
+
for (const entry of pending.values())
|
|
452
|
+
entry.reject(closedError);
|
|
453
|
+
pending.clear();
|
|
454
|
+
void lease.release();
|
|
455
|
+
};
|
|
456
|
+
// Installed before bootstrap starts: pagehide must also terminate a worker
|
|
457
|
+
// whose first open/import is still pending. Termination closes its SAHs and
|
|
458
|
+
// releases its physical Web Lock; the next document waits for that lock.
|
|
459
|
+
globalThis.addEventListener?.('pagehide', terminate);
|
|
440
460
|
const invoke = (method, args) => {
|
|
461
|
+
if (terminated)
|
|
462
|
+
return Promise.reject(closedError);
|
|
441
463
|
const id = nextId.value++;
|
|
442
464
|
return new Promise((resolve, reject) => {
|
|
443
465
|
pending.set(id, { resolve, reject });
|
|
@@ -451,6 +473,7 @@ async function startWorkerCore(options) {
|
|
|
451
473
|
};
|
|
452
474
|
let bridge;
|
|
453
475
|
const ready = new Promise((resolve, reject) => {
|
|
476
|
+
rejectReady = reject;
|
|
454
477
|
const onMessage = (event) => {
|
|
455
478
|
const message = event.data;
|
|
456
479
|
switch (message.t) {
|
|
@@ -507,27 +530,30 @@ async function startWorkerCore(options) {
|
|
|
507
530
|
invoke,
|
|
508
531
|
bridge,
|
|
509
532
|
lease,
|
|
510
|
-
close: async (
|
|
533
|
+
close: async (terminateWorker) => {
|
|
534
|
+
if (terminated)
|
|
535
|
+
return;
|
|
511
536
|
bridge?.close();
|
|
512
537
|
try {
|
|
513
538
|
await invoke('close', []);
|
|
514
539
|
}
|
|
515
540
|
catch {
|
|
516
|
-
//
|
|
541
|
+
// A terminated or failed worker has no further close RPC to service.
|
|
542
|
+
}
|
|
543
|
+
if (terminateWorker)
|
|
544
|
+
terminate();
|
|
545
|
+
else {
|
|
546
|
+
globalThis.removeEventListener?.('pagehide', terminate);
|
|
547
|
+
for (const entry of pending.values())
|
|
548
|
+
entry.reject(closedError);
|
|
549
|
+
pending.clear();
|
|
550
|
+
await lease.release();
|
|
517
551
|
}
|
|
518
|
-
if (terminate)
|
|
519
|
-
worker.terminate();
|
|
520
|
-
const closedError = new ClientSyncError(WORKER_FAILED_CODE, 'the handle was closed');
|
|
521
|
-
for (const entry of pending.values())
|
|
522
|
-
entry.reject(closedError);
|
|
523
|
-
pending.clear();
|
|
524
|
-
await lease.release();
|
|
525
552
|
},
|
|
526
553
|
};
|
|
527
554
|
}
|
|
528
555
|
catch (error) {
|
|
529
|
-
|
|
530
|
-
await lease.release();
|
|
556
|
+
terminate();
|
|
531
557
|
throw error;
|
|
532
558
|
}
|
|
533
559
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.11",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -89,10 +89,10 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
92
|
-
"@syncular/core": "0.30.
|
|
92
|
+
"@syncular/core": "0.30.11"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.30.
|
|
95
|
+
"@syncular/server": "0.30.11",
|
|
96
96
|
"playwright": "1.63.0",
|
|
97
97
|
"@electric-sql/pglite": "^0.5.4"
|
|
98
98
|
}
|
package/src/apply.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retainedBaseWrite } from './failed-overlay';
|
|
1
|
+
import { retainedBaseWrite, restoreFailedBases } from './failed-overlay';
|
|
2
2
|
/**
|
|
3
3
|
* Local application of server data: `COMMIT` frames (§4.5), rows segments
|
|
4
4
|
* (§5.2, §5.6), and the scope-matched delete shared by the §3.3 purge
|
|
@@ -37,8 +37,7 @@ function upsertSql(table: CompiledClientTable): string {
|
|
|
37
37
|
.filter((name) => name !== quoteIdent(table.primaryKey))
|
|
38
38
|
.map((name) => `${name}=excluded.${name}`)
|
|
39
39
|
.join(', ');
|
|
40
|
-
return `INSERT INTO ${quoteIdent(table.name)}
|
|
41
|
-
ON CONFLICT (${quoteIdent(table.primaryKey)}) DO UPDATE SET ${updates}`;
|
|
40
|
+
return `INSERT INTO ${quoteIdent(table.name)}(${names.join(', ')})VALUES(${placeholders})ON CONFLICT(${quoteIdent(table.primaryKey)})DO UPDATE SET ${updates}`;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
export function upsertLocalRow(
|
|
@@ -48,10 +47,11 @@ export function upsertLocalRow(
|
|
|
48
47
|
syncVersion: number,
|
|
49
48
|
serverWrite = true,
|
|
50
49
|
): void {
|
|
50
|
+
db.exec(upsertSql(table), [...values.map(toSqlValue), syncVersion]);
|
|
51
51
|
if (serverWrite)
|
|
52
52
|
retainedBaseWrite(
|
|
53
53
|
db,
|
|
54
|
-
table
|
|
54
|
+
table,
|
|
55
55
|
String(values[table.primaryKeyIndex]),
|
|
56
56
|
Object.fromEntries(
|
|
57
57
|
table.columns.map((column, index) => [
|
|
@@ -61,7 +61,6 @@ export function upsertLocalRow(
|
|
|
61
61
|
),
|
|
62
62
|
syncVersion,
|
|
63
63
|
);
|
|
64
|
-
db.exec(upsertSql(table), [...values.map(toSqlValue), syncVersion]);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
export function deleteLocalRow(
|
|
@@ -70,9 +69,9 @@ export function deleteLocalRow(
|
|
|
70
69
|
rowId: string,
|
|
71
70
|
serverWrite = true,
|
|
72
71
|
): void {
|
|
73
|
-
if (serverWrite) retainedBaseWrite(db, table
|
|
72
|
+
if (serverWrite) retainedBaseWrite(db, table, rowId);
|
|
74
73
|
db.exec(
|
|
75
|
-
`DELETE FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
74
|
+
`DELETE FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
76
75
|
[rowId],
|
|
77
76
|
);
|
|
78
77
|
}
|
|
@@ -139,6 +138,7 @@ export async function applyCommitFrame(
|
|
|
139
138
|
});
|
|
140
139
|
}
|
|
141
140
|
transaction(() => {
|
|
141
|
+
restoreFailedBases(db, schema);
|
|
142
142
|
for (const change of resolved) {
|
|
143
143
|
if (change.op === 'delete') {
|
|
144
144
|
deleteLocalRow(db, change.table, change.rowId);
|
|
@@ -222,7 +222,7 @@ export function deleteScopedRows(
|
|
|
222
222
|
`SELECT ${quoteIdent(table.primaryKey)} AS id FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')}`,
|
|
223
223
|
params,
|
|
224
224
|
)) {
|
|
225
|
-
retainedBaseWrite(db, table
|
|
225
|
+
retainedBaseWrite(db, table, String(row.id));
|
|
226
226
|
}
|
|
227
227
|
db.exec(
|
|
228
228
|
`DELETE FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')}`,
|
|
@@ -273,9 +273,7 @@ export function evictScopedRows(
|
|
|
273
273
|
}
|
|
274
274
|
const target = quoteIdent(table.name);
|
|
275
275
|
db.exec(
|
|
276
|
-
`DELETE FROM ${target} WHERE ${pk} IN (
|
|
277
|
-
SELECT ${pk} FROM ${target} WHERE ${clauses.join(' AND ')}${pinnedClause} LIMIT 1024
|
|
278
|
-
)`,
|
|
276
|
+
`DELETE FROM ${target} WHERE ${pk} IN(SELECT ${pk} FROM ${target} WHERE ${clauses.join(' AND ')}${pinnedClause} LIMIT 1024)`,
|
|
279
277
|
params,
|
|
280
278
|
);
|
|
281
279
|
const remaining =
|
|
@@ -343,10 +341,7 @@ export async function applySqliteSegment(
|
|
|
343
341
|
let meta: ReturnType<ClientDatabase['query']>;
|
|
344
342
|
try {
|
|
345
343
|
meta = db.query(
|
|
346
|
-
`SELECT format,
|
|
347
|
-
"asOfCommitSeq" AS pin, "scopeDigest" AS sd,
|
|
348
|
-
"rowCount" AS rc
|
|
349
|
-
FROM ${IMAGE_ALIAS}."_syncular_segment"`,
|
|
344
|
+
`SELECT format,"table" AS tbl,"schemaVersion" AS sv,"asOfCommitSeq" AS pin,"scopeDigest" AS sd,"rowCount" AS rc FROM ${IMAGE_ALIAS}."_syncular_segment"`,
|
|
350
345
|
);
|
|
351
346
|
} catch {
|
|
352
347
|
imageInvalid('bytes are not a SQLite image with _syncular_segment');
|
|
@@ -396,7 +391,7 @@ export async function applySqliteSegment(
|
|
|
396
391
|
if (primaryKeys.length !== 1 || primaryKeys[0]?.name !== table.primaryKey)
|
|
397
392
|
imageInvalid('image primary key does not match generated schema');
|
|
398
393
|
const source = `${IMAGE_ALIAS}.${quoteIdent(table.name)}`;
|
|
399
|
-
const count = Number(db.query(`SELECT count(*)
|
|
394
|
+
const count = Number(db.query(`SELECT count(*)AS n FROM ${source}`)[0]?.n);
|
|
400
395
|
if (count !== descriptor.rowCount)
|
|
401
396
|
imageInvalid('image row count does not match descriptor');
|
|
402
397
|
const names = table.columns.map((column) => quoteIdent(column.name));
|
|
@@ -411,9 +406,7 @@ export async function applySqliteSegment(
|
|
|
411
406
|
for (;;) {
|
|
412
407
|
if (options.isCurrent?.() === false) return applied;
|
|
413
408
|
const boundary = db.query(
|
|
414
|
-
`SELECT ${primaryKey} AS boundary FROM ${source}
|
|
415
|
-
${after === undefined ? '' : `WHERE ${primaryKey} > ?`}
|
|
416
|
-
ORDER BY ${primaryKey} LIMIT 1 OFFSET 1023`,
|
|
409
|
+
`SELECT ${primaryKey} AS boundary FROM ${source} ${after === undefined ? '' : `WHERE ${primaryKey} > ?`} ORDER BY ${primaryKey} LIMIT 1 OFFSET 1023`,
|
|
417
410
|
after === undefined ? [] : [after],
|
|
418
411
|
)[0]?.boundary;
|
|
419
412
|
if (boundary === null) imageInvalid('image primary key must not be null');
|
|
@@ -428,23 +421,21 @@ export async function applySqliteSegment(
|
|
|
428
421
|
params.push(boundary);
|
|
429
422
|
}
|
|
430
423
|
(options.transaction ?? ((fn) => db.transaction(fn)))(() => {
|
|
424
|
+
restoreFailedBases(db, schema);
|
|
431
425
|
if (after === undefined && options.clearFirst)
|
|
432
426
|
deleteScopedRows(db, table, options.effective);
|
|
433
427
|
db.exec(
|
|
434
|
-
`INSERT INTO ${quoteIdent(table.name)}
|
|
435
|
-
SELECT ${[...names, quoteIdent('_syncular_version')].join(', ')}
|
|
436
|
-
FROM ${source} WHERE ${predicates.join(' AND ') || 'true'}
|
|
437
|
-
ON CONFLICT (${primaryKey}) DO UPDATE SET ${updates}`,
|
|
428
|
+
`INSERT INTO ${quoteIdent(table.name)}(${insertNames.join(', ')})SELECT ${[...names, quoteIdent('_syncular_version')].join(', ')} FROM ${source} WHERE ${predicates.join(' AND ') || 'true'} ON CONFLICT(${primaryKey})DO UPDATE SET ${updates}`,
|
|
438
429
|
params,
|
|
439
430
|
);
|
|
440
|
-
applied += Number(db.query('SELECT changes()
|
|
431
|
+
applied += Number(db.query('SELECT changes()AS n')[0]?.n ?? 0);
|
|
441
432
|
for (const row of db.query(
|
|
442
433
|
`SELECT * FROM ${source} WHERE ${predicates.join(' AND ') || 'true'}`,
|
|
443
434
|
params,
|
|
444
435
|
)) {
|
|
445
436
|
retainedBaseWrite(
|
|
446
437
|
db,
|
|
447
|
-
table
|
|
438
|
+
table,
|
|
448
439
|
String(row[table.primaryKey]),
|
|
449
440
|
Object.fromEntries(
|
|
450
441
|
table.columns.map((column) => [
|
|
@@ -515,6 +506,7 @@ export async function applyRowsSegment(
|
|
|
515
506
|
first = false;
|
|
516
507
|
if (!clearThisBlock && rows.length === 0) continue;
|
|
517
508
|
(options.transaction ?? ((fn) => db.transaction(fn)))(() => {
|
|
509
|
+
restoreFailedBases(db, schema);
|
|
518
510
|
if (clearThisBlock) {
|
|
519
511
|
deleteScopedRows(db, table, options.effective);
|
|
520
512
|
}
|
package/src/blob.ts
CHANGED
|
@@ -105,7 +105,7 @@ export async function computeBlobId(bytes: Uint8Array): Promise<string> {
|
|
|
105
105
|
|
|
106
106
|
export function ensureBlobSchema(db: ClientDatabase): void {
|
|
107
107
|
const existing = db.query(
|
|
108
|
-
"SELECT 1 FROM sqlite_master WHERE type
|
|
108
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='_syncular_blobs'",
|
|
109
109
|
);
|
|
110
110
|
if (existing.length > 0) {
|
|
111
111
|
const columns = db
|
|
@@ -129,15 +129,13 @@ export function ensureBlobSchema(db: ClientDatabase): void {
|
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
} else {
|
|
132
|
-
db.exec(
|
|
133
|
-
blob_id TEXT PRIMARY KEY,
|
|
134
|
-
|
|
135
|
-
byte_length INTEGER NOT NULL,
|
|
136
|
-
media_type TEXT,
|
|
137
|
-
created_at_ms INTEGER NOT NULL)`);
|
|
132
|
+
db.exec(
|
|
133
|
+
'CREATE TABLE _syncular_blobs(blob_id TEXT PRIMARY KEY,bytes BLOB NOT NULL,byte_length INTEGER NOT NULL,media_type TEXT,created_at_ms INTEGER NOT NULL)',
|
|
134
|
+
);
|
|
138
135
|
}
|
|
139
|
-
db.exec(
|
|
140
|
-
blob_id TEXT PRIMARY KEY,
|
|
136
|
+
db.exec(
|
|
137
|
+
'CREATE TABLE IF NOT EXISTS _syncular_blob_uploads(blob_id TEXT PRIMARY KEY,media_type TEXT,created_at_ms INTEGER NOT NULL)',
|
|
138
|
+
);
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
/** Put bytes into the content-addressed cache without rewriting an existing body. */
|
|
@@ -149,9 +147,7 @@ export function putCachedBlob(
|
|
|
149
147
|
mediaType?: string,
|
|
150
148
|
): void {
|
|
151
149
|
db.exec(
|
|
152
|
-
|
|
153
|
-
blob_id, bytes, byte_length, media_type, created_at_ms)
|
|
154
|
-
VALUES (?,?,?,?,?) ON CONFLICT(blob_id) DO NOTHING`,
|
|
150
|
+
'INSERT INTO _syncular_blobs(blob_id,bytes,byte_length,media_type,created_at_ms)VALUES(?,?,?,?,?)ON CONFLICT(blob_id)DO NOTHING',
|
|
155
151
|
[blobId, bytes, bytes.length, mediaType ?? null, nowMs],
|
|
156
152
|
);
|
|
157
153
|
}
|
|
@@ -161,7 +157,7 @@ export function getCachedBlob(
|
|
|
161
157
|
blobId: string,
|
|
162
158
|
): CachedBlob | undefined {
|
|
163
159
|
const rows = db.query(
|
|
164
|
-
'SELECT bytes,
|
|
160
|
+
'SELECT bytes,byte_length,media_type FROM _syncular_blobs WHERE blob_id=?',
|
|
165
161
|
[blobId],
|
|
166
162
|
);
|
|
167
163
|
const row = rows[0];
|
|
@@ -185,24 +181,20 @@ export function enforceBlobCacheCap(
|
|
|
185
181
|
maxBytes: number,
|
|
186
182
|
): string[] {
|
|
187
183
|
const totalRow = db.query(
|
|
188
|
-
'SELECT COALESCE(SUM(byte_length),
|
|
184
|
+
'SELECT COALESCE(SUM(byte_length),0)AS total FROM _syncular_blobs',
|
|
189
185
|
)[0];
|
|
190
186
|
let total = Number(totalRow?.total ?? 0);
|
|
191
187
|
if (total <= maxBytes) return [];
|
|
192
188
|
// Oldest eligible body first, with blob ID as a stable tie.
|
|
193
189
|
const candidates = db.query(
|
|
194
|
-
`SELECT blob_id,
|
|
195
|
-
WHERE blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
|
|
196
|
-
AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_commit_refs)
|
|
197
|
-
AND blob_id NOT IN (${visibleBlobIdsSql(schema)})
|
|
198
|
-
ORDER BY created_at_ms ASC, blob_id ASC`,
|
|
190
|
+
`SELECT blob_id,byte_length FROM _syncular_blobs WHERE blob_id NOT IN(SELECT blob_id FROM _syncular_blob_uploads)AND blob_id NOT IN(SELECT blob_id FROM _syncular_blob_commit_refs)AND blob_id NOT IN(${visibleBlobIdsSql(schema)})ORDER BY created_at_ms ASC,blob_id ASC`,
|
|
199
191
|
);
|
|
200
192
|
const evicted: string[] = [];
|
|
201
193
|
db.transaction(() => {
|
|
202
194
|
for (const row of candidates) {
|
|
203
195
|
if (total <= maxBytes) break;
|
|
204
196
|
const blobId = row.blob_id as string;
|
|
205
|
-
db.exec('DELETE FROM _syncular_blobs WHERE blob_id
|
|
197
|
+
db.exec('DELETE FROM _syncular_blobs WHERE blob_id=?', [blobId]);
|
|
206
198
|
total -= Number(row.byte_length);
|
|
207
199
|
evicted.push(blobId);
|
|
208
200
|
}
|
|
@@ -215,13 +207,7 @@ export function listPendingUploads(
|
|
|
215
207
|
): { blobId: string; mediaType?: string }[] {
|
|
216
208
|
return db
|
|
217
209
|
.query(
|
|
218
|
-
|
|
219
|
-
SELECT blob_id, media_type, created_at_ms FROM _syncular_blob_uploads
|
|
220
|
-
UNION ALL
|
|
221
|
-
SELECT DISTINCT r.blob_id, b.media_type, 9223372036854775807 AS created_at_ms
|
|
222
|
-
FROM _syncular_blob_commit_refs r LEFT JOIN _syncular_blobs b ON b.blob_id = r.blob_id
|
|
223
|
-
WHERE NOT EXISTS (SELECT 1 FROM _syncular_blob_uploads u WHERE u.blob_id = r.blob_id)
|
|
224
|
-
) ORDER BY created_at_ms, blob_id`,
|
|
210
|
+
'SELECT blob_id,media_type FROM(SELECT blob_id,media_type,created_at_ms FROM _syncular_blob_uploads UNION ALL SELECT DISTINCT r.blob_id,b.media_type,9223372036854775807 AS created_at_ms FROM _syncular_blob_commit_refs r LEFT JOIN _syncular_blobs b ON b.blob_id=r.blob_id WHERE NOT EXISTS(SELECT 1 FROM _syncular_blob_uploads u WHERE u.blob_id=r.blob_id))ORDER BY created_at_ms,blob_id',
|
|
225
211
|
)
|
|
226
212
|
.map((row) => {
|
|
227
213
|
if (
|
|
@@ -247,14 +233,13 @@ export function recordPendingUpload(
|
|
|
247
233
|
mediaType?: string,
|
|
248
234
|
): void {
|
|
249
235
|
db.exec(
|
|
250
|
-
|
|
251
|
-
VALUES (?,?,?)`,
|
|
236
|
+
'INSERT OR IGNORE INTO _syncular_blob_uploads(blob_id,media_type,created_at_ms)VALUES(?,?,?)',
|
|
252
237
|
[blobId, mediaType ?? null, nowMs],
|
|
253
238
|
);
|
|
254
239
|
}
|
|
255
240
|
|
|
256
241
|
export function clearPendingUpload(db: ClientDatabase, blobId: string): void {
|
|
257
|
-
db.exec('DELETE FROM _syncular_blob_uploads WHERE blob_id
|
|
242
|
+
db.exec('DELETE FROM _syncular_blob_uploads WHERE blob_id=?', [blobId]);
|
|
258
243
|
}
|
|
259
244
|
|
|
260
245
|
/**
|
|
@@ -286,11 +271,7 @@ function visibleBlobIdsSql(schema: CompiledClientSchema): string {
|
|
|
286
271
|
for (const column of columns) {
|
|
287
272
|
const identifier = quoteIdent(column);
|
|
288
273
|
selects.push(
|
|
289
|
-
`SELECT json_extract(${identifier},
|
|
290
|
-
FROM ${quoteIdent(tableName)}
|
|
291
|
-
WHERE typeof(${identifier}) = 'text'
|
|
292
|
-
AND json_valid(${identifier})
|
|
293
|
-
AND json_type(${identifier}, '$.blobId') = 'text'`,
|
|
274
|
+
`SELECT json_extract(${identifier},'$.blobId')AS blob_id FROM ${quoteIdent(tableName)} WHERE typeof(${identifier})='text' AND json_valid(${identifier})AND json_type(${identifier},'$.blobId')='text'`,
|
|
294
275
|
);
|
|
295
276
|
}
|
|
296
277
|
}
|
|
@@ -304,10 +285,7 @@ export function deleteUnreferencedCachedBlobs(
|
|
|
304
285
|
schema: CompiledClientSchema,
|
|
305
286
|
): void {
|
|
306
287
|
db.exec(
|
|
307
|
-
`DELETE FROM _syncular_blobs
|
|
308
|
-
WHERE blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
|
|
309
|
-
AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_commit_refs)
|
|
310
|
-
AND blob_id NOT IN (${visibleBlobIdsSql(schema)})`,
|
|
288
|
+
`DELETE FROM _syncular_blobs WHERE blob_id NOT IN(SELECT blob_id FROM _syncular_blob_uploads)AND blob_id NOT IN(SELECT blob_id FROM _syncular_blob_commit_refs)AND blob_id NOT IN(${visibleBlobIdsSql(schema)})`,
|
|
311
289
|
);
|
|
312
290
|
}
|
|
313
291
|
|