@syncular/client 0.30.11 → 0.30.13

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.
@@ -1,3 +1,4 @@
1
+ export declare const RETAINED_ROWS = "_syncular_failed_rows";
1
2
  import type { ClientDatabase } from './database.js';
2
3
  import type { CompiledClientSchema, CompiledClientTable, JsonRowValue } from './schema.js';
3
4
  import type { RowValue } from '@syncular/core';
@@ -6,9 +7,9 @@ import type { OutboxCommit, OutboxBeforeImage } from './outbox.js';
6
7
  /** Protected failure state. The server base advances independently of intent. */
7
8
  export declare function ensureFailedOverlaySchema(db: ClientDatabase): void;
8
9
  export declare function retainedBaseWrite(db: ClientDatabase, table: CompiledClientTable, rowId: string, values?: Readonly<Record<string, JsonRowValue>>, version?: number): void;
9
- export declare function retainFailedRows(db: ClientDatabase, commit: OutboxCommit, images: readonly OutboxBeforeImage[]): void;
10
+ export declare function retainFailedRows(db: ClientDatabase, commit: OutboxCommit, images: readonly OutboxBeforeImage[], commitSeq?: number): void;
10
11
  /** Match declared keys through SQLite, preserving index affinity and NULL semantics. */
11
12
  export declare function uniqueConflicts(db: ClientDatabase, table: CompiledClientTable, values: Readonly<Record<string, RowValue>>): NonNullable<RetainedCommitRow['uniqueConflicts']>;
12
- export declare function restoreFailedBases(db: ClientDatabase, schema: CompiledClientSchema, absentIntent?: boolean): void;
13
- export declare function failedOverlayCommits(db: ClientDatabase): OutboxCommit[];
13
+ export declare function restoreFailedBases(db: ClientDatabase, schema: CompiledClientSchema, absentIntent?: boolean, rowKeys?: readonly string[]): boolean;
14
+ export declare function failedOverlayCommits(db: ClientDatabase, acknowledged?: boolean, rowKeys?: readonly string[]): OutboxCommit[];
14
15
  export declare function dropFailedRows(db: ClientDatabase, matches: (table: string, values: Readonly<Record<string, JsonRowValue>>) => boolean): OutboxCommit[];
@@ -1,16 +1,17 @@
1
+ export const RETAINED_ROWS = '_syncular_failed_rows';
1
2
  import { ClientSyncError } from './errors.js';
2
3
  import { mapRowValues, fromSqlValue, jsonToRowValue, quoteIdent, rowValueToJson, SYNC_VERSION_COLUMN, toSqlValue, } from './schema.js';
3
4
  import { deleteLocalRow, upsertLocalRow } from './apply.js';
4
5
  /** Protected failure state. The server base advances independently of intent. */
5
6
  export function ensureFailedOverlaySchema(db) {
6
- db.exec('CREATE TABLE IF NOT EXISTS _syncular_failed_rows(commit_id TEXT NOT NULL,idx INTEGER NOT NULL,tbl TEXT NOT NULL,id TEXT NOT NULL,at INTEGER NOT NULL,op TEXT NOT NULL,intent TEXT,base TEXT,version INTEGER,unique_conflicts TEXT,PRIMARY KEY(commit_id,idx))');
7
- if (!db
8
- .query('PRAGMA table_info(_syncular_failed_rows)')
9
- .some((row) => row.name === 'unique_conflicts'))
10
- db.exec('ALTER TABLE _syncular_failed_rows ADD COLUMN unique_conflicts TEXT');
7
+ db.exec(`CREATE TABLE IF NOT EXISTS ${RETAINED_ROWS}(commit_id TEXT NOT NULL,idx INTEGER NOT NULL,tbl TEXT NOT NULL,id TEXT NOT NULL,at INTEGER NOT NULL,op TEXT NOT NULL,intent TEXT,base TEXT,version INTEGER,unique_conflicts TEXT,commit_seq INTEGER,PRIMARY KEY(commit_id,idx))`);
8
+ const columns = db.query(`PRAGMA table_info(${RETAINED_ROWS})`);
9
+ for (const column of ['unique_conflicts TEXT', 'commit_seq INTEGER'])
10
+ if (!columns.some((row) => row.name === column.split(' ')[0]))
11
+ db.exec(`ALTER TABLE ${RETAINED_ROWS} ADD COLUMN ${column}`);
11
12
  }
