@syncular/client 0.30.13 → 0.30.14
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/apply.d.ts +3 -2
- package/dist/apply.js +63 -15
- package/dist/blob.d.ts +1 -0
- package/dist/blob.js +4 -3
- package/dist/client.js +51 -38
- package/dist/failed-overlay.d.ts +6 -3
- package/dist/failed-overlay.js +50 -48
- package/dist/outbox.d.ts +4 -1
- package/dist/outbox.js +18 -12
- package/dist/outcomes.d.ts +1 -0
- package/dist/outcomes.js +12 -7
- package/dist/previous-version.js +3 -3
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +23 -14
- package/dist/state.d.ts +2 -0
- package/dist/state.js +15 -15
- package/dist/window.d.ts +2 -0
- package/dist/window.js +13 -10
- package/package.json +3 -3
- package/src/apply.ts +106 -37
- package/src/blob.ts +4 -3
- package/src/client.ts +77 -48
- package/src/failed-overlay.ts +96 -78
- package/src/outbox.ts +19 -16
- package/src/outcomes.ts +12 -12
- package/src/previous-version.ts +3 -3
- package/src/schema.ts +24 -18
- package/src/state.ts +15 -15
- package/src/window.ts +13 -15
package/src/previous-version.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
quoteIdent,
|
|
43
43
|
rowValueToJson,
|
|
44
44
|
} from './schema';
|
|
45
|
-
import { getMeta, setMeta } from './state';
|
|
45
|
+
import { getMeta, setMeta, META_TABLE } from './state';
|
|
46
46
|
|
|
47
47
|
/** `_syncular_meta` key holding the persisted {@link LocalSchemaDescriptor}. */
|
|
48
48
|
const LOCAL_SCHEMA_DESCRIPTOR_KEY = 'localSchemaDescriptor';
|
|
@@ -724,7 +724,7 @@ export function writePreviousVersionRefusal(
|
|
|
724
724
|
|
|
725
725
|
/** Delete the durable refusal. Part of the orphan sweep and of a successful capture. */
|
|
726
726
|
export function clearPreviousVersionRefusal(db: ClientDatabase): void {
|
|
727
|
-
db.exec(
|
|
727
|
+
db.exec(`DELETE FROM ${META_TABLE} WHERE key=?`, [
|
|
728
728
|
PREVIOUS_VERSION_CONTEXT_KEY,
|
|
729
729
|
]);
|
|
730
730
|
}
|
|
@@ -892,7 +892,7 @@ export function writePreviousVersionAudit(
|
|
|
892
892
|
|
|
893
893
|
/** Delete the advisory audit. Part of a discard, never of a normal bump. */
|
|
894
894
|
export function clearPreviousVersionAudit(db: ClientDatabase): void {
|
|
895
|
-
db.exec(
|
|
895
|
+
db.exec(`DELETE FROM ${META_TABLE} WHERE key=?`, [
|
|
896
896
|
PREVIOUS_VERSION_AUDIT_KEY,
|
|
897
897
|
]);
|
|
898
898
|
}
|
package/src/schema.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { BLOB_COMMIT_REFS } from './blob';
|
|
2
|
+
import { PENDING_EVICTIONS } from './window';
|
|
3
|
+
import { WINDOWS_TABLE } from './window';
|
|
4
|
+
import { OUTBOX_TABLE, OUTBOX_IMAGES } from './outbox';
|
|
5
|
+
import { OUTCOMES_TABLE } from './outcomes';
|
|
6
|
+
import { SUBSCRIPTIONS_TABLE, META_TABLE } from './state';
|
|
1
7
|
import { ensureFailedOverlaySchema } from './failed-overlay';
|
|
2
8
|
/**
|
|
3
9
|
* Client schema IR (SPEC.md §2.4, §3.1) — the same shape the server
|
|
@@ -265,6 +271,10 @@ export function stripSyncColumns(rows: SqlRow[]): SqlRow[] {
|
|
|
265
271
|
});
|
|
266
272
|
}
|
|
267
273
|
|
|
274
|
+
export function hasUniqueIndex(table: CompiledClientTable): boolean {
|
|
275
|
+
return table.indexes.some((index) => index.unique);
|
|
276
|
+
}
|
|
277
|
+
|
|
268
278
|
export function matchesLocalScopes(
|
|
269
279
|
table: CompiledClientTable,
|
|
270
280
|
scopes: Readonly<Record<string, readonly string[]>>,
|
|
@@ -531,64 +541,60 @@ export function ensureLocalBookkeepingSchema(db: ClientDatabase): void {
|
|
|
531
541
|
ensureFailedOverlaySchema(db);
|
|
532
542
|
const createTable = (name: string, columns: string) =>
|
|
533
543
|
db.exec(`CREATE TABLE IF NOT EXISTS ${name}(${columns})`);
|
|
534
|
-
createTable(
|
|
544
|
+
createTable(META_TABLE, 'key TEXT PRIMARY KEY,value TEXT NOT NULL');
|
|
535
545
|
createTable(
|
|
536
|
-
|
|
546
|
+
BLOB_COMMIT_REFS,
|
|
537
547
|
'commit_id TEXT NOT NULL,blob_id TEXT NOT NULL,PRIMARY KEY(commit_id,blob_id)',
|
|
538
548
|
);
|
|
539
549
|
db.exec(
|
|
540
|
-
|
|
550
|
+
`CREATE INDEX IF NOT EXISTS ${BLOB_COMMIT_REFS}_body ON ${BLOB_COMMIT_REFS}(blob_id)`,
|
|
541
551
|
);
|
|
542
552
|
db.exec(
|
|
543
|
-
|
|
553
|
+
`INSERT OR IGNORE INTO ${META_TABLE}(key,value)VALUES('localRevision','0')`,
|
|
544
554
|
);
|
|
545
555
|
createTable(
|
|
546
|
-
|
|
556
|
+
OUTBOX_TABLE,
|
|
547
557
|
'seq INTEGER PRIMARY KEY AUTOINCREMENT,client_commit_id TEXT NOT NULL UNIQUE,created_at_ms INTEGER NOT NULL,operations TEXT NOT NULL',
|
|
548
558
|
);
|
|
549
559
|
createTable(
|
|
550
|
-
|
|
560
|
+
OUTBOX_IMAGES,
|
|
551
561
|
'client_commit_id TEXT NOT NULL,op_index INTEGER NOT NULL,existed INTEGER NOT NULL CHECK(existed IN (0,1)),sync_version INTEGER,values_json TEXT,PRIMARY KEY(client_commit_id,op_index)',
|
|
552
562
|
);
|
|
553
563
|
if (
|
|
554
564
|
!db
|
|
555
|
-
.query(
|
|
565
|
+
.query(`PRAGMA table_info(${OUTBOX_IMAGES})`)
|
|
556
566
|
.some((row) => row.name === 'delivery_seq')
|
|
557
567
|
)
|
|
558
|
-
db.exec(
|
|
559
|
-
'ALTER TABLE _syncular_outbox_before_images ADD COLUMN delivery_seq INTEGER',
|
|
560
|
-
);
|
|
568
|
+
db.exec(`ALTER TABLE ${OUTBOX_IMAGES} ADD COLUMN delivery_seq INTEGER`);
|
|
561
569
|
createTable(
|
|
562
|
-
|
|
570
|
+
OUTCOMES_TABLE,
|
|
563
571
|
"seq INTEGER PRIMARY KEY AUTOINCREMENT,client_commit_id TEXT NOT NULL UNIQUE,status TEXT NOT NULL CHECK(status IN ('applied','cached','conflict','rejected')),recorded_at_ms INTEGER NOT NULL,results TEXT NOT NULL,operations TEXT,resolution TEXT NOT NULL DEFAULT 'active' CHECK(resolution IN ('active','resolved_keep_server','superseded','dismissed')),resolved_at_ms INTEGER,replacement_client_commit_id TEXT",
|
|
564
572
|
);
|
|
565
573
|
// Outcomes created before durable aggregate recovery retain no commit
|
|
566
574
|
// envelope. New failed outcomes store it in the protected client DB.
|
|
567
575
|
try {
|
|
568
|
-
db.exec(
|
|
569
|
-
'ALTER TABLE _syncular_commit_outcomes ADD COLUMN operations TEXT',
|
|
570
|
-
);
|
|
576
|
+
db.exec(`ALTER TABLE ${OUTCOMES_TABLE} ADD COLUMN operations TEXT`);
|
|
571
577
|
} catch {
|
|
572
578
|
// column already exists — the CREATE above included it
|
|
573
579
|
}
|
|
574
580
|
db.exec(
|
|
575
|
-
|
|
581
|
+
`CREATE INDEX IF NOT EXISTS ${OUTCOMES_TABLE}_resolution_seq ON ${OUTCOMES_TABLE}(resolution,seq)`,
|
|
576
582
|
);
|
|
577
583
|
createTable(
|
|
578
|
-
|
|
584
|
+
SUBSCRIPTIONS_TABLE,
|
|
579
585
|
"id TEXT PRIMARY KEY,tbl TEXT NOT NULL,requested_scopes TEXT NOT NULL,params TEXT,cursor INTEGER NOT NULL DEFAULT -1,bootstrap_state TEXT,effective_scopes TEXT,status TEXT NOT NULL DEFAULT 'active',reason_code TEXT",
|
|
580
586
|
);
|
|
581
587
|
// §4.8 window registry: which units (scope values) of a window base are
|
|
582
588
|
// live locally — the completeness oracle (I3) and the shrink driver
|
|
583
589
|
// (a unit's omission from the next pull unregisters it, §4.1).
|
|
584
590
|
createTable(
|
|
585
|
-
|
|
591
|
+
WINDOWS_TABLE,
|
|
586
592
|
'base TEXT NOT NULL,unit TEXT NOT NULL,sub_id TEXT NOT NULL,PRIMARY KEY(base,unit)',
|
|
587
593
|
);
|
|
588
594
|
// §4.8 E1: units that left the window but still had outbox-pinned rows.
|
|
589
595
|
// Retried when the outbox drains; cancelled if the unit re-enters.
|
|
590
596
|
createTable(
|
|
591
|
-
|
|
597
|
+
PENDING_EVICTIONS,
|
|
592
598
|
'sub_id TEXT PRIMARY KEY,tbl TEXT NOT NULL,effective_scopes TEXT NOT NULL',
|
|
593
599
|
);
|
|
594
600
|
});
|
package/src/state.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export const META_TABLE = '_syncular_meta';
|
|
2
|
+
import { PENDING_EVICTIONS } from './window';
|
|
3
|
+
import { WINDOWS_TABLE } from './window';
|
|
4
|
+
export const SUBSCRIPTIONS_TABLE = '_syncular_subscriptions';
|
|
1
5
|
/**
|
|
2
6
|
* Durable client sync state: per-subscription cursor, bootstrap resume
|
|
3
7
|
* token (round-tripped opaquely, §4.7), and the last-echoed effective
|
|
@@ -58,7 +62,7 @@ function rowToRecord(row: Record<string, unknown>): SubscriptionRecord {
|
|
|
58
62
|
|
|
59
63
|
export function loadSubscriptions(db: ClientDatabase): SubscriptionRecord[] {
|
|
60
64
|
return db
|
|
61
|
-
.query(
|
|
65
|
+
.query(`SELECT * FROM ${SUBSCRIPTIONS_TABLE} ORDER BY rowid ASC`)
|
|
62
66
|
.map(rowToRecord);
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -66,7 +70,7 @@ export function getSubscription(
|
|
|
66
70
|
db: ClientDatabase,
|
|
67
71
|
id: string,
|
|
68
72
|
): SubscriptionRecord | undefined {
|
|
69
|
-
const row = db.query(
|
|
73
|
+
const row = db.query(`SELECT * FROM ${SUBSCRIPTIONS_TABLE} WHERE id=?`, [
|
|
70
74
|
id,
|
|
71
75
|
])[0];
|
|
72
76
|
return row === undefined ? undefined : rowToRecord(row);
|
|
@@ -77,7 +81,7 @@ export function saveSubscription(
|
|
|
77
81
|
record: SubscriptionRecord,
|
|
78
82
|
): void {
|
|
79
83
|
db.exec(
|
|
80
|
-
|
|
84
|
+
`INSERT OR REPLACE INTO ${SUBSCRIPTIONS_TABLE}(id,tbl,requested_scopes,params,cursor,bootstrap_state,effective_scopes,status,reason_code)VALUES(?,?,?,?,?,?,?,?,?)`,
|
|
81
85
|
[
|
|
82
86
|
record.id,
|
|
83
87
|
record.table,
|
|
@@ -95,7 +99,7 @@ export function saveSubscription(
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
export function deleteSubscription(db: ClientDatabase, id: string): void {
|
|
98
|
-
db.exec(
|
|
102
|
+
db.exec(`DELETE FROM ${SUBSCRIPTIONS_TABLE} WHERE id=?`, [id]);
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
/**
|
|
@@ -107,7 +111,7 @@ export function deleteSubscription(db: ClientDatabase, id: string): void {
|
|
|
107
111
|
*/
|
|
108
112
|
export function resetSubscriptionsForBump(db: ClientDatabase): void {
|
|
109
113
|
db.exec(
|
|
110
|
-
|
|
114
|
+
`UPDATE ${SUBSCRIPTIONS_TABLE} SET cursor=-1,bootstrap_state=NULL,effective_scopes=NULL,status='active',reason_code=NULL`,
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -184,7 +188,7 @@ export function pruneUnknownSubscriptions(
|
|
|
184
188
|
// Shrinking a pinned window already removed its registration. Its deferred
|
|
185
189
|
// eviction still carries the old scope declaration and needs the same fence.
|
|
186
190
|
for (const row of db.query(
|
|
187
|
-
|
|
191
|
+
`SELECT sub_id,tbl,effective_scopes FROM ${PENDING_EVICTIONS}`,
|
|
188
192
|
)) {
|
|
189
193
|
if (
|
|
190
194
|
!compatible({
|
|
@@ -197,25 +201,21 @@ export function pruneUnknownSubscriptions(
|
|
|
197
201
|
if (staleIds.size === 0) return retained;
|
|
198
202
|
db.transaction(() => {
|
|
199
203
|
for (const id of staleIds) {
|
|
200
|
-
db.exec(
|
|
201
|
-
db.exec(
|
|
202
|
-
|
|
203
|
-
]);
|
|
204
|
-
db.exec('DELETE FROM _syncular_subscriptions WHERE id=?', [id]);
|
|
204
|
+
db.exec(`DELETE FROM ${WINDOWS_TABLE} WHERE sub_id=?`, [id]);
|
|
205
|
+
db.exec(`DELETE FROM ${PENDING_EVICTIONS} WHERE sub_id=?`, [id]);
|
|
206
|
+
db.exec(`DELETE FROM ${SUBSCRIPTIONS_TABLE} WHERE id=?`, [id]);
|
|
205
207
|
}
|
|
206
208
|
});
|
|
207
209
|
return retained;
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
export function getMeta(db: ClientDatabase, key: string): string | undefined {
|
|
211
|
-
const row = db.query(
|
|
212
|
-
key,
|
|
213
|
-
])[0];
|
|
213
|
+
const row = db.query(`SELECT value FROM ${META_TABLE} WHERE key=?`, [key])[0];
|
|
214
214
|
return row === undefined ? undefined : (row.value as string);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
export function setMeta(db: ClientDatabase, key: string, value: string): void {
|
|
218
|
-
db.exec(
|
|
218
|
+
db.exec(`INSERT OR REPLACE INTO ${META_TABLE}(key,value)VALUES(?,?)`, [
|
|
219
219
|
key,
|
|
220
220
|
value,
|
|
221
221
|
]);
|
package/src/window.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export const PENDING_EVICTIONS = '_syncular_window_pending_evict';
|
|
2
|
+
export const WINDOWS_TABLE = '_syncular_windows';
|
|
1
3
|
/**
|
|
2
4
|
* Windowed subscriptions (SPEC.md §4.8).
|
|
3
5
|
*
|
|
@@ -151,7 +153,7 @@ export function loadWindowUnits(
|
|
|
151
153
|
): WindowUnit[] {
|
|
152
154
|
return db
|
|
153
155
|
.query(
|
|
154
|
-
|
|
156
|
+
`SELECT unit,sub_id FROM ${WINDOWS_TABLE} WHERE base=? ORDER BY unit`,
|
|
155
157
|
[baseKey],
|
|
156
158
|
)
|
|
157
159
|
.map((row) => ({ unit: row.unit as string, subId: row.sub_id as string }));
|
|
@@ -163,7 +165,7 @@ export function getWindowUnitBySubId(
|
|
|
163
165
|
subId: string,
|
|
164
166
|
): RegisteredWindowUnit | undefined {
|
|
165
167
|
const row = db.query(
|
|
166
|
-
`SELECT base, unit, sub_id FROM
|
|
168
|
+
`SELECT base, unit, sub_id FROM ${WINDOWS_TABLE}
|
|
167
169
|
WHERE sub_id = ? LIMIT 1`,
|
|
168
170
|
[subId],
|
|
169
171
|
)[0];
|
|
@@ -183,7 +185,7 @@ export function insertWindowUnit(
|
|
|
183
185
|
subId: string,
|
|
184
186
|
): void {
|
|
185
187
|
db.exec(
|
|
186
|
-
|
|
188
|
+
`INSERT OR REPLACE INTO ${WINDOWS_TABLE}(base,unit,sub_id)VALUES(?,?,?)`,
|
|
187
189
|
[baseKey, unit, subId],
|
|
188
190
|
);
|
|
189
191
|
}
|
|
@@ -193,7 +195,7 @@ export function deleteWindowUnit(
|
|
|
193
195
|
baseKey: string,
|
|
194
196
|
unit: string,
|
|
195
197
|
): void {
|
|
196
|
-
db.exec(
|
|
198
|
+
db.exec(`DELETE FROM ${WINDOWS_TABLE} WHERE base=? AND unit=?`, [
|
|
197
199
|
baseKey,
|
|
198
200
|
unit,
|
|
199
201
|
]);
|
|
@@ -206,10 +208,10 @@ export function isUnitLive(
|
|
|
206
208
|
unit: string,
|
|
207
209
|
): boolean {
|
|
208
210
|
return (
|
|
209
|
-
db.query(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
).length > 0
|
|
211
|
+
db.query(`SELECT 1 FROM ${WINDOWS_TABLE} WHERE base=? AND unit=? LIMIT 1`, [
|
|
212
|
+
baseKey,
|
|
213
|
+
unit,
|
|
214
|
+
]).length > 0
|
|
213
215
|
);
|
|
214
216
|
}
|
|
215
217
|
|
|
@@ -230,23 +232,19 @@ export function savePendingEviction(
|
|
|
230
232
|
effective: ScopeMap,
|
|
231
233
|
): void {
|
|
232
234
|
db.exec(
|
|
233
|
-
`INSERT OR REPLACE INTO
|
|
235
|
+
`INSERT OR REPLACE INTO ${PENDING_EVICTIONS}(sub_id, tbl, effective_scopes)
|
|
234
236
|
VALUES (?, ?, ?)`,
|
|
235
237
|
[subId, table, JSON.stringify(effective)],
|
|
236
238
|
);
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
export function deletePendingEviction(db: ClientDatabase, subId: string): void {
|
|
240
|
-
db.exec(
|
|
241
|
-
subId,
|
|
242
|
-
]);
|
|
242
|
+
db.exec(`DELETE FROM ${PENDING_EVICTIONS} WHERE sub_id = ?`, [subId]);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
export function loadPendingEvictions(db: ClientDatabase): PendingEviction[] {
|
|
246
246
|
return db
|
|
247
|
-
.query(
|
|
248
|
-
'SELECT sub_id, tbl, effective_scopes FROM _syncular_window_pending_evict',
|
|
249
|
-
)
|
|
247
|
+
.query(`SELECT sub_id, tbl, effective_scopes FROM ${PENDING_EVICTIONS}`)
|
|
250
248
|
.map((row) => ({
|
|
251
249
|
subId: row.sub_id as string,
|
|
252
250
|
table: row.tbl as string,
|