@syncular/relay 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.
Files changed (68) hide show
  1. package/dist/client-role/forward-engine.d.ts +63 -0
  2. package/dist/client-role/forward-engine.d.ts.map +1 -0
  3. package/dist/client-role/forward-engine.js +263 -0
  4. package/dist/client-role/forward-engine.js.map +1 -0
  5. package/dist/client-role/index.d.ts +9 -0
  6. package/dist/client-role/index.d.ts.map +1 -0
  7. package/dist/client-role/index.js +9 -0
  8. package/dist/client-role/index.js.map +1 -0
  9. package/dist/client-role/pull-engine.d.ts +70 -0
  10. package/dist/client-role/pull-engine.d.ts.map +1 -0
  11. package/dist/client-role/pull-engine.js +233 -0
  12. package/dist/client-role/pull-engine.js.map +1 -0
  13. package/dist/client-role/sequence-mapper.d.ts +65 -0
  14. package/dist/client-role/sequence-mapper.d.ts.map +1 -0
  15. package/dist/client-role/sequence-mapper.js +161 -0
  16. package/dist/client-role/sequence-mapper.js.map +1 -0
  17. package/dist/index.d.ts +37 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +44 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/migrate.d.ts +18 -0
  22. package/dist/migrate.d.ts.map +1 -0
  23. package/dist/migrate.js +99 -0
  24. package/dist/migrate.js.map +1 -0
  25. package/dist/mode-manager.d.ts +60 -0
  26. package/dist/mode-manager.d.ts.map +1 -0
  27. package/dist/mode-manager.js +114 -0
  28. package/dist/mode-manager.js.map +1 -0
  29. package/dist/realtime.d.ts +102 -0
  30. package/dist/realtime.d.ts.map +1 -0
  31. package/dist/realtime.js +305 -0
  32. package/dist/realtime.js.map +1 -0
  33. package/dist/relay.d.ts +188 -0
  34. package/dist/relay.d.ts.map +1 -0
  35. package/dist/relay.js +315 -0
  36. package/dist/relay.js.map +1 -0
  37. package/dist/schema.d.ts +158 -0
  38. package/dist/schema.d.ts.map +1 -0
  39. package/dist/schema.js +7 -0
  40. package/dist/schema.js.map +1 -0
  41. package/dist/server-role/index.d.ts +54 -0
  42. package/dist/server-role/index.d.ts.map +1 -0
  43. package/dist/server-role/index.js +198 -0
  44. package/dist/server-role/index.js.map +1 -0
  45. package/dist/server-role/pull.d.ts +25 -0
  46. package/dist/server-role/pull.d.ts.map +1 -0
  47. package/dist/server-role/pull.js +24 -0
  48. package/dist/server-role/pull.js.map +1 -0
  49. package/dist/server-role/push.d.ts +27 -0
  50. package/dist/server-role/push.d.ts.map +1 -0
  51. package/dist/server-role/push.js +98 -0
  52. package/dist/server-role/push.js.map +1 -0
  53. package/package.json +61 -0
  54. package/src/__tests__/relay.test.ts +464 -0
  55. package/src/bun-types.d.ts +50 -0
  56. package/src/client-role/forward-engine.ts +352 -0
  57. package/src/client-role/index.ts +9 -0
  58. package/src/client-role/pull-engine.ts +301 -0
  59. package/src/client-role/sequence-mapper.ts +201 -0
  60. package/src/index.ts +50 -0
  61. package/src/migrate.ts +113 -0
  62. package/src/mode-manager.ts +142 -0
  63. package/src/realtime.ts +370 -0
  64. package/src/relay.ts +421 -0
  65. package/src/schema.ts +171 -0
  66. package/src/server-role/index.ts +342 -0
  67. package/src/server-role/pull.ts +37 -0
  68. package/src/server-role/push.ts +130 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @syncular/relay - Forward Engine
