@syncular/client 0.0.6-56 → 0.0.6-67

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 (49) hide show
  1. package/dist/client.d.ts +2 -2
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/create-client.d.ts +2 -3
  4. package/dist/create-client.d.ts.map +1 -1
  5. package/dist/create-client.js +12 -8
  6. package/dist/create-client.js.map +1 -1
  7. package/dist/engine/SyncEngine.d.ts.map +1 -1
  8. package/dist/engine/SyncEngine.js +35 -29
  9. package/dist/engine/SyncEngine.js.map +1 -1
  10. package/dist/engine/types.d.ts +3 -3
  11. package/dist/engine/types.d.ts.map +1 -1
  12. package/dist/handlers/collection.d.ts +6 -0
  13. package/dist/handlers/collection.d.ts.map +1 -0
  14. package/dist/handlers/collection.js +21 -0
  15. package/dist/handlers/collection.js.map +1 -0
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/pull-engine.d.ts +3 -3
  21. package/dist/pull-engine.d.ts.map +1 -1
  22. package/dist/pull-engine.js +11 -4
  23. package/dist/pull-engine.js.map +1 -1
  24. package/dist/sync-loop.d.ts +2 -2
  25. package/dist/sync-loop.d.ts.map +1 -1
  26. package/dist/sync-loop.js.map +1 -1
  27. package/dist/sync.d.ts +32 -0
  28. package/dist/sync.d.ts.map +1 -0
  29. package/dist/sync.js +55 -0
  30. package/dist/sync.js.map +1 -0
  31. package/package.json +3 -3
  32. package/src/client.test.ts +4 -4
  33. package/src/client.ts +2 -2
  34. package/src/create-client.test.ts +79 -0
  35. package/src/create-client.ts +15 -10
  36. package/src/engine/SyncEngine.test.ts +77 -29
  37. package/src/engine/SyncEngine.ts +39 -33
  38. package/src/engine/types.ts +3 -3
  39. package/src/handlers/collection.ts +36 -0
  40. package/src/index.ts +2 -1
  41. package/src/pull-engine.test.ts +4 -4
  42. package/src/pull-engine.ts +16 -7
  43. package/src/sync-loop.ts +4 -4
  44. package/src/sync.ts +170 -0
  45. package/dist/handlers/registry.d.ts +0 -15
  46. package/dist/handlers/registry.d.ts.map +0 -1
  47. package/dist/handlers/registry.js +0 -29
  48. package/dist/handlers/registry.js.map +0 -1
  49. package/src/handlers/registry.ts +0 -36
@@ -0,0 +1,79 @@
1
+ import { afterEach, describe, expect, it } from 'bun:test';
2
+ import type { Kysely } from 'kysely';
3
+ import { createBunSqliteDb } from '../../dialect-bun-sqlite/src';
4
+ import { createClient } from './create-client';
5
+ import type { SyncClientDb } from './schema';
6
+
7
+ interface TasksTable {
8
+ id: string;
9
+ user_id: string;
10
+ title: string;
11
+ server_version: number;
12
+ }
13
+
14
+ interface TestDb extends SyncClientDb {
15
+ tasks: TasksTable;
16
+ }
17
+
18
+ async function createTestDb(): Promise<Kysely<TestDb>> {
19
+ const db = createBunSqliteDb<TestDb>({ path: ':memory:' });
20
+ await db.schema
21
+ .createTable('tasks')
22
+ .addColumn('id', 'text', (col) => col.primaryKey())
23
+ .addColumn('user_id', 'text', (col) => col.notNull())
24
+ .addColumn('title', 'text', (col) => col.notNull())
25
+ .addColumn('server_version', 'integer', (col) => col.notNull().defaultTo(0))
26
+ .execute();
27
+ return db;
28
+ }
29
+
30
+ describe('createClient url normalization', () => {
31
+ const originalFetch = globalThis.fetch;
32
+
33
+ afterEach(() => {
34
+ globalThis.fetch = originalFetch;
35
+ });
36
+
37
+ it('accepts sync endpoint URLs without duplicating /sync', async () => {
38
+ const requests: string[] = [];
39
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
40
+ const request = input instanceof Request ? input : new Request(input);
41
+ requests.push(request.url);
42
+ return new Response(
43
+ JSON.stringify({
44
+ ok: true,
45
+ pull: { ok: true, subscriptions: [] },
46
+ }),
47
+ {
48
+ status: 200,
49
+ headers: { 'content-type': 'application/json' },
50
+ }
51
+ );
52
+ }) as typeof fetch;
53
+
54
+ const db = await createTestDb();
55
+ try {
56
+ const { destroy } = await createClient<TestDb>({
57
+ db,
58
+ actorId: 'user-1',
59
+ clientId: 'client-1',
60
+ url: 'http://localhost:4311/api/sync',
61
+ handlers: [
62
+ {
63
+ table: 'tasks',
64
+ subscribe: false,
65
+ async applySnapshot() {},
66
+ async clearAll() {},
67
+ async applyChange() {},
68
+ },
69
+ ],
70
+ });
71
+
72
+ destroy();
73
+ expect(requests).toContain('http://localhost:4311/api/sync');
74
+ expect(requests).not.toContain('http://localhost:4311/api/sync/sync');
75
+ } finally {
76
+ await db.destroy();
77
+ }
78
+ });
79
+ });
@@ -2,7 +2,7 @@
2
2
  * Simplified client factory
