@syncular/server 0.15.47 → 0.16.1
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/README.md +48 -5
- package/dist/admin.d.ts +1 -5
- package/dist/admin.js +3 -12
- package/dist/authoritative-query.d.ts +14 -6
- package/dist/authoritative-query.js +60 -82
- package/dist/blob-handlers.js +4 -1
- package/dist/context.d.ts +3 -1
- package/dist/context.js +4 -0
- package/dist/d1-storage.d.ts +8 -2
- package/dist/d1-storage.js +134 -26
- package/dist/errors.js +6 -0
- package/dist/events.d.ts +2 -1
- package/dist/frame-bytes.d.ts +2 -2
- package/dist/frame-bytes.js +33 -14
- package/dist/handler.js +58 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/operations.d.ts +2 -0
- package/dist/operations.js +25 -5
- package/dist/postgres-storage.d.ts +9 -3
- package/dist/postgres-storage.js +110 -20
- package/dist/prune.d.ts +3 -1
- package/dist/prune.js +18 -14
- package/dist/pull.d.ts +1 -1
- package/dist/pull.js +74 -37
- package/dist/push.js +5 -5
- package/dist/realtime.d.ts +4 -1
- package/dist/realtime.js +33 -9
- package/dist/restore.d.ts +13 -0
- package/dist/restore.js +13 -0
- package/dist/s3-segment-store.js +10 -1
- package/dist/seed.js +36 -4
- package/dist/segment-download.js +5 -2
- package/dist/segment-store.d.ts +3 -0
- package/dist/segment-store.js +1 -0
- package/dist/sqlite-bun-driver.d.ts +2 -1
- package/dist/sqlite-bun-driver.js +5 -2
- package/dist/sqlite-bun.js +2 -2
- package/dist/sqlite-dialect.d.ts +1 -1
- package/dist/sqlite-dialect.js +7 -0
- package/dist/sqlite-image.d.ts +3 -3
- package/dist/sqlite-image.js +10 -6
- package/dist/sqlite-node.js +2 -2
- package/dist/sqlite-segment-store.js +14 -5
- package/dist/sqlite-storage.d.ts +8 -2
- package/dist/sqlite-storage.js +147 -36
- package/dist/storage-errors.d.ts +1 -1
- package/dist/storage-errors.js +3 -0
- package/dist/storage.d.ts +38 -10
- package/package.json +2 -2
- package/src/admin.ts +7 -15
- package/src/authoritative-query.ts +89 -94
- package/src/blob-handlers.ts +8 -1
- package/src/context.ts +16 -1
- package/src/d1-storage.ts +193 -29
- package/src/errors.ts +6 -0
- package/src/events.ts +2 -1
- package/src/frame-bytes.ts +40 -14
- package/src/handler.ts +102 -29
- package/src/index.ts +1 -0
- package/src/operations.ts +37 -4
- package/src/postgres-storage.ts +184 -38
- package/src/prune.ts +29 -15
- package/src/pull.ts +90 -41
- package/src/push.ts +5 -5
- package/src/realtime.ts +46 -7
- package/src/restore.ts +28 -0
- package/src/s3-segment-store.ts +10 -1
- package/src/seed.ts +46 -4
- package/src/segment-download.ts +11 -2
- package/src/segment-store.ts +4 -0
- package/src/sqlite-bun-driver.ts +6 -2
- package/src/sqlite-bun.ts +2 -2
- package/src/sqlite-dialect.ts +7 -0
- package/src/sqlite-image.ts +26 -15
- package/src/sqlite-node.ts +2 -2
- package/src/sqlite-segment-store.ts +18 -4
- package/src/sqlite-storage.ts +203 -39
- package/src/storage-errors.ts +10 -1
- package/src/storage.ts +57 -10
package/src/frame-bytes.ts
CHANGED
|
@@ -18,7 +18,14 @@ import {
|
|
|
18
18
|
type SubStartFrame,
|
|
19
19
|
} from '@syncular/core';
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const stubHeader = (wireVersion: number): RespHeaderFrame =>
|
|
22
|
+
wireVersion >= 2
|
|
23
|
+
? {
|
|
24
|
+
type: 'RESP_HEADER',
|
|
25
|
+
logEpoch: 'frame-probe',
|
|
26
|
+
resetRequired: false,
|
|
27
|
+
}
|
|
28
|
+
: { type: 'RESP_HEADER' };
|
|
22
29
|
const STUB_SUB_START: SubStartFrame = {
|
|
23
30
|
type: 'SUB_START',
|
|
24
31
|
id: '',
|
|
@@ -30,18 +37,28 @@ const STUB_SUB_START: SubStartFrame = {
|
|
|
30
37
|
const STUB_SUB_END: SubEndFrame = { type: 'SUB_END', nextCursor: 0 };
|
|
31
38
|
|
|
32
39
|
const probe = encodeMessage({
|
|
33
|
-
wireVersion:
|
|
40
|
+
wireVersion: 1,
|
|
34
41
|
msgKind: 'response',
|
|
35
|
-
frames: [
|
|
42
|
+
frames: [stubHeader(1)],
|
|
36
43
|
});
|
|
37
44
|
|
|
38
45
|
/** The 8-byte SSP2 response envelope header (§1.2). */
|
|
39
|
-
export
|
|
46
|
+
export function responseEnvelopeHeader(wireVersion: number): Uint8Array {
|
|
47
|
+
const encoded = encodeMessage({
|
|
48
|
+
wireVersion,
|
|
49
|
+
msgKind: 'response',
|
|
50
|
+
frames: [stubHeader(wireVersion)],
|
|
51
|
+
});
|
|
52
|
+
return encoded.slice(0, 8);
|
|
53
|
+
}
|
|
40
54
|
|
|
41
55
|
/** The terminating END frame (§1.2 rule 1). */
|
|
42
56
|
export const END_FRAME_BYTES: Uint8Array = probe.slice(probe.length - 5);
|
|
43
57
|
|
|
44
|
-
function wrapperFor(
|
|
58
|
+
function wrapperFor(
|
|
59
|
+
frame: ResponseFrame,
|
|
60
|
+
wireVersion: number,
|
|
61
|
+
): {
|
|
45
62
|
frames: ResponseFrame[];
|
|
46
63
|
index: number;
|
|
47
64
|
} {
|
|
@@ -50,11 +67,11 @@ function wrapperFor(frame: ResponseFrame): {
|
|
|
50
67
|
return { frames: [frame], index: 0 };
|
|
51
68
|
case 'LEASE':
|
|
52
69
|
// §7.3.2: LEASE rides immediately after RESP_HEADER.
|
|
53
|
-
return { frames: [
|
|
70
|
+
return { frames: [stubHeader(wireVersion), frame], index: 1 };
|
|
54
71
|
case 'PUSH_RESULT':
|
|
55
72
|
case 'ERROR':
|
|
56
73
|
case 'UNKNOWN':
|
|
57
|
-
return { frames: [
|
|
74
|
+
return { frames: [stubHeader(wireVersion), frame], index: 1 };
|
|
58
75
|
case 'PUSH_RESULT_DETAILS': {
|
|
59
76
|
const result: PushResultFrame = {
|
|
60
77
|
type: 'PUSH_RESULT',
|
|
@@ -68,17 +85,23 @@ function wrapperFor(frame: ResponseFrame): {
|
|
|
68
85
|
retryable: false,
|
|
69
86
|
})),
|
|
70
87
|
};
|
|
71
|
-
return { frames: [
|
|
88
|
+
return { frames: [stubHeader(wireVersion), result, frame], index: 2 };
|
|
72
89
|
}
|
|
73
90
|
case 'SUB_START':
|
|
74
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
frames: [stubHeader(wireVersion), frame, STUB_SUB_END],
|
|
93
|
+
index: 1,
|
|
94
|
+
};
|
|
75
95
|
case 'SUB_END':
|
|
76
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
frames: [stubHeader(wireVersion), STUB_SUB_START, frame],
|
|
98
|
+
index: 2,
|
|
99
|
+
};
|
|
77
100
|
case 'COMMIT':
|
|
78
101
|
case 'SEGMENT_REF':
|
|
79
102
|
case 'SEGMENT_INLINE':
|
|
80
103
|
return {
|
|
81
|
-
frames: [
|
|
104
|
+
frames: [stubHeader(wireVersion), STUB_SUB_START, frame, STUB_SUB_END],
|
|
82
105
|
index: 2,
|
|
83
106
|
};
|
|
84
107
|
}
|
|
@@ -88,10 +111,13 @@ function wrapperFor(frame: ResponseFrame): {
|
|
|
88
111
|
* Encode one response frame (5-byte frame header + payload) using the
|
|
89
112
|
* reference codec.
|
|
90
113
|
*/
|
|
91
|
-
export function encodeResponseFrame(
|
|
92
|
-
|
|
114
|
+
export function encodeResponseFrame(
|
|
115
|
+
frame: ResponseFrame,
|
|
116
|
+
wireVersion = PROTOCOL_WIRE_VERSION,
|
|
117
|
+
): Uint8Array {
|
|
118
|
+
const { frames, index } = wrapperFor(frame, wireVersion);
|
|
93
119
|
const encoded = encodeMessage({
|
|
94
|
-
wireVersion
|
|
120
|
+
wireVersion,
|
|
95
121
|
msgKind: 'response',
|
|
96
122
|
frames,
|
|
97
123
|
});
|
package/src/handler.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
limitsOf,
|
|
29
29
|
REMOTE_COMMAND_CLIENT_ID_PREFIX,
|
|
30
30
|
RESOLVER_OUTAGE,
|
|
31
|
+
touchAuthenticatedPartition,
|
|
31
32
|
} from './context';
|
|
32
33
|
import { SyncError, syncError } from './errors';
|
|
33
34
|
import {
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
import {
|
|
39
40
|
END_FRAME_BYTES,
|
|
40
41
|
encodeResponseFrame,
|
|
41
|
-
|
|
42
|
+
responseEnvelopeHeader,
|
|
42
43
|
} from './frame-bytes';
|
|
43
44
|
import type { LeaseRecord } from './lease-store';
|
|
44
45
|
import {
|
|
@@ -53,9 +54,12 @@ import { type ProcessedPushCommit, processPushCommitWithTrace } from './push';
|
|
|
53
54
|
import type { CompiledSchema } from './schema';
|
|
54
55
|
import { compileSchema } from './schema';
|
|
55
56
|
import { computeEffective, type ResolvedScopes } from './scopes';
|
|
56
|
-
import type { ClientSubscription } from './storage';
|
|
57
|
+
import type { ClientSubscription, PartitionRegistryEntry } from './storage';
|
|
57
58
|
|
|
58
59
|
interface RequestPlan {
|
|
60
|
+
readonly wireVersion: number;
|
|
61
|
+
readonly logEpoch: string;
|
|
62
|
+
readonly epochReset: boolean;
|
|
59
63
|
readonly header: ReqHeaderFrame;
|
|
60
64
|
readonly pushes: readonly PushCommitFrame[];
|
|
61
65
|
readonly pull: PullHeaderFrame | undefined;
|
|
@@ -192,6 +196,7 @@ async function planRequest(
|
|
|
192
196
|
request: RequestMessage,
|
|
193
197
|
ctx: SyncRequestContext,
|
|
194
198
|
schema: CompiledSchema,
|
|
199
|
+
registry: PartitionRegistryEntry,
|
|
195
200
|
): Promise<RequestPlan> {
|
|
196
201
|
const header = request.frames[0];
|
|
197
202
|
if (header === undefined || header.type !== 'REQ_HEADER') {
|
|
@@ -206,10 +211,32 @@ async function planRequest(
|
|
|
206
211
|
else if (frame.type === 'SUBSCRIPTION') subFrames.push(frame);
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
if (request.wireVersion === 1 && registry.epochRequired) {
|
|
215
|
+
throw syncError(
|
|
216
|
+
'sync.client_wire_unsupported',
|
|
217
|
+
'this partition requires a client that validates log epochs (§2.1)',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
request.wireVersion >= 2 &&
|
|
222
|
+
header.logEpoch === undefined &&
|
|
223
|
+
pushes.length > 0
|
|
224
|
+
) {
|
|
225
|
+
throw syncError(
|
|
226
|
+
'sync.invalid_request',
|
|
227
|
+
'epoch acquisition requests must not carry push commits (§2.1)',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const epochReset =
|
|
231
|
+
request.wireVersion >= 2 && header.logEpoch !== registry.logEpoch;
|
|
232
|
+
|
|
209
233
|
if (header.schemaVersion !== schema.version) {
|
|
210
234
|
// §2.4: no degraded encoding — answer with the schema floor (§1.6).
|
|
211
235
|
// No lease is issued on a floor round (§7.3.3).
|
|
212
236
|
return {
|
|
237
|
+
wireVersion: request.wireVersion,
|
|
238
|
+
logEpoch: registry.logEpoch,
|
|
239
|
+
epochReset,
|
|
213
240
|
header,
|
|
214
241
|
pushes,
|
|
215
242
|
pull,
|
|
@@ -220,6 +247,21 @@ async function planRequest(
|
|
|
220
247
|
};
|
|
221
248
|
}
|
|
222
249
|
|
|
250
|
+
if (epochReset) {
|
|
251
|
+
return {
|
|
252
|
+
wireVersion: request.wireVersion,
|
|
253
|
+
logEpoch: registry.logEpoch,
|
|
254
|
+
epochReset: true,
|
|
255
|
+
header,
|
|
256
|
+
pushes: [],
|
|
257
|
+
pull: undefined,
|
|
258
|
+
subscriptions: [],
|
|
259
|
+
resolved: { ok: false },
|
|
260
|
+
schemaFloor: false,
|
|
261
|
+
leaseToEmit: undefined,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
223
265
|
if (header.clientId.startsWith(REMOTE_COMMAND_CLIENT_ID_PREFIX)) {
|
|
224
266
|
throw syncError(
|
|
225
267
|
'sync.invalid_client_id',
|
|
@@ -313,6 +355,9 @@ async function planRequest(
|
|
|
313
355
|
});
|
|
314
356
|
|
|
315
357
|
return {
|
|
358
|
+
wireVersion: request.wireVersion,
|
|
359
|
+
logEpoch: registry.logEpoch,
|
|
360
|
+
epochReset: false,
|
|
316
361
|
header,
|
|
317
362
|
pushes,
|
|
318
363
|
pull,
|
|
@@ -325,7 +370,7 @@ async function planRequest(
|
|
|
325
370
|
|
|
326
371
|
/** Mutable outcome box shared with the instrumented stream wrapper. */
|
|
327
372
|
interface RequestReport {
|
|
328
|
-
outcome: 'ok' | 'schema_floor' | 'error';
|
|
373
|
+
outcome: 'ok' | 'schema_floor' | 'reset' | 'error';
|
|
329
374
|
errorCode?: string;
|
|
330
375
|
}
|
|
331
376
|
|
|
@@ -413,28 +458,48 @@ async function* streamResponse(
|
|
|
413
458
|
report?: RequestReport,
|
|
414
459
|
): AsyncGenerator<Uint8Array> {
|
|
415
460
|
const events = ctx.events;
|
|
416
|
-
yield
|
|
461
|
+
yield responseEnvelopeHeader(plan.wireVersion);
|
|
417
462
|
if (plan.schemaFloor) {
|
|
418
463
|
if (report !== undefined) report.outcome = 'schema_floor';
|
|
419
|
-
yield encodeResponseFrame(
|
|
464
|
+
yield encodeResponseFrame(
|
|
465
|
+
{
|
|
466
|
+
type: 'RESP_HEADER',
|
|
467
|
+
requiredSchemaVersion: schema.version,
|
|
468
|
+
latestSchemaVersion: schema.version,
|
|
469
|
+
...(plan.wireVersion >= 2
|
|
470
|
+
? { logEpoch: plan.logEpoch, resetRequired: plan.epochReset }
|
|
471
|
+
: {}),
|
|
472
|
+
},
|
|
473
|
+
plan.wireVersion,
|
|
474
|
+
);
|
|
475
|
+
yield END_FRAME_BYTES;
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
yield encodeResponseFrame(
|
|
479
|
+
{
|
|
420
480
|
type: 'RESP_HEADER',
|
|
421
|
-
requiredSchemaVersion: schema.version,
|
|
422
481
|
latestSchemaVersion: schema.version,
|
|
423
|
-
|
|
482
|
+
...(plan.wireVersion >= 2
|
|
483
|
+
? { logEpoch: plan.logEpoch, resetRequired: plan.epochReset }
|
|
484
|
+
: {}),
|
|
485
|
+
},
|
|
486
|
+
plan.wireVersion,
|
|
487
|
+
);
|
|
488
|
+
if (plan.epochReset) {
|
|
489
|
+
if (report !== undefined) report.outcome = 'reset';
|
|
424
490
|
yield END_FRAME_BYTES;
|
|
425
491
|
return;
|
|
426
492
|
}
|
|
427
|
-
yield encodeResponseFrame({
|
|
428
|
-
type: 'RESP_HEADER',
|
|
429
|
-
latestSchemaVersion: schema.version,
|
|
430
|
-
});
|
|
431
493
|
// §7.3.2: the LEASE frame rides immediately after RESP_HEADER.
|
|
432
494
|
if (plan.leaseToEmit !== undefined) {
|
|
433
|
-
yield encodeResponseFrame(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
495
|
+
yield encodeResponseFrame(
|
|
496
|
+
{
|
|
497
|
+
type: 'LEASE',
|
|
498
|
+
leaseId: plan.leaseToEmit.leaseId,
|
|
499
|
+
expiresAtMs: plan.leaseToEmit.expiresAtMs,
|
|
500
|
+
},
|
|
501
|
+
plan.wireVersion,
|
|
502
|
+
);
|
|
438
503
|
if (events !== undefined) {
|
|
439
504
|
emitEvent(events, {
|
|
440
505
|
type: 'lease.issued',
|
|
@@ -461,9 +526,11 @@ async function* streamResponse(
|
|
|
461
526
|
if (events !== undefined) {
|
|
462
527
|
emitPushEvent(events, ctx, plan.header.clientId, push, processed);
|
|
463
528
|
}
|
|
464
|
-
yield encodeResponseFrame(frame);
|
|
529
|
+
yield encodeResponseFrame(frame, plan.wireVersion);
|
|
465
530
|
const details = pushResultDetailsFrame(frame);
|
|
466
|
-
if (details !== undefined)
|
|
531
|
+
if (details !== undefined) {
|
|
532
|
+
yield encodeResponseFrame(details, plan.wireVersion);
|
|
533
|
+
}
|
|
467
534
|
}
|
|
468
535
|
|
|
469
536
|
// Pull half (§4): subscriptions echoed in request order.
|
|
@@ -485,6 +552,7 @@ async function* streamResponse(
|
|
|
485
552
|
maxSeq,
|
|
486
553
|
horizonSeq,
|
|
487
554
|
trace,
|
|
555
|
+
plan.logEpoch,
|
|
488
556
|
);
|
|
489
557
|
let status: 'active' | 'revoked' | 'reset' = 'active';
|
|
490
558
|
let bootstrap = false;
|
|
@@ -502,7 +570,7 @@ async function* streamResponse(
|
|
|
502
570
|
changes += frame.changes.length;
|
|
503
571
|
}
|
|
504
572
|
}
|
|
505
|
-
yield encodeResponseFrame(frame);
|
|
573
|
+
yield encodeResponseFrame(frame, plan.wireVersion);
|
|
506
574
|
step = await section.next();
|
|
507
575
|
}
|
|
508
576
|
if (step.value.active) cursors.push(step.value.nextCursor);
|
|
@@ -556,6 +624,7 @@ async function* streamResponse(
|
|
|
556
624
|
await ctx.storage.putClientRecord(ctx.partition, {
|
|
557
625
|
clientId: plan.header.clientId,
|
|
558
626
|
actorId: ctx.actorId,
|
|
627
|
+
wireVersion: plan.wireVersion,
|
|
559
628
|
cursor,
|
|
560
629
|
updatedAtMs: clockOf(ctx)(),
|
|
561
630
|
subscriptions,
|
|
@@ -567,15 +636,18 @@ async function* streamResponse(
|
|
|
567
636
|
report.errorCode = error.code;
|
|
568
637
|
}
|
|
569
638
|
// §1.6: in-band ERROR, then END, nothing else.
|
|
570
|
-
yield encodeResponseFrame(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
639
|
+
yield encodeResponseFrame(
|
|
640
|
+
{
|
|
641
|
+
type: 'ERROR',
|
|
642
|
+
code: error.code,
|
|
643
|
+
message: error.message,
|
|
644
|
+
category: error.category,
|
|
645
|
+
retryable: error.retryable,
|
|
646
|
+
recommendedAction: error.recommendedAction,
|
|
647
|
+
...(error.details !== undefined ? { details: error.details } : {}),
|
|
648
|
+
},
|
|
649
|
+
plan.wireVersion,
|
|
650
|
+
);
|
|
579
651
|
yield END_FRAME_BYTES;
|
|
580
652
|
return;
|
|
581
653
|
}
|
|
@@ -644,7 +716,8 @@ async function createStreamCore(
|
|
|
644
716
|
// Relational row tables: create/
|
|
645
717
|
// migrate on first contact; memoized per storage instance thereafter.
|
|
646
718
|
await ctx.storage.ensureSchema(schema);
|
|
647
|
-
const
|
|
719
|
+
const registry = await touchAuthenticatedPartition(ctx);
|
|
720
|
+
const plan = await planRequest(request, ctx, schema, registry);
|
|
648
721
|
if (events === undefined) return streamResponse(plan, ctx, schema);
|
|
649
722
|
const report: RequestReport = { outcome: 'ok' };
|
|
650
723
|
return instrumentedStream(
|
package/src/index.ts
CHANGED
package/src/operations.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthoritativeRelationPlan,
|
|
3
|
+
validateAuthoritativeRelationPlan,
|
|
4
|
+
} from './authoritative-query';
|
|
1
5
|
import {
|
|
2
6
|
decodeRow,
|
|
3
7
|
decodeRemoteOperationRequest,
|
|
@@ -9,7 +13,11 @@ import {
|
|
|
9
13
|
type ScopeMap,
|
|
10
14
|
} from '@syncular/core';
|
|
11
15
|
import type { SyncRequestContext } from './context';
|
|
12
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
REMOTE_COMMAND_CLIENT_ID_PREFIX,
|
|
18
|
+
RESOLVER_OUTAGE,
|
|
19
|
+
touchAuthenticatedPartition,
|
|
20
|
+
} from './context';
|
|
13
21
|
import { SyncError, syncError } from './errors';
|
|
14
22
|
import { processPushOperationsWithTrace } from './push';
|
|
15
23
|
import { compileSchema } from './schema';
|
|
@@ -38,6 +46,7 @@ export interface AuthoritativeQueryDescriptor<Params = undefined> {
|
|
|
38
46
|
readonly hasParams: boolean;
|
|
39
47
|
readonly sql: string;
|
|
40
48
|
readonly tables: readonly string[];
|
|
49
|
+
readonly relationPlans: readonly AuthoritativeRelationPlan[];
|
|
41
50
|
readonly resultColumns: readonly {
|
|
42
51
|
readonly name: string;
|
|
43
52
|
readonly type:
|
|
@@ -276,6 +285,9 @@ export function registerRemoteQuery<Params>(
|
|
|
276
285
|
options: RemoteQueryOptions<Params>,
|
|
277
286
|
): RegisteredRemoteQuery {
|
|
278
287
|
if (
|
|
288
|
+
!Array.isArray(descriptor.relationPlans) ||
|
|
289
|
+
!descriptor.relationPlans.some((plan) => plan.sql === descriptor.sql) ||
|
|
290
|
+
descriptor.relationPlans.some((plan) => !Array.isArray(plan.relations)) ||
|
|
279
291
|
descriptor.id.length === 0 ||
|
|
280
292
|
new Set(descriptor.tables).size !== descriptor.tables.length ||
|
|
281
293
|
!Array.isArray(descriptor.resultColumns) ||
|
|
@@ -284,9 +296,12 @@ export function registerRemoteQuery<Params>(
|
|
|
284
296
|
descriptor.resultColumns.length
|
|
285
297
|
) {
|
|
286
298
|
throw new Error(
|
|
287
|
-
'remote query requires a non-empty id and unique tables and result columns',
|
|
299
|
+
'remote query requires generated relation plans, a non-empty id, and unique tables and result columns; regenerate queries',
|
|
288
300
|
);
|
|
289
301
|
}
|
|
302
|
+
for (const plan of descriptor.relationPlans) {
|
|
303
|
+
validateAuthoritativeRelationPlan(plan, descriptor.tables);
|
|
304
|
+
}
|
|
290
305
|
if (
|
|
291
306
|
!Number.isSafeInteger(options.maxRows) ||
|
|
292
307
|
options.maxRows < 1 ||
|
|
@@ -390,12 +405,29 @@ export function registerRemoteQuery<Params>(
|
|
|
390
405
|
'configured storage does not implement authoritative queries',
|
|
391
406
|
);
|
|
392
407
|
}
|
|
393
|
-
await ctx.storage.ensureSchema(schema);
|
|
394
408
|
const selectedSql = descriptor.sqlFor?.(params) ?? descriptor.sql;
|
|
409
|
+
const plan = descriptor.relationPlans.find(
|
|
410
|
+
(candidate) => candidate.sql === selectedSql,
|
|
411
|
+
);
|
|
412
|
+
if (plan === undefined) {
|
|
413
|
+
throw syncError(
|
|
414
|
+
'operation.invalid_request',
|
|
415
|
+
'selected SQL has no generated relation plan; regenerate queries',
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
await ctx.storage.ensureSchema(schema);
|
|
419
|
+
const prefix = 'SELECT * FROM (';
|
|
395
420
|
let result;
|
|
396
421
|
try {
|
|
397
422
|
result = await ctx.storage.queryAuthoritative(ctx.partition, {
|
|
398
|
-
|
|
423
|
+
plan: {
|
|
424
|
+
sql: `${prefix}${selectedSql}) AS "_syncular_registered_query" LIMIT ?`,
|
|
425
|
+
relations: plan.relations.map((relation) => ({
|
|
426
|
+
...relation,
|
|
427
|
+
start: relation.start + prefix.length,
|
|
428
|
+
end: relation.end + prefix.length,
|
|
429
|
+
})),
|
|
430
|
+
},
|
|
399
431
|
params: [...descriptor.bind(params), options.maxRows + 1],
|
|
400
432
|
tables: descriptor.tables,
|
|
401
433
|
});
|
|
@@ -673,6 +705,7 @@ export async function handleRemoteOperation(
|
|
|
673
705
|
return encodeRemoteOperationError(error, 'operation.invalid_request');
|
|
674
706
|
}
|
|
675
707
|
try {
|
|
708
|
+
await touchAuthenticatedPartition(ctx);
|
|
676
709
|
if (request.clientId.startsWith(REMOTE_COMMAND_CLIENT_ID_PREFIX)) {
|
|
677
710
|
throw syncError(
|
|
678
711
|
'sync.invalid_client_id',
|