@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.
- package/dist/client.d.ts +3 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -1
- package/dist/client.js.map +1 -1
- package/dist/create-client.d.ts +3 -4
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +16 -12
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +49 -29
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +3 -3
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/handlers/collection.d.ts +6 -0
- package/dist/handlers/collection.d.ts.map +1 -0
- package/dist/handlers/collection.js +21 -0
- package/dist/handlers/collection.js.map +1 -0
- package/dist/handlers/create-handler.d.ts +1 -1
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +3 -3
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/migrate.js +12 -0
- package/dist/migrate.js.map +1 -1
- package/dist/mutations.d.ts +1 -1
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +3 -3
- package/dist/mutations.js.map +1 -1
- package/dist/pull-engine.d.ts +11 -14
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +68 -6
- package/dist/pull-engine.js.map +1 -1
- package/dist/push-engine.d.ts.map +1 -1
- package/dist/push-engine.js +12 -0
- package/dist/push-engine.js.map +1 -1
- package/dist/sync-loop.d.ts +2 -2
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +5 -2
- package/dist/sync-loop.js.map +1 -1
- package/dist/sync.d.ts +32 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +55 -0
- package/dist/sync.js.map +1 -0
- package/package.json +4 -4
- package/src/client.test.ts +18 -9
- package/src/client.ts +11 -4
- package/src/create-client.test.ts +83 -0
- package/src/create-client.ts +21 -16
- package/src/engine/SyncEngine.test.ts +241 -32
- package/src/engine/SyncEngine.ts +53 -33
- package/src/engine/types.ts +3 -3
- package/src/handlers/collection.ts +36 -0
- package/src/handlers/create-handler.ts +4 -4
- package/src/index.ts +2 -1
- package/src/migrate.ts +14 -0
- package/src/mutations.ts +4 -4
- package/src/pull-engine.test.ts +151 -6
- package/src/pull-engine.ts +93 -21
- package/src/push-engine.ts +15 -0
- package/src/sync-loop.ts +13 -5
- package/src/sync.ts +170 -0
- package/dist/handlers/registry.d.ts +0 -15
- package/dist/handlers/registry.d.ts.map +0 -1
- package/dist/handlers/registry.js +0 -29
- package/dist/handlers/registry.js.map +0 -1
- package/src/handlers/registry.ts +0 -36
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ColumnCodecDialect, ColumnCodecSource, ScopeDefinition, ScopeValuesFromPatterns, SyncSubscriptionRequest } from '@syncular/core';
|
|
2
|
+
import { type CreateClientHandlerOptions } from './handlers/create-handler';
|
|
3
|
+
import type { ClientTableHandler } from './handlers/types';
|
|
4
|
+
import type { SyncClientDb } from './schema';
|
|
5
|
+
type ClientSyncSubscription<ScopeDefs extends readonly ScopeDefinition[]> = Omit<SyncSubscriptionRequest, 'cursor' | 'table' | 'scopes'> & {
|
|
6
|
+
table: string;
|
|
7
|
+
scopes?: ScopeValuesFromPatterns<ScopeDefs>;
|
|
8
|
+
};
|
|
9
|
+
type SharedTableName<DB extends SyncClientDb> = keyof DB & string;
|
|
10
|
+
export type ClientSyncHandlerOptionsForTable<DB extends SyncClientDb, TableName extends SharedTableName<DB>, ScopeDefs extends readonly ScopeDefinition[], Identity> = Omit<CreateClientHandlerOptions<DB, TableName, ScopeDefs>, 'codecs' | 'codecDialect' | 'subscribe'> & {
|
|
11
|
+
codecs?: ColumnCodecSource;
|
|
12
|
+
codecDialect?: ColumnCodecDialect;
|
|
13
|
+
subscribe?: ClientSyncSubscription<ScopeDefs> | ClientSyncSubscription<ScopeDefs>[] | null | ((args: {
|
|
14
|
+
identity: Identity;
|
|
15
|
+
}) => ClientSyncSubscription<ScopeDefs> | ClientSyncSubscription<ScopeDefs>[] | null);
|
|
16
|
+
};
|
|
17
|
+
export interface ClientSyncConfig<DB extends SyncClientDb = SyncClientDb, Identity = {
|
|
18
|
+
actorId: string;
|
|
19
|
+
}> {
|
|
20
|
+
handlers: ClientTableHandler<DB>[];
|
|
21
|
+
subscriptions(identity: Identity): Array<Omit<SyncSubscriptionRequest, 'cursor'>>;
|
|
22
|
+
}
|
|
23
|
+
export interface DefineClientSyncOptions {
|
|
24
|
+
codecs?: ColumnCodecSource;
|
|
25
|
+
codecDialect?: ColumnCodecDialect;
|
|
26
|
+
}
|
|
27
|
+
export interface ClientSyncBuilder<DB extends SyncClientDb, ScopeDefs extends readonly ScopeDefinition[], Identity> extends ClientSyncConfig<DB, Identity> {
|
|
28
|
+
addHandler<TableName extends SharedTableName<DB>>(options: ClientSyncHandlerOptionsForTable<DB, TableName, ScopeDefs, Identity>): this;
|
|
29
|
+
}
|
|
30
|
+
export declare function defineClientSync<DB extends SyncClientDb, ScopeDefs extends readonly ScopeDefinition[], Identity>(options: DefineClientSyncOptions): ClientSyncBuilder<DB, ScopeDefs, Identity>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAEf,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,0BAA0B,EAEhC,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,KAAK,sBAAsB,CAAC,SAAS,SAAS,SAAS,eAAe,EAAE,IACtE,IAAI,CAAC,uBAAuB,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC,GAAG;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;CAC7C,CAAC;AAEJ,KAAK,eAAe,CAAC,EAAE,SAAS,YAAY,IAAI,MAAM,EAAE,GAAG,MAAM,CAAC;AAElE,MAAM,MAAM,gCAAgC,CAC1C,EAAE,SAAS,YAAY,EACvB,SAAS,SAAS,eAAe,CAAC,EAAE,CAAC,EACrC,SAAS,SAAS,SAAS,eAAe,EAAE,EAC5C,QAAQ,IACN,IAAI,CACN,0BAA0B,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC,EACpD,QAAQ,GAAG,cAAc,GAAG,WAAW,CACxC,GAAG;IACF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,SAAS,CAAC,EACN,sBAAsB,CAAC,SAAS,CAAC,GACjC,sBAAsB,CAAC,SAAS,CAAC,EAAE,GACnC,IAAI,GACJ,CAAC,CAAC,IAAI,EAAE;QACN,QAAQ,EAAE,QAAQ,CAAC;KACpB,KACG,sBAAsB,CAAC,SAAS,CAAC,GACjC,sBAAsB,CAAC,SAAS,CAAC,EAAE,GACnC,IAAI,CAAC,CAAC;CACf,CAAC;AAEF,MAAM,WAAW,gBAAgB,CAC/B,EAAE,SAAS,YAAY,GAAG,YAAY,EACtC,QAAQ,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE;IAE9B,QAAQ,EAAE,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;IACnC,aAAa,CACX,QAAQ,EAAE,QAAQ,GACjB,KAAK,CAAC,IAAI,CAAC,uBAAuB,EAAE,QAAQ,CAAC,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB,CAChC,EAAE,SAAS,YAAY,EACvB,SAAS,SAAS,SAAS,eAAe,EAAE,EAC5C,QAAQ,CACR,SAAQ,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtC,UAAU,CAAC,SAAS,SAAS,eAAe,CAAC,EAAE,CAAC,EAC9C,OAAO,EAAE,gCAAgC,CACvC,EAAE,EACF,SAAS,EACT,SAAS,EACT,QAAQ,CACT,GACA,IAAI,CAAC;CACT;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,SAAS,YAAY,EACvB,SAAS,SAAS,SAAS,eAAe,EAAE,EAC5C,QAAQ,EAER,OAAO,EAAE,uBAAuB,GAC/B,iBAAiB,CAAC,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAuF5C"}
|
package/dist/sync.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createClientHandler, } from './handlers/create-handler.js';
|
|
2
|
+
export function defineClientSync(options) {
|
|
3
|
+
const handlers = [];
|
|
4
|
+
const registeredTables = new Set();
|
|
5
|
+
const subscriptionsByTable = new Map();
|
|
6
|
+
const toScopeValues = (value) => {
|
|
7
|
+
const result = {};
|
|
8
|
+
for (const [key, scopeValue] of Object.entries((value ?? {}))) {
|
|
9
|
+
if (scopeValue === undefined)
|
|
10
|
+
continue;
|
|
11
|
+
result[key] = scopeValue;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
const sync = {
|
|
16
|
+
handlers,
|
|
17
|
+
addHandler(handlerOptions) {
|
|
18
|
+
if (registeredTables.has(handlerOptions.table)) {
|
|
19
|
+
throw new Error(`Client table handler already registered: ${handlerOptions.table}`);
|
|
20
|
+
}
|
|
21
|
+
handlers.push(createClientHandler({
|
|
22
|
+
...handlerOptions,
|
|
23
|
+
subscribe: false,
|
|
24
|
+
codecs: options.codecs,
|
|
25
|
+
codecDialect: options.codecDialect,
|
|
26
|
+
}));
|
|
27
|
+
subscriptionsByTable.set(handlerOptions.table, handlerOptions.subscribe);
|
|
28
|
+
registeredTables.add(handlerOptions.table);
|
|
29
|
+
return sync;
|
|
30
|
+
},
|
|
31
|
+
subscriptions(identity) {
|
|
32
|
+
const resolved = [];
|
|
33
|
+
for (const [table, subscribe] of subscriptionsByTable.entries()) {
|
|
34
|
+
if (!subscribe)
|
|
35
|
+
continue;
|
|
36
|
+
const value = typeof subscribe === 'function' ? subscribe({ identity }) : subscribe;
|
|
37
|
+
if (!value)
|
|
38
|
+
continue;
|
|
39
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
resolved.push({
|
|
42
|
+
id: entry.id,
|
|
43
|
+
table: entry.table ?? table,
|
|
44
|
+
scopes: toScopeValues(entry.scopes),
|
|
45
|
+
params: entry.params,
|
|
46
|
+
bootstrapState: entry.bootstrapState,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return resolved;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return sync;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=sync.js.map
|
package/dist/sync.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AAiEnC,MAAM,UAAU,gBAAgB,CAK9B,OAAgC,EACY;IAC5C,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAC9C,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAQjC,CAAC;IAEJ,MAAM,aAAa,GAAG,CACpB,KAAqD,EACzB,EAAE,CAAC;QAC/B,MAAM,MAAM,GAA+B,EAAE,CAAC;QAC9C,KAAK,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAC5C,CAAC,KAAK,IAAI,EAAE,CAA2C,CACxD,EAAE,CAAC;YACF,IAAI,UAAU,KAAK,SAAS;gBAAE,SAAS;YACvC,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC;QAC3B,CAAC;QACD,OAAO,MAAM,CAAC;IAAA,CACf,CAAC;IAEF,MAAM,IAAI,GAA+C;QACvD,QAAQ;QACR,UAAU,CACR,cAKC,EACD;YACA,IAAI,gBAAgB,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/C,MAAM,IAAI,KAAK,CACb,4CAA4C,cAAc,CAAC,KAAK,EAAE,CACnE,CAAC;YACJ,CAAC;YAED,QAAQ,CAAC,IAAI,CACX,mBAAmB,CAAC;gBAClB,GAAG,cAAc;gBACjB,SAAS,EAAE,KAAK;gBAChB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,YAAY,EAAE,OAAO,CAAC,YAAY;aACnC,CAAC,CACH,CAAC;YACF,oBAAoB,CAAC,GAAG,CACtB,cAAc,CAAC,KAAK,EACpB,cAAc,CAAC,SAKD,CACf,CAAC;YACF,gBAAgB,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC;QAAA,CACb;QACD,aAAa,CACX,QAAkB,EAC8B;YAChD,MAAM,QAAQ,GAAmD,EAAE,CAAC;YACpE,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,oBAAoB,CAAC,OAAO,EAAE,EAAE,CAAC;gBAChE,IAAI,CAAC,SAAS;oBAAE,SAAS;gBACzB,MAAM,KAAK,GACT,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACxE,IAAI,CAAC,KAAK;oBAAE,SAAS;gBACrB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;gBACvD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;oBAC5B,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,KAAK,CAAC,EAAE;wBACZ,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,KAAK;wBAC3B,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC;wBACnC,MAAM,EAAE,KAAK,CAAC,MAAM;wBACpB,cAAc,EAAE,KAAK,CAAC,cAAc;qBACrC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YACD,OAAO,QAAQ,CAAC;QAAA,CACjB;KACF,CAAC;IAEF,OAAO,IAAI,CAAC;AAAA,CACb"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6-101",
|
|
4
4
|
"description": "Client-side sync engine with offline-first support, outbox, and conflict resolution",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
7
7
|
"homepage": "https://syncular.dev",
|
|
8
8
|
"repository": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"release": "bunx syncular-publish"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@syncular/core": "0.0.
|
|
50
|
-
"@syncular/transport-http": "0.0.
|
|
49
|
+
"@syncular/core": "0.0.6-101",
|
|
50
|
+
"@syncular/transport-http": "0.0.6-101"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"kysely": "*"
|
package/src/client.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
-
import type
|
|
2
|
+
import { createDatabase, type SyncTransport } from '@syncular/core';
|
|
3
3
|
import type { Kysely } from 'kysely';
|
|
4
4
|
import { sql } from 'kysely';
|
|
5
|
-
import {
|
|
5
|
+
import { createBunSqliteDialect } from '../../dialect-bun-sqlite/src';
|
|
6
6
|
import { ensureClientBlobSchema } from './blobs/migrate';
|
|
7
7
|
import { Client, type ClientBlobStorage } from './client';
|
|
8
8
|
import { SyncEngine } from './engine/SyncEngine';
|
|
9
|
-
import {
|
|
9
|
+
import type { ClientHandlerCollection } from './handlers/collection';
|
|
10
10
|
import { ensureClientSyncSchema } from './migrate';
|
|
11
11
|
import type { SyncClientDb } from './schema';
|
|
12
12
|
|
|
@@ -151,7 +151,10 @@ describe('Client conflict events', () => {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
beforeEach(async () => {
|
|
154
|
-
db =
|
|
154
|
+
db = createDatabase<TestDb>({
|
|
155
|
+
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
156
|
+
family: 'sqlite',
|
|
157
|
+
});
|
|
155
158
|
await ensureClientSyncSchema(db);
|
|
156
159
|
await db.schema
|
|
157
160
|
.createTable('tasks')
|
|
@@ -162,7 +165,7 @@ describe('Client conflict events', () => {
|
|
|
162
165
|
)
|
|
163
166
|
.execute();
|
|
164
167
|
|
|
165
|
-
const handlers
|
|
168
|
+
const handlers: ClientHandlerCollection<TestDb> = [];
|
|
166
169
|
client = new Client<TestDb>({
|
|
167
170
|
db,
|
|
168
171
|
transport: noopTransport,
|
|
@@ -270,12 +273,15 @@ describe('Client blob upload queue recovery', () => {
|
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
beforeEach(async () => {
|
|
273
|
-
db =
|
|
276
|
+
db = createDatabase<TestDb>({
|
|
277
|
+
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
278
|
+
family: 'sqlite',
|
|
279
|
+
});
|
|
274
280
|
await ensureClientSyncSchema(db);
|
|
275
281
|
await ensureClientBlobSchema(db);
|
|
276
282
|
initiateCalls = 0;
|
|
277
283
|
|
|
278
|
-
const handlers
|
|
284
|
+
const handlers: ClientHandlerCollection<TestDb> = [];
|
|
279
285
|
const transport: SyncTransport = {
|
|
280
286
|
...noopTransport,
|
|
281
287
|
blobs: {
|
|
@@ -373,10 +379,13 @@ describe('Client inspector snapshot', () => {
|
|
|
373
379
|
let client: Client<TestDb>;
|
|
374
380
|
|
|
375
381
|
beforeEach(async () => {
|
|
376
|
-
db =
|
|
382
|
+
db = createDatabase<TestDb>({
|
|
383
|
+
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
384
|
+
family: 'sqlite',
|
|
385
|
+
});
|
|
377
386
|
await ensureClientSyncSchema(db);
|
|
378
387
|
|
|
379
|
-
const handlers
|
|
388
|
+
const handlers: ClientHandlerCollection<TestDb> = [];
|
|
380
389
|
client = new Client<TestDb>({
|
|
381
390
|
db,
|
|
382
391
|
transport: noopTransport,
|
package/src/client.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ColumnCodecSource,
|
|
16
16
|
SyncTransport,
|
|
17
17
|
} from '@syncular/core';
|
|
18
|
+
import { countSyncMetric } from '@syncular/core';
|
|
18
19
|
import type { Kysely } from 'kysely';
|
|
19
20
|
import { sql } from 'kysely';
|
|
20
21
|
import { ensureClientBlobSchema } from './blobs/migrate';
|
|
@@ -37,7 +38,7 @@ import type {
|
|
|
37
38
|
SyncResult,
|
|
38
39
|
TransportHealth,
|
|
39
40
|
} from './engine/types';
|
|
40
|
-
import type {
|
|
41
|
+
import type { ClientHandlerCollection } from './handlers/collection';
|
|
41
42
|
import { ensureClientSyncSchema } from './migrate';
|
|
42
43
|
import {
|
|
43
44
|
createMutationsApi,
|
|
@@ -91,7 +92,7 @@ export interface ClientOptions<DB extends SyncClientDb> {
|
|
|
91
92
|
transport: SyncTransport;
|
|
92
93
|
|
|
93
94
|
/** Table handlers for applying snapshots and changes */
|
|
94
|
-
tableHandlers:
|
|
95
|
+
tableHandlers: ClientHandlerCollection<DB>;
|
|
95
96
|
|
|
96
97
|
/** Unique client identifier (e.g., device ID) */
|
|
97
98
|
clientId: string;
|
|
@@ -132,7 +133,7 @@ export interface ClientOptions<DB extends SyncClientDb> {
|
|
|
132
133
|
omitColumns?: string[];
|
|
133
134
|
|
|
134
135
|
/** Optional: Column codec resolver */
|
|
135
|
-
|
|
136
|
+
codecs?: ColumnCodecSource;
|
|
136
137
|
|
|
137
138
|
/** Optional: Codec dialect override (default: 'sqlite') */
|
|
138
139
|
codecDialect?: ColumnCodecDialect;
|
|
@@ -341,7 +342,7 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
341
342
|
idColumn: options.idColumn ?? 'id',
|
|
342
343
|
versionColumn: options.versionColumn ?? 'server_version',
|
|
343
344
|
omitColumns: options.omitColumns ?? [],
|
|
344
|
-
|
|
345
|
+
codecs: options.codecs,
|
|
345
346
|
codecDialect: options.codecDialect,
|
|
346
347
|
});
|
|
347
348
|
this.mutations = createMutationsApi(commitFn) as MutationsApi<DB>;
|
|
@@ -714,6 +715,12 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
714
715
|
|
|
715
716
|
await resolveConflict(this.options.db, { id, resolution: resolutionStr });
|
|
716
717
|
|
|
718
|
+
countSyncMetric('sync.conflicts.resolved', 1, {
|
|
719
|
+
attributes: {
|
|
720
|
+
strategy: resolution.strategy,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
|
|
717
724
|
this.emittedConflictIds.delete(id);
|
|
718
725
|
if (resolvedConflict) {
|
|
719
726
|
this.emit('conflict:resolved', resolvedConflict);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { createDatabase } from '@syncular/core';
|
|
3
|
+
import type { Kysely } from 'kysely';
|
|
4
|
+
import { createBunSqliteDialect } from '../../dialect-bun-sqlite/src';
|
|
5
|
+
import { createClient } from './create-client';
|
|
6
|
+
import type { SyncClientDb } from './schema';
|
|
7
|
+
|
|
8
|
+
interface TasksTable {
|
|
9
|
+
id: string;
|
|
10
|
+
user_id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
server_version: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TestDb extends SyncClientDb {
|
|
16
|
+
tasks: TasksTable;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function createTestDb(): Promise<Kysely<TestDb>> {
|
|
20
|
+
const db = createDatabase<TestDb>({
|
|
21
|
+
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
22
|
+
family: 'sqlite',
|
|
23
|
+
});
|
|
24
|
+
await db.schema
|
|
25
|
+
.createTable('tasks')
|
|
26
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
27
|
+
.addColumn('user_id', 'text', (col) => col.notNull())
|
|
28
|
+
.addColumn('title', 'text', (col) => col.notNull())
|
|
29
|
+
.addColumn('server_version', 'integer', (col) => col.notNull().defaultTo(0))
|
|
30
|
+
.execute();
|
|
31
|
+
return db;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('createClient url normalization', () => {
|
|
35
|
+
const originalFetch = globalThis.fetch;
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
globalThis.fetch = originalFetch;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('accepts sync endpoint URLs without duplicating /sync', async () => {
|
|
42
|
+
const requests: string[] = [];
|
|
43
|
+
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
44
|
+
const request = input instanceof Request ? input : new Request(input);
|
|
45
|
+
requests.push(request.url);
|
|
46
|
+
return new Response(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
ok: true,
|
|
49
|
+
pull: { ok: true, subscriptions: [] },
|
|
50
|
+
}),
|
|
51
|
+
{
|
|
52
|
+
status: 200,
|
|
53
|
+
headers: { 'content-type': 'application/json' },
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}) as typeof fetch;
|
|
57
|
+
|
|
58
|
+
const db = await createTestDb();
|
|
59
|
+
try {
|
|
60
|
+
const { destroy } = await createClient<TestDb>({
|
|
61
|
+
db,
|
|
62
|
+
actorId: 'user-1',
|
|
63
|
+
clientId: 'client-1',
|
|
64
|
+
url: 'http://localhost:4311/api/sync',
|
|
65
|
+
handlers: [
|
|
66
|
+
{
|
|
67
|
+
table: 'tasks',
|
|
68
|
+
subscribe: false,
|
|
69
|
+
async applySnapshot() {},
|
|
70
|
+
async clearAll() {},
|
|
71
|
+
async applyChange() {},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
destroy();
|
|
77
|
+
expect(requests).toContain('http://localhost:4311/api/sync');
|
|
78
|
+
expect(requests).not.toContain('http://localhost:4311/api/sync/sync');
|
|
79
|
+
} finally {
|
|
80
|
+
await db.destroy();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/create-client.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Simplified client factory
|
|
3
3
|
*
|
|
4
4
|
* Breaking changes from legacy Client:
|
|
5
|
-
* - handlers: array
|
|
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';
|
|
@@ -63,24 +63,34 @@ function createAutoHandler<
|
|
|
63
63
|
table: string,
|
|
64
64
|
scopes: string[],
|
|
65
65
|
options: {
|
|
66
|
-
|
|
66
|
+
codecs?: ColumnCodecSource;
|
|
67
67
|
codecDialect?: ColumnCodecDialect;
|
|
68
68
|
}
|
|
69
69
|
): ClientTableHandler<DB, TableName> {
|
|
70
70
|
return createClientHandler<DB, TableName>({
|
|
71
71
|
table: table as TableName,
|
|
72
72
|
scopes: scopes as ScopeDefinition[],
|
|
73
|
-
|
|
73
|
+
codecs: options.codecs,
|
|
74
74
|
codecDialect: options.codecDialect,
|
|
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
|
-
*
|
|
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.
|
|
@@ -144,7 +153,7 @@ interface CreateClientOptions<DB extends SyncClientDb> {
|
|
|
144
153
|
stateId?: string;
|
|
145
154
|
|
|
146
155
|
/** Optional: Column codec resolver */
|
|
147
|
-
|
|
156
|
+
codecs?: ColumnCodecSource;
|
|
148
157
|
|
|
149
158
|
/** Optional: Codec dialect override (default: 'sqlite') */
|
|
150
159
|
codecDialect?: ColumnCodecDialect;
|
|
@@ -211,7 +220,7 @@ export async function createClient<DB extends SyncClientDb>(
|
|
|
211
220
|
blobStorage,
|
|
212
221
|
plugins,
|
|
213
222
|
stateId,
|
|
214
|
-
|
|
223
|
+
codecs,
|
|
215
224
|
codecDialect,
|
|
216
225
|
autoStart = true,
|
|
217
226
|
} = options;
|
|
@@ -229,22 +238,18 @@ export async function createClient<DB extends SyncClientDb>(
|
|
|
229
238
|
providedHandlers ??
|
|
230
239
|
tables!.map((table) =>
|
|
231
240
|
createAutoHandler<DB, keyof DB & string>(table, scopes!, {
|
|
232
|
-
|
|
241
|
+
codecs,
|
|
233
242
|
codecDialect,
|
|
234
243
|
})
|
|
235
244
|
);
|
|
236
245
|
|
|
237
|
-
|
|
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
|
}
|
|
@@ -306,7 +311,7 @@ export async function createClient<DB extends SyncClientDb>(
|
|
|
306
311
|
blobStorage,
|
|
307
312
|
plugins,
|
|
308
313
|
stateId,
|
|
309
|
-
|
|
314
|
+
codecs,
|
|
310
315
|
codecDialect,
|
|
311
316
|
realtimeEnabled: sync.realtime ?? true,
|
|
312
317
|
pollIntervalMs: sync.pollIntervalMs,
|