3
3
  *
4
4
  * Breaking changes from legacy Client:
5
- * - handlers: array instead of ClientTableRegistry
5
+ * - handlers: plain array (no registry class)
6
6
  * - url: string instead of transport (transport auto-created)
7
7
  * - subscriptions: derived from handler.subscribe (no separate param)
8
8
  * - clientId: auto-generated (no longer required)
@@ -19,8 +19,8 @@ import { extractScopeVars } from '@syncular/core';
19
19
  import { createHttpTransport } from '@syncular/transport-http';
20
20
  import type { Kysely } from 'kysely';
21
21
  import { Client } from './client';
22
+ import { createClientHandlerCollection } from './handlers/collection';
22
23
  import { createClientHandler } from './handlers/create-handler';
23
- import { ClientTableRegistry } from './handlers/registry';
24
24
  import type { ClientTableHandler } from './handlers/types';
25
25
  import type { SyncClientDb } from './schema';
26
26
  import { randomUUID } from './utils/id';
@@ -75,12 +75,22 @@ function createAutoHandler<
75
75
  });
76
76
  }
77
77
 
78
+ function normalizeTransportBaseUrl(url: string): string {
79
+ const trimmed = url.trim().replace(/\/+$/, '');
80
+ if (!trimmed.endsWith('/sync')) {
81
+ return trimmed;
82
+ }
83
+
84
+ const baseUrl = trimmed.slice(0, -'/sync'.length);
85
+ return baseUrl.length > 0 ? baseUrl : '/';
86
+ }
87
+
78
88
  interface CreateClientOptions<DB extends SyncClientDb> {
79
89
  /** Kysely database instance */
80
90
  db: Kysely<DB>;
81
91
 
82
92
  /**
83
- * Server URL (e.g., '/api/sync' or 'https://api.example.com').
93
+ * Sync URL (e.g., '/api/sync') or base API URL (e.g., '/api').
84
94
  * Defaults to '/api/sync' if not provided.
85
95
  * Ignored if transport is provided.
86
96
  */
@@ -88,7 +98,6 @@ interface CreateClientOptions<DB extends SyncClientDb> {
88
98
 
89
99
  /**
90
100
  * Table handlers for applying snapshots and changes.
91
- * Array is auto-converted to ClientTableRegistry.
92
101
  * Handlers with `subscribe: true` (or an object) are synced.
93
102
  * Handlers with `subscribe: false` are local-only.
94
103
  * Either handlers or tables must be provided.
@@ -234,17 +243,13 @@ export async function createClient<DB extends SyncClientDb>(
234
243
  })
235
244
  );
236
245
 
237
- // Build registry from handlers array
238
- const tableHandlers = new ClientTableRegistry<DB>();
239
- for (const handler of handlers) {
240
- tableHandlers.register(handler);
241
- }
246
+ const tableHandlers = createClientHandlerCollection(handlers);
242
247
 
243
248
  // Create transport from URL if not provided
244
249
  let transport = customTransport;
245
250
  if (!transport && url) {
246
251
  transport = createHttpTransport({
247
- baseUrl: url,
252
+ baseUrl: normalizeTransportBaseUrl(url),
248
253
  getHeaders,
249
254
  });
250
255
  }
@@ -3,7 +3,7 @@ import type { SyncChange, SyncTransport } from '@syncular/core';
3
3
  import type { Kysely } from 'kysely';
4
4
  import { sql } from 'kysely';
5
5
  import { createBunSqliteDb } from '../../../dialect-bun-sqlite/src';
6
- import { ClientTableRegistry } from '../handlers/registry';
6
+ import type { ClientHandlerCollection } from '../handlers/collection';
7
7
  import { ensureClientSyncSchema } from '../migrate';
8
8
  import type { SyncClientDb } from '../schema';
9
9
  import { SyncEngine } from './SyncEngine';
@@ -74,29 +74,31 @@ describe('SyncEngine WS inline apply', () => {
74
74
  });
75
75
 
76
76
  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);
77
+ const handlers: ClientHandlerCollection<TestDb> = [
78
+ {
79
+ table: 'tasks',
80
+ async applySnapshot() {},
81
+ async clearAll() {},
82
+ async applyChange(ctx, change) {
83
+ if (change.row_id === 'fail') {
84
+ throw new Error('forced apply failure');
85
+ }
86
+ const rowJson =
87
+ change.row_json && typeof change.row_json === 'object'
88
+ ? change.row_json
89
+ : null;
90
+ const title =
91
+ rowJson && 'title' in rowJson ? String(rowJson.title ?? '') : '';
92
+ await sql`
93
+ update ${sql.table('tasks')}
94
+ set
95
+ ${sql.ref('title')} = ${sql.val(title)},
96
+ ${sql.ref('server_version')} = ${sql.val(Number(change.row_version ?? 0))}
97
+ where ${sql.ref('id')} = ${sql.val(change.row_id)}
98
+ `.execute(ctx.trx);
99
+ },
98
100
  },
99
- });
101
+ ];
100
102
 
101
103
  const engine = new SyncEngine<TestDb>({
102
104
  db,
@@ -156,12 +158,14 @@ describe('SyncEngine WS inline apply', () => {
156
158
  });
157
159
 
158
160
  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
- });
161
+ const handlers: ClientHandlerCollection<TestDb> = [
162
+ {
163
+ table: 'tasks',
164
+ async applySnapshot() {},
165
+ async clearAll() {},
166
+ async applyChange() {},
167
+ },
168
+ ];
165
169
 
166
170
  const engine = new SyncEngine<TestDb>({
167
171
  db,
@@ -193,4 +197,48 @@ describe('SyncEngine WS inline apply', () => {
193
197
  expect(typeof first.payload).toBe('object');
194
198
  expect(snapshot.diagnostics).toBeDefined();
195
199
  });
200
+
201
+ it('ensures sync schema on start without custom migrate callback', async () => {
202
+ const coldDb = createBunSqliteDb<TestDb>({ path: ':memory:' });
203
+ try {
204
+ await coldDb.schema
205
+ .createTable('tasks')
206
+ .addColumn('id', 'text', (col) => col.primaryKey())
207
+ .addColumn('title', 'text', (col) => col.notNull())
208
+ .addColumn('server_version', 'integer', (col) =>
209
+ col.notNull().defaultTo(0)
210
+ )
211
+ .execute();
212
+
213
+ const handlers: ClientHandlerCollection<TestDb> = [
214
+ {
215
+ table: 'tasks',
216
+ async applySnapshot() {},
217
+ async clearAll() {},
218
+ async applyChange() {},
219
+ },
220
+ ];
221
+
222
+ const engine = new SyncEngine<TestDb>({
223
+ db: coldDb,
224
+ transport: noopTransport,
225
+ handlers,
226
+ actorId: 'u1',
227
+ clientId: 'client-migrate',
228
+ subscriptions: [],
229
+ });
230
+
231
+ await engine.start();
232
+
233
+ const exists = await sql<{ count: number }>`
234
+ select count(*) as count
235
+ from sqlite_master
236
+ where type = 'table' and name = 'sync_subscription_state'
237
+ `.execute(coldDb);
238
+
239
+ expect(Number(exists.rows[0]?.count ?? 0)).toBe(1);
240
+ } finally {
241
+ await coldDb.destroy();
242
+ }
243
+ });
196
244
  });
@@ -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}"`
@@ -2287,7 +2293,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
2287
2293
 
2288
2294
  await db.transaction().execute(async (trx) => {
2289
2295
  for (const input of inputs) {
2290
- const handler = handlers.get(input.table);
2296
+ const handler = getClientHandler(handlers, input.table);
2291
2297
  if (!handler) continue;
2292
2298
 
2293
2299
  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
+ }
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';
@@ -7,8 +7,8 @@ import {
7
7
  } from '@syncular/core';
8
8
  import { type Kysely, sql } from 'kysely';
9
9
  import { createBunSqliteDb } from '../../dialect-bun-sqlite/src';
10
+ import type { ClientHandlerCollection } from './handlers/collection';
10
11
  import { createClientHandler } from './handlers/create-handler';
11
- import { ClientTableRegistry } from './handlers/registry';
12
12
  import { ensureClientSyncSchema } from './migrate';
13
13
  import { applyPullResponse, buildPullRequest } from './pull-engine';
14
14
  import type { SyncClientDb } from './schema';
@@ -75,12 +75,12 @@ describe('applyPullResponse chunk streaming', () => {
75
75
  },
76
76
  };
77
77
 
78
- const handlers = new ClientTableRegistry<TestDb>().register(
78
+ const handlers: ClientHandlerCollection<TestDb> = [
79
79
  createClientHandler({
80
80
  table: 'items',
81
81
  scopes: ['items:{id}'],
82
- })
83
- );
82
+ }),
83
+ ];
84
84
 
85
85
  const options = {
86
86
  clientId: 'client-1',
@@ -12,7 +12,10 @@ import type {
12
12
  } from '@syncular/core';
13
13
  import { decodeSnapshotRows } from '@syncular/core';
14
14
  import { type Kysely, sql, type Transaction } from 'kysely';
15
- import type { ClientTableRegistry } from './handlers/registry';
15
+ import {
16
+ type ClientHandlerCollection,
17
+ getClientHandlerOrThrow,
18
+ } from './handlers/collection';
16
19
  import type { ClientTableHandler } from './handlers/types';
17
20
  import type {
18
21
  SyncClientPlugin,
@@ -573,7 +576,7 @@ export async function buildPullRequest<DB extends SyncClientDb>(
573
576
  export async function applyPullResponse<DB extends SyncClientDb>(
574
577
  db: Kysely<DB>,
575
578
  transport: SyncTransport,
576
- handlers: ClientTableRegistry<DB>,
579
+ handlers: ClientHandlerCollection<DB>,
577
580
  options: SyncPullOnceOptions,
578
581
  pullState: {
579
582
  request: SyncPullRequest;
@@ -620,7 +623,10 @@ export async function applyPullResponse<DB extends SyncClientDb>(
620
623
  ? JSON.parse(row.scopes_json)
621
624
  : row.scopes_json
622
625
  : {};
623
- await handlers.getOrThrow(row.table).clearAll({ trx, scopes });
626
+ await getClientHandlerOrThrow(handlers, row.table).clearAll({
627
+ trx,
628
+ scopes,
629
+ });
624
630
  } catch {
625
631
  // ignore missing table handler
626
632
  }
@@ -649,7 +655,10 @@ export async function applyPullResponse<DB extends SyncClientDb>(
649
655
  ? JSON.parse(prev.scopes_json)
650
656
  : prev.scopes_json
651
657
  : {};
652
- await handlers.getOrThrow(prev.table).clearAll({ trx, scopes });
658
+ await getClientHandlerOrThrow(handlers, prev.table).clearAll({
659
+ trx,
660
+ scopes,
661
+ });
653
662
  } catch {
654
663
  // ignore missing handler
655
664
  }
@@ -666,7 +675,7 @@ export async function applyPullResponse<DB extends SyncClientDb>(
666
675
  // Apply snapshots (bootstrap mode)
667
676
  if (sub.bootstrap) {
668
677
  for (const snapshot of sub.snapshots ?? []) {
669
- const handler = handlers.getOrThrow(snapshot.table);
678
+ const handler = getClientHandlerOrThrow(handlers, snapshot.table);
670
679
  const hasChunkRefs =
671
680
  Array.isArray(snapshot.chunks) && snapshot.chunks.length > 0;
672
681
 
@@ -698,7 +707,7 @@ export async function applyPullResponse<DB extends SyncClientDb>(
698
707
  // Apply incremental changes
699
708
  for (const commit of sub.commits) {
700
709
  for (const change of commit.changes) {
701
- const handler = handlers.getOrThrow(change.table);
710
+ const handler = getClientHandlerOrThrow(handlers, change.table);
702
711
  await handler.applyChange({ trx }, change);
703
712
  }
704
713
  }
@@ -763,7 +772,7 @@ export async function applyPullResponse<DB extends SyncClientDb>(
763
772
  export async function syncPullOnce<DB extends SyncClientDb>(
764
773
  db: Kysely<DB>,
765
774
  transport: SyncTransport,
766
- handlers: ClientTableRegistry<DB>,
775
+ handlers: ClientHandlerCollection<DB>,
767
776
  options: SyncPullOnceOptions
768
777
  ): Promise<SyncPullResponse> {
769
778
  const pullState = await buildPullRequest(db, options);
package/src/sync-loop.ts CHANGED
@@ -14,7 +14,7 @@ import type {
14
14
  } from '@syncular/core';
15
15
  import type { Kysely } from 'kysely';
16
16
  import { upsertConflictsForRejectedCommit } from './conflicts';
17
- import type { ClientTableRegistry } from './handlers/registry';
17
+ import type { ClientHandlerCollection } from './handlers/collection';
18
18
  import {
19
19
  getNextSendableOutboxCommit,
20
20
  markOutboxCommitAcked,
@@ -119,7 +119,7 @@ function mergePullResponse(
119
119
  async function syncPullUntilSettled<DB extends SyncClientDb>(
120
120
  db: Kysely<DB>,
121
121
  transport: SyncTransport,
122
- handlers: ClientTableRegistry<DB>,
122
+ handlers: ClientHandlerCollection<DB>,
123
123
  options: SyncPullUntilSettledOptions
124
124
  ): Promise<SyncPullUntilSettledResult> {
125
125
  const maxRounds = Math.max(1, Math.min(1000, options.maxRounds ?? 20));
@@ -182,7 +182,7 @@ export interface SyncOnceResult {
182
182
  async function syncOnceCombined<DB extends SyncClientDb>(
183
183
  db: Kysely<DB>,
184
184
  transport: SyncTransport,
185
- handlers: ClientTableRegistry<DB>,
185
+ handlers: ClientHandlerCollection<DB>,
186
186
  options: SyncOnceOptions
187
187
  ): Promise<SyncOnceResult> {
188
188
  const pullOpts: SyncPullOnceOptions = {
@@ -364,7 +364,7 @@ async function syncOnceCombined<DB extends SyncClientDb>(
364
364
  export async function syncOnce<DB extends SyncClientDb>(
365
365
  db: Kysely<DB>,
366
366
  transport: SyncTransport,
367
- handlers: ClientTableRegistry<DB>,
367
+ handlers: ClientHandlerCollection<DB>,
368
368
  options: SyncOnceOptions
369
369
  ): Promise<SyncOnceResult> {
370
370
  return syncOnceCombined(db, transport, handlers, options);