@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.
- package/README.md +23 -0
- package/dist/blobs/index.js +3 -3
- package/dist/client.d.ts +10 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +70 -21
- package/dist/client.js.map +1 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/conflicts.js +1 -7
- package/dist/conflicts.js.map +1 -1
- package/dist/create-client.d.ts +5 -1
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +22 -10
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +24 -2
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +290 -43
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/index.js +2 -2
- package/dist/engine/types.d.ts +16 -4
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/handlers/create-handler.d.ts +15 -5
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +35 -24
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/handlers/types.d.ts +5 -5
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/index.js +19 -19
- package/dist/migrate.d.ts +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/migrate.js +148 -28
- package/dist/migrate.js.map +1 -1
- package/dist/mutations.d.ts +3 -1
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +93 -18
- package/dist/mutations.js.map +1 -1
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +1 -11
- package/dist/outbox.js.map +1 -1
- package/dist/plugins/incrementing-version.d.ts +1 -1
- package/dist/plugins/incrementing-version.js +2 -2
- package/dist/plugins/index.js +2 -2
- package/dist/proxy/dialect.js +1 -1
- package/dist/proxy/driver.js +1 -1
- package/dist/proxy/index.js +4 -4
- package/dist/proxy/mutations.js +1 -1
- package/dist/pull-engine.d.ts +29 -3
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +314 -78
- package/dist/pull-engine.js.map +1 -1
- package/dist/push-engine.d.ts.map +1 -1
- package/dist/push-engine.js +28 -3
- package/dist/push-engine.js.map +1 -1
- package/dist/query/QueryContext.js +1 -1
- package/dist/query/index.js +3 -3
- package/dist/query/tracked-select.d.ts +2 -1
- package/dist/query/tracked-select.d.ts.map +1 -1
- package/dist/query/tracked-select.js +1 -1
- package/dist/schema.d.ts +2 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/sync-loop.d.ts +5 -1
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +167 -18
- package/dist/sync-loop.js.map +1 -1
- package/package.json +30 -6
- package/src/client.test.ts +369 -0
- package/src/client.ts +101 -22
- package/src/conflicts.ts +1 -10
- package/src/create-client.ts +33 -5
- package/src/engine/SyncEngine.test.ts +157 -0
- package/src/engine/SyncEngine.ts +359 -40
- package/src/engine/types.ts +22 -4
- package/src/handlers/create-handler.ts +86 -37
- package/src/handlers/types.ts +10 -4
- package/src/migrate.ts +215 -33
- package/src/mutations.ts +143 -21
- package/src/outbox.ts +1 -15
- package/src/plugins/incrementing-version.ts +2 -2
- package/src/pull-engine.test.ts +147 -0
- package/src/pull-engine.ts +392 -77
- package/src/push-engine.ts +33 -1
- package/src/query/tracked-select.ts +1 -1
- package/src/schema.ts +2 -2
- package/src/sync-loop.ts +215 -19
package/src/create-client.ts
CHANGED
|
@@ -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
|
-
>(
|
|
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
|
-
|
|
272
|
+
table: handler.table,
|
|
251
273
|
scopes,
|
|
252
274
|
params: {},
|
|
253
275
|
};
|
|
254
276
|
}
|
|
255
277
|
// Custom subscription config
|
|
256
|
-
const scopes: ScopeValues =
|
|
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
|
-
|
|
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
|
+
});
|