@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.
- package/dist/client-role/forward-engine.d.ts +63 -0
- package/dist/client-role/forward-engine.d.ts.map +1 -0
- package/dist/client-role/forward-engine.js +263 -0
- package/dist/client-role/forward-engine.js.map +1 -0
- package/dist/client-role/index.d.ts +9 -0
- package/dist/client-role/index.d.ts.map +1 -0
- package/dist/client-role/index.js +9 -0
- package/dist/client-role/index.js.map +1 -0
- package/dist/client-role/pull-engine.d.ts +70 -0
- package/dist/client-role/pull-engine.d.ts.map +1 -0
- package/dist/client-role/pull-engine.js +233 -0
- package/dist/client-role/pull-engine.js.map +1 -0
- package/dist/client-role/sequence-mapper.d.ts +65 -0
- package/dist/client-role/sequence-mapper.d.ts.map +1 -0
- package/dist/client-role/sequence-mapper.js +161 -0
- package/dist/client-role/sequence-mapper.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +18 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +99 -0
- package/dist/migrate.js.map +1 -0
- package/dist/mode-manager.d.ts +60 -0
- package/dist/mode-manager.d.ts.map +1 -0
- package/dist/mode-manager.js +114 -0
- package/dist/mode-manager.js.map +1 -0
- package/dist/realtime.d.ts +102 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +305 -0
- package/dist/realtime.js.map +1 -0
- package/dist/relay.d.ts +188 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +315 -0
- package/dist/relay.js.map +1 -0
- package/dist/schema.d.ts +158 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/schema.js.map +1 -0
- package/dist/server-role/index.d.ts +54 -0
- package/dist/server-role/index.d.ts.map +1 -0
- package/dist/server-role/index.js +198 -0
- package/dist/server-role/index.js.map +1 -0
- package/dist/server-role/pull.d.ts +25 -0
- package/dist/server-role/pull.d.ts.map +1 -0
- package/dist/server-role/pull.js +24 -0
- package/dist/server-role/pull.js.map +1 -0
- package/dist/server-role/push.d.ts +27 -0
- package/dist/server-role/push.d.ts.map +1 -0
- package/dist/server-role/push.js +98 -0
- package/dist/server-role/push.js.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/relay.test.ts +464 -0
- package/src/bun-types.d.ts +50 -0
- package/src/client-role/forward-engine.ts +352 -0
- package/src/client-role/index.ts +9 -0
- package/src/client-role/pull-engine.ts +301 -0
- package/src/client-role/sequence-mapper.ts +201 -0
- package/src/index.ts +50 -0
- package/src/migrate.ts +113 -0
- package/src/mode-manager.ts +142 -0
- package/src/realtime.ts +370 -0
- package/src/relay.ts +421 -0
- package/src/schema.ts +171 -0
- package/src/server-role/index.ts +342 -0
- package/src/server-role/pull.ts +37 -0
- 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 @@
|
|
|
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 @@
|
|
|
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
|