@syncular/client 0.0.5-42 → 0.0.6-101

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.
Files changed (70) hide show
  1. package/dist/client.d.ts +3 -3
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +7 -1
  4. package/dist/client.js.map +1 -1
  5. package/dist/create-client.d.ts +3 -4
  6. package/dist/create-client.d.ts.map +1 -1
  7. package/dist/create-client.js +16 -12
  8. package/dist/create-client.js.map +1 -1
  9. package/dist/engine/SyncEngine.d.ts.map +1 -1
  10. package/dist/engine/SyncEngine.js +49 -29
  11. package/dist/engine/SyncEngine.js.map +1 -1
  12. package/dist/engine/types.d.ts +3 -3
  13. package/dist/engine/types.d.ts.map +1 -1
  14. package/dist/handlers/collection.d.ts +6 -0
  15. package/dist/handlers/collection.d.ts.map +1 -0
  16. package/dist/handlers/collection.js +21 -0
  17. package/dist/handlers/collection.js.map +1 -0
  18. package/dist/handlers/create-handler.d.ts +1 -1
  19. package/dist/handlers/create-handler.d.ts.map +1 -1
  20. package/dist/handlers/create-handler.js +3 -3
  21. package/dist/handlers/create-handler.js.map +1 -1
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/migrate.d.ts.map +1 -1
  27. package/dist/migrate.js +12 -0
  28. package/dist/migrate.js.map +1 -1
  29. package/dist/mutations.d.ts +1 -1
  30. package/dist/mutations.d.ts.map +1 -1
  31. package/dist/mutations.js +3 -3
  32. package/dist/mutations.js.map +1 -1
  33. package/dist/pull-engine.d.ts +11 -14
  34. package/dist/pull-engine.d.ts.map +1 -1
  35. package/dist/pull-engine.js +68 -6
  36. package/dist/pull-engine.js.map +1 -1
  37. package/dist/push-engine.d.ts.map +1 -1
  38. package/dist/push-engine.js +12 -0
  39. package/dist/push-engine.js.map +1 -1
  40. package/dist/sync-loop.d.ts +2 -2
  41. package/dist/sync-loop.d.ts.map +1 -1
  42. package/dist/sync-loop.js +5 -2
  43. package/dist/sync-loop.js.map +1 -1
  44. package/dist/sync.d.ts +32 -0
  45. package/dist/sync.d.ts.map +1 -0
  46. package/dist/sync.js +55 -0
  47. package/dist/sync.js.map +1 -0
  48. package/package.json +4 -4
  49. package/src/client.test.ts +18 -9
  50. package/src/client.ts +11 -4
  51. package/src/create-client.test.ts +83 -0
  52. package/src/create-client.ts +21 -16
  53. package/src/engine/SyncEngine.test.ts +241 -32
  54. package/src/engine/SyncEngine.ts +53 -33
  55. package/src/engine/types.ts +3 -3
  56. package/src/handlers/collection.ts +36 -0
  57. package/src/handlers/create-handler.ts +4 -4
  58. package/src/index.ts +2 -1
  59. package/src/migrate.ts +14 -0
  60. package/src/mutations.ts +4 -4
  61. package/src/pull-engine.test.ts +151 -6
  62. package/src/pull-engine.ts +93 -21
  63. package/src/push-engine.ts +15 -0
  64. package/src/sync-loop.ts +13 -5
  65. package/src/sync.ts +170 -0
  66. package/dist/handlers/registry.d.ts +0 -15
  67. package/dist/handlers/registry.d.ts.map +0 -1
  68. package/dist/handlers/registry.js +0 -29
  69. package/dist/handlers/registry.js.map +0 -1
  70. package/src/handlers/registry.ts +0 -36
@@ -1,9 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
- import type { SyncChange, SyncTransport } from '@syncular/core';
2
+ import {
3
+ createDatabase,
4
+ type SyncChange,
5
+ type SyncTransport,
6
+ SyncTransportError,
7
+ } from '@syncular/core';
3
8
  import type { Kysely } from 'kysely';
4
9
  import { sql } from 'kysely';
