@syncular/client 0.30.9 → 0.30.10
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 +6 -2
- package/dist/client.js +11 -10
- package/dist/failed-overlay.js +5 -0
- package/package.json +5 -4
- package/src/client.ts +17 -15
- package/src/failed-overlay.ts +9 -0
package/README.md
CHANGED
|
@@ -375,7 +375,9 @@ 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
376
|
full-row `mutate` upsert marks every column present. `mutate` also accepts
|
|
377
377
|
`op: 'patch'` with the primary key and supplied columns. A batch can mix sparse
|
|
378
|
-
patches, upserts and deletes atomically.
|
|
378
|
+
patches, upserts and deletes atomically. Every sparse patch requires a row in
|
|
379
|
+
the local replica at call time. An absent row rejects the entire batch with
|
|
380
|
+
`sync.row_missing` before any local write or outbox insert. Omitted encrypted columns require no
|
|
379
381
|
key and retain their ciphertext.
|
|
380
382
|
|
|
381
383
|
Set `retainFailedCommits: true` to keep the complete failed aggregate visible
|
|
@@ -383,7 +385,9 @@ and durable after its outbox drains. Incoming server state advances a separate
|
|
|
383
385
|
base. `outcome.retainedRows` exposes complete intended rows and the latest
|
|
384
386
|
server rows and versions. Resolve explicitly with `resolved_keep_server`, or
|
|
385
387
|
create a reviewed replacement and link it through `superseded`. Security purge
|
|
386
|
-
and scope revocation remove whole retained aggregates.
|
|
388
|
+
and scope revocation remove whole retained aggregates. If a sparse conflict
|
|
389
|
+
loses its server base, the client keeps the sparse operation and conflict
|
|
390
|
+
evidence without materializing a local row from the saved before-image.
|
|
387
391
|
|
|
388
392
|
## Application-authorized local security purge
|
|
389
393
|
|
package/dist/client.js
CHANGED
|
@@ -1827,6 +1827,10 @@ export class SyncClient {
|
|
|
1827
1827
|
const normalized = normalizeRecordKeys(table, mutation.values);
|
|
1828
1828
|
const scopeColumns = new Set(table.scopeColumnByVariable.values());
|
|
1829
1829
|
const pkColumn = table.columns[table.primaryKeyIndex];
|
|
1830
|
+
const pkValue = requireRowId(normalized.get(pkColumn.name));
|
|
1831
|
+
const local = this.#db.query(`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`, [pkValue])[0];
|
|
1832
|
+
if (!local)
|
|
1833
|
+
throw new ClientSyncError('sync.row_missing', 'a sparse patch requires a local row');
|
|
1830
1834
|
for (const column of table.columns) {
|
|
1831
1835
|
if (!normalized.has(column.name))
|
|
1832
1836
|
continue;
|
|
@@ -1841,12 +1845,7 @@ export class SyncClient {
|
|
|
1841
1845
|
// by construction the row id being patched (§6.1), so its value is
|
|
1842
1846
|
// proven equal already.
|
|
1843
1847
|
if (scopeColumns.has(column.name) && column.name !== pkColumn.name) {
|
|
1844
|
-
|
|
1845
|
-
const local = typeof pkValue === 'string' && pkValue.length > 0
|
|
1846
|
-
? this.#db.query(`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`, [pkValue])[0]
|
|
1847
|
-
: undefined;
|
|
1848
|
-
if (local === undefined ||
|
|
1849
|
-
!Object.is(rowValueToJson(fromSqlValue(column, local[column.name] ?? null)), rowValueToJson(coerceSqlRepresentation(column, normalized.get(column.name))))) {
|
|
1848
|
+
if (!Object.is(rowValueToJson(fromSqlValue(column, local[column.name] ?? null)), rowValueToJson(coerceSqlRepresentation(column, normalized.get(column.name))))) {
|
|
1850
1849
|
throw invalidRequest(`table ${table.name}: patch cannot write scope column ${JSON.stringify(column.name)} (§3.4)`);
|
|
1851
1850
|
}
|
|
1852
1851
|
continue;
|
|
@@ -1868,10 +1867,7 @@ export class SyncClient {
|
|
|
1868
1867
|
json[column.name] = rowValueToJson(values[index] ?? null);
|
|
1869
1868
|
});
|
|
1870
1869
|
}
|
|
1871
|
-
const pkValue = json[table.primaryKey];
|
|
1872
|
-
if (typeof pkValue !== 'string' || pkValue.length === 0) {
|
|
1873
|
-
throw invalidRequest(`table ${table.name}: upsert requires a non-empty string primary key`);
|
|
1874
|
-
}
|
|
1870
|
+
const pkValue = requireRowId(json[table.primaryKey]);
|
|
1875
1871
|
return {
|
|
1876
1872
|
table: mutation.table,
|
|
1877
1873
|
rowId: pkValue,
|
|
@@ -3826,3 +3822,8 @@ export class SyncClient {
|
|
|
3826
3822
|
}
|
|
3827
3823
|
}
|
|
3828
3824
|
}
|
|
3825
|
+
function requireRowId(value) {
|
|
3826
|
+
if (typeof value !== 'string' || !value.length)
|
|
3827
|
+
throw invalidRequest('a mutation requires a non-empty string primary key');
|
|
3828
|
+
return value;
|
|
3829
|
+
}
|
package/dist/failed-overlay.js
CHANGED
|
@@ -58,6 +58,11 @@ export function restoreFailedBases(db, schema, absentIntent = false) {
|
|
|
58
58
|
deleteLocalRow(db, table, String(row.id), false);
|
|
59
59
|
else {
|
|
60
60
|
const values = JSON.parse(String(json));
|
|
61
|
+
if (absentIntent) {
|
|
62
|
+
const operation = JSON.parse(String(row.op));
|
|
63
|
+
if (!table.columns.every((column) => Object.hasOwn(operation.values ?? {}, column.name)))
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
61
66
|
upsertLocalRow(db, table, table.columns.map((column) => jsonToRowValue(values[column.name] ?? null)), Number(row.version ?? -1), false);
|
|
62
67
|
}
|
|
63
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.10",
|
|
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,11 @@
|
|
|
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.10"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.30.
|
|
96
|
-
"playwright": "1.63.0"
|
|
95
|
+
"@syncular/server": "0.30.10",
|
|
96
|
+
"playwright": "1.63.0",
|
|
97
|
+
"@electric-sql/pglite": "^0.5.4"
|
|
97
98
|
}
|
|
98
99
|
}
|
package/src/client.ts
CHANGED
|
@@ -2750,6 +2750,16 @@ export class SyncClient {
|
|
|
2750
2750
|
const normalized = normalizeRecordKeys(table, mutation.values);
|
|
2751
2751
|
const scopeColumns = new Set(table.scopeColumnByVariable.values());
|
|
2752
2752
|
const pkColumn = table.columns[table.primaryKeyIndex] as RowColumn;
|
|
2753
|
+
const pkValue = requireRowId(normalized.get(pkColumn.name));
|
|
2754
|
+
const local = this.#db.query(
|
|
2755
|
+
`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`,
|
|
2756
|
+
[pkValue],
|
|
2757
|
+
)[0];
|
|
2758
|
+
if (!local)
|
|
2759
|
+
throw new ClientSyncError(
|
|
2760
|
+
'sync.row_missing',
|
|
2761
|
+
'a sparse patch requires a local row',
|
|
2762
|
+
);
|
|
2753
2763
|
for (const column of table.columns) {
|
|
2754
2764
|
if (!normalized.has(column.name)) continue;
|
|
2755
2765
|
// §3.4 rule 5 / §6.2: scope columns are immutable on update. The
|
|
@@ -2763,16 +2773,7 @@ export class SyncClient {
|
|
|
2763
2773
|
// by construction the row id being patched (§6.1), so its value is
|
|
2764
2774
|
// proven equal already.
|
|
2765
2775
|
if (scopeColumns.has(column.name) && column.name !== pkColumn.name) {
|
|
2766
|
-
const pkValue = normalized.get(pkColumn.name);
|
|
2767
|
-
const local =
|
|
2768
|
-
typeof pkValue === 'string' && pkValue.length > 0
|
|
2769
|
-
? this.#db.query(
|
|
2770
|
-
`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`,
|
|
2771
|
-
[pkValue],
|
|
2772
|
-
)[0]
|
|
2773
|
-
: undefined;
|
|
2774
2776
|
if (
|
|
2775
|
-
local === undefined ||
|
|
2776
2777
|
!Object.is(
|
|
2777
2778
|
rowValueToJson(
|
|
2778
2779
|
fromSqlValue(column, local[column.name] ?? null),
|
|
@@ -2811,12 +2812,7 @@ export class SyncClient {
|
|
|
2811
2812
|
json[column.name] = rowValueToJson(values[index] ?? null);
|
|
2812
2813
|
});
|
|
2813
2814
|
}
|
|
2814
|
-
const pkValue = json[table.primaryKey];
|
|
2815
|
-
if (typeof pkValue !== 'string' || pkValue.length === 0) {
|
|
2816
|
-
throw invalidRequest(
|
|
2817
|
-
`table ${table.name}: upsert requires a non-empty string primary key`,
|
|
2818
|
-
);
|
|
2819
|
-
}
|
|
2815
|
+
const pkValue = requireRowId(json[table.primaryKey]);
|
|
2820
2816
|
return {
|
|
2821
2817
|
table: mutation.table,
|
|
2822
2818
|
rowId: pkValue,
|
|
@@ -5239,3 +5235,9 @@ export class SyncClient {
|
|
|
5239
5235
|
}
|
|
5240
5236
|
}
|
|
5241
5237
|
}
|
|
5238
|
+
|
|
5239
|
+
function requireRowId(value: unknown): string {
|
|
5240
|
+
if (typeof value !== 'string' || !value.length)
|
|
5241
|
+
throw invalidRequest('a mutation requires a non-empty string primary key');
|
|
5242
|
+
return value;
|
|
5243
|
+
}
|
package/src/failed-overlay.ts
CHANGED
|
@@ -101,6 +101,15 @@ export function restoreFailedBases(
|
|
|
101
101
|
if (json === null) deleteLocalRow(db, table, String(row.id), false);
|
|
102
102
|
else {
|
|
103
103
|
const values: Record<string, JsonRowValue> = JSON.parse(String(json));
|
|
104
|
+
if (absentIntent) {
|
|
105
|
+
const operation: OutboxOperation = JSON.parse(String(row.op));
|
|
106
|
+
if (
|
|
107
|
+
!table.columns.every((column) =>
|
|
108
|
+
Object.hasOwn(operation.values ?? {}, column.name),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
104
113
|
upsertLocalRow(
|
|
105
114
|
db,
|
|
106
115
|
table,
|