@syncular/client 0.0.1-60
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/blobs/index.d.ts +7 -0
- package/dist/blobs/index.d.ts.map +1 -0
- package/dist/blobs/index.js +7 -0
- package/dist/blobs/index.js.map +1 -0
- package/dist/blobs/manager.d.ts +345 -0
- package/dist/blobs/manager.d.ts.map +1 -0
- package/dist/blobs/manager.js +749 -0
- package/dist/blobs/manager.js.map +1 -0
- package/dist/blobs/migrate.d.ts +14 -0
- package/dist/blobs/migrate.d.ts.map +1 -0
- package/dist/blobs/migrate.js +59 -0
- package/dist/blobs/migrate.js.map +1 -0
- package/dist/blobs/types.d.ts +62 -0
- package/dist/blobs/types.d.ts.map +1 -0
- package/dist/blobs/types.js +5 -0
- package/dist/blobs/types.js.map +1 -0
- package/dist/client.d.ts +338 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +834 -0
- package/dist/client.js.map +1 -0
- package/dist/conflicts.d.ts +31 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +118 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/create-client.d.ts +115 -0
- package/dist/create-client.d.ts.map +1 -0
- package/dist/create-client.js +162 -0
- package/dist/create-client.js.map +1 -0
- package/dist/engine/SyncEngine.d.ts +215 -0
- package/dist/engine/SyncEngine.d.ts.map +1 -0
- package/dist/engine/SyncEngine.js +1066 -0
- package/dist/engine/SyncEngine.js.map +1 -0
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +6 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/types.d.ts +230 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +7 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/handlers/create-handler.d.ts +110 -0
- package/dist/handlers/create-handler.d.ts.map +1 -0
- package/dist/handlers/create-handler.js +140 -0
- package/dist/handlers/create-handler.js.map +1 -0
- package/dist/handlers/registry.d.ts +15 -0
- package/dist/handlers/registry.d.ts.map +1 -0
- package/dist/handlers/registry.js +29 -0
- package/dist/handlers/registry.js.map +1 -0
- package/dist/handlers/types.d.ts +83 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +5 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +19 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +106 -0
- package/dist/migrate.js.map +1 -0
- package/dist/mutations.d.ts +138 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +611 -0
- package/dist/mutations.js.map +1 -0
- package/dist/outbox.d.ts +112 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +304 -0
- package/dist/outbox.js.map +1 -0
- package/dist/plugins/incrementing-version.d.ts +34 -0
- package/dist/plugins/incrementing-version.d.ts.map +1 -0
- package/dist/plugins/incrementing-version.js +83 -0
- package/dist/plugins/incrementing-version.js.map +1 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/types.d.ts +49 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +15 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/proxy/connection.d.ts +33 -0
- package/dist/proxy/connection.d.ts.map +1 -0
- package/dist/proxy/connection.js +153 -0
- package/dist/proxy/connection.js.map +1 -0
- package/dist/proxy/dialect.d.ts +46 -0
- package/dist/proxy/dialect.d.ts.map +1 -0
- package/dist/proxy/dialect.js +58 -0
- package/dist/proxy/dialect.js.map +1 -0
- package/dist/proxy/driver.d.ts +42 -0
- package/dist/proxy/driver.d.ts.map +1 -0
- package/dist/proxy/driver.js +78 -0
- package/dist/proxy/driver.js.map +1 -0
- package/dist/proxy/index.d.ts +10 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +10 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/mutations.d.ts +9 -0
- package/dist/proxy/mutations.d.ts.map +1 -0
- package/dist/proxy/mutations.js +11 -0
- package/dist/proxy/mutations.js.map +1 -0
- package/dist/pull-engine.d.ts +45 -0
- package/dist/pull-engine.d.ts.map +1 -0
- package/dist/pull-engine.js +391 -0
- package/dist/pull-engine.js.map +1 -0
- package/dist/push-engine.d.ts +18 -0
- package/dist/push-engine.d.ts.map +1 -0
- package/dist/push-engine.js +155 -0
- package/dist/push-engine.js.map +1 -0
- package/dist/query/FingerprintCollector.d.ts +18 -0
- package/dist/query/FingerprintCollector.d.ts.map +1 -0
- package/dist/query/FingerprintCollector.js +28 -0
- package/dist/query/FingerprintCollector.js.map +1 -0
- package/dist/query/QueryContext.d.ts +33 -0
- package/dist/query/QueryContext.d.ts.map +1 -0
- package/dist/query/QueryContext.js +16 -0
- package/dist/query/QueryContext.js.map +1 -0
- package/dist/query/fingerprint.d.ts +61 -0
- package/dist/query/fingerprint.d.ts.map +1 -0
- package/dist/query/fingerprint.js +91 -0
- package/dist/query/fingerprint.js.map +1 -0
- package/dist/query/index.d.ts +7 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +7 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/tracked-select.d.ts +18 -0
- package/dist/query/tracked-select.d.ts.map +1 -0
- package/dist/query/tracked-select.js +90 -0
- package/dist/query/tracked-select.js.map +1 -0
- package/dist/schema.d.ts +83 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/schema.js.map +1 -0
- package/dist/sync-loop.d.ts +32 -0
- package/dist/sync-loop.d.ts.map +1 -0
- package/dist/sync-loop.js +249 -0
- package/dist/sync-loop.js.map +1 -0
- package/dist/utils/id.d.ts +8 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +19 -0
- package/dist/utils/id.js.map +1 -0
- package/package.json +58 -0
- package/src/blobs/index.ts +7 -0
- package/src/blobs/manager.ts +1027 -0
- package/src/blobs/migrate.ts +67 -0
- package/src/blobs/types.ts +84 -0
- package/src/client.ts +1222 -0
- package/src/conflicts.ts +180 -0
- package/src/create-client.ts +297 -0
- package/src/engine/SyncEngine.ts +1337 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/types.ts +268 -0
- package/src/handlers/create-handler.ts +287 -0
- package/src/handlers/registry.ts +36 -0
- package/src/handlers/types.ts +102 -0
- package/src/index.ts +25 -0
- package/src/migrate.ts +122 -0
- package/src/mutations.ts +926 -0
- package/src/outbox.ts +397 -0
- package/src/plugins/incrementing-version.ts +133 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/types.ts +63 -0
- package/src/proxy/connection.ts +191 -0
- package/src/proxy/dialect.ts +76 -0
- package/src/proxy/driver.ts +126 -0
- package/src/proxy/index.ts +10 -0
- package/src/proxy/mutations.ts +18 -0
- package/src/pull-engine.ts +518 -0
- package/src/push-engine.ts +201 -0
- package/src/query/FingerprintCollector.ts +29 -0
- package/src/query/QueryContext.ts +54 -0
- package/src/query/fingerprint.ts +109 -0
- package/src/query/index.ts +10 -0
- package/src/query/tracked-select.ts +139 -0
- package/src/schema.ts +94 -0
- package/src/sync-loop.ts +368 -0
- package/src/utils/id.ts +20 -0
package/src/conflicts.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/client - Sync conflict storage helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SyncOperationResult, SyncPushResponse } from '@syncular/core';
|
|
6
|
+
import type { Kysely } from 'kysely';
|
|
7
|
+
import { sql } from 'kysely';
|
|
8
|
+
import type { SyncClientDb } from './schema';
|
|
9
|
+
|
|
10
|
+
function randomId(): string {
|
|
11
|
+
if (
|
|
12
|
+
typeof crypto !== 'undefined' &&
|
|
13
|
+
typeof crypto.randomUUID === 'function'
|
|
14
|
+
) {
|
|
15
|
+
return crypto.randomUUID();
|
|
16
|
+
}
|
|
17
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function messageFromResult(
|
|
21
|
+
r: Extract<SyncOperationResult, { status: 'conflict' | 'error' }>
|
|
22
|
+
): {
|
|
23
|
+
message: string;
|
|
24
|
+
code: string | null;
|
|
25
|
+
serverVersion: number | null;
|
|
26
|
+
serverRowJson: string | null;
|
|
27
|
+
} {
|
|
28
|
+
if (r.status === 'conflict') {
|
|
29
|
+
return {
|
|
30
|
+
message: r.message,
|
|
31
|
+
code: 'CONFLICT',
|
|
32
|
+
serverVersion: r.server_version,
|
|
33
|
+
serverRowJson: JSON.stringify(r.server_row),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
message: r.error,
|
|
39
|
+
code: r.code ?? null,
|
|
40
|
+
serverVersion: null,
|
|
41
|
+
serverRowJson: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function upsertConflictsForRejectedCommit<DB extends SyncClientDb>(
|
|
46
|
+
db: Kysely<DB>,
|
|
47
|
+
args: {
|
|
48
|
+
outboxCommitId: string;
|
|
49
|
+
clientCommitId: string;
|
|
50
|
+
response: SyncPushResponse;
|
|
51
|
+
nowMs?: number;
|
|
52
|
+
}
|
|
53
|
+
): Promise<number> {
|
|
54
|
+
const now = args.nowMs ?? Date.now();
|
|
55
|
+
|
|
56
|
+
// Remove any previous conflict rows for this outbox commit.
|
|
57
|
+
await sql`
|
|
58
|
+
delete from ${sql.table('sync_conflicts')}
|
|
59
|
+
where ${sql.ref('outbox_commit_id')} = ${sql.val(args.outboxCommitId)}
|
|
60
|
+
`.execute(db);
|
|
61
|
+
|
|
62
|
+
const conflictResults = args.response.results.filter(
|
|
63
|
+
(r) => r.status === 'conflict' || r.status === 'error'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (conflictResults.length === 0) return 0;
|
|
67
|
+
|
|
68
|
+
const rows = conflictResults.map((r) => {
|
|
69
|
+
const info = messageFromResult(r);
|
|
70
|
+
return {
|
|
71
|
+
id: randomId(),
|
|
72
|
+
outbox_commit_id: args.outboxCommitId,
|
|
73
|
+
client_commit_id: args.clientCommitId,
|
|
74
|
+
op_index: r.opIndex,
|
|
75
|
+
result_status: r.status,
|
|
76
|
+
message: info.message,
|
|
77
|
+
code: info.code,
|
|
78
|
+
server_version: info.serverVersion,
|
|
79
|
+
server_row_json: info.serverRowJson,
|
|
80
|
+
created_at: now,
|
|
81
|
+
resolved_at: null,
|
|
82
|
+
resolution: null,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const insertColumns = [
|
|
87
|
+
'id',
|
|
88
|
+
'outbox_commit_id',
|
|
89
|
+
'client_commit_id',
|
|
90
|
+
'op_index',
|
|
91
|
+
'result_status',
|
|
92
|
+
'message',
|
|
93
|
+
'code',
|
|
94
|
+
'server_version',
|
|
95
|
+
'server_row_json',
|
|
96
|
+
'created_at',
|
|
97
|
+
'resolved_at',
|
|
98
|
+
'resolution',
|
|
99
|
+
] as const;
|
|
100
|
+
|
|
101
|
+
await sql`
|
|
102
|
+
insert into ${sql.table('sync_conflicts')} (
|
|
103
|
+
${sql.join(insertColumns.map((c) => sql.ref(c)))}
|
|
104
|
+
) values ${sql.join(
|
|
105
|
+
rows.map(
|
|
106
|
+
(row) =>
|
|
107
|
+
sql`(${sql.join(
|
|
108
|
+
[
|
|
109
|
+
sql.val(row.id),
|
|
110
|
+
sql.val(row.outbox_commit_id),
|
|
111
|
+
sql.val(row.client_commit_id),
|
|
112
|
+
sql.val(row.op_index),
|
|
113
|
+
sql.val(row.result_status),
|
|
114
|
+
sql.val(row.message),
|
|
115
|
+
sql.val(row.code),
|
|
116
|
+
sql.val(row.server_version),
|
|
117
|
+
sql.val(row.server_row_json),
|
|
118
|
+
sql.val(row.created_at),
|
|
119
|
+
sql.val(row.resolved_at),
|
|
120
|
+
sql.val(row.resolution),
|
|
121
|
+
],
|
|
122
|
+
sql`, `
|
|
123
|
+
)})`
|
|
124
|
+
),
|
|
125
|
+
sql`, `
|
|
126
|
+
)}
|
|
127
|
+
`.execute(db);
|
|
128
|
+
|
|
129
|
+
return conflictResults.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type PendingConflictRow = {
|
|
133
|
+
id: string;
|
|
134
|
+
outbox_commit_id: string;
|
|
135
|
+
client_commit_id: string;
|
|
136
|
+
op_index: number;
|
|
137
|
+
result_status: string;
|
|
138
|
+
message: string;
|
|
139
|
+
code: string | null;
|
|
140
|
+
server_version: number | null;
|
|
141
|
+
server_row_json: string | null;
|
|
142
|
+
created_at: number;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export async function listPendingConflicts<DB extends SyncClientDb>(
|
|
146
|
+
db: Kysely<DB>
|
|
147
|
+
): Promise<PendingConflictRow[]> {
|
|
148
|
+
const res = await sql<PendingConflictRow>`
|
|
149
|
+
select
|
|
150
|
+
${sql.ref('id')},
|
|
151
|
+
${sql.ref('outbox_commit_id')},
|
|
152
|
+
${sql.ref('client_commit_id')},
|
|
153
|
+
${sql.ref('op_index')},
|
|
154
|
+
${sql.ref('result_status')},
|
|
155
|
+
${sql.ref('message')},
|
|
156
|
+
${sql.ref('code')},
|
|
157
|
+
${sql.ref('server_version')},
|
|
158
|
+
${sql.ref('server_row_json')},
|
|
159
|
+
${sql.ref('created_at')}
|
|
160
|
+
from ${sql.table('sync_conflicts')}
|
|
161
|
+
where ${sql.ref('resolved_at')} is null
|
|
162
|
+
order by ${sql.ref('created_at')} desc
|
|
163
|
+
`.execute(db);
|
|
164
|
+
|
|
165
|
+
return res.rows;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function resolveConflict<DB extends SyncClientDb>(
|
|
169
|
+
db: Kysely<DB>,
|
|
170
|
+
args: { id: string; resolution: string; nowMs?: number }
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
const now = args.nowMs ?? Date.now();
|
|
173
|
+
await sql`
|
|
174
|
+
update ${sql.table('sync_conflicts')}
|
|
175
|
+
set
|
|
176
|
+
${sql.ref('resolved_at')} = ${sql.val(now)},
|
|
177
|
+
${sql.ref('resolution')} = ${sql.val(args.resolution)}
|
|
178
|
+
where ${sql.ref('id')} = ${sql.val(args.id)}
|
|
179
|
+
`.execute(db);
|
|
180
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified client factory
|
|
3
|
+
*
|
|
4
|
+
* Breaking changes from legacy Client:
|
|
5
|
+
* - handlers: array instead of ClientTableRegistry
|
|
6
|
+
* - url: string instead of transport (transport auto-created)
|
|
7
|
+
* - subscriptions: derived from handler.subscribe (no separate param)
|
|
8
|
+
* - clientId: auto-generated (no longer required)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ScopeDefinition,
|
|
13
|
+
ScopeValues,
|
|
14
|
+
SyncTransport,
|
|
15
|
+
} from '@syncular/core';
|
|
16
|
+
import { extractScopeVars } from '@syncular/core';
|
|
17
|
+
import { createHttpTransport } from '@syncular/transport-http';
|
|
18
|
+
import type { Kysely } from 'kysely';
|
|
19
|
+
import { Client } from './client';
|
|
20
|
+
import { createClientHandler } from './handlers/create-handler';
|
|
21
|
+
import { ClientTableRegistry } from './handlers/registry';
|
|
22
|
+
import type { ClientTableHandler } from './handlers/types';
|
|
23
|
+
import type { SyncClientDb } from './schema';
|
|
24
|
+
import { randomUUID } from './utils/id';
|
|
25
|
+
|
|
26
|
+
function deriveDefaultSubscriptionScopes<DB>(args: {
|
|
27
|
+
handler: Pick<ClientTableHandler<DB>, 'table' | 'scopePatterns'>;
|
|
28
|
+
actorId: string;
|
|
29
|
+
}): ScopeValues {
|
|
30
|
+
const patterns = args.handler.scopePatterns ?? [];
|
|
31
|
+
const vars = new Set<string>();
|
|
32
|
+
for (const pattern of patterns) {
|
|
33
|
+
for (const v of extractScopeVars(pattern)) {
|
|
34
|
+
vars.add(v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const allVars = Array.from(vars);
|
|
39
|
+
if (allVars.length !== 1) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Handler "${args.handler.table}" has subscribe=true but no explicit subscription scopes. ` +
|
|
42
|
+
'Set subscribe: { scopes: { ... } } on the handler. ' +
|
|
43
|
+
`(Cannot infer defaults from scopePatterns: ${
|
|
44
|
+
patterns.length > 0 ? patterns.join(', ') : '(none)'
|
|
45
|
+
})`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const varName = allVars[0]!;
|
|
50
|
+
return { [varName]: args.actorId };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Auto-generate a simple handler for a table.
|
|
55
|
+
* Uses 'id' as primary key and provided scopes.
|
|
56
|
+
*/
|
|
57
|
+
function createAutoHandler<
|
|
58
|
+
DB extends SyncClientDb,
|
|
59
|
+
TableName extends keyof DB & string,
|
|
60
|
+
>(table: string, scopes: string[]): ClientTableHandler<DB, TableName> {
|
|
61
|
+
return createClientHandler<DB, TableName>({
|
|
62
|
+
table: table as TableName,
|
|
63
|
+
scopes: scopes as ScopeDefinition[],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface CreateClientOptions<DB extends SyncClientDb> {
|
|
68
|
+
/** Kysely database instance */
|
|
69
|
+
db: Kysely<DB>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Server URL (e.g., '/api/sync' or 'https://api.example.com').
|
|
73
|
+
* Defaults to '/api/sync' if not provided.
|
|
74
|
+
* Ignored if transport is provided.
|
|
75
|
+
*/
|
|
76
|
+
url?: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Table handlers for applying snapshots and changes.
|
|
80
|
+
* Array is auto-converted to ClientTableRegistry.
|
|
81
|
+
* Handlers with `subscribe: true` (or an object) are synced.
|
|
82
|
+
* Handlers with `subscribe: false` are local-only.
|
|
83
|
+
* Either handlers or tables must be provided.
|
|
84
|
+
*/
|
|
85
|
+
handlers?: Array<ClientTableHandler<DB>>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Table names to auto-generate handlers for.
|
|
89
|
+
* Uses default scopes and primary key 'id'.
|
|
90
|
+
* Either handlers or tables must be provided.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* tables: ['tasks', 'notes', 'projects']
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
tables?: string[];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default scopes for auto-generated table handlers.
|
|
101
|
+
* Required when using tables option.
|
|
102
|
+
* Ignored when handlers are provided.
|
|
103
|
+
*/
|
|
104
|
+
scopes?: string[];
|
|
105
|
+
|
|
106
|
+
/** Current actor/user identifier */
|
|
107
|
+
actorId: string;
|
|
108
|
+
|
|
109
|
+
/** Optional: Custom client ID (auto-generated UUID if not provided) */
|
|
110
|
+
clientId?: string;
|
|
111
|
+
|
|
112
|
+
/** Optional: Custom transport (overrides url) */
|
|
113
|
+
transport?: SyncTransport;
|
|
114
|
+
|
|
115
|
+
/** Optional: Function to get auth headers */
|
|
116
|
+
getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
117
|
+
|
|
118
|
+
/** Optional: Sync configuration */
|
|
119
|
+
sync?: {
|
|
120
|
+
/** Enable realtime/WebSocket mode (default: true) */
|
|
121
|
+
realtime?: boolean;
|
|
122
|
+
/** Polling interval in ms (default: 10000) */
|
|
123
|
+
pollIntervalMs?: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** Optional: Local blob storage adapter */
|
|
127
|
+
blobStorage?: import('./client').ClientBlobStorage;
|
|
128
|
+
|
|
129
|
+
/** Optional: Sync plugins */
|
|
130
|
+
plugins?: import('./plugins').SyncClientPlugin[];
|
|
131
|
+
|
|
132
|
+
/** Optional: State ID for multi-tenant scenarios */
|
|
133
|
+
stateId?: string;
|
|
134
|
+
|
|
135
|
+
/** Optional: Auto-start sync (default: true) */
|
|
136
|
+
autoStart?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface CreateClientResult<DB extends SyncClientDb> {
|
|
140
|
+
/** The client instance */
|
|
141
|
+
client: Client<DB>;
|
|
142
|
+
/** Stop sync */
|
|
143
|
+
stop: () => void;
|
|
144
|
+
/** Destroy client and cleanup */
|
|
145
|
+
destroy: () => void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a simplified sync client.
|
|
150
|
+
*
|
|
151
|
+
* Auto-generates clientId, creates transport from URL (default: '/api/sync'),
|
|
152
|
+
* builds subscriptions from handlers.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* // Auto-generate handlers for simple tables (default URL: '/api/sync')
|
|
157
|
+
* const { client } = await createClient({
|
|
158
|
+
* db,
|
|
159
|
+
* actorId: 'user-123',
|
|
160
|
+
* tables: ['tasks', 'notes'],
|
|
161
|
+
* scopes: ['user:{user_id}'],
|
|
162
|
+
* });
|
|
163
|
+
*
|
|
164
|
+
* // With custom handlers for advanced cases
|
|
165
|
+
* const { client } = await createClient({
|
|
166
|
+
* db,
|
|
167
|
+
* actorId: 'user-123',
|
|
168
|
+
* handlers: [
|
|
169
|
+
* tasksHandler, // subscribe: true by default
|
|
170
|
+
* notesHandler, // subscribe: true by default
|
|
171
|
+
* draftsHandler, // subscribe: false (local-only)
|
|
172
|
+
* ],
|
|
173
|
+
* });
|
|
174
|
+
*
|
|
175
|
+
* // Listen for events
|
|
176
|
+
* client.on('sync:error', (err) => console.error(err));
|
|
177
|
+
* client.on('data:change', (scopes) => console.log('Data changed:', scopes));
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export async function createClient<DB extends SyncClientDb>(
|
|
181
|
+
options: CreateClientOptions<DB>
|
|
182
|
+
): Promise<CreateClientResult<DB>> {
|
|
183
|
+
const {
|
|
184
|
+
db,
|
|
185
|
+
url = '/api/sync',
|
|
186
|
+
handlers: providedHandlers,
|
|
187
|
+
tables,
|
|
188
|
+
scopes,
|
|
189
|
+
actorId,
|
|
190
|
+
clientId = randomUUID(),
|
|
191
|
+
transport: customTransport,
|
|
192
|
+
getHeaders,
|
|
193
|
+
sync = {},
|
|
194
|
+
blobStorage,
|
|
195
|
+
plugins,
|
|
196
|
+
stateId,
|
|
197
|
+
autoStart = true,
|
|
198
|
+
} = options;
|
|
199
|
+
|
|
200
|
+
// Validate options
|
|
201
|
+
if (!providedHandlers && !tables) {
|
|
202
|
+
throw new Error('Either handlers or tables must be provided');
|
|
203
|
+
}
|
|
204
|
+
if (tables && !scopes) {
|
|
205
|
+
throw new Error('scopes is required when using tables option');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Auto-generate handlers from tables if needed
|
|
209
|
+
const handlers =
|
|
210
|
+
providedHandlers ??
|
|
211
|
+
tables!.map((table) =>
|
|
212
|
+
createAutoHandler<DB, keyof DB & string>(table, scopes!)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Build registry from handlers array
|
|
216
|
+
const tableHandlers = new ClientTableRegistry<DB>();
|
|
217
|
+
for (const handler of handlers) {
|
|
218
|
+
tableHandlers.register(handler);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create transport from URL if not provided
|
|
222
|
+
let transport = customTransport;
|
|
223
|
+
if (!transport && url) {
|
|
224
|
+
transport = createHttpTransport({
|
|
225
|
+
baseUrl: url,
|
|
226
|
+
getHeaders,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!transport) {
|
|
231
|
+
throw new Error('Either url or transport must be provided');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Build subscriptions from handlers
|
|
235
|
+
const subscriptions = handlers
|
|
236
|
+
.map((handler) => {
|
|
237
|
+
const sub = handler.subscribe;
|
|
238
|
+
// Skip handlers that are explicitly disabled
|
|
239
|
+
if (sub === false) return null;
|
|
240
|
+
|
|
241
|
+
if (sub === true || sub === undefined) {
|
|
242
|
+
// Default: subscribe to the handler's single scope var using actorId.
|
|
243
|
+
// This avoids sending `{}` which would be treated as revoked by the server.
|
|
244
|
+
const scopes = deriveDefaultSubscriptionScopes({
|
|
245
|
+
handler,
|
|
246
|
+
actorId,
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
id: handler.table,
|
|
250
|
+
shape: handler.table,
|
|
251
|
+
scopes,
|
|
252
|
+
params: {},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Custom subscription config
|
|
256
|
+
const scopes: ScopeValues = sub.scopes ?? {};
|
|
257
|
+
if (Object.keys(scopes).length === 0) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Handler "${handler.table}" subscription scopes cannot be empty. ` +
|
|
260
|
+
'Set subscribe: false or provide subscribe.scopes.'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
id: handler.table,
|
|
265
|
+
shape: handler.table,
|
|
266
|
+
scopes,
|
|
267
|
+
params: sub.params ?? {},
|
|
268
|
+
};
|
|
269
|
+
})
|
|
270
|
+
.filter((sub): sub is NonNullable<typeof sub> => sub !== null);
|
|
271
|
+
|
|
272
|
+
// Create client
|
|
273
|
+
const client = new Client({
|
|
274
|
+
db,
|
|
275
|
+
transport,
|
|
276
|
+
tableHandlers,
|
|
277
|
+
clientId,
|
|
278
|
+
actorId,
|
|
279
|
+
subscriptions,
|
|
280
|
+
blobStorage,
|
|
281
|
+
plugins,
|
|
282
|
+
stateId,
|
|
283
|
+
realtimeEnabled: sync.realtime ?? true,
|
|
284
|
+
pollIntervalMs: sync.pollIntervalMs,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Auto-start
|
|
288
|
+
if (autoStart) {
|
|
289
|
+
await client.start();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
client,
|
|
294
|
+
stop: () => client.stop(),
|
|
295
|
+
destroy: () => client.destroy(),
|
|
296
|
+
};
|
|
297
|
+
}
|