@syncular/client 0.20.0 → 0.21.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 +22 -5
- package/dist/apply.d.ts +14 -7
- package/dist/apply.js +75 -53
- package/dist/bun-database.d.ts +1 -1
- package/dist/bun-database.js +2 -2
- package/dist/client.d.ts +10 -10
- package/dist/client.js +162 -64
- package/dist/database.d.ts +3 -2
- package/dist/encryption.d.ts +1 -1
- package/dist/encryption.js +4 -2
- package/dist/node-database.d.ts +1 -1
- package/dist/node-database.js +2 -2
- package/dist/outbox.d.ts +5 -5
- package/dist/outbox.js +5 -7
- package/dist/outcomes.d.ts +6 -0
- package/dist/outcomes.js +2 -0
- package/dist/reactive-store.js +49 -8
- package/dist/remote.js +40 -6
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +19 -5
- package/dist/wasm-database.js +3 -3
- package/package.json +3 -3
- package/src/apply.ts +86 -60
- package/src/bun-database.ts +6 -2
- package/src/client.ts +195 -72
- package/src/database.ts +7 -2
- package/src/encryption.ts +13 -6
- package/src/node-database.ts +6 -2
- package/src/outbox.ts +12 -12
- package/src/outcomes.ts +14 -1
- package/src/reactive-store.ts +58 -8
- package/src/remote.ts +54 -7
- package/src/schema.ts +29 -5
- package/src/wasm-database.ts +7 -3
package/src/outcomes.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface ConflictRecord {
|
|
|
22
22
|
readonly serverVersion: number;
|
|
23
23
|
/** The current server row, decoded — resolve without a round-trip. */
|
|
24
24
|
readonly serverRow: Readonly<Record<string, RowValue>>;
|
|
25
|
+
/**
|
|
26
|
+
* §6.3: the present columns whose `column_version` exceeded the losing
|
|
27
|
+
* operation's `baseVersion`. A custom merge recomputes exactly these;
|
|
28
|
+
* keep-server and keep-local ignore them (§6.5).
|
|
29
|
+
*/
|
|
30
|
+
readonly conflictColumns: readonly string[];
|
|
25
31
|
/** The losing local operation (absent only for malformed op indexes). */
|
|
26
32
|
readonly operation?: OutboxOperation;
|
|
27
33
|
}
|
|
@@ -94,8 +100,13 @@ export interface ResolveCommitOutcomeInput {
|
|
|
94
100
|
readonly replacementClientCommitId?: string;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
interface StoredConflictRecord extends Omit<
|
|
103
|
+
interface StoredConflictRecord extends Omit<
|
|
104
|
+
ConflictRecord,
|
|
105
|
+
'serverRow' | 'conflictColumns'
|
|
106
|
+
> {
|
|
98
107
|
readonly serverRow: Readonly<Record<string, JsonRowValue>>;
|
|
108
|
+
/** Absent in journal entries written before wire version 3. */
|
|
109
|
+
readonly conflictColumns?: readonly string[];
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
type StoredCommitOperationOutcome =
|
|
@@ -130,6 +141,8 @@ function decodeResults(raw: string): CommitOperationOutcome[] {
|
|
|
130
141
|
status: 'conflict',
|
|
131
142
|
conflict: {
|
|
132
143
|
...result.conflict,
|
|
144
|
+
// Pre-column-version journal entries carry no conflictColumns.
|
|
145
|
+
conflictColumns: result.conflict.conflictColumns ?? [],
|
|
133
146
|
serverRow: Object.fromEntries(
|
|
134
147
|
Object.entries(result.conflict.serverRow).map(([key, value]) => [
|
|
135
148
|
key,
|
package/src/reactive-store.ts
CHANGED
|
@@ -359,7 +359,8 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
359
359
|
#running = false;
|
|
360
360
|
#requested = false;
|
|
361
361
|
#desiredRevision = 0n;
|
|
362
|
-
#
|
|
362
|
+
#claimError: Error | undefined;
|
|
363
|
+
#claimPending = false;
|
|
363
364
|
#offStatus: (() => void) | undefined;
|
|
364
365
|
|
|
365
366
|
constructor(
|
|
@@ -397,10 +398,25 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
397
398
|
),
|
|
398
399
|
);
|
|
399
400
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
const generation = this.#generation;
|
|
402
|
+
this.#claimPending = claims.length > 0;
|
|
403
|
+
if (claims.length > 0)
|
|
404
|
+
void Promise.all(claims).then(
|
|
405
|
+
() => {
|
|
406
|
+
if (generation !== this.#generation || this.#listeners.size === 0)
|
|
407
|
+
return;
|
|
408
|
+
this.#claimPending = false;
|
|
409
|
+
if (!this.#running && this.#state.phase !== 'ready')
|
|
410
|
+
this.#requestRead();
|
|
411
|
+
},
|
|
412
|
+
(error: unknown) => {
|
|
413
|
+
if (generation !== this.#generation || this.#listeners.size === 0)
|
|
414
|
+
return;
|
|
415
|
+
this.#claimPending = false;
|
|
416
|
+
this.#claimError = errorOf(error);
|
|
417
|
+
this.#requestRead();
|
|
418
|
+
},
|
|
419
|
+
);
|
|
404
420
|
}
|
|
405
421
|
this.#requestRead();
|
|
406
422
|
}
|
|
@@ -426,7 +442,8 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
426
442
|
this.#running = false;
|
|
427
443
|
this.#requested = false;
|
|
428
444
|
this.#desiredRevision = 0n;
|
|
429
|
-
this.#
|
|
445
|
+
this.#claimError = undefined;
|
|
446
|
+
this.#claimPending = false;
|
|
430
447
|
this.#state = {
|
|
431
448
|
rows: [],
|
|
432
449
|
phase: 'loading',
|
|
@@ -520,9 +537,11 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
520
537
|
do {
|
|
521
538
|
this.#requested = false;
|
|
522
539
|
if (this.store.availabilitySnapshot().state === 'blocked') break;
|
|
523
|
-
|
|
540
|
+
|
|
524
541
|
if (generation !== this.#generation || this.#listeners.size === 0)
|
|
525
542
|
return;
|
|
543
|
+
if (this.#claimError !== undefined) throw this.#claimError;
|
|
544
|
+
const claimWasPending = this.#claimPending;
|
|
526
545
|
const snapshot = await this.store.client.querySnapshot<
|
|
527
546
|
Record<string, SqlValue>
|
|
528
547
|
>({
|
|
@@ -536,6 +555,7 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
536
555
|
});
|
|
537
556
|
if (generation !== this.#generation || this.#listeners.size === 0)
|
|
538
557
|
return;
|
|
558
|
+
if (this.#claimError !== undefined) throw this.#claimError;
|
|
539
559
|
const availability = this.store.availabilitySnapshot();
|
|
540
560
|
if (availability.state === 'blocked') {
|
|
541
561
|
this.#publish({
|
|
@@ -550,6 +570,12 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
550
570
|
this.#requested = true;
|
|
551
571
|
continue;
|
|
552
572
|
}
|
|
573
|
+
if (
|
|
574
|
+
!snapshot.coverage.complete &&
|
|
575
|
+
claimWasPending &&
|
|
576
|
+
!this.#claimPending
|
|
577
|
+
)
|
|
578
|
+
this.#requested = true;
|
|
553
579
|
const mappedRows =
|
|
554
580
|
this.spec.mapRow === undefined
|
|
555
581
|
? (snapshot.rows as readonly Row[])
|
|
@@ -625,6 +651,8 @@ interface WindowClaimGroup {
|
|
|
625
651
|
readonly base: WindowBase;
|
|
626
652
|
readonly claims: Map<symbol, ReadonlySet<string>>;
|
|
627
653
|
appliedKey: string;
|
|
654
|
+
appliedUnits: ReadonlySet<string>;
|
|
655
|
+
inFlightUnits?: ReadonlySet<string> | undefined;
|
|
628
656
|
scheduled: boolean;
|
|
629
657
|
running: boolean;
|
|
630
658
|
requested: boolean;
|
|
@@ -889,6 +917,7 @@ export class ReactiveClientStore {
|
|
|
889
917
|
base,
|
|
890
918
|
claims: new Map(),
|
|
891
919
|
appliedKey: '',
|
|
920
|
+
appliedUnits: new Set(),
|
|
892
921
|
scheduled: false,
|
|
893
922
|
running: false,
|
|
894
923
|
requested: false,
|
|
@@ -897,6 +926,17 @@ export class ReactiveClientStore {
|
|
|
897
926
|
this.#windowClaims.set(key, group);
|
|
898
927
|
}
|
|
899
928
|
group.claims.set(owner, new Set(units));
|
|
929
|
+
const alreadyHeld =
|
|
930
|
+
units.length > 0 &&
|
|
931
|
+
units.every(
|
|
932
|
+
(unit) =>
|
|
933
|
+
group.appliedUnits.has(unit) &&
|
|
934
|
+
(group.inFlightUnits === undefined || group.inFlightUnits.has(unit)),
|
|
935
|
+
);
|
|
936
|
+
if (alreadyHeld) {
|
|
937
|
+
this.#scheduleWindow(group);
|
|
938
|
+
return Promise.resolve();
|
|
939
|
+
}
|
|
900
940
|
const result = new Promise<void>((resolve, reject) => {
|
|
901
941
|
group?.waiters.push({ resolve, reject });
|
|
902
942
|
});
|
|
@@ -932,13 +972,23 @@ export class ReactiveClientStore {
|
|
|
932
972
|
].sort();
|
|
933
973
|
const key = canonicalValue(units);
|
|
934
974
|
if (key !== group.appliedKey) {
|
|
935
|
-
|
|
975
|
+
group.inFlightUnits = new Set(units);
|
|
976
|
+
try {
|
|
977
|
+
await this.client.setWindow(group.base, units);
|
|
978
|
+
} finally {
|
|
979
|
+
group.inFlightUnits = undefined;
|
|
980
|
+
}
|
|
936
981
|
if (this.#windowClaims.get(baseKey) !== group) return;
|
|
937
982
|
group.appliedKey = key;
|
|
983
|
+
group.appliedUnits = new Set(units);
|
|
938
984
|
}
|
|
939
985
|
}
|
|
940
986
|
for (const waiter of group.waiters.splice(0)) waiter.resolve();
|
|
941
987
|
} catch (error) {
|
|
988
|
+
// A failed window edit may already have committed its first eviction chunk.
|
|
989
|
+
// Reconcile through the core before acknowledging any later claim.
|
|
990
|
+
group.appliedKey = '';
|
|
991
|
+
group.appliedUnits = new Set();
|
|
942
992
|
for (const waiter of group.waiters.splice(0)) waiter.reject(error);
|
|
943
993
|
} finally {
|
|
944
994
|
group.running = false;
|
package/src/remote.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
encodeMessage,
|
|
10
10
|
encodeRemoteOperationRequest,
|
|
11
11
|
encodeRemoteOperationRealtimeMessage,
|
|
12
|
-
|
|
12
|
+
encodeSparseRow,
|
|
13
|
+
PROTOCOL_WIRE_VERSION,
|
|
13
14
|
type PushOperation,
|
|
14
15
|
type PushOperationResult,
|
|
15
16
|
type PushResultDetailsFrame,
|
|
@@ -204,7 +205,8 @@ export class SyncRemoteClient {
|
|
|
204
205
|
}
|
|
205
206
|
>();
|
|
206
207
|
readonly #encryption: EncryptionConfig | undefined;
|
|
207
|
-
|
|
208
|
+
/** Pinned by config, or acquired on the first prepared commit (§2.1). */
|
|
209
|
+
#logEpoch: string | undefined;
|
|
208
210
|
|
|
209
211
|
constructor(config: SyncRemoteClientConfig) {
|
|
210
212
|
if (config.clientId.length === 0) {
|
|
@@ -242,6 +244,53 @@ export class SyncRemoteClient {
|
|
|
242
244
|
'a remote commit must carry at least one mutation (§6.1)',
|
|
243
245
|
);
|
|
244
246
|
}
|
|
247
|
+
let logEpoch = this.#logEpoch;
|
|
248
|
+
if (logEpoch === undefined) {
|
|
249
|
+
// §2.1: a push round binds to the partition log epoch; an acquisition
|
|
250
|
+
// round carries no push commits.
|
|
251
|
+
if (this.#transport === undefined) {
|
|
252
|
+
throw new ClientSyncError(
|
|
253
|
+
'client.remote_sync_unconfigured',
|
|
254
|
+
'SyncRemoteClient has no sync transport for ordinary commits',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const acquisition = decodeMessage(
|
|
258
|
+
await this.#transport(
|
|
259
|
+
encodeMessage({
|
|
260
|
+
wireVersion: PROTOCOL_WIRE_VERSION,
|
|
261
|
+
msgKind: 'request',
|
|
262
|
+
frames: [
|
|
263
|
+
{
|
|
264
|
+
type: 'REQ_HEADER',
|
|
265
|
+
clientId: this.#clientId,
|
|
266
|
+
schemaVersion: schema.version,
|
|
267
|
+
},
|
|
268
|
+
// A pull that asks for nothing: the round exists for its epoch.
|
|
269
|
+
{
|
|
270
|
+
type: 'PULL_HEADER',
|
|
271
|
+
limitCommits: 0,
|
|
272
|
+
limitSnapshotRows: 0,
|
|
273
|
+
maxSnapshotPages: 0,
|
|
274
|
+
accept: 0b0011,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
const acquisitionHeader = acquisition.frames[0];
|
|
281
|
+
if (
|
|
282
|
+
acquisition.msgKind !== 'response' ||
|
|
283
|
+
acquisitionHeader?.type !== 'RESP_HEADER' ||
|
|
284
|
+
acquisitionHeader.logEpoch === undefined
|
|
285
|
+
) {
|
|
286
|
+
throw new ClientSyncError(
|
|
287
|
+
'client.invalid_host_response',
|
|
288
|
+
'remote epoch acquisition returned an invalid response',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
logEpoch = acquisitionHeader.logEpoch;
|
|
292
|
+
this.#logEpoch = logEpoch;
|
|
293
|
+
}
|
|
245
294
|
const operations: PushOperation[] = [];
|
|
246
295
|
for (const mutation of input.mutations) {
|
|
247
296
|
const table = schema.tables.get(mutation.table);
|
|
@@ -277,22 +326,20 @@ export class SyncRemoteClient {
|
|
|
277
326
|
...(mutation.baseVersion !== undefined
|
|
278
327
|
? { baseVersion: mutation.baseVersion }
|
|
279
328
|
: {}),
|
|
280
|
-
payload:
|
|
329
|
+
payload: encodeSparseRow(table.columns, table.primaryKeyIndex, values),
|
|
281
330
|
});
|
|
282
331
|
}
|
|
283
332
|
return {
|
|
284
333
|
requestId: input.requestId,
|
|
285
334
|
bytes: encodeMessage({
|
|
286
|
-
wireVersion:
|
|
335
|
+
wireVersion: PROTOCOL_WIRE_VERSION,
|
|
287
336
|
msgKind: 'request',
|
|
288
337
|
frames: [
|
|
289
338
|
{
|
|
290
339
|
type: 'REQ_HEADER',
|
|
291
340
|
clientId: this.#clientId,
|
|
292
341
|
schemaVersion: schema.version,
|
|
293
|
-
|
|
294
|
-
? { logEpoch: this.#logEpoch }
|
|
295
|
-
: {}),
|
|
342
|
+
logEpoch,
|
|
296
343
|
},
|
|
297
344
|
{
|
|
298
345
|
type: 'PUSH_COMMIT',
|
package/src/schema.ts
CHANGED
|
@@ -318,6 +318,9 @@ function createSyncedTable(
|
|
|
318
318
|
db: ClientDatabase,
|
|
319
319
|
table: CompiledClientTable,
|
|
320
320
|
): void {
|
|
321
|
+
// §6.11: a declared reference is deliberately NOT emitted here. Windowing
|
|
322
|
+
// evicts a parent independently of its children, so local SQLite never
|
|
323
|
+
// enforces a reference; the server enforces it.
|
|
321
324
|
const columns = table.columns.map((column) => {
|
|
322
325
|
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
323
326
|
const pk = column.name === table.primaryKey ? ' PRIMARY KEY' : '';
|
|
@@ -374,6 +377,14 @@ function createFtsProjection(
|
|
|
374
377
|
`CREATE VIRTUAL TABLE IF NOT EXISTS ${quoteIdent(index.name)} USING fts5(${quoteIdent(FTS_SOURCE_ID_COLUMN)} UNINDEXED, ${indexedColumns.join(', ')}, tokenize='${tokenizer}')`,
|
|
375
378
|
);
|
|
376
379
|
|
|
380
|
+
const mapping = quoteIdent(`_syncular_fts_${index.name}`);
|
|
381
|
+
const mapped =
|
|
382
|
+
db.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", [
|
|
383
|
+
`_syncular_fts_${index.name}`,
|
|
384
|
+
]).length > 0;
|
|
385
|
+
db.exec(
|
|
386
|
+
`CREATE TABLE IF NOT EXISTS ${mapping} (id INTEGER PRIMARY KEY, source_id TEXT NOT NULL UNIQUE)`,
|
|
387
|
+
);
|
|
377
388
|
const sourceId = `CAST(${quoteIdent(table.primaryKey)} AS TEXT)`;
|
|
378
389
|
const newSourceId = `CAST(new.${quoteIdent(table.primaryKey)} AS TEXT)`;
|
|
379
390
|
const oldSourceId = `CAST(old.${quoteIdent(table.primaryKey)} AS TEXT)`;
|
|
@@ -386,9 +397,10 @@ function createFtsProjection(
|
|
|
386
397
|
...index.columns.map((column) => `new.${quoteIdent(column)}`),
|
|
387
398
|
].join(', ');
|
|
388
399
|
const deleteFor = (value: string) =>
|
|
389
|
-
`DELETE FROM ${quoteIdent(index.name)} WHERE ${
|
|
400
|
+
`DELETE FROM ${quoteIdent(index.name)} WHERE rowid = (SELECT id FROM ${mapping} WHERE source_id = ${value}); DELETE FROM ${mapping} WHERE source_id = ${value}`;
|
|
390
401
|
const deleteDisplaced = (select: string) =>
|
|
391
|
-
`DELETE FROM ${quoteIdent(index.name)} WHERE ${
|
|
402
|
+
`DELETE FROM ${quoteIdent(index.name)} WHERE rowid IN (SELECT id FROM ${mapping} WHERE source_id IN (${select})); DELETE FROM ${mapping} WHERE source_id IN (${select})`;
|
|
403
|
+
const insertNew = `INSERT INTO ${mapping}(source_id) VALUES (${newSourceId}); INSERT INTO ${quoteIdent(index.name)} (rowid, ${projectionColumns}) VALUES ((SELECT id FROM ${mapping} WHERE source_id = ${newSourceId}), ${newValues})`;
|
|
392
404
|
|
|
393
405
|
// A clean insert cannot already have a projection row because the source
|
|
394
406
|
// primary key is unique. Keep clean inserts linear by moving replacement
|
|
@@ -441,13 +453,13 @@ function createFtsProjection(
|
|
|
441
453
|
`CREATE TRIGGER ${quoteIdent(`${index.name}_bi`)} BEFORE INSERT ON ${quoteIdent(table.name)} WHEN ${insertGuardCondition} BEGIN ${insertGuardBody}; END`,
|
|
442
454
|
);
|
|
443
455
|
db.exec(
|
|
444
|
-
`CREATE TRIGGER ${quoteIdent(`${index.name}_ai`)} AFTER INSERT ON ${quoteIdent(table.name)} BEGIN
|
|
456
|
+
`CREATE TRIGGER ${quoteIdent(`${index.name}_ai`)} AFTER INSERT ON ${quoteIdent(table.name)} BEGIN ${insertNew}; END`,
|
|
445
457
|
);
|
|
446
458
|
db.exec(
|
|
447
459
|
`CREATE TRIGGER ${quoteIdent(`${index.name}_ad`)} AFTER DELETE ON ${quoteIdent(table.name)} BEGIN ${deleteFor(oldSourceId)}; END`,
|
|
448
460
|
);
|
|
449
461
|
db.exec(
|
|
450
|
-
`CREATE TRIGGER ${quoteIdent(`${index.name}_au`)} AFTER UPDATE ON ${quoteIdent(table.name)} BEGIN ${deleteFor(oldSourceId)}; ${deleteFor(newSourceId)};
|
|
462
|
+
`CREATE TRIGGER ${quoteIdent(`${index.name}_au`)} AFTER UPDATE ON ${quoteIdent(table.name)} BEGIN ${deleteFor(oldSourceId)}; ${deleteFor(newSourceId)}; ${insertNew}; END`,
|
|
451
463
|
);
|
|
452
464
|
// BEFORE UPDATE guard for `UPDATE OR REPLACE`: clear the projection of any
|
|
453
465
|
// different-PK row about to be displaced through a secondary unique index by
|
|
@@ -468,6 +480,12 @@ function createFtsProjection(
|
|
|
468
480
|
`INSERT INTO ${quoteIdent(index.name)} (${projectionColumns}) SELECT ${sourceId}, ${indexedColumns.join(', ')} FROM ${quoteIdent(table.name)}`,
|
|
469
481
|
);
|
|
470
482
|
}
|
|
483
|
+
if (!mapped || !existed) {
|
|
484
|
+
db.exec(`DELETE FROM ${mapping}`);
|
|
485
|
+
db.exec(
|
|
486
|
+
`INSERT INTO ${mapping}(id, source_id) SELECT rowid, ${quoteIdent(FTS_SOURCE_ID_COLUMN)} FROM ${quoteIdent(index.name)}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
471
489
|
}
|
|
472
490
|
|
|
473
491
|
/**
|
|
@@ -602,6 +620,9 @@ export function dropAndRecreateSyncedTables(
|
|
|
602
620
|
AND name NOT LIKE '${RESERVED_TABLE_PREFIX}%'`,
|
|
603
621
|
);
|
|
604
622
|
for (const row of virtualTables) {
|
|
623
|
+
db.exec(
|
|
624
|
+
`DROP TABLE IF EXISTS ${quoteIdent(`_syncular_fts_${String(row.name)}`)}`,
|
|
625
|
+
);
|
|
605
626
|
db.exec(`DROP TABLE IF EXISTS ${quoteIdent(String(row.name))}`);
|
|
606
627
|
}
|
|
607
628
|
const existing = db.query(
|
|
@@ -698,7 +719,10 @@ export function normalizeRecordKeys(
|
|
|
698
719
|
* may surface integers as bigint. Anything else passes through untouched —
|
|
699
720
|
* the codec still fails loud on genuine type garbage at encode time.
|
|
700
721
|
*/
|
|
701
|
-
function coerceSqlRepresentation(
|
|
722
|
+
export function coerceSqlRepresentation(
|
|
723
|
+
column: RowColumn,
|
|
724
|
+
value: unknown,
|
|
725
|
+
): unknown {
|
|
702
726
|
switch (localColumnType(column)) {
|
|
703
727
|
case 'boolean':
|
|
704
728
|
return value === 0 ? false : value === 1 ? true : value;
|
package/src/wasm-database.ts
CHANGED
|
@@ -151,9 +151,13 @@ class WasmClientDatabase implements ClientDatabase {
|
|
|
151
151
|
* §5.3 image import at near-file-copy speed: attach an empty in-memory
|
|
152
152
|
* schema, then `sqlite3_deserialize` the image bytes into it (the
|
|
153
153
|
* documented sqlite-wasm import path — no OPFS round-trip, no SQL-level
|
|
154
|
-
* row shuttling before the
|
|
154
|
+
* row shuttling before the chunked INSERT…SELECT statements the caller runs).
|
|
155
155
|
*/
|
|
156
|
-
withSqliteImage<T>(
|
|
156
|
+
async withSqliteImage<T>(
|
|
157
|
+
bytes: Uint8Array,
|
|
158
|
+
alias: string,
|
|
159
|
+
fn: () => T | Promise<T>,
|
|
160
|
+
): Promise<T> {
|
|
157
161
|
assertImageAlias(alias);
|
|
158
162
|
const pointer = this.#db.pointer;
|
|
159
163
|
if (pointer === undefined) {
|
|
@@ -182,7 +186,7 @@ class WasmClientDatabase implements ClientDatabase {
|
|
|
182
186
|
`sqlite3_deserialize failed with code ${rc} (§5.3)`,
|
|
183
187
|
);
|
|
184
188
|
}
|
|
185
|
-
return fn();
|
|
189
|
+
return await fn();
|
|
186
190
|
} finally {
|
|
187
191
|
this.exec(`DETACH ${alias}`);
|
|
188
192
|
}
|