@syncular/client 0.0.1 → 0.0.2-126

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 (83) hide show
  1. package/README.md +23 -0
  2. package/dist/blobs/index.js +3 -3
  3. package/dist/client.d.ts +10 -5
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +70 -21
  6. package/dist/client.js.map +1 -1
  7. package/dist/conflicts.d.ts.map +1 -1
  8. package/dist/conflicts.js +1 -7
  9. package/dist/conflicts.js.map +1 -1
  10. package/dist/create-client.d.ts +5 -1
  11. package/dist/create-client.d.ts.map +1 -1
  12. package/dist/create-client.js +22 -10
  13. package/dist/create-client.js.map +1 -1
  14. package/dist/engine/SyncEngine.d.ts +24 -2
  15. package/dist/engine/SyncEngine.d.ts.map +1 -1
  16. package/dist/engine/SyncEngine.js +290 -43
  17. package/dist/engine/SyncEngine.js.map +1 -1
  18. package/dist/engine/index.js +2 -2
  19. package/dist/engine/types.d.ts +16 -4
  20. package/dist/engine/types.d.ts.map +1 -1
  21. package/dist/handlers/create-handler.d.ts +15 -5
  22. package/dist/handlers/create-handler.d.ts.map +1 -1
  23. package/dist/handlers/create-handler.js +35 -24
  24. package/dist/handlers/create-handler.js.map +1 -1
  25. package/dist/handlers/types.d.ts +5 -5
  26. package/dist/handlers/types.d.ts.map +1 -1
  27. package/dist/index.js +19 -19
  28. package/dist/migrate.d.ts +1 -1
  29. package/dist/migrate.d.ts.map +1 -1
  30. package/dist/migrate.js +148 -28
  31. package/dist/migrate.js.map +1 -1
  32. package/dist/mutations.d.ts +3 -1
  33. package/dist/mutations.d.ts.map +1 -1
  34. package/dist/mutations.js +93 -18
  35. package/dist/mutations.js.map +1 -1
  36. package/dist/outbox.d.ts.map +1 -1
  37. package/dist/outbox.js +1 -11
  38. package/dist/outbox.js.map +1 -1
  39. package/dist/plugins/incrementing-version.d.ts +1 -1
  40. package/dist/plugins/incrementing-version.js +2 -2
  41. package/dist/plugins/index.js +2 -2
  42. package/dist/proxy/dialect.js +1 -1
  43. package/dist/proxy/driver.js +1 -1
  44. package/dist/proxy/index.js +4 -4
  45. package/dist/proxy/mutations.js +1 -1
  46. package/dist/pull-engine.d.ts +29 -3
  47. package/dist/pull-engine.d.ts.map +1 -1
  48. package/dist/pull-engine.js +314 -78
  49. package/dist/pull-engine.js.map +1 -1
  50. package/dist/push-engine.d.ts.map +1 -1
  51. package/dist/push-engine.js +28 -3
  52. package/dist/push-engine.js.map +1 -1
  53. package/dist/query/QueryContext.js +1 -1
  54. package/dist/query/index.js +3 -3
  55. package/dist/query/tracked-select.d.ts +2 -1
  56. package/dist/query/tracked-select.d.ts.map +1 -1
  57. package/dist/query/tracked-select.js +1 -1
  58. package/dist/schema.d.ts +2 -2
  59. package/dist/schema.d.ts.map +1 -1
  60. package/dist/sync-loop.d.ts +5 -1
  61. package/dist/sync-loop.d.ts.map +1 -1
  62. package/dist/sync-loop.js +167 -18
  63. package/dist/sync-loop.js.map +1 -1
  64. package/package.json +30 -6
  65. package/src/client.test.ts +369 -0
  66. package/src/client.ts +101 -22
  67. package/src/conflicts.ts +1 -10
  68. package/src/create-client.ts +33 -5
  69. package/src/engine/SyncEngine.test.ts +157 -0
  70. package/src/engine/SyncEngine.ts +359 -40
  71. package/src/engine/types.ts +22 -4
  72. package/src/handlers/create-handler.ts +86 -37
  73. package/src/handlers/types.ts +10 -4
  74. package/src/migrate.ts +215 -33
  75. package/src/mutations.ts +143 -21
  76. package/src/outbox.ts +1 -15
  77. package/src/plugins/incrementing-version.ts +2 -2
  78. package/src/pull-engine.test.ts +147 -0
  79. package/src/pull-engine.ts +392 -77
  80. package/src/push-engine.ts +33 -1
  81. package/src/query/tracked-select.ts +1 -1
  82. package/src/schema.ts +2 -2
  83. package/src/sync-loop.ts +215 -19
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
  import type {
12
+ ColumnCodecDialect,
13
+ ColumnCodecSource,
12
14
  ScopeDefinition,
13
15
  ScopeValues,
14
16
  SyncTransport,
@@ -57,10 +59,19 @@ function deriveDefaultSubscriptionScopes<DB>(args: {
57
59
  function createAutoHandler<
58
60
  DB extends SyncClientDb,
59
61
  TableName extends keyof DB & string,
60
- >(table: string, scopes: string[]): ClientTableHandler<DB, TableName> {
62
+ >(
63
+ table: string,
64
+ scopes: string[],
65
+ options: {
66
+ columnCodecs?: ColumnCodecSource;
67
+ codecDialect?: ColumnCodecDialect;
68
+ }
69
+ ): ClientTableHandler<DB, TableName> {
61
70
  return createClientHandler<DB, TableName>({
62
71
  table: table as TableName,
63
72
  scopes: scopes as ScopeDefinition[],
73
+ columnCodecs: options.columnCodecs,
74
+ codecDialect: options.codecDialect,
64
75
  });
65
76
  }
66
77
 
@@ -132,6 +143,12 @@ interface CreateClientOptions<DB extends SyncClientDb> {
132
143
  /** Optional: State ID for multi-tenant scenarios */
133
144
  stateId?: string;
134
145
 
146
+ /** Optional: Column codec resolver */
147
+ columnCodecs?: ColumnCodecSource;
148
+
149
+ /** Optional: Codec dialect override (default: 'sqlite') */
150
+ codecDialect?: ColumnCodecDialect;
151
+
135
152
  /** Optional: Auto-start sync (default: true) */
136
153
  autoStart?: boolean;
137
154
  }
@@ -194,6 +211,8 @@ export async function createClient<DB extends SyncClientDb>(
194
211
  blobStorage,
195
212
  plugins,
196
213
  stateId,
214
+ columnCodecs,
215
+ codecDialect,
197
216
  autoStart = true,
198
217
  } = options;
199
218
 
@@ -209,7 +228,10 @@ export async function createClient<DB extends SyncClientDb>(
209
228
  const handlers =
210
229
  providedHandlers ??
211
230
  tables!.map((table) =>
212
- createAutoHandler<DB, keyof DB & string>(table, scopes!)
231
+ createAutoHandler<DB, keyof DB & string>(table, scopes!, {
232
+ columnCodecs,
233
+ codecDialect,
234
+ })
213
235
  );
214
236
 
215
237
  // Build registry from handlers array
@@ -247,13 +269,17 @@ export async function createClient<DB extends SyncClientDb>(
247
269
  });
248
270
  return {
249
271
  id: handler.table,
250
- shape: handler.table,
272
+ table: handler.table,
251
273
  scopes,
252
274
  params: {},
253
275
  };
254
276
  }
255
277
  // Custom subscription config
256
- const scopes: ScopeValues = sub.scopes ?? {};
278
+ const scopes: ScopeValues = {};
279
+ for (const [scopeKey, scopeValue] of Object.entries(sub.scopes ?? {})) {
280
+ if (scopeValue === undefined) continue;
281
+ scopes[scopeKey] = scopeValue;
282
+ }
257
283
  if (Object.keys(scopes).length === 0) {
258
284
  throw new Error(
259
285
  `Handler "${handler.table}" subscription scopes cannot be empty. ` +
@@ -262,7 +288,7 @@ export async function createClient<DB extends SyncClientDb>(
262
288
  }
263
289
  return {
264
290
  id: handler.table,
265
- shape: handler.table,
291
+ table: handler.table,
266
292
  scopes,
267
293
  params: sub.params ?? {},
268
294
  };
@@ -280,6 +306,8 @@ export async function createClient<DB extends SyncClientDb>(
280
306
  blobStorage,
281
307
  plugins,
282
308
  stateId,
309
+ columnCodecs,
310
+ codecDialect,
283
311
  realtimeEnabled: sync.realtime ?? true,
284
312
  pollIntervalMs: sync.pollIntervalMs,
285
313
  });
@@ -0,0 +1,157 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
+ import type { SyncChange, SyncTransport } from '@syncular/core';
3
+ import type { Kysely } from 'kysely';
4
+ import { sql } from 'kysely';
5
+ import { createBunSqliteDb } from '../../../dialect-bun-sqlite/src';
6
+ import { ClientTableRegistry } from '../handlers/registry';
7
+ import { ensureClientSyncSchema } from '../migrate';
8
+ import type { SyncClientDb } from '../schema';
9
+ import { SyncEngine } from './SyncEngine';
10
+
11
+ interface TasksTable {
12
+ id: string;
13
+ title: string;
14
+ server_version: number;
15
+ }
16
+
17
+ interface TestDb extends SyncClientDb {
18
+ tasks: TasksTable;
19
+ }
20
+
21
+ const noopTransport: SyncTransport = {
22
+ async sync() {
23
+ return {};
24
+ },
25
+ async fetchSnapshotChunk() {
26
+ return new Uint8Array();
27
+ },
28
+ };
29
+
30
+ describe('SyncEngine WS inline apply', () => {
31
+ let db: Kysely<TestDb>;
32
+
33
+ beforeEach(async () => {
34
+ db = createBunSqliteDb<TestDb>({ path: ':memory:' });
35
+ await ensureClientSyncSchema(db);
36
+
37
+ await db.schema
38
+ .createTable('tasks')
39
+ .addColumn('id', 'text', (col) => col.primaryKey())
40
+ .addColumn('title', 'text', (col) => col.notNull())
41
+ .addColumn('server_version', 'integer', (col) =>
42
+ col.notNull().defaultTo(0)
43
+ )
44
+ .execute();
45
+
46
+ await db
47
+ .insertInto('tasks')
48
+ .values({
49
+ id: 't1',
50
+ title: 'old',
51
+ server_version: 1,
52
+ })
53
+ .execute();
54
+
55
+ await db
56
+ .insertInto('sync_subscription_state')
57
+ .values({
58
+ state_id: 'default',
59
+ subscription_id: 'sub-1',
60
+ table: 'tasks',
61
+ scopes_json: '{}',
62
+ params_json: '{}',
63
+ cursor: 0,
64
+ bootstrap_state_json: null,
65
+ status: 'active',
66
+ created_at: Date.now(),
67
+ updated_at: Date.now(),
68
+ })
69
+ .execute();
70
+ });
71
+
72
+ afterEach(async () => {
73
+ await db.destroy();
74
+ });
75
+
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);
98
+ },
99
+ });
100
+
101
+ const engine = new SyncEngine<TestDb>({
102
+ db,
103
+ transport: noopTransport,
104
+ handlers,
105
+ actorId: 'u1',
106
+ clientId: 'client-1',
107
+ subscriptions: [],
108
+ stateId: 'default',
109
+ });
110
+
111
+ const changes: SyncChange[] = [
112
+ {
113
+ table: 'tasks',
114
+ row_id: 't1',
115
+ op: 'upsert',
116
+ row_json: { id: 't1', title: 'new' },
117
+ row_version: 2,
118
+ scopes: {},
119
+ },
120
+ {
121
+ table: 'tasks',
122
+ row_id: 'fail',
123
+ op: 'upsert',
124
+ row_json: { id: 'fail', title: 'bad' },
125
+ row_version: 1,
126
+ scopes: {},
127
+ },
128
+ ];
129
+
130
+ const applyWsDeliveredChanges = Reflect.get(
131
+ engine,
132
+ 'applyWsDeliveredChanges'
133
+ );
134
+ if (typeof applyWsDeliveredChanges !== 'function') {
135
+ throw new Error('Expected applyWsDeliveredChanges to be callable');
136
+ }
137
+ const applied = await applyWsDeliveredChanges.call(engine, changes, 10);
138
+
139
+ expect(applied).toBe(false);
140
+
141
+ const task = await db
142
+ .selectFrom('tasks')
143
+ .select(['title', 'server_version'])
144
+ .where('id', '=', 't1')
145
+ .executeTakeFirstOrThrow();
146
+ expect(task.title).toBe('old');
147
+ expect(task.server_version).toBe(1);
148
+
149
+ const state = await db
150
+ .selectFrom('sync_subscription_state')
151
+ .select(['cursor'])
152
+ .where('state_id', '=', 'default')
153
+ .where('subscription_id', '=', 'sub-1')
154
+ .executeTakeFirstOrThrow();
155
+ expect(state.cursor).toBe(0);
156
+ });
157
+ });