@syncular/client 0.0.3-3 → 0.0.3-7
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 +69 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +104 -0
- package/dist/client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +57 -1
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +707 -9
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +115 -2
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/subscription-state.d.ts +46 -0
- package/dist/subscription-state.d.ts.map +1 -0
- package/dist/subscription-state.js +185 -0
- package/dist/subscription-state.js.map +1 -0
- package/dist/utils/id.d.ts +9 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +27 -0
- package/dist/utils/id.js.map +1 -1
- package/package.json +3 -3
- package/src/client.test.ts +39 -0
- package/src/client.ts +158 -0
- package/src/engine/SyncEngine.test.ts +39 -0
- package/src/engine/SyncEngine.ts +906 -21
- package/src/engine/types.ts +150 -1
- package/src/index.ts +1 -0
- package/src/subscription-state.ts +259 -0
- package/src/utils/id.ts +35 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/client - Subscription state helpers
|
|
3
|
+
*
|
|
4
|
+
* Stable accessors for sync subscription metadata.
|
|
5
|
+
*/
|
|
6
|
+
import { isRecord } from '@syncular/core';
|
|
7
|
+
import { sql } from 'kysely';
|
|
8
|
+
export const DEFAULT_SYNC_STATE_ID = 'default';
|
|
9
|
+
function isScopeValues(value) {
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
return false;
|
|
12
|
+
for (const entry of Object.values(value)) {
|
|
13
|
+
if (typeof entry === 'string')
|
|
14
|
+
continue;
|
|
15
|
+
if (Array.isArray(entry) && entry.every((v) => typeof v === 'string')) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
export function parseBootstrapState(value) {
|
|
23
|
+
if (!value)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
27
|
+
if (!isRecord(parsed))
|
|
28
|
+
return null;
|
|
29
|
+
if (typeof parsed.asOfCommitSeq !== 'number')
|
|
30
|
+
return null;
|
|
31
|
+
if (!Array.isArray(parsed.tables))
|
|
32
|
+
return null;
|
|
33
|
+
if (!parsed.tables.every((table) => typeof table === 'string'))
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof parsed.tableIndex !== 'number')
|
|
36
|
+
return null;
|
|
37
|
+
if (parsed.rowCursor !== null && typeof parsed.rowCursor !== 'string') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
asOfCommitSeq: parsed.asOfCommitSeq,
|
|
42
|
+
tables: parsed.tables,
|
|
43
|
+
tableIndex: parsed.tableIndex,
|
|
44
|
+
rowCursor: parsed.rowCursor,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function parseScopes(value) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(value);
|
|
54
|
+
return isScopeValues(parsed) ? parsed : {};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function parseParams(value) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(value);
|
|
63
|
+
return isRecord(parsed) ? parsed : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function mapSubscriptionState(row) {
|
|
70
|
+
return {
|
|
71
|
+
stateId: row.state_id,
|
|
72
|
+
subscriptionId: row.subscription_id,
|
|
73
|
+
table: row.table,
|
|
74
|
+
scopes: parseScopes(row.scopes_json),
|
|
75
|
+
params: parseParams(row.params_json),
|
|
76
|
+
cursor: row.cursor,
|
|
77
|
+
bootstrapState: parseBootstrapState(row.bootstrap_state_json),
|
|
78
|
+
status: row.status,
|
|
79
|
+
createdAt: row.created_at,
|
|
80
|
+
updatedAt: row.updated_at,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function listSubscriptionStates(db, options = {}) {
|
|
84
|
+
const filters = [];
|
|
85
|
+
if (options.stateId) {
|
|
86
|
+
filters.push(sql `${sql.ref('state_id')} = ${sql.val(options.stateId)}`);
|
|
87
|
+
}
|
|
88
|
+
if (options.table) {
|
|
89
|
+
filters.push(sql `${sql.ref('table')} = ${sql.val(options.table)}`);
|
|
90
|
+
}
|
|
91
|
+
if (options.status) {
|
|
92
|
+
filters.push(sql `${sql.ref('status')} = ${sql.val(options.status)}`);
|
|
93
|
+
}
|
|
94
|
+
const whereClause = filters.length > 0 ? sql `where ${sql.join(filters, sql ` and `)}` : sql ``;
|
|
95
|
+
const rows = await sql `
|
|
96
|
+
select
|
|
97
|
+
${sql.ref('state_id')},
|
|
98
|
+
${sql.ref('subscription_id')},
|
|
99
|
+
${sql.ref('table')},
|
|
100
|
+
${sql.ref('scopes_json')},
|
|
101
|
+
${sql.ref('params_json')},
|
|
102
|
+
${sql.ref('cursor')},
|
|
103
|
+
${sql.ref('bootstrap_state_json')},
|
|
104
|
+
${sql.ref('status')},
|
|
105
|
+
${sql.ref('created_at')},
|
|
106
|
+
${sql.ref('updated_at')}
|
|
107
|
+
from ${sql.table('sync_subscription_state')}
|
|
108
|
+
${whereClause}
|
|
109
|
+
order by ${sql.ref('state_id')} asc, ${sql.ref('subscription_id')} asc
|
|
110
|
+
`.execute(db);
|
|
111
|
+
return rows.rows.map((row) => mapSubscriptionState(row));
|
|
112
|
+
}
|
|
113
|
+
export async function getSubscriptionState(db, options) {
|
|
114
|
+
const stateId = options.stateId ?? DEFAULT_SYNC_STATE_ID;
|
|
115
|
+
const rows = await sql `
|
|
116
|
+
select
|
|
117
|
+
${sql.ref('state_id')},
|
|
118
|
+
${sql.ref('subscription_id')},
|
|
119
|
+
${sql.ref('table')},
|
|
120
|
+
${sql.ref('scopes_json')},
|
|
121
|
+
${sql.ref('params_json')},
|
|
122
|
+
${sql.ref('cursor')},
|
|
123
|
+
${sql.ref('bootstrap_state_json')},
|
|
124
|
+
${sql.ref('status')},
|
|
125
|
+
${sql.ref('created_at')},
|
|
126
|
+
${sql.ref('updated_at')}
|
|
127
|
+
from ${sql.table('sync_subscription_state')}
|
|
128
|
+
where
|
|
129
|
+
${sql.ref('state_id')} = ${sql.val(stateId)}
|
|
130
|
+
and ${sql.ref('subscription_id')} = ${sql.val(options.subscriptionId)}
|
|
131
|
+
limit 1
|
|
132
|
+
`.execute(db);
|
|
133
|
+
const row = rows.rows[0];
|
|
134
|
+
return row ? mapSubscriptionState(row) : null;
|
|
135
|
+
}
|
|
136
|
+
export async function upsertSubscriptionState(db, input) {
|
|
137
|
+
const now = input.nowMs ?? Date.now();
|
|
138
|
+
const stateId = input.stateId ?? DEFAULT_SYNC_STATE_ID;
|
|
139
|
+
const bootstrapStateJson = input.bootstrapState === null || input.bootstrapState === undefined
|
|
140
|
+
? null
|
|
141
|
+
: JSON.stringify(input.bootstrapState);
|
|
142
|
+
await sql `
|
|
143
|
+
insert into ${sql.table('sync_subscription_state')} (
|
|
144
|
+
${sql.ref('state_id')},
|
|
145
|
+
${sql.ref('subscription_id')},
|
|
146
|
+
${sql.ref('table')},
|
|
147
|
+
${sql.ref('scopes_json')},
|
|
148
|
+
${sql.ref('params_json')},
|
|
149
|
+
${sql.ref('cursor')},
|
|
150
|
+
${sql.ref('bootstrap_state_json')},
|
|
151
|
+
${sql.ref('status')},
|
|
152
|
+
${sql.ref('created_at')},
|
|
153
|
+
${sql.ref('updated_at')}
|
|
154
|
+
) values (
|
|
155
|
+
${sql.val(stateId)},
|
|
156
|
+
${sql.val(input.subscriptionId)},
|
|
157
|
+
${sql.val(input.table)},
|
|
158
|
+
${sql.val(JSON.stringify(input.scopes ?? {}))},
|
|
159
|
+
${sql.val(JSON.stringify(input.params ?? {}))},
|
|
160
|
+
${sql.val(input.cursor)},
|
|
161
|
+
${sql.val(bootstrapStateJson)},
|
|
162
|
+
${sql.val(input.status ?? 'active')},
|
|
163
|
+
${sql.val(now)},
|
|
164
|
+
${sql.val(now)}
|
|
165
|
+
)
|
|
166
|
+
on conflict (${sql.join([sql.ref('state_id'), sql.ref('subscription_id')])})
|
|
167
|
+
do update set
|
|
168
|
+
${sql.ref('table')} = ${sql.val(input.table)},
|
|
169
|
+
${sql.ref('scopes_json')} = ${sql.val(JSON.stringify(input.scopes ?? {}))},
|
|
170
|
+
${sql.ref('params_json')} = ${sql.val(JSON.stringify(input.params ?? {}))},
|
|
171
|
+
${sql.ref('cursor')} = ${sql.val(input.cursor)},
|
|
172
|
+
${sql.ref('bootstrap_state_json')} = ${sql.val(bootstrapStateJson)},
|
|
173
|
+
${sql.ref('status')} = ${sql.val(input.status ?? 'active')},
|
|
174
|
+
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
175
|
+
`.execute(db);
|
|
176
|
+
const next = await getSubscriptionState(db, {
|
|
177
|
+
stateId,
|
|
178
|
+
subscriptionId: input.subscriptionId,
|
|
179
|
+
});
|
|
180
|
+
if (!next) {
|
|
181
|
+
throw new Error(`[subscription-state] Failed to load upserted state for "${input.subscriptionId}"`);
|
|
182
|
+
}
|
|
183
|
+
return next;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=subscription-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-state.js","sourceRoot":"","sources":["../src/subscription-state.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/id.d.ts
CHANGED
|
@@ -5,4 +5,13 @@
|
|
|
5
5
|
* Generate a random UUID v4
|
|
6
6
|
*/
|
|
7
7
|
export declare function randomUUID(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Build a stable state id from meaningful segments.
|
|
10
|
+
* Empty/undefined segments are ignored.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildStateId(...segments: Array<string | null | undefined>): string;
|
|
13
|
+
/**
|
|
14
|
+
* Create a deterministic fingerprint string for scope values.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createScopeFingerprint(scopes: Record<string, string | string[]>): string;
|
|
8
17
|
//# sourceMappingURL=id.d.ts.map
|
package/dist/utils/id.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAYnC"}
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAYnC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAC5C,MAAM,CAOR;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,MAAM,CAaR"}
|
package/dist/utils/id.js
CHANGED
|
@@ -16,4 +16,31 @@ export function randomUUID() {
|
|
|
16
16
|
return v.toString(16);
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a stable state id from meaningful segments.
|
|
21
|
+
* Empty/undefined segments are ignored.
|
|
22
|
+
*/
|
|
23
|
+
export function buildStateId(...segments) {
|
|
24
|
+
const normalized = segments
|
|
25
|
+
.map((segment) => segment?.trim())
|
|
26
|
+
.filter((segment) => !!segment && segment.length > 0);
|
|
27
|
+
if (normalized.length === 0)
|
|
28
|
+
return 'default';
|
|
29
|
+
return normalized.join(':');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a deterministic fingerprint string for scope values.
|
|
33
|
+
*/
|
|
34
|
+
export function createScopeFingerprint(scopes) {
|
|
35
|
+
const entries = Object.entries(scopes)
|
|
36
|
+
.map(([key, value]) => {
|
|
37
|
+
const encodedValues = (Array.isArray(value) ? [...value] : [value])
|
|
38
|
+
.map((item) => item.trim())
|
|
39
|
+
.filter((item) => item.length > 0)
|
|
40
|
+
.sort();
|
|
41
|
+
return `${key}:${encodedValues.join('|')}`;
|
|
42
|
+
})
|
|
43
|
+
.sort();
|
|
44
|
+
return entries.join(';');
|
|
45
|
+
}
|
|
19
46
|
//# sourceMappingURL=id.js.map
|
package/dist/utils/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,UAAU,UAAU,GAAW;IACnC,kEAAkE;IAClE,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACvD,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IAED,0BAA0B;IAC1B,OAAO,sCAAsC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACpE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QAC1C,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAAA,CACvB,CAAC,CAAC;AAAA,CACJ"}
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,UAAU,UAAU,GAAW;IACnC,kEAAkE;IAClE,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACvD,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IAED,0BAA0B;IAC1B,OAAO,sCAAsC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACpE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QAC1C,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAAA,CACvB,CAAC,CAAC;AAAA,CACJ;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAG,QAA0C,EACrC;IACR,MAAM,UAAU,GAAG,QAAQ;SACxB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;SACjC,MAAM,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE3E,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CAC7B;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAyC,EACjC;IACR,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;SACnC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;QACrB,MAAM,aAAa,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;aAChE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;aAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;aACjC,IAAI,EAAE,CAAC;QAEV,OAAO,GAAG,GAAG,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAAA,CAC5C,CAAC;SACD,IAAI,EAAE,CAAC;IAEV,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CAC1B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.0.3-
|
|
3
|
+
"version": "0.0.3-7",
|
|
4
4
|
"description": "Client-side sync engine with offline-first support, outbox, and conflict resolution",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"release": "bunx syncular-publish"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@syncular/core": "0.0.3-
|
|
50
|
-
"@syncular/transport-http": "0.0.3-
|
|
49
|
+
"@syncular/core": "0.0.3-7",
|
|
50
|
+
"@syncular/transport-http": "0.0.3-7"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"kysely": "*"
|
package/src/client.test.ts
CHANGED
|
@@ -367,3 +367,42 @@ describe('Client blob upload queue recovery', () => {
|
|
|
367
367
|
expect(row.error).toContain('Upload timed out while in uploading state');
|
|
368
368
|
});
|
|
369
369
|
});
|
|
370
|
+
|
|
371
|
+
describe('Client inspector snapshot', () => {
|
|
372
|
+
let db: Kysely<TestDb>;
|
|
373
|
+
let client: Client<TestDb>;
|
|
374
|
+
|
|
375
|
+
beforeEach(async () => {
|
|
376
|
+
db = createBunSqliteDb<TestDb>({ path: ':memory:' });
|
|
377
|
+
await ensureClientSyncSchema(db);
|
|
378
|
+
|
|
379
|
+
const handlers = new ClientTableRegistry<TestDb>();
|
|
380
|
+
client = new Client<TestDb>({
|
|
381
|
+
db,
|
|
382
|
+
transport: noopTransport,
|
|
383
|
+
tableHandlers: handlers,
|
|
384
|
+
clientId: 'client-inspector',
|
|
385
|
+
actorId: 'u1',
|
|
386
|
+
subscriptions: [],
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
afterEach(async () => {
|
|
391
|
+
client.destroy();
|
|
392
|
+
await db.destroy();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('returns a serializable inspector snapshot', async () => {
|
|
396
|
+
await client.start();
|
|
397
|
+
await client.sync();
|
|
398
|
+
|
|
399
|
+
const snapshot = await client.getInspectorSnapshot({ eventLimit: 20 });
|
|
400
|
+
|
|
401
|
+
expect(snapshot).not.toBeNull();
|
|
402
|
+
expect(snapshot?.version).toBe(1);
|
|
403
|
+
expect(snapshot?.generatedAt).toBeGreaterThan(0);
|
|
404
|
+
expect(Array.isArray(snapshot?.recentEvents)).toBe(true);
|
|
405
|
+
expect(snapshot?.recentEvents.length).toBeGreaterThan(0);
|
|
406
|
+
expect(snapshot?.diagnostics).toBeDefined();
|
|
407
|
+
});
|
|
408
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -23,8 +23,19 @@ import type {
|
|
|
23
23
|
ConflictInfo,
|
|
24
24
|
OutboxStats,
|
|
25
25
|
PresenceEntry,
|
|
26
|
+
SubscriptionProgress,
|
|
27
|
+
SyncAwaitBootstrapOptions,
|
|
28
|
+
SyncAwaitPhaseOptions,
|
|
29
|
+
SyncDiagnostics,
|
|
26
30
|
SyncEngineState,
|
|
31
|
+
SyncInspectorOptions,
|
|
32
|
+
SyncInspectorSnapshot,
|
|
33
|
+
SyncProgress,
|
|
34
|
+
SyncRepairOptions,
|
|
35
|
+
SyncResetOptions,
|
|
36
|
+
SyncResetResult,
|
|
27
37
|
SyncResult,
|
|
38
|
+
TransportHealth,
|
|
28
39
|
} from './engine/types';
|
|
29
40
|
import type { ClientTableRegistry } from './handlers/registry';
|
|
30
41
|
import { ensureClientSyncSchema } from './migrate';
|
|
@@ -35,6 +46,7 @@ import {
|
|
|
35
46
|
} from './mutations';
|
|
36
47
|
import type { SyncClientPlugin } from './plugins/types';
|
|
37
48
|
import type { SyncClientDb } from './schema';
|
|
49
|
+
import type { SubscriptionState } from './subscription-state';
|
|
38
50
|
|
|
39
51
|
// ============================================================================
|
|
40
52
|
// Types
|
|
@@ -216,7 +228,11 @@ export interface MigrationInfo {
|
|
|
216
228
|
type ClientEventType =
|
|
217
229
|
| 'sync:start'
|
|
218
230
|
| 'sync:complete'
|
|
231
|
+
| 'sync:live'
|
|
219
232
|
| 'sync:error'
|
|
233
|
+
| 'bootstrap:start'
|
|
234
|
+
| 'bootstrap:progress'
|
|
235
|
+
| 'bootstrap:complete'
|
|
220
236
|
| 'connection:change'
|
|
221
237
|
| 'data:change'
|
|
222
238
|
| 'outbox:change'
|
|
@@ -229,7 +245,25 @@ type ClientEventType =
|
|
|
229
245
|
type ClientEventPayloads = {
|
|
230
246
|
'sync:start': { timestamp: number };
|
|
231
247
|
'sync:complete': SyncResult;
|
|
248
|
+
'sync:live': { timestamp: number };
|
|
232
249
|
'sync:error': { code: string; message: string };
|
|
250
|
+
'bootstrap:start': {
|
|
251
|
+
timestamp: number;
|
|
252
|
+
stateId: string;
|
|
253
|
+
subscriptionId: string;
|
|
254
|
+
};
|
|
255
|
+
'bootstrap:progress': {
|
|
256
|
+
timestamp: number;
|
|
257
|
+
stateId: string;
|
|
258
|
+
subscriptionId: string;
|
|
259
|
+
progress: SubscriptionProgress;
|
|
260
|
+
};
|
|
261
|
+
'bootstrap:complete': {
|
|
262
|
+
timestamp: number;
|
|
263
|
+
stateId: string;
|
|
264
|
+
subscriptionId: string;
|
|
265
|
+
durationMs: number;
|
|
266
|
+
};
|
|
233
267
|
'connection:change': { previous: string; current: string };
|
|
234
268
|
'data:change': { scopes: string[]; timestamp: number };
|
|
235
269
|
'outbox:change': OutboxStats;
|
|
@@ -464,6 +498,29 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
464
498
|
}));
|
|
465
499
|
}
|
|
466
500
|
|
|
501
|
+
/**
|
|
502
|
+
* List persisted subscription metadata rows.
|
|
503
|
+
*/
|
|
504
|
+
async listSubscriptionStates(args?: {
|
|
505
|
+
stateId?: string;
|
|
506
|
+
table?: string;
|
|
507
|
+
status?: 'active' | 'revoked';
|
|
508
|
+
}): Promise<SubscriptionState[]> {
|
|
509
|
+
if (!this.engine) return [];
|
|
510
|
+
return this.engine.listSubscriptionStates(args);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Read one persisted subscription metadata row.
|
|
515
|
+
*/
|
|
516
|
+
async getSubscriptionState(
|
|
517
|
+
subscriptionId: string,
|
|
518
|
+
options?: { stateId?: string }
|
|
519
|
+
): Promise<SubscriptionState | null> {
|
|
520
|
+
if (!this.engine) return null;
|
|
521
|
+
return this.engine.getSubscriptionState(subscriptionId, options);
|
|
522
|
+
}
|
|
523
|
+
|
|
467
524
|
// ===========================================================================
|
|
468
525
|
// State
|
|
469
526
|
// ===========================================================================
|
|
@@ -488,6 +545,91 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
488
545
|
};
|
|
489
546
|
}
|
|
490
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Get current transport health details.
|
|
550
|
+
*/
|
|
551
|
+
getTransportHealth(): TransportHealth | null {
|
|
552
|
+
if (!this.engine) return null;
|
|
553
|
+
return this.engine.getTransportHealth();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get computed sync progress across subscriptions.
|
|
558
|
+
*/
|
|
559
|
+
async getProgress(): Promise<SyncProgress | null> {
|
|
560
|
+
if (!this.engine) return null;
|
|
561
|
+
return this.engine.getProgress();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get a diagnostics snapshot for support/debug flows.
|
|
566
|
+
*/
|
|
567
|
+
async getDiagnostics(): Promise<SyncDiagnostics | null> {
|
|
568
|
+
if (!this.engine) return null;
|
|
569
|
+
return this.engine.getDiagnostics();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get a serializable inspector snapshot for in-app debug tooling.
|
|
574
|
+
*/
|
|
575
|
+
async getInspectorSnapshot(
|
|
576
|
+
options?: SyncInspectorOptions
|
|
577
|
+
): Promise<SyncInspectorSnapshot | null> {
|
|
578
|
+
if (!this.engine) return null;
|
|
579
|
+
return this.engine.getInspectorSnapshot(options);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Reset local sync metadata (and optionally synced app rows/outbox/conflicts).
|
|
584
|
+
*/
|
|
585
|
+
async reset(options: SyncResetOptions): Promise<SyncResetResult> {
|
|
586
|
+
if (!this.engine) {
|
|
587
|
+
return {
|
|
588
|
+
deletedSubscriptionStates: 0,
|
|
589
|
+
deletedOutboxCommits: 0,
|
|
590
|
+
deletedConflicts: 0,
|
|
591
|
+
clearedTables: [],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return this.engine.reset(options);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Run a built-in repair flow for common corruption scenarios.
|
|
599
|
+
*/
|
|
600
|
+
async repair(options: SyncRepairOptions): Promise<SyncResetResult> {
|
|
601
|
+
if (!this.engine) {
|
|
602
|
+
return {
|
|
603
|
+
deletedSubscriptionStates: 0,
|
|
604
|
+
deletedOutboxCommits: 0,
|
|
605
|
+
deletedConflicts: 0,
|
|
606
|
+
clearedTables: [],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return this.engine.repair(options);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Wait until the channel reaches a target phase.
|
|
614
|
+
*/
|
|
615
|
+
async awaitPhase(
|
|
616
|
+
phase: SyncProgress['channelPhase'],
|
|
617
|
+
options: SyncAwaitPhaseOptions = {}
|
|
618
|
+
): Promise<SyncProgress | null> {
|
|
619
|
+
if (!this.engine) return null;
|
|
620
|
+
return this.engine.awaitPhase(phase, options);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Wait until bootstrap completes for the default state or a specific subscription.
|
|
625
|
+
*/
|
|
626
|
+
async awaitBootstrapComplete(
|
|
627
|
+
options: SyncAwaitBootstrapOptions = {}
|
|
628
|
+
): Promise<SyncProgress | null> {
|
|
629
|
+
if (!this.engine) return null;
|
|
630
|
+
return this.engine.awaitBootstrapComplete(options);
|
|
631
|
+
}
|
|
632
|
+
|
|
491
633
|
/**
|
|
492
634
|
* Subscribe to state changes (for useSyncExternalStore).
|
|
493
635
|
*/
|
|
@@ -783,6 +925,10 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
783
925
|
});
|
|
784
926
|
});
|
|
785
927
|
|
|
928
|
+
this.engine.on('sync:live', (payload) => {
|
|
929
|
+
this.emit('sync:live', payload);
|
|
930
|
+
});
|
|
931
|
+
|
|
786
932
|
this.engine.on('sync:error', (error) => {
|
|
787
933
|
this.emit('sync:error', { code: error.code, message: error.message });
|
|
788
934
|
|
|
@@ -790,6 +936,18 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
|
790
936
|
this.checkForNewConflicts();
|
|
791
937
|
});
|
|
792
938
|
|
|
939
|
+
this.engine.on('bootstrap:start', (payload) => {
|
|
940
|
+
this.emit('bootstrap:start', payload);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
this.engine.on('bootstrap:progress', (payload) => {
|
|
944
|
+
this.emit('bootstrap:progress', payload);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
this.engine.on('bootstrap:complete', (payload) => {
|
|
948
|
+
this.emit('bootstrap:complete', payload);
|
|
949
|
+
});
|
|
950
|
+
|
|
793
951
|
this.engine.on('connection:change', (payload) => {
|
|
794
952
|
this.emit('connection:change', payload);
|
|
795
953
|
});
|
|
@@ -154,4 +154,43 @@ describe('SyncEngine WS inline apply', () => {
|
|
|
154
154
|
.executeTakeFirstOrThrow();
|
|
155
155
|
expect(state.cursor).toBe(0);
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
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
|
+
});
|
|
165
|
+
|
|
166
|
+
const engine = new SyncEngine<TestDb>({
|
|
167
|
+
db,
|
|
168
|
+
transport: noopTransport,
|
|
169
|
+
handlers,
|
|
170
|
+
actorId: 'u1',
|
|
171
|
+
clientId: 'client-inspector',
|
|
172
|
+
subscriptions: [],
|
|
173
|
+
stateId: 'default',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await engine.start();
|
|
177
|
+
await engine.sync();
|
|
178
|
+
|
|
179
|
+
const snapshot = await engine.getInspectorSnapshot({ eventLimit: 5 });
|
|
180
|
+
|
|
181
|
+
expect(snapshot.version).toBe(1);
|
|
182
|
+
expect(snapshot.generatedAt).toBeGreaterThan(0);
|
|
183
|
+
expect(snapshot.recentEvents.length).toBeLessThanOrEqual(5);
|
|
184
|
+
expect(snapshot.recentEvents.length).toBeGreaterThan(0);
|
|
185
|
+
|
|
186
|
+
const first = snapshot.recentEvents[0];
|
|
187
|
+
if (!first) {
|
|
188
|
+
throw new Error('Expected at least one inspector event');
|
|
189
|
+
}
|
|
190
|
+
expect(typeof first.id).toBe('number');
|
|
191
|
+
expect(typeof first.event).toBe('string');
|
|
192
|
+
expect(typeof first.timestamp).toBe('number');
|
|
193
|
+
expect(typeof first.payload).toBe('object');
|
|
194
|
+
expect(snapshot.diagnostics).toBeDefined();
|
|
195
|
+
});
|
|
157
196
|
});
|