@syncular/relay 0.0.1-100

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 +257 -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 +247 -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 +189 -0
  34. package/dist/relay.d.ts.map +1 -0
  35. package/dist/relay.js +319 -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 +195 -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 +94 -0
  52. package/dist/server-role/push.js.map +1 -0
  53. package/package.json +61 -0
  54. package/src/__tests__/relay.test.ts +781 -0
  55. package/src/bun-types.d.ts +50 -0
  56. package/src/client-role/forward-engine.ts +343 -0
  57. package/src/client-role/index.ts +9 -0
  58. package/src/client-role/pull-engine.ts +321 -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 +424 -0
  65. package/src/schema.ts +171 -0
  66. package/src/server-role/index.ts +339 -0
  67. package/src/server-role/pull.ts +37 -0
  68. package/src/server-role/push.ts +123 -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;AAExB,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;AAExD;;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,257 @@
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 { randomId } from '@syncular/core';
8
+ import { sql } from 'kysely';
9
+ /**
10
+ * Forward engine for sending local commits to the main server.
11
+ */
12
+ export class ForwardEngine {
13
+ db;
14
+ transport;
15
+ clientId;
16
+ sequenceMapper;
17
+ retryIntervalMs;
18
+ onConflict;
19
+ onError;
20
+ running = false;
21
+ timer = null;
22
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: assigned in scheduleWakeUp and read in tick
23
+ wakeUpRequested = false;
24
+ constructor(options) {
25
+ this.db = options.db;
26
+ this.transport = options.transport;
27
+ this.clientId = options.clientId;
28
+ this.sequenceMapper = options.sequenceMapper;
29
+ this.retryIntervalMs = options.retryIntervalMs ?? 5000;
30
+ this.onConflict = options.onConflict;
31
+ this.onError = options.onError;
32
+ }
33
+ /**
34
+ * Start the forward engine loop.
35
+ */
36
+ start() {
37
+ if (this.running)
38
+ return;
39
+ this.running = true;
40
+ this.scheduleNext(0);
41
+ }
42
+ /**
43
+ * Stop the forward engine.
44
+ */
45
+ stop() {
46
+ this.running = false;
47
+ if (this.timer) {
48
+ clearTimeout(this.timer);
49
+ this.timer = null;
50
+ }
51
+ }
52
+ /**
53
+ * Wake up the engine to process immediately.
54
+ */
55
+ wakeUp() {
56
+ if (!this.running)
57
+ return;
58
+ this.wakeUpRequested = true;
59
+ if (this.timer) {
60
+ clearTimeout(this.timer);
61
+ this.timer = null;
62
+ }
63
+ this.scheduleNext(0);
64
+ }
65
+ /**
66
+ * Forward a single commit (for manual/testing use).
67
+ */
68
+ async forwardOnce() {
69
+ return this.processOne();
70
+ }
71
+ scheduleNext(delayMs) {
72
+ if (!this.running)
73
+ return;
74
+ if (this.timer)
75
+ return;
76
+ this.timer = setTimeout(async () => {
77
+ this.timer = null;
78
+ this.wakeUpRequested = false;
79
+ try {
80
+ const forwarded = await this.processOne();
81
+ // If we forwarded something, immediately try again
82
+ const nextDelay = forwarded ? 0 : this.retryIntervalMs;
83
+ this.scheduleNext(nextDelay);
84
+ }
85
+ catch (err) {
86
+ this.onError?.(err instanceof Error ? err : new Error(String(err)));
87
+ this.scheduleNext(this.retryIntervalMs);
88
+ }
89
+ }, delayMs);
90
+ }
91
+ async processOne() {
92
+ const next = await this.getNextSendable();
93
+ if (!next)
94
+ return false;
95
+ await this.markSending(next.id);
96
+ let response;
97
+ try {
98
+ const combined = await this.transport.sync({
99
+ clientId: next.client_id,
100
+ push: {
101
+ clientCommitId: next.client_commit_id,
102
+ operations: next.operations,
103
+ schemaVersion: next.schema_version,
104
+ },
105
+ });
106
+ if (!combined.push) {
107
+ throw new Error('Server returned no push response');
108
+ }
109
+ response = combined.push;
110
+ }
111
+ catch (err) {
112
+ // Network error - mark as pending for retry
113
+ await this.markPending(next.id, String(err));
114
+ throw err;
115
+ }
116
+ const responseJson = JSON.stringify(response);
117
+ if (response.status === 'applied' || response.status === 'cached') {
118
+ const mainCommitSeq = response.commitSeq ?? null;
119
+ // Update outbox entry
120
+ await this.markForwarded(next.id, mainCommitSeq, responseJson);
121
+ // Update sequence mapper
122
+ if (mainCommitSeq != null) {
123
+ await this.sequenceMapper.markForwarded(next.local_commit_seq, mainCommitSeq);
124
+ }
125
+ return true;
126
+ }
127
+ // Rejected - store conflict and mark as failed
128
+ const conflict = await this.recordConflict(next, responseJson);
129
+ await this.markFailed(next.id, 'REJECTED', responseJson);
130
+ this.onConflict?.(conflict);
131
+ return true;
132
+ }
133
+ async getNextSendable() {
134
+ const staleThreshold = Date.now() - 30000;
135
+ const rowResult = await sql `
136
+ select
137
+ id,
138
+ local_commit_seq,
139
+ client_id,
140
+ client_commit_id,
141
+ operations_json,
142
+ schema_version,
143
+ status,
144
+ main_commit_seq,
145
+ error,
146
+ last_response_json,
147
+ created_at,
148
+ updated_at,
149
+ attempt_count
150
+ from ${sql.table('relay_forward_outbox')}
151
+ where
152
+ status = 'pending'
153
+ or (status = 'forwarding' and updated_at < ${staleThreshold})
154
+ order by created_at asc
155
+ limit 1
156
+ `.execute(this.db);
157
+ const row = rowResult.rows[0];
158
+ if (!row)
159
+ return null;
160
+ const operations = typeof row.operations_json === 'string'
161
+ ? JSON.parse(row.operations_json)
162
+ : row.operations_json;
163
+ return {
164
+ id: row.id,
165
+ local_commit_seq: row.local_commit_seq,
166
+ client_id: row.client_id,
167
+ client_commit_id: row.client_commit_id,
168
+ operations,
169
+ schema_version: row.schema_version,
170
+ status: row.status,
171
+ main_commit_seq: row.main_commit_seq,
172
+ error: row.error,
173
+ created_at: row.created_at,
174
+ updated_at: row.updated_at,
175
+ attempt_count: row.attempt_count,
176
+ };
177
+ }
178
+ async markSending(id) {
179
+ const now = Date.now();
180
+ await sql `
181
+ update ${sql.table('relay_forward_outbox')}
182
+ set
183
+ status = 'forwarding',
184
+ updated_at = ${now},
185
+ attempt_count = attempt_count + 1,
186
+ error = ${null}
187
+ where id = ${id}
188
+ `.execute(this.db);
189
+ }
190
+ async markPending(id, error) {
191
+ const now = Date.now();
192
+ await sql `
193
+ update ${sql.table('relay_forward_outbox')}
194
+ set status = 'pending', updated_at = ${now}, error = ${error}
195
+ where id = ${id}
196
+ `.execute(this.db);
197
+ }
198
+ async markForwarded(id, mainCommitSeq, responseJson) {
199
+ const now = Date.now();
200
+ await sql `
201
+ update ${sql.table('relay_forward_outbox')}
202
+ set
203
+ status = 'forwarded',
204
+ main_commit_seq = ${mainCommitSeq},
205
+ updated_at = ${now},
206
+ error = ${null},
207
+ last_response_json = ${responseJson}
208
+ where id = ${id}
209
+ `.execute(this.db);
210
+ }
211
+ async markFailed(id, error, responseJson) {
212
+ const now = Date.now();
213
+ await sql `
214
+ update ${sql.table('relay_forward_outbox')}
215
+ set
216
+ status = 'failed',
217
+ updated_at = ${now},
218
+ error = ${error},
219
+ last_response_json = ${responseJson}
220
+ where id = ${id}
221
+ `.execute(this.db);
222
+ }
223
+ async recordConflict(entry, responseJson) {
224
+ const now = Date.now();
225
+ const id = randomId();
226
+ await sql `
227
+ insert into ${sql.table('relay_forward_conflicts')} (
228
+ id,
229
+ local_commit_seq,
230
+ client_id,
231
+ client_commit_id,
232
+ response_json,
233
+ created_at,
234
+ resolved_at
235
+ )
236
+ values (
237
+ ${id},
238
+ ${entry.local_commit_seq},
239
+ ${entry.client_id},
240
+ ${entry.client_commit_id},
241
+ ${responseJson},
242
+ ${now},
243
+ ${null}
244
+ )
245
+ `.execute(this.db);
246
+ return {
247
+ id,
248
+ local_commit_seq: entry.local_commit_seq,
249
+ client_id: entry.client_id,
250
+ client_commit_id: entry.client_commit_id,
251
+ response: JSON.parse(responseJson),
252
+ created_at: now,
253
+ resolved_at: null,
254
+ };
255
+ }
256
+ }
257
+ //# 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.js';
7
+ export * from './pull-engine.js';
8
+ export * from './sequence-mapper.js';
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,CAcZ;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;YAqFV,kBAAkB;CA6DjC"}
@@ -0,0 +1,247 @@
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
+ void this.loadCursors()
50
+ .catch((error) => {
51
+ this.onError?.(error instanceof Error ? error : new Error(String(error)));
52
+ })
53
+ .finally(() => {
54
+ if (this.running) {
55
+ this.scheduleNext(0);
56
+ }
57
+ });
58
+ }
59
+ /**
60
+ * Stop the pull engine.
61
+ */
62
+ stop() {
63
+ this.running = false;
64
+ if (this.timer) {
65
+ clearTimeout(this.timer);
66
+ this.timer = null;
67
+ }
68
+ }
69
+ /**
70
+ * Pull once (for manual/testing use).
71
+ */
72
+ async pullOnce() {
73
+ return this.processOne();
74
+ }
75
+ async loadCursors() {
76
+ try {
77
+ // Load cursors from config
78
+ const rowResult = await sql `
79
+ select value_json
80
+ from ${sql.table('relay_config')}
81
+ where key = 'main_cursors'
82
+ limit 1
83
+ `.execute(this.db);
84
+ const row = rowResult.rows[0];
85
+ if (row?.value_json) {
86
+ const parsed = JSON.parse(row.value_json);
87
+ if (typeof parsed === 'object' && parsed !== null) {
88
+ for (const [key, value] of Object.entries(parsed)) {
89
+ if (typeof value === 'number') {
90
+ this.cursors.set(key, value);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch {
97
+ // Ignore - start from scratch
98
+ }
99
+ }
100
+ async saveCursors() {
101
+ const cursorObj = {};
102
+ for (const [key, value] of this.cursors) {
103
+ cursorObj[key] = value;
104
+ }
105
+ const valueJson = JSON.stringify(cursorObj);
106
+ await sql `
107
+ insert into ${sql.table('relay_config')} (key, value_json)
108
+ values ('main_cursors', ${valueJson})
109
+ on conflict (key)
110
+ do update set value_json = ${valueJson}
111
+ `.execute(this.db);
112
+ }
113
+ scheduleNext(delayMs) {
114
+ if (!this.running)
115
+ return;
116
+ if (this.timer)
117
+ return;
118
+ this.timer = setTimeout(async () => {
119
+ this.timer = null;
120
+ try {
121
+ const pulled = await this.processOne();
122
+ // If we pulled something, immediately try again
123
+ const nextDelay = pulled ? 0 : this.intervalMs;
124
+ this.scheduleNext(nextDelay);
125
+ }
126
+ catch (err) {
127
+ this.onError?.(err instanceof Error ? err : new Error(String(err)));
128
+ this.scheduleNext(this.intervalMs);
129
+ }
130
+ }, delayMs);
131
+ }
132
+ async processOne() {
133
+ // Build subscriptions for each table
134
+ const subscriptionRequests = this.tables.map((table) => ({
135
+ id: table,
136
+ shape: table,
137
+ scopes: this.scopes,
138
+ cursor: this.cursors.get(table) ?? -1,
139
+ }));
140
+ let response;
141
+ try {
142
+ const combined = await this.transport.sync({
143
+ clientId: this.clientId,
144
+ pull: {
145
+ subscriptions: subscriptionRequests,
146
+ limitCommits: 100,
147
+ },
148
+ });
149
+ if (!combined.pull) {
150
+ return false;
151
+ }
152
+ response = combined.pull;
153
+ }
154
+ catch {
155
+ // Network error - will retry
156
+ return false;
157
+ }
158
+ if (!response.ok) {
159
+ return false;
160
+ }
161
+ let hasChanges = false;
162
+ const affectedTables = new Set();
163
+ for (const sub of response.subscriptions) {
164
+ if (sub.status !== 'active')
165
+ continue;
166
+ const table = sub.id;
167
+ // Process commits
168
+ let canAdvanceCursor = true;
169
+ for (const commit of sub.commits) {
170
+ const outcome = await this.applyCommitLocally(commit, table);
171
+ if (outcome === 'applied') {
172
+ hasChanges = true;
173
+ affectedTables.add(table);
174
+ }
175
+ if (outcome === 'rejected') {
176
+ canAdvanceCursor = false;
177
+ break;
178
+ }
179
+ }
180
+ // Update cursor
181
+ if (canAdvanceCursor &&
182
+ sub.nextCursor > (this.cursors.get(table) ?? -1)) {
183
+ this.cursors.set(table, sub.nextCursor);
184
+ }
185
+ }
186
+ // Save updated cursors
187
+ await this.saveCursors();
188
+ // Notify local clients if we have changes
189
+ if (hasChanges && affectedTables.size > 0) {
190
+ const maxCursor = await this.dialect.readMaxCommitSeq(this.db);
191
+ this.realtime.notifyScopeKeys(Array.from(affectedTables), maxCursor);
192
+ }
193
+ // Trigger rate-limited prune after successful pull
194
+ await this.onPullComplete?.();
195
+ return hasChanges;
196
+ }
197
+ /**
198
+ * Apply a commit from main server locally.
199
+ *
200
+ * This re-applies the commit through the local shape handlers
201
+ * to ensure proper indexing and scope assignment.
202
+ */
203
+ async applyCommitLocally(commit, table) {
204
+ if (commit.changes.length === 0)
205
+ return 'cached';
206
+ // Convert changes to operations
207
+ const operations = commit.changes.map((change) => ({
208
+ table: change.table,
209
+ row_id: change.row_id,
210
+ op: change.op,
211
+ payload: change.row_json,
212
+ }));
213
+ // Generate a unique commit ID for this relay instance
214
+ const relayCommitId = `main:${commit.commitSeq}:${table}`;
215
+ // Push through local handler
216
+ const result = await pushCommit({
217
+ db: this.db,
218
+ dialect: this.dialect,
219
+ shapes: this.shapes,
220
+ actorId: commit.actorId,
221
+ request: {
222
+ clientId: `relay:${this.clientId}`,
223
+ clientCommitId: relayCommitId,
224
+ operations,
225
+ schemaVersion: 1,
226
+ },
227
+ });
228
+ if (result.response.ok === true &&
229
+ result.response.status === 'applied' &&
230
+ typeof result.response.commitSeq === 'number') {
231
+ // Record sequence mapping
232
+ await this.sequenceMapper.createConfirmedMapping(result.response.commitSeq, commit.commitSeq);
233
+ return 'applied';
234
+ }
235
+ // Already applied (cached) - that's fine
236
+ if (result.response.status === 'cached') {
237
+ return 'cached';
238
+ }
239
+ // Rejected - this shouldn't happen for pulls from main
240
+ // Do not advance cursor; signal error so caller can react.
241
+ const error = new Error(`Relay: Failed to apply commit ${commit.commitSeq} locally (status=${result.response.status})`);
242
+ console.warn(`Relay: Failed to apply commit ${commit.commitSeq} locally:`, result.response);
243
+ this.onError?.(error);
244
+ return 'rejected';
245
+ }
246
+ }
247
+ //# sourceMappingURL=pull-engine.js.map