5
- import { createBunSqliteDb } from '../../../dialect-bun-sqlite/src';
6
- import { ClientTableRegistry } from '../handlers/registry';
10
+ import { createBunSqliteDialect } from '../../../dialect-bun-sqlite/src';
11
+ import type { ClientHandlerCollection } from '../handlers/collection';
7
12
  import { ensureClientSyncSchema } from '../migrate';
8
13
  import type { SyncClientDb } from '../schema';
9
14
  import { SyncEngine } from './SyncEngine';
@@ -31,7 +36,10 @@ describe('SyncEngine WS inline apply', () => {
31
36
  let db: Kysely<TestDb>;
32
37
 
33
38
  beforeEach(async () => {
34
- db = createBunSqliteDb<TestDb>({ path: ':memory:' });
39
+ db = createDatabase<TestDb>({
40
+ dialect: createBunSqliteDialect({ path: ':memory:' }),
41
+ family: 'sqlite',
42
+ });
35
43
  await ensureClientSyncSchema(db);
36
44
 
37
45
  await db.schema
@@ -74,29 +82,31 @@ describe('SyncEngine WS inline apply', () => {
74
82
  });
75
83
 
76
84
  it('rolls back row updates and cursor when any inline WS change fails', async () => {
77
- const handlers = new ClientTableRegistry<TestDb>().register({
78
- table: 'tasks',
79
- async applySnapshot() {},
80
- async clearAll() {},
81
- async applyChange(ctx, change) {
82
- if (change.row_id === 'fail') {
83
- throw new Error('forced apply failure');
84
- }
85
- const rowJson =
86
- change.row_json && typeof change.row_json === 'object'
87
- ? change.row_json
88
- : null;
89
- const title =
90
- rowJson && 'title' in rowJson ? String(rowJson.title ?? '') : '';
91
- await sql`
92
- update ${sql.table('tasks')}
93
- set
94
- ${sql.ref('title')} = ${sql.val(title)},
95
- ${sql.ref('server_version')} = ${sql.val(Number(change.row_version ?? 0))}
96
- where ${sql.ref('id')} = ${sql.val(change.row_id)}
97
- `.execute(ctx.trx);
85
+ const handlers: ClientHandlerCollection<TestDb> = [
86
+ {
87
+ table: 'tasks',
88
+ async applySnapshot() {},
89
+ async clearAll() {},
90
+ async applyChange(ctx, change) {
91
+ if (change.row_id === 'fail') {
92
+ throw new Error('forced apply failure');
93
+ }
94
+ const rowJson =
95
+ change.row_json && typeof change.row_json === 'object'
96
+ ? change.row_json
97
+ : null;
98
+ const title =
99
+ rowJson && 'title' in rowJson ? String(rowJson.title ?? '') : '';
100
+ await sql`
101
+ update ${sql.table('tasks')}
102
+ set
103
+ ${sql.ref('title')} = ${sql.val(title)},
104
+ ${sql.ref('server_version')} = ${sql.val(Number(change.row_version ?? 0))}
105
+ where ${sql.ref('id')} = ${sql.val(change.row_id)}
106
+ `.execute(ctx.trx);
107
+ },
98
108
  },
99
- });
109
+ ];
100
110
 
101
111
  const engine = new SyncEngine<TestDb>({
102
112
  db,
@@ -156,12 +166,14 @@ describe('SyncEngine WS inline apply', () => {
156
166
  });
157
167
 
158
168
  it('returns a bounded inspector snapshot with serializable events', async () => {
159
- const handlers = new ClientTableRegistry<TestDb>().register({
160
- table: 'tasks',
161
- async applySnapshot() {},
162
- async clearAll() {},
163
- async applyChange() {},
164
- });
169
+ const handlers: ClientHandlerCollection<TestDb> = [
170
+ {
171
+ table: 'tasks',
172
+ async applySnapshot() {},
173
+ async clearAll() {},
174
+ async applyChange() {},
175
+ },
176
+ ];
165
177
 
166
178
  const engine = new SyncEngine<TestDb>({
167
179
  db,
@@ -193,4 +205,201 @@ describe('SyncEngine WS inline apply', () => {
193
205
  expect(typeof first.payload).toBe('object');
194
206
  expect(snapshot.diagnostics).toBeDefined();
195
207
  });
208
+
209
+ it('ensures sync schema on start without custom migrate callback', async () => {
210
+ const coldDb = createDatabase<TestDb>({
211
+ dialect: createBunSqliteDialect({ path: ':memory:' }),
212
+ family: 'sqlite',
213
+ });
214
+ try {
215
+ await coldDb.schema
216
+ .createTable('tasks')
217
+ .addColumn('id', 'text', (col) => col.primaryKey())
218
+ .addColumn('title', 'text', (col) => col.notNull())
219
+ .addColumn('server_version', 'integer', (col) =>
220
+ col.notNull().defaultTo(0)
221
+ )
222
+ .execute();
223
+
224
+ const handlers: ClientHandlerCollection<TestDb> = [
225
+ {
226
+ table: 'tasks',
227
+ async applySnapshot() {},
228
+ async clearAll() {},
229
+ async applyChange() {},
230
+ },
231
+ ];
232
+
233
+ const engine = new SyncEngine<TestDb>({
234
+ db: coldDb,
235
+ transport: noopTransport,
236
+ handlers,
237
+ actorId: 'u1',
238
+ clientId: 'client-migrate',
239
+ subscriptions: [],
240
+ });
241
+
242
+ await engine.start();
243
+
244
+ const exists = await sql<{ count: number }>`
245
+ select count(*) as count
246
+ from sqlite_master
247
+ where type = 'table' and name = 'sync_subscription_state'
248
+ `.execute(coldDb);
249
+
250
+ expect(Number(exists.rows[0]?.count ?? 0)).toBe(1);
251
+ } finally {
252
+ await coldDb.destroy();
253
+ }
254
+ });
255
+
256
+ it('classifies missing snapshot chunk pull failures as non-retryable', async () => {
257
+ const missingChunkTransport: SyncTransport = {
258
+ async sync() {
259
+ throw new SyncTransportError('snapshot chunk not found', 404);
260
+ },
261
+ async fetchSnapshotChunk() {
262
+ return new Uint8Array();
263
+ },
264
+ };
265
+
266
+ const handlers: ClientHandlerCollection<TestDb> = [
267
+ {
268
+ table: 'tasks',
269
+ async applySnapshot() {},
270
+ async clearAll() {},
271
+ async applyChange() {},
272
+ },
273
+ ];
274
+
275
+ const engine = new SyncEngine<TestDb>({
276
+ db,
277
+ transport: missingChunkTransport,
278
+ handlers,
279
+ actorId: 'u1',
280
+ clientId: 'client-missing-chunk',
281
+ subscriptions: [
282
+ {
283
+ id: 'sub-1',
284
+ table: 'tasks',
285
+ scopes: {},
286
+ },
287
+ ],
288
+ stateId: 'default',
289
+ pollIntervalMs: 60_000,
290
+ maxRetries: 3,
291
+ });
292
+
293
+ await engine.start();
294
+ engine.stop();
295
+
296
+ const state = engine.getState();
297
+ expect(state.error?.code).toBe('SNAPSHOT_CHUNK_NOT_FOUND');
298
+ expect(state.error?.retryable).toBe(false);
299
+ expect(state.retryCount).toBe(1);
300
+ expect(state.isRetrying).toBe(false);
301
+ });
302
+
303
+ it('repairs rebootstrap-missing-chunks by clearing synced state and data', async () => {
304
+ const outboxId = 'outbox-1';
305
+ const now = Date.now();
306
+
307
+ await db
308
+ .insertInto('tasks')
309
+ .values({
310
+ id: 't2',
311
+ title: 'to-clear',
312
+ server_version: 2,
313
+ })
314
+ .execute();
315
+
316
+ await db
317
+ .insertInto('sync_outbox_commits')
318
+ .values({
319
+ id: outboxId,
320
+ client_commit_id: 'client-commit-1',
321
+ status: 'pending',
322
+ operations_json: '[]',
323
+ last_response_json: null,
324
+ error: null,
325
+ created_at: now,
326
+ updated_at: now,
327
+ acked_commit_seq: null,
328
+ })
329
+ .execute();
330
+
331
+ await db
332
+ .insertInto('sync_conflicts')
333
+ .values({
334
+ id: 'conflict-1',
335
+ outbox_commit_id: outboxId,
336
+ client_commit_id: 'client-commit-1',
337
+ op_index: 0,
338
+ result_status: 'conflict',
339
+ message: 'forced conflict',
340
+ code: 'TEST_CONFLICT',
341
+ server_version: 1,
342
+ server_row_json: '{}',
343
+ created_at: now,
344
+ resolved_at: null,
345
+ resolution: null,
346
+ })
347
+ .execute();
348
+
349
+ const handlers: ClientHandlerCollection<TestDb> = [
350
+ {
351
+ table: 'tasks',
352
+ async applySnapshot() {},
353
+ async clearAll(ctx) {
354
+ await sql`delete from ${sql.table('tasks')}`.execute(ctx.trx);
355
+ },
356
+ async applyChange() {},
357
+ },
358
+ ];
359
+
360
+ const engine = new SyncEngine<TestDb>({
361
+ db,
362
+ transport: noopTransport,
363
+ handlers,
364
+ actorId: 'u1',
365
+ clientId: 'client-repair',
366
+ subscriptions: [],
367
+ stateId: 'default',
368
+ });
369
+
370
+ const result = await engine.repair({
371
+ mode: 'rebootstrap-missing-chunks',
372
+ clearOutbox: true,
373
+ clearConflicts: true,
374
+ });
375
+
376
+ expect(result.deletedSubscriptionStates).toBe(1);
377
+ expect(result.deletedOutboxCommits).toBe(1);
378
+ expect(result.deletedConflicts).toBe(1);
379
+ expect(result.clearedTables).toEqual(['tasks']);
380
+
381
+ const tasksCount = await db
382
+ .selectFrom('tasks')
383
+ .select(({ fn }) => fn.countAll().as('total'))
384
+ .executeTakeFirst();
385
+ expect(Number(tasksCount?.total ?? 0)).toBe(0);
386
+
387
+ const subscriptionsCount = await db
388
+ .selectFrom('sync_subscription_state')
389
+ .select(({ fn }) => fn.countAll().as('total'))
390
+ .executeTakeFirst();
391
+ expect(Number(subscriptionsCount?.total ?? 0)).toBe(0);
392
+
393
+ const outboxCount = await db
394
+ .selectFrom('sync_outbox_commits')
395
+ .select(({ fn }) => fn.countAll().as('total'))
396
+ .executeTakeFirst();
397
+ expect(Number(outboxCount?.total ?? 0)).toBe(0);
398
+
399
+ const conflictsCount = await db
400
+ .selectFrom('sync_conflicts')
401
+ .select(({ fn }) => fn.countAll().as('total'))
402
+ .executeTakeFirst();
403
+ expect(Number(conflictsCount?.total ?? 0)).toBe(0);
404
+ });
196
405
  });
@@ -18,6 +18,8 @@ import {
18
18
  startSyncSpan,
19
19
  } from '@syncular/core';
20
20
  import { type Kysely, sql, type Transaction } from 'kysely';
21
+ import { getClientHandler } from '../handlers/collection';
22
+ import { ensureClientSyncSchema } from '../migrate';
21
23
  import { syncPushOnce } from '../push-engine';
22
24
  import type {
23
25
  ConflictResultStatus,
@@ -1005,7 +1007,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1005
1007
  }
1006
1008
 
1007
1009
  if (options.scope === 'all') {
1008
- for (const handler of this.config.handlers.getAll()) {
1010
+ for (const handler of this.config.handlers) {
1009
1011
  await handler.clearAll({ trx, scopes: {} });
1010
1012
  clearedTables.push(handler.table);
1011
1013
  }
@@ -1014,7 +1016,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1014
1016
 
1015
1017
  const seen = new Set<string>();
1016
1018
  for (const target of targets) {
1017
- const handler = this.config.handlers.get(target.table);
1019
+ const handler = getClientHandler(this.config.handlers, target.table);
1018
1020
  if (!handler) continue;
1019
1021
 
1020
1022
  const key = `${target.table}:${JSON.stringify(target.scopes)}`;
@@ -1220,40 +1222,44 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1220
1222
 
1221
1223
  this.updateState({ enabled: true });
1222
1224
 
1223
- // Run migration if provided
1224
- if (this.config.migrate && !this.migrated) {
1225
- // Best-effort: push any pending outbox commits before migration
1226
- // (migration may reset the DB, so we try to save unsynced changes)
1227
- try {
1228
- const hasOutbox = await sql`
1229
- select 1 from ${sql.table('sync_outbox_commits')} limit 1
1230
- `
1231
- .execute(this.config.db)
1232
- .then((r) => r.rows.length > 0)
1233
- .catch(() => false);
1234
-
1235
- if (hasOutbox) {
1236
- // Push all pending commits (best effort)
1237
- let pushed = true;
1238
- while (pushed) {
1239
- const result = await syncPushOnce(
1240
- this.config.db,
1241
- this.config.transport,
1242
- {
1243
- clientId: this.config.clientId!,
1244
- actorId: this.config.actorId ?? undefined,
1245
- plugins: this.config.plugins,
1246
- }
1247
- );
1248
- pushed = result.pushed;
1225
+ // Run migrations before first sync.
1226
+ if (!this.migrated) {
1227
+ // Best-effort: push pending commits before user migration, because
1228
+ // app migrations may reset tables and discard unsynced local writes.
1229
+ if (this.config.migrate) {
1230
+ try {
1231
+ const hasOutbox = await sql`
1232
+ select 1 from ${sql.table('sync_outbox_commits')} limit 1
1233
+ `
1234
+ .execute(this.config.db)
1235
+ .then((r) => r.rows.length > 0)
1236
+ .catch(() => false);
1237
+
1238
+ if (hasOutbox) {
1239
+ let pushed = true;
1240
+ while (pushed) {
1241
+ const result = await syncPushOnce(
1242
+ this.config.db,
1243
+ this.config.transport,
1244
+ {
1245
+ clientId: this.config.clientId!,
1246
+ actorId: this.config.actorId ?? undefined,
1247
+ plugins: this.config.plugins,
1248
+ }
1249
+ );
1250
+ pushed = result.pushed;
1251
+ }
1249
1252
  }
1253
+ } catch {
1254
+ // Best-effort: continue even if pre-migration push fails.
1250
1255
  }
1251
- } catch {
1252
- // Best-effort: if push fails (network down, table missing), continue
1253
1256
  }
1254
1257
 
1255
1258
  try {
1256
- await this.config.migrate(this.config.db);
1259
+ if (this.config.migrate) {
1260
+ await this.config.migrate(this.config.db);
1261
+ }
1262
+ await ensureClientSyncSchema(this.config.db);
1257
1263
  this.migrated = true;
1258
1264
  } catch (err) {
1259
1265
  const migrationError =
@@ -1597,7 +1603,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1597
1603
  try {
1598
1604
  await this.config.db.transaction().execute(async (trx) => {
1599
1605
  for (const change of changes) {
1600
- const handler = this.config.handlers.get(change.table);
1606
+ const handler = getClientHandler(this.config.handlers, change.table);
1601
1607
  if (!handler) {
1602
1608
  throw new Error(
1603
1609
  `Missing client table handler for WS change table "${change.table}"`
@@ -1852,6 +1858,13 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1852
1858
  }
1853
1859
 
1854
1860
  const delay = calculateRetryDelay(this.state.retryCount);
1861
+ if (this.state.pendingCount > 0) {
1862
+ countSyncMetric('sync.outbox.retry_count', 1, {
1863
+ attributes: {
1864
+ retryCount: this.state.retryCount,
1865
+ },
1866
+ });
1867
+ }
1855
1868
  this.updateState({ isRetrying: true });
1856
1869
 
1857
1870
  this.retryTimeoutId = setTimeout(() => {
@@ -1975,6 +1988,13 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1975
1988
  case 'connected': {
1976
1989
  const wasConnectedBefore = this.hasRealtimeConnectedOnce;
1977
1990
  this.hasRealtimeConnectedOnce = true;
1991
+ if (wasConnectedBefore) {
1992
+ countSyncMetric('sync.transport.reconnects', 1, {
1993
+ attributes: {
1994
+ source: 'client',
1995
+ },
1996
+ });
1997
+ }
1978
1998
  this.setConnectionState('connected');
1979
1999
  this.updateTransportHealth({
1980
2000
  mode: 'realtime',
@@ -2287,7 +2307,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
2287
2307
 
2288
2308
  await db.transaction().execute(async (trx) => {
2289
2309
  for (const input of inputs) {
2290
- const handler = handlers.get(input.table);
2310
+ const handler = getClientHandler(handlers, input.table);
2291
2311
  if (!handler) continue;
2292
2312
 
2293
2313
  affectedTables.add(input.table);
@@ -12,7 +12,7 @@ import type {
12
12
  SyncTransport,
13
13
  } from '@syncular/core';
14
14
  import type { Kysely } from 'kysely';
15
- import type { ClientTableRegistry } from '../handlers/registry';
15
+ import type { ClientHandlerCollection } from '../handlers/collection';
16
16
  import type { SyncClientPlugin } from '../plugins/types';
17
17
  import type { SyncClientDb } from '../schema';
18
18
  import type { SubscriptionState } from '../subscription-state';
@@ -225,7 +225,7 @@ export interface SyncEngineConfig<DB extends SyncClientDb = SyncClientDb> {
225
225
  /** Sync transport */
226
226
  transport: SyncTransport;
227
227
  /** Client table handler registry */
228
- handlers: ClientTableRegistry<DB>;
228
+ handlers: ClientHandlerCollection<DB>;
229
229
  /** Actor id for sync scoping (null/undefined disables sync) */
230
230
  actorId: string | null | undefined;
231
231
  /** Stable device/app installation id */
@@ -244,7 +244,7 @@ export interface SyncEngineConfig<DB extends SyncClientDb = SyncClientDb> {
244
244
  pollIntervalMs?: number;
245
245
  /** Max retries before giving up */
246
246
  maxRetries?: number;
247
- /** Migration function to run before first sync */
247
+ /** Optional app migration to run before sync schema migration. */
248
248
  migrate?: (db: Kysely<DB>) => Promise<void>;
249
249
  /** Called when migration fails. Receives the error. */
250
250
  onMigrationError?: (error: Error) => void;
@@ -0,0 +1,36 @@
1
+ import type { ClientTableHandler } from './types';
2
+
3
+ export type ClientHandlerCollection<DB> = ClientTableHandler<DB>[];
4
+
5
+ export function createClientHandlerCollection<DB>(
6
+ handlers: ClientTableHandler<DB>[]
7
+ ): ClientHandlerCollection<DB> {
8
+ const tables = new Set<string>();
9
+ for (const handler of handlers) {
10
+ if (tables.has(handler.table)) {
11
+ throw new Error(
12
+ `Client table handler already registered: ${handler.table}`
13
+ );
14
+ }
15
+ tables.add(handler.table);
16
+ }
17
+ return handlers;
18
+ }
19
+
20
+ export function getClientHandler<DB>(
21
+ handlers: ClientHandlerCollection<DB>,
22
+ table: string
23
+ ): ClientTableHandler<DB> | undefined {
24
+ return handlers.find((handler) => handler.table === table);
25
+ }
26
+
27
+ export function getClientHandlerOrThrow<DB>(
28
+ handlers: ClientHandlerCollection<DB>,
29
+ table: string
30
+ ): ClientTableHandler<DB> {
31
+ const handler = getClientHandler(handlers, table);
32
+ if (!handler) {
33
+ throw new Error(`Missing client table handler for table: ${table}`);
34
+ }
35
+ return handler;
36
+ }
@@ -82,7 +82,7 @@ export interface CreateClientHandlerOptions<
82
82
  * Optional column codec resolver.
83
83
  * Receives `{ table, column, sqlType?, dialect? }` and returns a codec.
84
84
  */
85
- columnCodecs?: ColumnCodecSource;
85
+ codecs?: ColumnCodecSource;
86
86
 
87
87
  /**
88
88
  * Dialect used for codec dialect overrides.
@@ -178,14 +178,14 @@ export function createClientHandler<
178
178
  const codecDialect = options.codecDialect ?? 'sqlite';
179
179
  const codecCache = new Map<string, ReturnType<typeof toTableColumnCodecs>>();
180
180
  const resolveTableCodecs = (row: Record<string, unknown>) => {
181
- const columnCodecs = options.columnCodecs;
182
- if (!columnCodecs) return {};
181
+ const codecs = options.codecs;
182
+ if (!codecs) return {};
183
183
  const columns = Object.keys(row);
184
184
  if (columns.length === 0) return {};
185
185
  const cacheKey = columns.slice().sort().join('\u0000');
186
186
  const cached = codecCache.get(cacheKey);
187
187
  if (cached) return cached;
188
- const resolved = toTableColumnCodecs(table, columnCodecs, columns, {
188
+ const resolved = toTableColumnCodecs(table, codecs, columns, {
189
189
  dialect: codecDialect,
190
190
  });
191
191
  codecCache.set(cacheKey, resolved);
package/src/index.ts CHANGED
@@ -9,8 +9,8 @@ export * from './client';
9
9
  export * from './conflicts';
10
10
  export * from './create-client';
11
11
  export * from './engine';
12
+ export * from './handlers/collection';
12
13
  export * from './handlers/create-handler';
13
- export * from './handlers/registry';
14
14
  export * from './handlers/types';
15
15
  export * from './migrate';
16
16
  export * from './mutations';
@@ -22,5 +22,6 @@ export * from './push-engine';
22
22
  export * from './query';
23
23
  export * from './schema';
24
24
  export * from './subscription-state';
25
+ export * from './sync';
25
26
  export * from './sync-loop';
26
27
  export * from './utils/id';
package/src/migrate.ts CHANGED
@@ -182,6 +182,13 @@ async function ensureClientSyncSchemaCompat<DB extends SyncClientDb>(
182
182
  .addColumn('resolution', 'text')
183
183
  .execute();
184
184
  });
185
+
186
+ await db.schema
187
+ .createIndex('idx_sync_outbox_commits_status_updated_at')
188
+ .ifNotExists()
189
+ .on('sync_outbox_commits')
190
+ .columns(['status', 'updated_at', 'created_at'])
191
+ .execute();
185
192
  }
186
193
 
187
194
  /**
@@ -275,6 +282,13 @@ export async function ensureClientSyncSchema<DB extends SyncClientDb>(
275
282
  .columns(['status', 'created_at'])
276
283
  .execute();
277
284
 
285
+ await db.schema
286
+ .createIndex('idx_sync_outbox_commits_status_updated_at')
287
+ .ifNotExists()
288
+ .on('sync_outbox_commits')
289
+ .columns(['status', 'updated_at', 'created_at'])
290
+ .execute();
291
+
278
292
  await db.schema
279
293
  .createIndex('idx_sync_conflicts_outbox_commit')
280
294
  .ifNotExists()
package/src/mutations.ts CHANGED
@@ -400,7 +400,7 @@ export interface OutboxCommitConfig<DB extends SyncClientDb> {
400
400
  idColumn?: string;
401
401
  versionColumn?: string | null;
402
402
  omitColumns?: string[];
403
- columnCodecs?: ColumnCodecSource;
403
+ codecs?: ColumnCodecSource;
404
404
  codecDialect?: ColumnCodecDialect;
405
405
  }
406
406
 
@@ -432,8 +432,8 @@ export function createOutboxCommit<DB extends SyncClientDb>(
432
432
  table: string,
433
433
  row: Record<string, unknown>
434
434
  ) => {
435
- const columnCodecs = config.columnCodecs;
436
- if (!columnCodecs) return {};
435
+ const codecs = config.codecs;
436
+ if (!codecs) return {};
437
437
  const columns = Object.keys(row);
438
438
  if (columns.length === 0) return {};
439
439
 
@@ -450,7 +450,7 @@ export function createOutboxCommit<DB extends SyncClientDb>(
450
450
  const cached = tableCache.get(cacheKey);
451
451
  if (cached) return cached;
452
452
 
453
- const resolved = toTableColumnCodecs(table, columnCodecs, columns, {
453
+ const resolved = toTableColumnCodecs(table, codecs, columns, {
454
454
  dialect: codecDialect,
455
455
  });
456
456
  tableCache.set(cacheKey, resolved);