12
13
  export function retainedBaseWrite(db, table, rowId, values, version) {
13
- db.exec('UPDATE _syncular_failed_rows SET base=?,version=? WHERE tbl=? AND id=?', [
14
+ db.exec(`UPDATE ${RETAINED_ROWS} SET base=?,version=? WHERE tbl=? AND id=?`, [
14
15
  values === undefined ? null : JSON.stringify(values),
15
16
  version ?? null,
16
17
  table.name,
@@ -18,13 +19,13 @@ export function retainedBaseWrite(db, table, rowId, values, version) {
18
19
  ]);
19
20
  if (!table.indexes.some((index) => index.unique))
20
21
  return;
21
- for (const retained of db.query('SELECT commit_id,idx,intent,unique_conflicts FROM _syncular_failed_rows WHERE tbl=?', [table.name])) {
22
+ for (const retained of db.query(`SELECT commit_id,idx,intent,unique_conflicts FROM ${RETAINED_ROWS} WHERE tbl=? AND commit_seq IS NULL`, [table.name])) {
22
23
  const prior = retained.unique_conflicts === null
23
24
  ? []
24
- : JSON.parse(String(retained.unique_conflicts));
25
+ : JSON.parse(retained.unique_conflicts);
25
26
  const conflicts = prior.filter((conflict) => conflict.rowId !== rowId);
26
27
  if (values !== undefined && retained.intent !== null) {
27
- const intent = JSON.parse(String(retained.intent));
28
+ const intent = JSON.parse(retained.intent);
28
29
  conflicts.push(...uniqueConflicts(db, table, mapRowValues(intent, jsonToRowValue))
29
30
  .filter((conflict) => conflict.rowId === rowId)
30
31
  .map((conflict) => ({
@@ -32,25 +33,27 @@ export function retainedBaseWrite(db, table, rowId, values, version) {
32
33
  serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
33
34
  })));
34
35
  }
35
- db.exec('UPDATE _syncular_failed_rows SET unique_conflicts=? WHERE commit_id=? AND idx=?', [
36
+ db.exec(`UPDATE ${RETAINED_ROWS} SET unique_conflicts=? WHERE commit_id=? AND idx=?`, [
36
37
  conflicts.length
37
38
  ? JSON.stringify(conflicts.sort((a, b) => table.indexes.findIndex((index) => index.name === a.index) -
38
39
  table.indexes.findIndex((index) => index.name === b.index)))
39
40
  : null,
40
- String(retained.commit_id),
41
+ retained.commit_id,
41
42
  Number(retained.idx),
42
43
  ]);
43
44
  }
44
45
  }
45
- export function retainFailedRows(db, commit, images) {
46
+ export function retainFailedRows(db, commit, images, commitSeq) {
46
47
  for (const [index, operation] of commit.operations.entries()) {
47
48
  const image = images.find((image) => image.opIndex === index);
49
+ if (commitSeq !== undefined && (image?.deliverySeq ?? -1) >= commitSeq)
50
+ continue;
48
51
  const initial = operation.op === 'upsert'
49
52
  ? { ...image?.values, ...operation.values }
50
53
  : undefined;
51
54
  if (!image)
52
- throw new ClientSyncError('sync.local_corrupt', 'retained intent has no before-image');
53
- db.exec('INSERT INTO _syncular_failed_rows(commit_id,idx,tbl,id,at,op,intent,base,version)VALUES(?,?,?,?,?,?,?,?,?)', [
55
+ throw new ClientSyncError('sync.local_corrupt', 'missing intent before-image');
56
+ db.exec(`INSERT INTO ${RETAINED_ROWS}(commit_id,idx,tbl,id,at,op,intent,base,version,commit_seq)VALUES(?,?,?,?,?,?,?,?,?,?)`, [
54
57
  commit.clientCommitId,
55
58
  index,
56
59
  operation.table,
@@ -60,6 +63,7 @@ export function retainFailedRows(db, commit, images) {
60
63
  initial ? JSON.stringify(initial) : null,
61
64
  image.values ? JSON.stringify(image.values) : null,
62
65
  image.syncVersion ?? null,
66
+ commitSeq ?? null,
63
67
  ]);
64
68
  }
65
69
  }
@@ -83,33 +87,36 @@ export function uniqueConflicts(db, table, values) {
83
87
  serverVersion: Number(row[SYNC_VERSION_COLUMN]),
84
88
  })));
85
89
  }
86
- export function restoreFailedBases(db, schema, absentIntent = false) {
90
+ function retainedRows(db, rowKeys) {
91
+ return db.query(`SELECT * FROM ${RETAINED_ROWS} WHERE ?1 IS NULL OR json_array(tbl,id) IN(SELECT value FROM json_each(?1)) ORDER BY commit_seq,at,commit_id,idx`, [rowKeys ? JSON.stringify(rowKeys) : null]);
92
+ }
93
+ export function restoreFailedBases(db, schema, absentIntent = false, rowKeys) {
87
94
  const seen = new Set();
88
- for (const row of db.query('SELECT * FROM _syncular_failed_rows ORDER BY at,commit_id,idx')) {
95
+ for (const row of retainedRows(db, rowKeys)) {
89
96
  const key = JSON.stringify([row.tbl, row.id]);
90
97
  if (!absentIntent && seen.has(key))
91
98
  continue;
92
99
  seen.add(key);
93
- const table = schema.tables.get(String(row.tbl));
100
+ const table = schema.tables.get(row.tbl);
94
101
  if (!table)
95
- throw new ClientSyncError('sync.unknown_table', 'retained intent references an unknown table');
102
+ throw new ClientSyncError('sync.unknown_table', 'retained table is unknown');
96
103
  const json = absentIntent ? row.intent : row.base;
97
104
  if (absentIntent && row.intent !== null) {
98
- const values = JSON.parse(String(row.intent));
105
+ const values = JSON.parse(row.intent);
99
106
  const conflicts = uniqueConflicts(db, table, mapRowValues(values, jsonToRowValue));
100
107
  // Hydrate a pre-0.30.11 failure after its winner was already imported.
101
108
  // Pending and retained overlays cannot supply authoritative row evidence.
102
- if (row.unique_conflicts === null) {
109
+ if (row.commit_seq === null && row.unique_conflicts === null) {
103
110
  const authorized = conflicts.filter((conflict) => conflict.serverVersion >= 0 &&
104
- !db.query("SELECT 1 FROM _syncular_failed_rows WHERE tbl=? AND id=? UNION ALL SELECT 1 FROM _syncular_outbox,json_each(operations)WHERE json_extract(json_each.value,'$.table')=? AND json_extract(json_each.value,'$.rowId')=? LIMIT 1", [table.name, conflict.rowId, table.name, conflict.rowId]).length);
111
+ !db.query(`SELECT 1 FROM ${RETAINED_ROWS} WHERE tbl=? AND id=? UNION ALL SELECT 1 FROM _syncular_outbox,json_each(operations)WHERE json_extract(json_each.value,'$.table')=? AND json_extract(json_each.value,'$.rowId')=? LIMIT 1`, [table.name, conflict.rowId, table.name, conflict.rowId]).length);
105
112
  if (authorized.length)
106
- db.exec('UPDATE _syncular_failed_rows SET unique_conflicts=? WHERE commit_id=? AND idx=?', [
113
+ db.exec(`UPDATE ${RETAINED_ROWS} SET unique_conflicts=? WHERE commit_id=? AND idx=?`, [
107
114
  JSON.stringify(authorized.map((conflict) => ({
108
115
  ...conflict,
109
116
  serverRow: mapRowValues(conflict.serverRow, rowValueToJson),
110
117
  }))),
111
- String(row.commit_id),
112
- Number(row.idx),
118
+ row.commit_id,
119
+ row.idx,
113
120
  ]);
114
121
  }
115
122
  if (conflicts.length)
@@ -117,26 +124,30 @@ export function restoreFailedBases(db, schema, absentIntent = false) {
117
124
  }
118
125
  if (absentIntent &&
119
126
  (json === null ||
120
- db.query(`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`, [String(row.id)]).length))
127
+ db.query(`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`, [row.id]).length))
121
128
  continue;
122
129
  if (json === null)
123
- deleteLocalRow(db, table, String(row.id), false);
130
+ deleteLocalRow(db, table, row.id, false);
124
131
  else {
125
132
  const values = JSON.parse(String(json));
126
133
  if (absentIntent) {
127
- const operation = JSON.parse(String(row.op));
128
- if (!table.columns.every((column) => Object.hasOwn(operation.values ?? {}, column.name)))
134
+ const operation = JSON.parse(row.op);
135
+ if (row.commit_seq === null &&
136
+ !table.columns.every((column) => Object.hasOwn(operation.values ?? {}, column.name)))
129
137
  continue;
130
138
  }
131
- upsertLocalRow(db, table, table.columns.map((column) => jsonToRowValue(values[column.name] ?? null)), Number(row.version ?? -1), false);
139
+ upsertLocalRow(db, table, table.columns.map((column) => jsonToRowValue(values[column.name] ?? null)), (row.version ?? -1), false);
132
140
  }
133
141
  }
142
+ return seen.size > 0;
134
143
  }
135
- export function failedOverlayCommits(db) {
144
+ export function failedOverlayCommits(db, acknowledged = false, rowKeys) {
136
145
  const commits = new Map();
137
- for (const row of db.query('SELECT * FROM _syncular_failed_rows ORDER BY at,commit_id,idx')) {
138
- const id = String(row.commit_id);
139
- const operation = JSON.parse(String(row.op));
146
+ for (const row of retainedRows(db, rowKeys)) {
147
+ if ((row.commit_seq !== null) !== acknowledged)
148
+ continue;
149
+ const id = row.commit_id;
150
+ const operation = JSON.parse(row.op);
140
151
  const prior = commits.get(id);
141
152
  if (prior)
142
153
  prior.operations.push(operation);
@@ -144,7 +155,7 @@ export function failedOverlayCommits(db) {
144
155
  commits.set(id, {
145
156
  seq: 0,
146
157
  clientCommitId: id,
147
- createdAtMs: Number(row.at),
158
+ createdAtMs: row.at,
148
159
  operations: [operation],
149
160
  });
150
161
  }
@@ -152,17 +163,21 @@ export function failedOverlayCommits(db) {
152
163
  }
153
164
  export function dropFailedRows(db, matches) {
154
165
  const ids = new Set();
155
- for (const row of db.query('SELECT * FROM _syncular_failed_rows')) {
156
- const json = row.intent ?? row.base;
157
- if (json === null)
158
- continue;
159
- const values = JSON.parse(String(json));
160
- if (matches(String(row.tbl), values))
161
- ids.add(String(row.commit_id));
166
+ for (const row of retainedRows(db)) {
167
+ for (const json of [row.intent, row.base]) {
168
+ if (json === null)
169
+ continue;
170
+ const values = JSON.parse(String(json));
171
+ if (matches(row.tbl, values))
172
+ ids.add(row.commit_id);
173
+ }
162
174
  }
163
- const dropped = failedOverlayCommits(db).filter((commit) => ids.has(commit.clientCommitId));
175
+ const dropped = [
176
+ ...failedOverlayCommits(db),
177
+ ...failedOverlayCommits(db, true),
178
+ ].filter((commit) => ids.has(commit.clientCommitId));
164
179
  for (const id of ids) {
165
- db.exec('DELETE FROM _syncular_failed_rows WHERE commit_id=?', [id]);
180
+ db.exec(`DELETE FROM ${RETAINED_ROWS} WHERE commit_id=?`, [id]);
166
181
  db.exec("UPDATE _syncular_commit_outcomes SET operations=NULL,results='[]' WHERE client_commit_id=?", [id]);
167
182
  }
168
183
  return dropped;
package/dist/outbox.d.ts CHANGED
@@ -34,12 +34,13 @@ export interface OutboxCommit {
34
34
  export interface OutboxBeforeImage {
35
35
  readonly opIndex: number;
36
36
  readonly existed: boolean;
37
+ readonly deliverySeq?: number;
37
38
  readonly syncVersion?: number;
38
39
  readonly values?: Readonly<Record<string, JsonRowValue>>;
39
40
  }
40
41
  export declare function appendOutboxCommit(db: ClientDatabase, clientCommitId: string, operations: readonly OutboxOperation[], nowMs: number, beforeImages?: readonly OutboxBeforeImage[]): void;
41
42
  /** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
42
- export declare function listOutbox(db: ClientDatabase): OutboxCommit[];
43
+ export declare function listOutbox(db: ClientDatabase, rowKeys?: readonly string[]): OutboxCommit[];
43
44
  /** Keyset pages bound staging; laziness decodes only commits consumed by the encoder. */
44
45
  export declare function iterateOutbox(db: ClientDatabase, throughSeq: number): Generator<OutboxCommit>;
45
46
  /** Routine status reads never load operation bodies. */
package/dist/outbox.js CHANGED
@@ -7,26 +7,20 @@
7
7
  */
8
8
  import { encodeSparseRow, } from '@syncular/core';
9
9
  import { ClientSyncError } from './errors.js';
10
- import { jsonToRowValue, } from './schema.js';
10
+ import { jsonToRowValue, matchesLocalScopes, } from './schema.js';
11
11
  export function appendOutboxCommit(db, clientCommitId, operations, nowMs, beforeImages = []) {
12
12
  if (operations.length === 0) {
13
13
  throw new ClientSyncError('sync.empty_commit', 'a local commit must carry at least one operation (§6.1)');
14
14
  }
15
15
  db.exec('INSERT INTO _syncular_outbox(client_commit_id,created_at_ms,operations)VALUES(?,?,?)', [clientCommitId, nowMs, JSON.stringify(operations)]);
16
- for (const image of beforeImages) {
17
- db.exec('INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?)', [
18
- clientCommitId,
19
- image.opIndex,
20
- image.existed ? 1 : 0,
21
- image.syncVersion ?? null,
22
- image.values === undefined ? null : JSON.stringify(image.values),
23
- ]);
24
- }
16
+ replaceOutboxBeforeImages(db, clientCommitId, beforeImages);
25
17
  }
26
18
  /** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
27
- export function listOutbox(db) {
19
+ export function listOutbox(db, rowKeys) {
28
20
  return db
29
- .query('SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox ORDER BY seq ASC')
21
+ .query(rowKeys === undefined
22
+ ? 'SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox ORDER BY seq ASC'
23
+ : "SELECT * FROM _syncular_outbox WHERE EXISTS(SELECT 1 FROM json_each(operations) WHERE json_array(json_extract(value,'$.table'),json_extract(value,'$.rowId')) IN(SELECT value FROM json_each(?))) ORDER BY seq", rowKeys && [JSON.stringify(rowKeys)])
30
24
  .map(decodeOutboxRow);
31
25
  }
32
26
  function decodeOutboxRow(row) {
@@ -67,10 +61,13 @@ export function deleteOutboxCommit(db, clientCommitId) {
67
61
  }
68
62
  export function listOutboxBeforeImages(db, clientCommitId) {
69
63
  return db
70
- .query('SELECT op_index,existed,sync_version,values_json FROM _syncular_outbox_before_images WHERE client_commit_id=? ORDER BY op_index', [clientCommitId])
64
+ .query('SELECT op_index,existed,sync_version,values_json,delivery_seq FROM _syncular_outbox_before_images WHERE client_commit_id=? ORDER BY op_index', [clientCommitId])
71
65
  .map((row) => ({
72
66
  opIndex: row.op_index,
73
67
  existed: row.existed === 1,
68
+ ...(typeof row.delivery_seq === 'number'
69
+ ? { deliverySeq: row.delivery_seq }
70
+ : {}),
74
71
  ...(typeof row.sync_version === 'number'
75
72
  ? { syncVersion: row.sync_version }
76
73
  : {}),
@@ -83,8 +80,7 @@ export function listOutboxBeforeImages(db, clientCommitId) {
83
80
  }
84
81
  export function replaceOutboxBeforeImages(db, clientCommitId, replacements) {
85
82
  for (const image of replacements) {
86
- db.exec('DELETE FROM _syncular_outbox_before_images WHERE client_commit_id=? AND op_index=?', [clientCommitId, image.opIndex]);
87
- db.exec('INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?)', [
83
+ db.exec('INSERT INTO _syncular_outbox_before_images(client_commit_id,op_index,existed,sync_version,values_json)VALUES(?,?,?,?,?) ON CONFLICT(client_commit_id,op_index)DO UPDATE SET existed=excluded.existed,sync_version=excluded.sync_version,values_json=excluded.values_json', [
88
84
  clientCommitId,
89
85
  image.opIndex,
90
86
  image.existed ? 1 : 0,
@@ -109,7 +105,7 @@ function sparseValues(table, values) {
109
105
  // bump removed (or renamed) it — the commit cannot be expressed now.
110
106
  for (const key of Object.keys(values)) {
111
107
  if (!table.columnIndex.has(key)) {
112
- throw new OutboxEncodeError(`outbox commit references column ${JSON.stringify(key)} on ${JSON.stringify(table.name)}, which the current schema no longer has (§7.4.4)`);
108
+ throw new OutboxEncodeError(`outbox column ${JSON.stringify(key)} is absent from table ${JSON.stringify(table.name)} (§7.4.4)`);
113
109
  }
114
110
  }
115
111
  return table.columns.map((column) => {
@@ -142,7 +138,7 @@ export async function encodeOutboxCommit(schema, commit, encryption, storedKeyId
142
138
  const table = schema.tables.get(op.table);
143
139
  if (table === undefined) {
144
140
  // §7.4.4: the bump removed this table — the commit cannot be encoded.
145
- throw new OutboxEncodeError(`outbox commit ${commit.clientCommitId} targets table ${JSON.stringify(op.table)}, which the current schema no longer has (§7.4.4)`);
141
+ throw new OutboxEncodeError(`outbox table ${JSON.stringify(op.table)} is absent from the current schema (§7.4.4)`);
146
142
  }
147
143
  if (op.values === undefined) {
148
144
  throw new ClientSyncError('sync.invalid_request', `outbox upsert on ${op.table}/${op.rowId} has no values`);
@@ -183,13 +179,7 @@ export function dropOutboxCommitsInScope(db, table, effective) {
183
179
  const inScope = commit.operations.some((op) => {
184
180
  if (op.table !== table.name || op.values === undefined)
185
181
  return false;
186
- return entries.every(([variable, values]) => {
187
- const column = table.scopeColumnByVariable.get(variable);
188
- if (column === undefined)
189
- return false;
190
- const value = op.values?.[column];
191
- return typeof value === 'string' && values.includes(value);
192
- });
182
+ return matchesLocalScopes(table, effective, op.values);
193
183
  });
194
184
  if (inScope) {
195
185
  deleteOutboxCommit(db, commit.clientCommitId);
package/dist/outcomes.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { RETAINED_ROWS } from './failed-overlay.js';
1
2
  import { ClientSyncError } from './errors.js';
2
3
  import { mapRowValues, jsonToRowValue, rowValueToJson, } from './schema.js';
3
4
  function encodeResults(results) {
@@ -32,25 +33,25 @@ function decodeResults(raw) {
32
33
  }
33
34
  function parseOutcome(db, row) {
34
35
  const retainedRows = db
35
- .query('SELECT tbl,id,intent,base,version,unique_conflicts FROM _syncular_failed_rows WHERE commit_id=? ORDER BY idx', [String(row.client_commit_id)])
36
+ .query(`SELECT tbl,id,intent,base,version,unique_conflicts FROM ${RETAINED_ROWS} WHERE commit_seq IS NULL AND commit_id=? ORDER BY idx`, [row.client_commit_id])
36
37
  .map((retained) => {
37
38
  const decode = (value) => value === null
38
39
  ? null
39
40
  : Object.fromEntries(Object.entries(JSON.parse(String(value))).map(([key, value]) => [key, jsonToRowValue(value)]));
40
41
  return {
41
- table: String(retained.tbl),
42
- rowId: String(retained.id),
42
+ table: retained.tbl,
43
+ rowId: retained.id,
43
44
  localRow: decode(retained.intent),
44
45
  ...(retained.unique_conflicts === null
45
46
  ? {}
46
47
  : {
47
- uniqueConflicts: JSON.parse(String(retained.unique_conflicts)).map((conflict) => ({
48
+ uniqueConflicts: JSON.parse(retained.unique_conflicts).map((conflict) => ({
48
49
  ...conflict,
49
50
  serverRow: decode(JSON.stringify(conflict.serverRow)),
50
51
  })),
51
52
  }),
52
53
  serverRow: decode(retained.base),
53
- serverVersion: retained.version === null ? null : Number(retained.version),
54
+ serverVersion: retained.version === null ? null : retained.version,
54
55
  };
55
56
  });
56
57
  return {
package/dist/schema.d.ts CHANGED
@@ -93,6 +93,7 @@ export declare const OPTIMISTIC_VERSION = -1;
93
93
  * pass through this filter.
94
94
  */
95
95
  export declare function stripSyncColumns(rows: SqlRow[]): SqlRow[];
96
+ export declare function matchesLocalScopes(table: CompiledClientTable, scopes: Readonly<Record<string, readonly string[]>>, values: Readonly<Record<string, JsonRowValue>>): boolean;
96
97
  export declare function quoteIdent(name: string): string;
97
98
  /**
98
99
  * §5.11: the app-side type of a column for local (plaintext) storage. For an