@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/src/apply.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { retainedBaseWrite } from './failed-overlay';
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
@@ -13,13 +14,14 @@ import {
13
14
  } from '@syncular/core';
14
15
  import type { ClientDatabase, SqlValue } from './database';
15
16
  import type { EncryptionConfig } from './encryption';
16
- import { ClientSyncError } from './errors';
17
+ import { ClientSyncError, invalidRequest } from './errors';
17
18
  import {
18
19
  type CompiledClientSchema,
19
20
  type CompiledClientTable,
20
21
  quoteIdent,
21
22
  SYNC_VERSION_COLUMN,
22
23
  toSqlValue,
24
+ rowValueToJson,
23
25
  } from './schema';
24
26
 
25
27
  /** Lets the client wrap the physical write in its revision transaction. */
@@ -44,7 +46,21 @@ export function upsertLocalRow(
44
46
  table: CompiledClientTable,
45
47
  values: readonly RowValue[],
46
48
  syncVersion: number,
49
+ serverWrite = true,
47
50
  ): void {
51
+ if (serverWrite)
52
+ retainedBaseWrite(
53
+ db,
54
+ table.name,
55
+ String(values[table.primaryKeyIndex]),
56
+ Object.fromEntries(
57
+ table.columns.map((column, index) => [
58
+ column.name,
59
+ rowValueToJson(values[index] ?? null),
60
+ ]),
61
+ ),
62
+ syncVersion,
63
+ );
48
64
  db.exec(upsertSql(table), [...values.map(toSqlValue), syncVersion]);
49
65
  }
50
66
 
@@ -52,7 +68,9 @@ export function deleteLocalRow(
52
68
  db: ClientDatabase,
53
69
  table: CompiledClientTable,
54
70
  rowId: string,
71
+ serverWrite = true,
55
72
  ): void {
73
+ if (serverWrite) retainedBaseWrite(db, table.name, rowId);
56
74
  db.exec(
57
75
  `DELETE FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)} = ?`,
58
76
  [rowId],
@@ -90,8 +108,7 @@ export async function applyCommitFrame(
90
108
  for (const change of frame.changes) {
91
109
  const tableName = frame.tables[change.tableIndex];
92
110
  if (tableName === undefined) {
93
- throw new ClientSyncError(
94
- 'sync.invalid_request',
111
+ throw invalidRequest(
95
112
  `COMMIT change tableIndex ${change.tableIndex} out of range`,
96
113
  );
97
114
  }
@@ -107,10 +124,7 @@ export async function applyCommitFrame(
107
124
  continue;
108
125
  }
109
126
  if (change.row === undefined || change.rowVersion === undefined) {
110
- throw new ClientSyncError(
111
- 'sync.invalid_request',
112
- 'upsert change without row payload',
113
- );
127
+ throw invalidRequest('upsert change without row payload');
114
128
  }
115
129
  let values = decodeRow(table.columns, change.row);
116
130
  if (encryption !== undefined && table.hasEncryptedColumns) {
@@ -204,6 +218,12 @@ export function deleteScopedRows(
204
218
  );
205
219
  params.push(...values);
206
220
  }
221
+ for (const row of db.query(
222
+ `SELECT ${quoteIdent(table.primaryKey)} AS id FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')}`,
223
+ params,
224
+ )) {
225
+ retainedBaseWrite(db, table.name, String(row.id));
226
+ }
207
227
  db.exec(
208
228
  `DELETE FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')}`,
209
229
  params,
@@ -283,10 +303,7 @@ export interface SqliteSegmentDescriptor {
283
303
  const IMAGE_ALIAS = 'syncular_image';
284
304
 
285
305
  function imageInvalid(detail: string): never {
286
- throw new ClientSyncError(
287
- 'sync.invalid_request',
288
- `sqlite segment rejected: ${detail} (§5.3)`,
289
- );
306
+ throw invalidRequest(`sqlite segment rejected: ${detail} (§5.3)`);
290
307
  }
291
308
 
292
309
  /**
@@ -313,8 +330,7 @@ export async function applySqliteSegment(
313
330
  ): Promise<number> {
314
331
  const withImage = db.withSqliteImage?.bind(db);
315
332
  if (withImage === undefined) {
316
- throw new ClientSyncError(
317
- 'sync.invalid_request',
333
+ throw invalidRequest(
318
334
  'received a sqlite segment but the database backend cannot import images (§4.2: do not advertise accept bit 2)',
319
335
  );
320
336
  }
@@ -422,6 +438,23 @@ export async function applySqliteSegment(
422
438
  params,
423
439
  );
424
440
  applied += Number(db.query('SELECT changes() AS n')[0]?.n ?? 0);
441
+ for (const row of db.query(
442
+ `SELECT * FROM ${source} WHERE ${predicates.join(' AND ') || 'true'}`,
443
+ params,
444
+ )) {
445
+ retainedBaseWrite(
446
+ db,
447
+ table.name,
448
+ String(row[table.primaryKey]),
449
+ Object.fromEntries(
450
+ table.columns.map((column) => [
451
+ column.name,
452
+ rowValueToJson(row[column.name] as RowValue),
453
+ ]),
454
+ ),
455
+ Number(row._syncular_version),
456
+ );
457
+ }
425
458
  });
426
459
  options.onProgress?.(applied);
427
460
  await yieldToHost();