3
+ *
4
+ * Forwards commits from the relay's local outbox to the main server.
5
+ * Preserves original client_id + client_commit_id for idempotency.
6
+ */
7
+ import type { SyncTransport } from '@syncular/core';
8
+ import type { Kysely } from 'kysely';
9
+ import type { ForwardConflictEntry, RelayDatabase } from '../schema';
10
+ import type { SequenceMapper } from './sequence-mapper';
11
+ /**
12
+ * Forward engine options.
13
+ */
14
+ export interface ForwardEngineOptions<DB extends RelayDatabase = RelayDatabase> {
15
+ db: Kysely<DB>;
16
+ transport: SyncTransport;
17
+ clientId: string;
18
+ sequenceMapper: SequenceMapper<DB>;
19
+ retryIntervalMs?: number;
20
+ onConflict?: (conflict: ForwardConflictEntry) => void;
21
+ onError?: (error: Error) => void;
22
+ }
23
+ /**
24
+ * Forward engine for sending local commits to the main server.
25
+ */
26
+ export declare class ForwardEngine<DB extends RelayDatabase = RelayDatabase> {
27
+ private readonly db;
28
+ private readonly transport;
29
+ private readonly clientId;
30
+ private readonly sequenceMapper;
31
+ private readonly retryIntervalMs;
32
+ private readonly onConflict?;
33
+ private readonly onError?;
34
+ private running;
35
+ private timer;
36
+ private wakeUpRequested;
37
+ constructor(options: ForwardEngineOptions<DB>);
38
+ /**
39
+ * Start the forward engine loop.
40
+ */
41
+ start(): void;
42
+ /**
43
+ * Stop the forward engine.
44
+ */
45
+ stop(): void;
46
+ /**
47
+ * Wake up the engine to process immediately.
48
+ */
49
+ wakeUp(): void;
50
+ /**
51
+ * Forward a single commit (for manual/testing use).
52
+ */
53
+ forwardOnce(): Promise<boolean>;
54
+ private scheduleNext;
55
+ private processOne;
56
+ private getNextSendable;
57
+ private markSending;
58
+ private markPending;
59
+ private markForwarded;
60
+ private markFailed;
61
+ private recordConflict;
62
+ }
63
+ //# sourceMappingURL=forward-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forward-engine.d.ts","sourceRoot":"","sources":["../../src/client-role/forward-engine.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAGV,aAAa,EACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EACV,oBAAoB,EAEpB,aAAa,EAEd,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAYxD;;GAEG;AACH,MAAM,WAAW,oBAAoB,CACnC,EAAE,SAAS,aAAa,GAAG,aAAa;IAExC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,qBAAa,aAAa,CAAC,EAAE,SAAS,aAAa,GAAG,aAAa;IACjE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAA2C;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAyB;IAElD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA8C;IAE3D,OAAO,CAAC,eAAe,CAAS;IAEhC,YAAY,OAAO,EAAE,oBAAoB,CAAC,EAAE,CAAC,EAQ5C;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAIZ;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,CAMX;IAED;;OAEG;IACH,MAAM,IAAI,IAAI,CAQb;IAED;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAEpC;IAED,OAAO,CAAC,YAAY;YAoBN,UAAU;YAsDV,eAAe;YAgEf,WAAW;YAcX,WAAW;YAUX,aAAa;YAmBb,UAAU;YAkBV,cAAc;CAsC7B"}
@@ -0,0 +1,263 @@
1
+ /**
2
+ * @syncular/relay - Forward Engine
3
+ *
4
+ * Forwards commits from the relay's local outbox to the main server.
5
+ * Preserves original client_id + client_commit_id for idempotency.
6
+ */
7
+ import { sql } from 'kysely';
8
+ function randomId() {
9
+ if (typeof crypto !== 'undefined' &&
10
+ typeof crypto.randomUUID === 'function') {
11
+ return crypto.randomUUID();
12
+ }
13
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
14
+ }
15
+ /**
16
+ * Forward engine for sending local commits to the main server.
17
+ */
18
+ export class ForwardEngine {
19
+ db;
20
+ transport;
21
+ clientId;
22
+ sequenceMapper;
23
+ retryIntervalMs;
24
+ onConflict;
25
+ onError;
26
+ running = false;
27
+ timer = null;
28
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: assigned in scheduleWakeUp and read in tick
29
+ wakeUpRequested = false;
30
+ constructor(options) {
31
+ this.db = options.db;
32
+ this.transport = options.transport;
33
+ this.clientId = options.clientId;
34
+ this.sequenceMapper = options.sequenceMapper;
35
+ this.retryIntervalMs = options.retryIntervalMs ?? 5000;
36
+ this.onConflict = options.onConflict;
37
+ this.onError = options.onError;
38
+ }
39
+ /**
40
+ * Start the forward engine loop.
41
+ */
42
+ start() {
43
+ if (this.running)
44
+ return;
45
+ this.running = true;
46
+ this.scheduleNext(0);
47
+ }
48
+ /**
49
+ * Stop the forward engine.
50
+ */
51
+ stop() {
52
+ this.running = false;
53
+ if (this.timer) {
54
+ clearTimeout(this.timer);
55
+ this.timer = null;
56
+ }
57
+ }
58
+ /**
59
+ * Wake up the engine to process immediately.
60
+ */
61
+ wakeUp() {
62
+ if (!this.running)
63
+ return;
64
+ this.wakeUpRequested = true;
65
+ if (this.timer) {
66
+ clearTimeout(this.timer);
67
+ this.timer = null;
68
+ }
69
+ this.scheduleNext(0);
70
+ }
71
+ /**
72
+ * Forward a single commit (for manual/testing use).
73
+ */
74
+ async forwardOnce() {
75
+ return this.processOne();
76
+ }
77
+ scheduleNext(delayMs) {
78
+ if (!this.running)
79
+ return;
80
+ if (this.timer)
81
+ return;
82
+ this.timer = setTimeout(async () => {
83
+ this.timer = null;
84
+ this.wakeUpRequested = false;
85
+ try {
86
+ const forwarded = await this.processOne();
87
+ // If we forwarded something, immediately try again
88
+ const nextDelay = forwarded ? 0 : this.retryIntervalMs;
89
+ this.scheduleNext(nextDelay);
90
+ }
91
+ catch (err) {
92
+ this.onError?.(err instanceof Error ? err : new Error(String(err)));
93
+ this.scheduleNext(this.retryIntervalMs);
94
+ }
95
+ }, delayMs);
96
+ }
97
+ async processOne() {
98
+ const next = await this.getNextSendable();
99
+ if (!next)
100
+ return false;
101
+ await this.markSending(next.id);
102
+ let response;
103
+ try {
104
+ const combined = await this.transport.sync({
105
+ clientId: next.client_id,
106
+ push: {
107
+ clientCommitId: next.client_commit_id,
108
+ operations: next.operations,
109
+ schemaVersion: next.schema_version,
110
+ },
111
+ });
112
+ if (!combined.push) {
113
+ throw new Error('Server returned no push response');
114
+ }
115
+ response = combined.push;
116
+ }
117
+ catch (err) {
118
+ // Network error - mark as pending for retry
119
+ await this.markPending(next.id, String(err));
120
+ throw err;
121
+ }
122
+ const responseJson = JSON.stringify(response);
123
+ if (response.status === 'applied' || response.status === 'cached') {
124
+ const mainCommitSeq = response.commitSeq ?? null;
125
+ // Update outbox entry
126
+ await this.markForwarded(next.id, mainCommitSeq, responseJson);
127
+ // Update sequence mapper
128
+ if (mainCommitSeq != null) {
129
+ await this.sequenceMapper.markForwarded(next.local_commit_seq, mainCommitSeq);
130
+ }
131
+ return true;
132
+ }
133
+ // Rejected - store conflict and mark as failed
134
+ const conflict = await this.recordConflict(next, responseJson);
135
+ await this.markFailed(next.id, 'REJECTED', responseJson);
136
+ this.onConflict?.(conflict);
137
+ return true;
138
+ }
139
+ async getNextSendable() {
140
+ const staleThreshold = Date.now() - 30000;
141
+ const rowResult = await sql `
142
+ select
143
+ id,
144
+ local_commit_seq,
145
+ client_id,
146
+ client_commit_id,
147
+ operations_json,
148
+ schema_version,
149
+ status,
150
+ main_commit_seq,
151
+ error,
152
+ last_response_json,
153
+ created_at,
154
+ updated_at,
155
+ attempt_count
156
+ from ${sql.table('relay_forward_outbox')}
157
+ where
158
+ status = 'pending'
159
+ or (status = 'forwarding' and updated_at < ${staleThreshold})
160
+ order by created_at asc
161
+ limit 1
162
+ `.execute(this.db);
163
+ const row = rowResult.rows[0];
164
+ if (!row)
165
+ return null;
166
+ const operations = typeof row.operations_json === 'string'
167
+ ? JSON.parse(row.operations_json)
168
+ : row.operations_json;
169
+ return {
170
+ id: row.id,
171
+ local_commit_seq: row.local_commit_seq,
172
+ client_id: row.client_id,
173
+ client_commit_id: row.client_commit_id,
174
+ operations,
175
+ schema_version: row.schema_version,
176
+ status: row.status,
177
+ main_commit_seq: row.main_commit_seq,
178
+ error: row.error,
179
+ created_at: row.created_at,
180
+ updated_at: row.updated_at,
181
+ attempt_count: row.attempt_count,
182
+ };
183
+ }
184
+ async markSending(id) {
185
+ const now = Date.now();
186
+ await sql `
187
+ update ${sql.table('relay_forward_outbox')}
188
+ set
189
+ status = 'forwarding',
190
+ updated_at = ${now},
191
+ attempt_count = attempt_count + 1,
192
+ error = ${null}
193
+ where id = ${id}
194
+ `.execute(this.db);
195
+ }
196
+ async markPending(id, error) {
197
+ const now = Date.now();
198
+ await sql `
199
+ update ${sql.table('relay_forward_outbox')}
200
+ set status = 'pending', updated_at = ${now}, error = ${error}
201
+ where id = ${id}
202
+ `.execute(this.db);
203
+ }
204
+ async markForwarded(id, mainCommitSeq, responseJson) {
205
+ const now = Date.now();
206
+ await sql `
207
+ update ${sql.table('relay_forward_outbox')}
208
+ set
209
+ status = 'forwarded',
210
+ main_commit_seq = ${mainCommitSeq},
211
+ updated_at = ${now},
212
+ error = ${null},
213
+ last_response_json = ${responseJson}
214
+ where id = ${id}
215
+ `.execute(this.db);
216
+ }
217
+ async markFailed(id, error, responseJson) {
218
+ const now = Date.now();
219
+ await sql `
220
+ update ${sql.table('relay_forward_outbox')}
221
+ set
222
+ status = 'failed',
223
+ updated_at = ${now},
224
+ error = ${error},
225
+ last_response_json = ${responseJson}
226
+ where id = ${id}
227
+ `.execute(this.db);
228
+ }
229
+ async recordConflict(entry, responseJson) {
230
+ const now = Date.now();
231
+ const id = randomId();
232
+ await sql `
233
+ insert into ${sql.table('relay_forward_conflicts')} (
234
+ id,
235
+ local_commit_seq,
236
+ client_id,
237
+ client_commit_id,
238
+ response_json,
239
+ created_at,
240
+ resolved_at
241
+ )
242
+ values (
243
+ ${id},
244
+ ${entry.local_commit_seq},
245
+ ${entry.client_id},
246
+ ${entry.client_commit_id},
247
+ ${responseJson},
248
+ ${now},
249
+ ${null}
250
+ )
251
+ `.execute(this.db);
252
+ return {
253
+ id,
254
+ local_commit_seq: entry.local_commit_seq,
255
+ client_id: entry.client_id,
256
+ client_commit_id: entry.client_commit_id,
257
+ response: JSON.parse(responseJson),
258
+ created_at: now,
259
+ resolved_at: null,
260
+ };
261
+ }
262
+ }
263
+ //# sourceMappingURL=forward-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forward-engine.js","sourceRoot":"","sources":["../../src/client-role/forward-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @syncular/relay - Client Role
3
+ *
4
+ * Components for the relay's role as a client to the main server.
5
+ */
6
+ export * from './forward-engine';
7
+ export * from './pull-engine';
8
+ export * from './sequence-mapper';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client-role/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @syncular/relay - Client Role
3
+ *
4
+ * Components for the relay's role as a client to the main server.
5
+ */
6
+ export * from './forward-engine';
7
+ export * from './pull-engine';
8
+ export * from './sequence-mapper';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client-role/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @syncular/relay - Pull Engine
3
+ *
4
+ * Pulls changes from the main server and stores them locally
5
+ * on the relay for local clients to access.
6
+ */
7
+ import type { ScopeValues, SyncTransport } from '@syncular/core';
8
+ import type { ServerSyncDialect, TableRegistry } from '@syncular/server';
9
+ import { type Kysely } from 'kysely';
10
+ import type { RelayRealtime } from '../realtime';
11
+ import type { RelayDatabase } from '../schema';
12
+ import type { SequenceMapper } from './sequence-mapper';
13
+ /**
14
+ * Pull engine options.
15
+ */
16
+ export interface PullEngineOptions<DB extends RelayDatabase = RelayDatabase> {
17
+ db: Kysely<DB>;
18
+ dialect: ServerSyncDialect;
19
+ transport: SyncTransport;
20
+ clientId: string;
21
+ /** Tables to subscribe to */
22
+ tables: string[];
23
+ /** Scope values for subscriptions */
24
+ scopes: ScopeValues;
25
+ shapes: TableRegistry<DB>;
26
+ sequenceMapper: SequenceMapper<DB>;
27
+ realtime: RelayRealtime;
28
+ intervalMs?: number;
29
+ onError?: (error: Error) => void;
30
+ onPullComplete?: () => Promise<void>;
31
+ }
32
+ /**
33
+ * Pull engine for receiving changes from the main server.
34
+ */
35
+ export declare class PullEngine<DB extends RelayDatabase = RelayDatabase> {
36
+ private readonly db;
37
+ private readonly dialect;
38
+ private readonly transport;
39
+ private readonly clientId;
40
+ private readonly tables;
41
+ private readonly scopes;
42
+ private readonly shapes;
43
+ private readonly sequenceMapper;
44
+ private readonly realtime;
45
+ private readonly intervalMs;
46
+ private readonly onError?;
47
+ private readonly onPullComplete?;
48
+ private running;
49
+ private timer;
50
+ private cursors;
51
+ constructor(options: PullEngineOptions<DB>);
52
+ /**
53
+ * Start the pull engine loop.
54
+ */
55
+ start(): void;
56
+ /**
57
+ * Stop the pull engine.
58
+ */
59
+ stop(): void;
60
+ /**
61
+ * Pull once (for manual/testing use).
62
+ */
63
+ pullOnce(): Promise<boolean>;
64
+ private loadCursors;
65
+ private saveCursors;
66
+ private scheduleNext;
67
+ private processOne;
68
+ private applyCommitLocally;
69
+ }
70
+ //# sourceMappingURL=pull-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull-engine.d.ts","sourceRoot":"","sources":["../../src/client-role/pull-engine.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,WAAW,EAIX,aAAa,EACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEzE,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,EAAE,SAAS,aAAa,GAAG,aAAa;IACzE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,EAAE,iBAAiB,CAAC;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,qCAAqC;IACrC,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;IAC1B,cAAc,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;IACnC,QAAQ,EAAE,aAAa,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAED;;GAEG;AACH,qBAAa,UAAU,CAAC,EAAE,SAAS,aAAa,GAAG,aAAa;IAC9D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAyB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAsB;IAEtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAA6B;IAE5C,YAAY,OAAO,EAAE,iBAAiB,CAAC,EAAE,CAAC,EAazC;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAMZ;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,CAMX;IAED;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,CAEjC;YAEa,WAAW;YA0BX,WAAW;IAezB,OAAO,CAAC,YAAY;YAmBN,UAAU;YA6EV,kBAAkB;CAyDjC"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * @syncular/relay - Pull Engine
3
+ *
4
+ * Pulls changes from the main server and stores them locally
5
+ * on the relay for local clients to access.
6
+ */
7
+ import { pushCommit } from '@syncular/server';
8
+ import { sql } from 'kysely';
9
+ /**
10
+ * Pull engine for receiving changes from the main server.
11
+ */
12
+ export class PullEngine {
13
+ db;
14
+ dialect;
15
+ transport;
16
+ clientId;
17
+ tables;
18
+ scopes;
19
+ shapes;
20
+ sequenceMapper;
21
+ realtime;
22
+ intervalMs;
23
+ onError;
24
+ onPullComplete;
25
+ running = false;
26
+ timer = null;
27
+ cursors = new Map();
28
+ constructor(options) {
29
+ this.db = options.db;
30
+ this.dialect = options.dialect;
31
+ this.transport = options.transport;
32
+ this.clientId = options.clientId;
33
+ this.tables = options.tables;
34
+ this.scopes = options.scopes;
35
+ this.shapes = options.shapes;
36
+ this.sequenceMapper = options.sequenceMapper;
37
+ this.realtime = options.realtime;
38
+ this.intervalMs = options.intervalMs ?? 10000;
39
+ this.onError = options.onError;
40
+ this.onPullComplete = options.onPullComplete;
41
+ }
42
+ /**
43
+ * Start the pull engine loop.
44
+ */
45
+ start() {
46
+ if (this.running)
47
+ return;
48
+ this.running = true;
49
+ this.loadCursors().then(() => {
50
+ this.scheduleNext(0);
51
+ });
52
+ }
53
+ /**
54
+ * Stop the pull engine.
55
+ */
56
+ stop() {
57
+ this.running = false;
58
+ if (this.timer) {
59
+ clearTimeout(this.timer);
60
+ this.timer = null;
61
+ }
62
+ }
63
+ /**
64
+ * Pull once (for manual/testing use).
65
+ */
66
+ async pullOnce() {
67
+ return this.processOne();
68
+ }
69
+ async loadCursors() {
70
+ try {
71
+ // Load cursors from config
72
+ const rowResult = await sql `
73
+ select value_json
74
+ from ${sql.table('relay_config')}
75
+ where key = 'main_cursors'
76
+ limit 1
77
+ `.execute(this.db);
78
+ const row = rowResult.rows[0];
79
+ if (row?.value_json) {
80
+ const parsed = JSON.parse(row.value_json);
81
+ if (typeof parsed === 'object' && parsed !== null) {
82
+ for (const [key, value] of Object.entries(parsed)) {
83
+ if (typeof value === 'number') {
84
+ this.cursors.set(key, value);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch {
91
+ // Ignore - start from scratch
92
+ }
93
+ }
94
+ async saveCursors() {
95
+ const cursorObj = {};
96
+ for (const [key, value] of this.cursors) {
97
+ cursorObj[key] = value;
98
+ }
99
+ const valueJson = JSON.stringify(cursorObj);
100
+ await sql `
101
+ insert into ${sql.table('relay_config')} (key, value_json)
102
+ values ('main_cursors', ${valueJson})
103
+ on conflict (key)
104
+ do update set value_json = ${valueJson}
105
+ `.execute(this.db);
106
+ }
107
+ scheduleNext(delayMs) {
108
+ if (!this.running)
109
+ return;
110
+ if (this.timer)
111
+ return;
112
+ this.timer = setTimeout(async () => {
113
+ this.timer = null;
114
+ try {
115
+ const pulled = await this.processOne();
116
+ // If we pulled something, immediately try again
117
+ const nextDelay = pulled ? 0 : this.intervalMs;
118
+ this.scheduleNext(nextDelay);
119
+ }
120
+ catch (err) {
121
+ this.onError?.(err instanceof Error ? err : new Error(String(err)));
122
+ this.scheduleNext(this.intervalMs);
123
+ }
124
+ }, delayMs);
125
+ }
126
+ async processOne() {
127
+ // Build subscriptions for each table
128
+ const subscriptionRequests = this.tables.map((table) => ({
129
+ id: table,
130
+ shape: table,
131
+ scopes: this.scopes,
132
+ cursor: this.cursors.get(table) ?? -1,
133
+ }));
134
+ let response;
135
+ try {
136
+ const combined = await this.transport.sync({
137
+ clientId: this.clientId,
138
+ pull: {
139
+ subscriptions: subscriptionRequests,
140
+ limitCommits: 100,
141
+ },
142
+ });
143
+ if (!combined.pull) {
144
+ return false;
145
+ }
146
+ response = combined.pull;
147
+ }
148
+ catch {
149
+ // Network error - will retry
150
+ return false;
151
+ }
152
+ if (!response.ok) {
153
+ return false;
154
+ }
155
+ let hasChanges = false;
156
+ const affectedTables = new Set();
157
+ for (const sub of response.subscriptions) {
158
+ if (sub.status !== 'active')
159
+ continue;
160
+ const table = sub.id;
161
+ // Process commits
162
+ for (const commit of sub.commits) {
163
+ const applied = await this.applyCommitLocally(commit, table);
164
+ if (applied) {
165
+ hasChanges = true;
166
+ affectedTables.add(table);
167
+ }
168
+ }
169
+ // Update cursor
170
+ if (sub.nextCursor > (this.cursors.get(table) ?? -1)) {
171
+ this.cursors.set(table, sub.nextCursor);
172
+ }
173
+ }
174
+ // Save updated cursors
175
+ await this.saveCursors();
176
+ // Notify local clients if we have changes
177
+ if (hasChanges && affectedTables.size > 0) {
178
+ const maxCursor = await this.dialect.readMaxCommitSeq(this.db);
179
+ this.realtime.notifyScopeKeys(Array.from(affectedTables), maxCursor);
180
+ }
181
+ // Trigger rate-limited prune after successful pull
182
+ await this.onPullComplete?.();
183
+ return hasChanges;
184
+ }
185
+ /**
186
+ * Apply a commit from main server locally.
187
+ *
188
+ * This re-applies the commit through the local shape handlers
189
+ * to ensure proper indexing and scope assignment.
190
+ */
191
+ async applyCommitLocally(commit, table) {
192
+ if (commit.changes.length === 0)
193
+ return false;
194
+ // Convert changes to operations
195
+ const operations = commit.changes.map((change) => ({
196
+ table: change.table,
197
+ row_id: change.row_id,
198
+ op: change.op,
199
+ payload: change.row_json,
200
+ }));
201
+ // Generate a unique commit ID for this relay instance
202
+ const relayCommitId = `main:${commit.commitSeq}:${table}`;
203
+ // Push through local handler
204
+ const result = await pushCommit({
205
+ db: this.db,
206
+ dialect: this.dialect,
207
+ shapes: this.shapes,
208
+ actorId: commit.actorId,
209
+ request: {
210
+ clientId: `relay:${this.clientId}`,
211
+ clientCommitId: relayCommitId,
212
+ operations,
213
+ schemaVersion: 1,
214
+ },
215
+ });
216
+ if (result.response.ok === true &&
217
+ result.response.status === 'applied' &&
218
+ typeof result.response.commitSeq === 'number') {
219
+ // Record sequence mapping
220
+ await this.sequenceMapper.createConfirmedMapping(result.response.commitSeq, commit.commitSeq);
221
+ return true;
222
+ }
223
+ // Already applied (cached) - that's fine
224
+ if (result.response.status === 'cached') {
225
+ return false;
226
+ }
227
+ // Rejected - this shouldn't happen for pulls from main
228
+ // Log but don't fail
229
+ console.warn(`Relay: Failed to apply commit ${commit.commitSeq} locally:`, result.response);
230
+ return false;
231
+ }
232
+ }
233
+ //# sourceMappingURL=pull-engine.js.map