@syncular/client 0.0.2-2 → 0.0.3-6

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-6",
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-6",
50
+ "@syncular/transport-http": "0.0.3-6"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "kysely": "*"
package/src/client.ts CHANGED
@@ -23,8 +23,17 @@ import type {
23
23
  ConflictInfo,
24
24
  OutboxStats,
25
25
  PresenceEntry,
26
+ SubscriptionProgress,
27
+ SyncAwaitBootstrapOptions,
28
+ SyncAwaitPhaseOptions,
29
+ SyncDiagnostics,
26
30
  SyncEngineState,
31
+ SyncProgress,
32
+ SyncRepairOptions,
33
+ SyncResetOptions,
34
+ SyncResetResult,
27
35
  SyncResult,
36
+ TransportHealth,
28
37
  } from './engine/types';
29
38
  import type { ClientTableRegistry } from './handlers/registry';
30
39
  import { ensureClientSyncSchema } from './migrate';
@@ -35,6 +44,7 @@ import {
35
44
  } from './mutations';
36
45
  import type { SyncClientPlugin } from './plugins/types';
37
46
  import type { SyncClientDb } from './schema';
47
+ import type { SubscriptionState } from './subscription-state';
38
48
 
39
49
  // ============================================================================
40
50
  // Types
@@ -216,7 +226,11 @@ export interface MigrationInfo {
216
226
  type ClientEventType =
217
227
  | 'sync:start'
218
228
  | 'sync:complete'
229
+ | 'sync:live'
219
230
  | 'sync:error'
231
+ | 'bootstrap:start'
232
+ | 'bootstrap:progress'
233
+ | 'bootstrap:complete'
220
234
  | 'connection:change'
221
235
  | 'data:change'
222
236
  | 'outbox:change'
@@ -229,7 +243,25 @@ type ClientEventType =
229
243
  type ClientEventPayloads = {
230
244
  'sync:start': { timestamp: number };
231
245
  'sync:complete': SyncResult;
246
+ 'sync:live': { timestamp: number };
232
247
  'sync:error': { code: string; message: string };
248
+ 'bootstrap:start': {
249
+ timestamp: number;
250
+ stateId: string;
251
+ subscriptionId: string;
252
+ };
253
+ 'bootstrap:progress': {
254
+ timestamp: number;
255
+ stateId: string;
256
+ subscriptionId: string;
257
+ progress: SubscriptionProgress;
258
+ };
259
+ 'bootstrap:complete': {
260
+ timestamp: number;
261
+ stateId: string;
262
+ subscriptionId: string;
263
+ durationMs: number;
264
+ };
233
265
  'connection:change': { previous: string; current: string };
234
266
  'data:change': { scopes: string[]; timestamp: number };
235
267
  'outbox:change': OutboxStats;
@@ -464,6 +496,29 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
464
496
  }));
465
497
  }
466
498
 
499
+ /**
500
+ * List persisted subscription metadata rows.
501
+ */
502
+ async listSubscriptionStates(args?: {
503
+ stateId?: string;
504
+ table?: string;
505
+ status?: 'active' | 'revoked';
506
+ }): Promise<SubscriptionState[]> {
507
+ if (!this.engine) return [];
508
+ return this.engine.listSubscriptionStates(args);
509
+ }
510
+
511
+ /**
512
+ * Read one persisted subscription metadata row.
513
+ */
514
+ async getSubscriptionState(
515
+ subscriptionId: string,
516
+ options?: { stateId?: string }
517
+ ): Promise<SubscriptionState | null> {
518
+ if (!this.engine) return null;
519
+ return this.engine.getSubscriptionState(subscriptionId, options);
520
+ }
521
+
467
522
  // ===========================================================================
468
523
  // State
469
524
  // ===========================================================================
@@ -488,6 +543,81 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
488
543
  };
489
544
  }
490
545
 
546
+ /**
547
+ * Get current transport health details.
548
+ */
549
+ getTransportHealth(): TransportHealth | null {
550
+ if (!this.engine) return null;
551
+ return this.engine.getTransportHealth();
552
+ }
553
+
554
+ /**
555
+ * Get computed sync progress across subscriptions.
556
+ */
557
+ async getProgress(): Promise<SyncProgress | null> {
558
+ if (!this.engine) return null;
559
+ return this.engine.getProgress();
560
+ }
561
+
562
+ /**
563
+ * Get a diagnostics snapshot for support/debug flows.
564
+ */
565
+ async getDiagnostics(): Promise<SyncDiagnostics | null> {
566
+ if (!this.engine) return null;
567
+ return this.engine.getDiagnostics();
568
+ }
569
+
570
+ /**
571
+ * Reset local sync metadata (and optionally synced app rows/outbox/conflicts).
572
+ */
573
+ async reset(options: SyncResetOptions): Promise<SyncResetResult> {
574
+ if (!this.engine) {
575
+ return {
576
+ deletedSubscriptionStates: 0,
577
+ deletedOutboxCommits: 0,
578
+ deletedConflicts: 0,
579
+ clearedTables: [],
580
+ };
581
+ }
582
+ return this.engine.reset(options);
583
+ }
584
+
585
+ /**
586
+ * Run a built-in repair flow for common corruption scenarios.
587
+ */
588
+ async repair(options: SyncRepairOptions): Promise<SyncResetResult> {
589
+ if (!this.engine) {
590
+ return {
591
+ deletedSubscriptionStates: 0,
592
+ deletedOutboxCommits: 0,
593
+ deletedConflicts: 0,
594
+ clearedTables: [],
595
+ };
596
+ }
597
+ return this.engine.repair(options);
598
+ }
599
+
600
+ /**
601
+ * Wait until the channel reaches a target phase.
602
+ */
603
+ async awaitPhase(
604
+ phase: SyncProgress['channelPhase'],
605
+ options: SyncAwaitPhaseOptions = {}
606
+ ): Promise<SyncProgress | null> {
607
+ if (!this.engine) return null;
608
+ return this.engine.awaitPhase(phase, options);
609
+ }
610
+
611
+ /**
612
+ * Wait until bootstrap completes for the default state or a specific subscription.
613
+ */
614
+ async awaitBootstrapComplete(
615
+ options: SyncAwaitBootstrapOptions = {}
616
+ ): Promise<SyncProgress | null> {
617
+ if (!this.engine) return null;
618
+ return this.engine.awaitBootstrapComplete(options);
619
+ }
620
+
491
621
  /**
492
622
  * Subscribe to state changes (for useSyncExternalStore).
493
623
  */
@@ -783,6 +913,10 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
783
913
  });
784
914
  });
785
915
 
916
+ this.engine.on('sync:live', (payload) => {
917
+ this.emit('sync:live', payload);
918
+ });
919
+
786
920
  this.engine.on('sync:error', (error) => {
787
921
  this.emit('sync:error', { code: error.code, message: error.message });
788
922
 
@@ -790,6 +924,18 @@ export class Client<DB extends SyncClientDb = SyncClientDb> {
790
924
  this.checkForNewConflicts();
791
925
  });
792
926
 
927
+ this.engine.on('bootstrap:start', (payload) => {
928
+ this.emit('bootstrap:start', payload);
929
+ });
930
+
931
+ this.engine.on('bootstrap:progress', (payload) => {
932
+ this.emit('bootstrap:progress', payload);
933
+ });
934
+
935
+ this.engine.on('bootstrap:complete', (payload) => {
936
+ this.emit('bootstrap:complete', payload);
937
+ });
938
+
793
939
  this.engine.on('connection:change', (payload) => {
794
940
  this.emit('connection:change', payload);
795
941
  });