@syncular/client 0.30.6 → 0.30.9
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 +12 -2
- package/dist/apply.d.ts +2 -2
- package/dist/apply.js +25 -8
- package/dist/client.d.ts +6 -4
- package/dist/client.js +114 -75
- package/dist/database.d.ts +3 -0
- package/dist/database.js +26 -7
- package/dist/diagnostics.d.ts +1 -1
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +61 -15
- package/dist/failed-overlay.d.ts +10 -0
- package/dist/failed-overlay.js +97 -0
- package/dist/invalidation.d.ts +1 -0
- package/dist/invalidation.js +3 -0
- package/dist/outcomes.d.ts +10 -0
- package/dist/outcomes.js +18 -3
- package/dist/remote.js +7 -2
- package/dist/schema.js +7 -5
- package/dist/wasm-database.js +5 -5
- package/dist/window.js +4 -4
- package/dist/worker-entry.js +6 -2
- package/dist/worker-host.d.ts +1 -0
- package/dist/worker-host.js +3 -0
- package/dist/worker-protocol.d.ts +1 -0
- package/package.json +3 -3
- package/src/apply.ts +46 -13
- package/src/client.ts +184 -171
- package/src/database.ts +24 -8
- package/src/diagnostics.ts +1 -0
- package/src/errors.ts +82 -20
- package/src/failed-overlay.ts +165 -0
- package/src/invalidation.ts +4 -0
- package/src/outcomes.ts +41 -3
- package/src/remote.ts +15 -1
- package/src/schema.ts +7 -9
- package/src/wasm-database.ts +5 -8
- package/src/window.ts +4 -7
- package/src/worker-entry.ts +10 -2
- package/src/worker-host.ts +4 -0
- package/src/worker-protocol.ts +1 -0
package/README.md
CHANGED
|
@@ -361,7 +361,7 @@ that rolled back with the terminating operation. It stays in the protected
|
|
|
361
361
|
client database and is never added to the wire protocol, preferences, or
|
|
362
362
|
telemetry; successful and historical outcomes may omit it.
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
By default, rejected optimistic state is removed immediately, even when a validator emits
|
|
365
365
|
no server row in the pull half. The TypeScript client stores protected,
|
|
366
366
|
restart-safe before-images beside each pending outbox commit, restores the last
|
|
367
367
|
confirmed rows (including rejected deletes and atomic siblings), and then
|
|
@@ -373,7 +373,17 @@ Use `patch(table, rowId, partial, { baseVersion? })` for editor-style partial
|
|
|
373
373
|
updates. The wire carries a sparse operation: the primary key plus the supplied
|
|
374
374
|
columns are present, and the durable local operation keeps the same presence
|
|
375
375
|
set so conflict and rejection UI knows which columns the user touched. A
|
|
376
|
-
full-row `mutate` marks every column present.
|
|
376
|
+
full-row `mutate` upsert marks every column present. `mutate` also accepts
|
|
377
|
+
`op: 'patch'` with the primary key and supplied columns. A batch can mix sparse
|
|
378
|
+
patches, upserts and deletes atomically. Omitted encrypted columns require no
|
|
379
|
+
key and retain their ciphertext.
|
|
380
|
+
|
|
381
|
+
Set `retainFailedCommits: true` to keep the complete failed aggregate visible
|
|
382
|
+
and durable after its outbox drains. Incoming server state advances a separate
|
|
383
|
+
base. `outcome.retainedRows` exposes complete intended rows and the latest
|
|
384
|
+
server rows and versions. Resolve explicitly with `resolved_keep_server`, or
|
|
385
|
+
create a reviewed replacement and link it through `superseded`. Security purge
|
|
386
|
+
and scope revocation remove whole retained aggregates.
|
|
377
387
|
|
|
378
388
|
## Application-authorized local security purge
|
|
379
389
|
|
package/dist/apply.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ import type { EncryptionConfig } from './encryption.js';
|
|
|
9
9
|
import { type CompiledClientSchema, type CompiledClientTable } from './schema.js';
|
|
10
10
|
/** Lets the client wrap the physical write in its revision transaction. */
|
|
11
11
|
export type ApplyTransaction = <T>(fn: () => T) => T;
|
|
12
|
-
export declare function upsertLocalRow(db: ClientDatabase, table: CompiledClientTable, values: readonly RowValue[], syncVersion: number): void;
|
|
13
|
-
export declare function deleteLocalRow(db: ClientDatabase, table: CompiledClientTable, rowId: string): void;
|
|
12
|
+
export declare function upsertLocalRow(db: ClientDatabase, table: CompiledClientTable, values: readonly RowValue[], syncVersion: number, serverWrite?: boolean): void;
|
|
13
|
+
export declare function deleteLocalRow(db: ClientDatabase, table: CompiledClientTable, rowId: string, serverWrite?: boolean): void;
|
|
14
14
|
/**
|
|
15
15
|
* Apply one `COMMIT` frame in one local transaction (§1.4 rule 4).
|
|
16
16
|
* Upserts land with `_sync_version = rowVersion`; deletes remove the row.
|
package/dist/apply.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { retainedBaseWrite } from './failed-overlay.js';
|
|
1
2
|
/**
|
|
2
3
|
* Local application of server data: `COMMIT` frames (§4.5), rows segments
|
|
3
4
|
* (§5.2, §5.6), and the scope-matched delete shared by the §3.3 purge
|
|
4
5
|
* contract and the §5.6 first-page rule.
|
|
5
6
|
*/
|
|
6
7
|
import { decodeRow, } from '@syncular/core';
|
|
7
|
-
import { ClientSyncError } from './errors.js';
|
|
8
|
-
import { quoteIdent, SYNC_VERSION_COLUMN, toSqlValue, } from './schema.js';
|
|
8
|
+
import { ClientSyncError, invalidRequest } from './errors.js';
|
|
9
|
+
import { quoteIdent, SYNC_VERSION_COLUMN, toSqlValue, rowValueToJson, } from './schema.js';
|
|
9
10
|
function upsertSql(table) {
|
|
10
11
|
const names = [
|
|
11
12
|
...table.columns.map((column) => quoteIdent(column.name)),
|
|
@@ -19,10 +20,17 @@ function upsertSql(table) {
|
|
|
19
20
|
return `INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')}) VALUES (${placeholders})
|
|
20
21
|
ON CONFLICT (${quoteIdent(table.primaryKey)}) DO UPDATE SET ${updates}`;
|
|
21
22
|
}
|
|
22
|
-
export function upsertLocalRow(db, table, values, syncVersion) {
|
|
23
|
+
export function upsertLocalRow(db, table, values, syncVersion, serverWrite = true) {
|
|
24
|
+
if (serverWrite)
|
|
25
|
+
retainedBaseWrite(db, table.name, String(values[table.primaryKeyIndex]), Object.fromEntries(table.columns.map((column, index) => [
|
|
26
|
+
column.name,
|
|
27
|
+
rowValueToJson(values[index] ?? null),
|
|
28
|
+
])), syncVersion);
|
|
23
29
|
db.exec(upsertSql(table), [...values.map(toSqlValue), syncVersion]);
|
|
24
30
|
}
|
|
25
|
-
export function deleteLocalRow(db, table, rowId) {
|
|
31
|
+
export function deleteLocalRow(db, table, rowId, serverWrite = true) {
|
|
32
|
+
if (serverWrite)
|
|
33
|
+
retainedBaseWrite(db, table.name, rowId);
|
|
26
34
|
db.exec(`DELETE FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`, [rowId]);
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
@@ -42,7 +50,7 @@ export async function applyCommitFrame(db, schema, frame, encryption, transactio
|
|
|
42
50
|
for (const change of frame.changes) {
|
|
43
51
|
const tableName = frame.tables[change.tableIndex];
|
|
44
52
|
if (tableName === undefined) {
|
|
45
|
-
throw
|
|
53
|
+
throw invalidRequest(`COMMIT change tableIndex ${change.tableIndex} out of range`);
|
|
46
54
|
}
|
|
47
55
|
const table = schema.tables.get(tableName);
|
|
48
56
|
if (table === undefined) {
|
|
@@ -53,7 +61,7 @@ export async function applyCommitFrame(db, schema, frame, encryption, transactio
|
|
|
53
61
|
continue;
|
|
54
62
|
}
|
|
55
63
|
if (change.row === undefined || change.rowVersion === undefined) {
|
|
56
|
-
throw
|
|
64
|
+
throw invalidRequest('upsert change without row payload');
|
|
57
65
|
}
|
|
58
66
|
let values = decodeRow(table.columns, change.row);
|
|
59
67
|
if (encryption !== undefined && table.hasEncryptedColumns) {
|
|
@@ -131,6 +139,9 @@ export function deleteScopedRows(db, table, effective) {
|
|
|
131
139
|
clauses.push(`${quoteIdent(column)} IN (${values.map(() => '?').join(', ')})`);
|
|
132
140
|
params.push(...values);
|
|
133
141
|
}
|
|
142
|
+
for (const row of db.query(`SELECT ${quoteIdent(table.primaryKey)} AS id FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')}`, params)) {
|
|
143
|
+
retainedBaseWrite(db, table.name, String(row.id));
|
|
144
|
+
}
|
|
134
145
|
db.exec(`DELETE FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')}`, params);
|
|
135
146
|
}
|
|
136
147
|
/**
|
|
@@ -177,7 +188,7 @@ export function evictScopedRows(db, table, effective, pinnedRowIds) {
|
|
|
177
188
|
}
|
|
178
189
|
const IMAGE_ALIAS = 'syncular_image';
|
|
179
190
|
function imageInvalid(detail) {
|
|
180
|
-
throw
|
|
191
|
+
throw invalidRequest(`sqlite segment rejected: ${detail} (§5.3)`);
|
|
181
192
|
}
|
|
182
193
|
/**
|
|
183
194
|
* Apply a §5.3 sqlite image in committed chunks of at most 1,024 rows: validate
|
|
@@ -190,7 +201,7 @@ function imageInvalid(detail) {
|
|
|
190
201
|
export async function applySqliteSegment(db, schema, table, bytes, descriptor, options) {
|
|
191
202
|
const withImage = db.withSqliteImage?.bind(db);
|
|
192
203
|
if (withImage === undefined) {
|
|
193
|
-
throw
|
|
204
|
+
throw invalidRequest('received a sqlite segment but the database backend cannot import images (§4.2: do not advertise accept bit 2)');
|
|
194
205
|
}
|
|
195
206
|
if (descriptor.table !== table.name) {
|
|
196
207
|
imageInvalid(`descriptor table ${JSON.stringify(descriptor.table)}`);
|
|
@@ -283,6 +294,12 @@ export async function applySqliteSegment(db, schema, table, bytes, descriptor, o
|
|
|
283
294
|
FROM ${source} WHERE ${predicates.join(' AND ') || 'true'}
|
|
284
295
|
ON CONFLICT (${primaryKey}) DO UPDATE SET ${updates}`, params);
|
|
285
296
|
applied += Number(db.query('SELECT changes() AS n')[0]?.n ?? 0);
|
|
297
|
+
for (const row of db.query(`SELECT * FROM ${source} WHERE ${predicates.join(' AND ') || 'true'}`, params)) {
|
|
298
|
+
retainedBaseWrite(db, table.name, String(row[table.primaryKey]), Object.fromEntries(table.columns.map((column) => [
|
|
299
|
+
column.name,
|
|
300
|
+
rowValueToJson(row[column.name]),
|
|
301
|
+
])), Number(row._syncular_version));
|
|
302
|
+
}
|
|
286
303
|
});
|
|
287
304
|
options.onProgress?.(applied);
|
|
288
305
|
await yieldToHost();
|
package/dist/client.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ import type { RealtimeConnector, SegmentDownloader, SyncTransport } from './tran
|
|
|
26
26
|
import { type WindowBase } from './window.js';
|
|
27
27
|
export type MutationInput = {
|
|
28
28
|
readonly table: string;
|
|
29
|
-
readonly op: 'upsert';
|
|
30
|
-
/**
|
|
29
|
+
readonly op: 'upsert' | 'patch';
|
|
30
|
+
/** Column-keyed full row for upsert, or primary key plus present columns for patch. */
|
|
31
31
|
readonly values: Readonly<Record<string, unknown>>;
|
|
32
32
|
readonly baseVersion?: number;
|
|
33
33
|
} | {
|
|
@@ -36,7 +36,7 @@ export type MutationInput = {
|
|
|
36
36
|
readonly rowId: string;
|
|
37
37
|
readonly baseVersion?: number;
|
|
38
38
|
};
|
|
39
|
-
export type { CommitOperationOutcome, CommitOutcome, CommitOutcomeQuery, ConflictRecord, RejectionRecord, ResolveCommitOutcomeInput, } from './outcomes.js';
|
|
39
|
+
export type { CommitOperationOutcome, CommitOutcome, CommitOutcomeQuery, RetainedCommitRow, ConflictRecord, RejectionRecord, ResolveCommitOutcomeInput, } from './outcomes.js';
|
|
40
40
|
export interface SchemaFloor {
|
|
41
41
|
readonly requiredSchemaVersion?: number;
|
|
42
42
|
readonly latestSchemaVersion?: number;
|
|
@@ -104,6 +104,8 @@ export interface SyncClientLimits {
|
|
|
104
104
|
readonly outcomeRetentionMaxEntries?: number;
|
|
105
105
|
}
|
|
106
106
|
export interface SyncClientConfig {
|
|
107
|
+
/** Keep rejected/conflicting local intent visible until explicit resolution. */
|
|
108
|
+
readonly retainFailedCommits?: boolean;
|
|
107
109
|
readonly database: ClientDatabase;
|
|
108
110
|
readonly schema: ClientSchema;
|
|
109
111
|
readonly transport: SyncTransport;
|
|
@@ -408,7 +410,7 @@ export declare class SyncClient {
|
|
|
408
410
|
/**
|
|
409
411
|
* Mark a durable failure handled without deleting its evidence. Conflicts
|
|
410
412
|
* may keep the server row or link to a replacement commit; rejections may
|
|
411
|
-
*
|
|
413
|
+
* also keep the server row or link to a named replacement. Applied/cached history may be
|
|
412
414
|
* dismissed. The transition is one-way and survives restart.
|
|
413
415
|
*/
|
|
414
416
|
resolveCommitOutcome(input: ResolveCommitOutcomeInput): CommitOutcome;
|