@syncular/client 0.0.2-2 → 0.0.3-14

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.
@@ -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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"}
@@ -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
@@ -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
@@ -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.2-2",
3
+ "version": "0.0.3-14",
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.2-2",
50
- "@syncular/transport-http": "0.0.2-2"
49
+ "@syncular/core": "0.0.3-14",
50
+ "@syncular/transport-http": "0.0.3-14"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "kysely": "*"
@@ -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
  });