@syncular/server 0.22.0 → 0.23.0
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/admin.d.ts +3 -1
- package/dist/admin.js +7 -1
- package/dist/blob-handlers.js +1 -1
- package/dist/context.d.ts +8 -1
- package/dist/d1-storage.d.ts +11 -3
- package/dist/d1-storage.js +84 -2
- package/dist/errors.js +10 -0
- package/dist/handler.js +50 -4
- package/dist/operations.js +3 -3
- package/dist/postgres-storage.d.ts +12 -4
- package/dist/postgres-storage.js +266 -6
- package/dist/push.js +18 -0
- package/dist/readiness.d.ts +9 -2
- package/dist/readiness.js +27 -2
- package/dist/realtime.js +20 -1
- package/dist/segment-download.js +8 -0
- package/dist/sqlite-dialect.d.ts +1 -1
- package/dist/sqlite-dialect.js +15 -0
- package/dist/sqlite-storage.d.ts +11 -3
- package/dist/sqlite-storage.js +296 -33
- package/dist/storage-errors.d.ts +1 -1
- package/dist/storage-errors.js +4 -0
- package/dist/storage.d.ts +127 -2
- package/dist/storage.js +40 -1
- package/package.json +2 -2
- package/src/admin.ts +13 -1
- package/src/blob-handlers.ts +1 -1
- package/src/context.ts +8 -0
- package/src/d1-storage.ts +143 -1
- package/src/errors.ts +10 -0
- package/src/handler.ts +62 -4
- package/src/operations.ts +17 -13
- package/src/postgres-storage.ts +430 -3
- package/src/push.ts +18 -0
- package/src/readiness.ts +39 -4
- package/src/realtime.ts +22 -1
- package/src/segment-download.ts +16 -0
- package/src/sqlite-dialect.ts +15 -0
- package/src/sqlite-storage.ts +439 -36
- package/src/storage-errors.ts +13 -1
- package/src/storage.ts +218 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"verify:node": "cd ../.. && bun build ./packages/server/test/sqlite-runtime/verify-node.mjs --target=node --conditions=bun --outfile=./packages/server/.verify-node.built.mjs && node ./packages/server/.verify-node.built.mjs"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@syncular/core": "0.
|
|
71
|
+
"@syncular/core": "0.23.0"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@electric-sql/pglite": "^0.5.4"
|
package/src/admin.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { DEFAULT_RETENTION, type RetentionPolicy } from './prune';
|
|
|
22
22
|
import { compileSchema, type ServerSchema } from './schema';
|
|
23
23
|
import type { SegmentStore, SegmentStoreStats } from './segment-store';
|
|
24
24
|
import type {
|
|
25
|
+
CheckpointDeclaration,
|
|
25
26
|
ClientRecord,
|
|
26
27
|
CommitMetadata,
|
|
27
28
|
ReactionStatus,
|
|
@@ -157,6 +158,8 @@ export interface SyncularAdminOptions {
|
|
|
157
158
|
* a storage instance that has not served a sync request yet.
|
|
158
159
|
*/
|
|
159
160
|
readonly schema?: ServerSchema;
|
|
161
|
+
/** RFC 0007: declarations `inspectRow` carries into `ensureSchema`. */
|
|
162
|
+
readonly checkpoints?: readonly CheckpointDeclaration[];
|
|
160
163
|
/** The event ring feeding the event tail. Absent ⇒ `events()` is empty. */
|
|
161
164
|
readonly ring?: RingBufferEvents;
|
|
162
165
|
readonly segments?: SegmentStore;
|
|
@@ -218,6 +221,7 @@ function toAdminClient(
|
|
|
218
221
|
export class SyncularAdmin {
|
|
219
222
|
readonly #storage: ServerStorage;
|
|
220
223
|
readonly #schema?: ServerSchema;
|
|
224
|
+
readonly #checkpoints?: readonly CheckpointDeclaration[];
|
|
221
225
|
readonly #ring?: RingBufferEvents;
|
|
222
226
|
readonly #segments?: SegmentStore;
|
|
223
227
|
readonly #blobs?: BlobStore;
|
|
@@ -228,6 +232,8 @@ export class SyncularAdmin {
|
|
|
228
232
|
constructor(options: SyncularAdminOptions) {
|
|
229
233
|
this.#storage = options.storage;
|
|
230
234
|
if (options.schema !== undefined) this.#schema = options.schema;
|
|
235
|
+
if (options.checkpoints !== undefined)
|
|
236
|
+
this.#checkpoints = options.checkpoints;
|
|
231
237
|
if (options.ring !== undefined) this.#ring = options.ring;
|
|
232
238
|
if (options.segments !== undefined) this.#segments = options.segments;
|
|
233
239
|
if (options.blobs !== undefined) this.#blobs = options.blobs;
|
|
@@ -248,6 +254,9 @@ export class SyncularAdmin {
|
|
|
248
254
|
return new SyncularAdmin({
|
|
249
255
|
storage: config.storage,
|
|
250
256
|
schema: config.schema,
|
|
257
|
+
...(config.checkpoints !== undefined
|
|
258
|
+
? { checkpoints: config.checkpoints }
|
|
259
|
+
: {}),
|
|
251
260
|
segments: config.segments,
|
|
252
261
|
...(config.blobs !== undefined ? { blobs: config.blobs } : {}),
|
|
253
262
|
...(config.leases !== undefined ? { leases: config.leases.store } : {}),
|
|
@@ -352,7 +361,10 @@ export class SyncularAdmin {
|
|
|
352
361
|
'storage',
|
|
353
362
|
);
|
|
354
363
|
if (this.#schema !== undefined) {
|
|
355
|
-
await this.#storage.ensureSchema(
|
|
364
|
+
await this.#storage.ensureSchema(
|
|
365
|
+
compileSchema(this.#schema),
|
|
366
|
+
this.#checkpoints,
|
|
367
|
+
);
|
|
356
368
|
}
|
|
357
369
|
const row = await read(partition, table, rowId);
|
|
358
370
|
if (row === undefined) {
|
package/src/blob-handlers.ts
CHANGED
|
@@ -233,7 +233,7 @@ async function downloadBlob(
|
|
|
233
233
|
const schema = compileSchema(ctx.schema);
|
|
234
234
|
// A blob download can hit a fresh storage instance before any sync
|
|
235
235
|
// request has run (Workers): make sure the row tables exist first.
|
|
236
|
-
await ctx.storage.ensureSchema(schema);
|
|
236
|
+
await ctx.storage.ensureSchema(schema, ctx.checkpoints);
|
|
237
237
|
const rows = await listRows.call(ctx.storage, ctx.partition, blobId);
|
|
238
238
|
let authorized = false;
|
|
239
239
|
for (const row of rows) {
|
package/src/context.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from './signed-url';
|
|
20
20
|
import type { SqliteImageBuilder } from './sqlite-image';
|
|
21
21
|
import type {
|
|
22
|
+
CheckpointDeclaration,
|
|
22
23
|
PartitionRegistryEntry,
|
|
23
24
|
ServerStorage,
|
|
24
25
|
StoredCommit,
|
|
@@ -96,6 +97,13 @@ export interface RealtimeNotifier {
|
|
|
96
97
|
export interface SyncServerConfig {
|
|
97
98
|
readonly schema: ServerSchema;
|
|
98
99
|
readonly storage: ServerStorage;
|
|
100
|
+
/**
|
|
101
|
+
* RFC 0007: the backfill checkpoints this host installs. Every readiness
|
|
102
|
+
* path carries this set into `ensureSchema`. A process that omits it
|
|
103
|
+
* declares nothing, and the storage refuses to serve a partition whose
|
|
104
|
+
* stored state shows a checkpoint that is not activated.
|
|
105
|
+
*/
|
|
106
|
+
readonly checkpoints?: readonly CheckpointDeclaration[];
|
|
99
107
|
readonly segments: SegmentStore;
|
|
100
108
|
/**
|
|
101
109
|
* Blob store (§5.9). Absent ⇒ blobs unsupported: a table with a
|
package/src/d1-storage.ts
CHANGED
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
import type {
|
|
91
91
|
AuthoritativeQueryRequest,
|
|
92
92
|
AuthoritativeQueryResult,
|
|
93
|
+
CheckpointDeclaration,
|
|
93
94
|
ClientCursorInfo,
|
|
94
95
|
ClientRecord,
|
|
95
96
|
ClientSubscription,
|
|
@@ -111,8 +112,10 @@ import type {
|
|
|
111
112
|
ScopeActivityQuery,
|
|
112
113
|
ScopeCommitActivity,
|
|
113
114
|
ServerStorage,
|
|
115
|
+
ServeGate,
|
|
114
116
|
StorageTransaction,
|
|
115
117
|
StoredCommit,
|
|
118
|
+
StoredCheckpoint,
|
|
116
119
|
StoredPushResult,
|
|
117
120
|
StoredReaction,
|
|
118
121
|
StoredRow,
|
|
@@ -447,6 +450,31 @@ class D1Transaction implements StorageTransaction {
|
|
|
447
450
|
this.#pushApplyCheckpoint = this.#buffer.length;
|
|
448
451
|
}
|
|
449
452
|
|
|
453
|
+
advanceCheckpoint(
|
|
454
|
+
_name: string,
|
|
455
|
+
_ownerEpoch: number,
|
|
456
|
+
_watermark: number,
|
|
457
|
+
_observedRows: number,
|
|
458
|
+
_nowMs: number,
|
|
459
|
+
): Promise<boolean> {
|
|
460
|
+
// D1 installs no fence and declares no checkpoints in this release.
|
|
461
|
+
return Promise.reject(
|
|
462
|
+
new StorageQueryError('sync.storage.checkpoint_unsupported'),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async readServeGate(_runningSchemaVersion: number): Promise<ServeGate> {
|
|
467
|
+
this.#assertOpen();
|
|
468
|
+
const marker = await this.#db
|
|
469
|
+
.prepare('SELECT schema_version FROM sync_schema_meta WHERE id=1')
|
|
470
|
+
.first<{ schema_version: number }>();
|
|
471
|
+
return {
|
|
472
|
+
storedSchemaVersion: marker?.schema_version ?? 0,
|
|
473
|
+
logEpoch: undefined,
|
|
474
|
+
pending: [],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
450
478
|
async commitRejectedPushResult(
|
|
451
479
|
clientId: string,
|
|
452
480
|
clientCommitId: string,
|
|
@@ -834,7 +862,15 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
834
862
|
return table;
|
|
835
863
|
}
|
|
836
864
|
|
|
837
|
-
async ensureSchema(
|
|
865
|
+
async ensureSchema(
|
|
866
|
+
schema: CompiledSchema,
|
|
867
|
+
checkpoints?: readonly CheckpointDeclaration[],
|
|
868
|
+
): Promise<void> {
|
|
869
|
+
if (checkpoints !== undefined && checkpoints.length > 0) {
|
|
870
|
+
// No database-side fence can be installed atomically with schema
|
|
871
|
+
// visibility here; refuse instead of accepting a weaker guarantee.
|
|
872
|
+
throw new StorageQueryError('sync.storage.checkpoint_unsupported');
|
|
873
|
+
}
|
|
838
874
|
if (this.#schemaVersion === schema.version) {
|
|
839
875
|
await this.#schemaDatabase().batch([]);
|
|
840
876
|
return;
|
|
@@ -1427,7 +1463,11 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
1427
1463
|
async queryAuthoritative(
|
|
1428
1464
|
partition: string,
|
|
1429
1465
|
query: AuthoritativeQueryRequest,
|
|
1466
|
+
_checkpoints?: readonly CheckpointDeclaration[],
|
|
1430
1467
|
): Promise<AuthoritativeQueryResult> {
|
|
1468
|
+
// D1 declares no checkpoints. `#schemaDatabase` prepends a guard that
|
|
1469
|
+
// re-checks the published marker inside this same batch, so condition 2
|
|
1470
|
+
// is already evaluated with the query (§2.4 D1 schema readiness).
|
|
1431
1471
|
const db = this.#schemaDatabase();
|
|
1432
1472
|
if (this.#tables === undefined) {
|
|
1433
1473
|
throw new Error(
|
|
@@ -1591,6 +1631,108 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
1591
1631
|
return row?.seq ?? 0;
|
|
1592
1632
|
}
|
|
1593
1633
|
|
|
1634
|
+
async readCheckpoints(_partition: string): Promise<StoredCheckpoint[]> {
|
|
1635
|
+
// D1 supports no declared backfill checkpoints in this release: migrations
|
|
1636
|
+
// run out of band and no database-side fence can be installed atomically
|
|
1637
|
+
// with schema visibility. No checkpoint row can exist.
|
|
1638
|
+
return [];
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async readServeGate(
|
|
1642
|
+
partition: string,
|
|
1643
|
+
_runningSchemaVersion: number,
|
|
1644
|
+
): Promise<ServeGate> {
|
|
1645
|
+
// D1 has no checkpoints. The published marker is the only gate input;
|
|
1646
|
+
// `#schemaDatabase` already re-checks it per protected batch.
|
|
1647
|
+
const marker = await this.#db
|
|
1648
|
+
.prepare('SELECT schema_version FROM sync_schema_meta WHERE id=1')
|
|
1649
|
+
.first<{ schema_version: number }>();
|
|
1650
|
+
return {
|
|
1651
|
+
storedSchemaVersion: marker?.schema_version ?? 0,
|
|
1652
|
+
logEpoch: await this.getPartitionLogEpoch(partition),
|
|
1653
|
+
pending: [],
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
async declareCheckpoint(
|
|
1658
|
+
_partition: string,
|
|
1659
|
+
_name: string,
|
|
1660
|
+
_schemaVersion: number,
|
|
1661
|
+
_nowMs: number,
|
|
1662
|
+
): Promise<StoredCheckpoint> {
|
|
1663
|
+
throw new StorageQueryError('sync.storage.checkpoint_unsupported');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
async activateCheckpoint(
|
|
1667
|
+
_partition: string,
|
|
1668
|
+
_name: string,
|
|
1669
|
+
_ownerEpoch: number,
|
|
1670
|
+
_watermark: number,
|
|
1671
|
+
_sources: readonly string[],
|
|
1672
|
+
_nowMs: number,
|
|
1673
|
+
): Promise<'activated' | 'stale' | 'unverifiable'> {
|
|
1674
|
+
throw new StorageQueryError('sync.storage.checkpoint_unsupported');
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
async claimCheckpoint(
|
|
1678
|
+
_partition: string,
|
|
1679
|
+
_name: string,
|
|
1680
|
+
_schemaVersion: number,
|
|
1681
|
+
_nowMs: number,
|
|
1682
|
+
): Promise<StoredCheckpoint> {
|
|
1683
|
+
throw new StorageQueryError('sync.storage.checkpoint_unsupported');
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
async advanceCheckpoint(
|
|
1687
|
+
_partition: string,
|
|
1688
|
+
_name: string,
|
|
1689
|
+
_ownerEpoch: number,
|
|
1690
|
+
_watermark: number,
|
|
1691
|
+
_observedRows: number,
|
|
1692
|
+
_nowMs: number,
|
|
1693
|
+
): Promise<boolean> {
|
|
1694
|
+
throw new StorageQueryError('sync.storage.checkpoint_unsupported');
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
async sourceCoverageSeq(
|
|
1698
|
+
partition: string,
|
|
1699
|
+
tables: readonly string[],
|
|
1700
|
+
): Promise<number> {
|
|
1701
|
+
if (tables.length === 0) return 0;
|
|
1702
|
+
const tableParams = tables.map(() => '?').join(',');
|
|
1703
|
+
const row = await this.#db
|
|
1704
|
+
.prepare(
|
|
1705
|
+
`SELECT max(commit_seq) AS seq FROM sync_changes
|
|
1706
|
+
WHERE partition=? AND tbl IN (${tableParams})`,
|
|
1707
|
+
)
|
|
1708
|
+
.bind(partition, ...tables)
|
|
1709
|
+
.first<{ seq: number | null }>();
|
|
1710
|
+
return row?.seq ?? 0;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
async hasSourceChangesAbove(
|
|
1714
|
+
partition: string,
|
|
1715
|
+
tables: readonly string[],
|
|
1716
|
+
seq: number,
|
|
1717
|
+
): Promise<'clean' | 'changed' | 'unverifiable'> {
|
|
1718
|
+
// Read the horizon before the window scan: a horizon past `seq` means the
|
|
1719
|
+
// history that would answer the question is gone, so an empty scan proves
|
|
1720
|
+
// nothing and must not be reported as `clean`.
|
|
1721
|
+
const horizon = await this.getHorizonSeq(partition);
|
|
1722
|
+
if (tables.length === 0) return 'clean';
|
|
1723
|
+
const tableParams = tables.map(() => '?').join(',');
|
|
1724
|
+
const hit = await this.#db
|
|
1725
|
+
.prepare(
|
|
1726
|
+
`SELECT 1 AS hit FROM sync_changes
|
|
1727
|
+
WHERE partition=? AND tbl IN (${tableParams}) AND commit_seq>?
|
|
1728
|
+
LIMIT 1`,
|
|
1729
|
+
)
|
|
1730
|
+
.bind(partition, ...tables, seq)
|
|
1731
|
+
.first<{ hit: number }>();
|
|
1732
|
+
if (hit !== null) return 'changed';
|
|
1733
|
+
return horizon > seq ? 'unverifiable' : 'clean';
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1594
1736
|
async getRow(
|
|
1595
1737
|
partition: string,
|
|
1596
1738
|
table: string,
|
package/src/errors.ts
CHANGED
|
@@ -167,6 +167,16 @@ export const ERROR_CATALOG: Readonly<Record<string, ErrorCatalogEntry>> = {
|
|
|
167
167
|
recommendedAction: 'retryLater',
|
|
168
168
|
httpStatus: 500,
|
|
169
169
|
},
|
|
170
|
+
// §2.4 on a non-D1 backend: the server refuses while a declared backfill
|
|
171
|
+
// checkpoint is incomplete or the database is newer than the running build.
|
|
172
|
+
// Formerly host-only; promoted to the §10.2 catalogue so an in-flight
|
|
173
|
+
// request can be refused with a distinguishable identity.
|
|
174
|
+
'sync.schema_not_ready': {
|
|
175
|
+
category: 'internal',
|
|
176
|
+
retryable: true,
|
|
177
|
+
recommendedAction: 'retryLater',
|
|
178
|
+
httpStatus: 503,
|
|
179
|
+
},
|
|
170
180
|
'sync.too_many_operations': {
|
|
171
181
|
category: 'invalid-request',
|
|
172
182
|
retryable: false,
|
package/src/handler.ts
CHANGED
|
@@ -51,10 +51,17 @@ import {
|
|
|
51
51
|
subscriptionSection,
|
|
52
52
|
} from './pull';
|
|
53
53
|
import { type ProcessedPushCommit, processPushCommitWithTrace } from './push';
|
|
54
|
+
import { serveNotReadyError } from './readiness';
|
|
54
55
|
import type { CompiledSchema } from './schema';
|
|
55
56
|
import { compileSchema } from './schema';
|
|
56
57
|
import { computeEffective, type ResolvedScopes } from './scopes';
|
|
57
|
-
import
|
|
58
|
+
import { StorageQueryError } from './storage-errors';
|
|
59
|
+
import {
|
|
60
|
+
type ClientSubscription,
|
|
61
|
+
type PartitionRegistryEntry,
|
|
62
|
+
serveGateRefusal,
|
|
63
|
+
serveGateTokenChanged,
|
|
64
|
+
} from './storage';
|
|
58
65
|
|
|
59
66
|
interface RequestPlan {
|
|
60
67
|
readonly wireVersion: number;
|
|
@@ -709,13 +716,64 @@ async function createStreamCore(
|
|
|
709
716
|
const schema = compileSchema(ctx.schema);
|
|
710
717
|
// Relational row tables: create/
|
|
711
718
|
// migrate on first contact; memoized per storage instance thereafter.
|
|
712
|
-
|
|
719
|
+
try {
|
|
720
|
+
await ctx.storage.ensureSchema(schema, ctx.checkpoints);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (
|
|
723
|
+
error instanceof StorageQueryError &&
|
|
724
|
+
error.code === 'sync.storage.checkpoint_incomplete'
|
|
725
|
+
) {
|
|
726
|
+
const refusal = serveGateRefusal(
|
|
727
|
+
await ctx.storage.readServeGate(ctx.partition, schema.version),
|
|
728
|
+
schema.version,
|
|
729
|
+
ctx.checkpoints,
|
|
730
|
+
);
|
|
731
|
+
throw refusal !== undefined ? serveNotReadyError(refusal) : error;
|
|
732
|
+
}
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
// RFC 0007 serve gate: re-read stored state on every request so an
|
|
736
|
+
// already-serving process notices a migration or a newly declared
|
|
737
|
+
// checkpoint. Read after `touchAuthenticatedPartition` establishes the
|
|
738
|
+
// partition's log epoch, so the entry token is the epoch this request
|
|
739
|
+
// serves under. Nothing has escaped at this point; the push transaction
|
|
740
|
+
// re-evaluates the gate under the partition lock.
|
|
713
741
|
const registry = await touchAuthenticatedPartition(ctx);
|
|
742
|
+
const gate = await ctx.storage.readServeGate(ctx.partition, schema.version);
|
|
743
|
+
const refusal = serveGateRefusal(gate, schema.version, ctx.checkpoints);
|
|
744
|
+
if (refusal !== undefined) throw serveNotReadyError(refusal);
|
|
714
745
|
const plan = await planRequest(request, ctx, schema, registry);
|
|
715
|
-
if (events === undefined) return streamResponse(plan, ctx, schema);
|
|
716
746
|
const report: RequestReport = { outcome: 'ok' };
|
|
747
|
+
// RFC 0007 read-verify-refuse for the streamed read path: the generator
|
|
748
|
+
// builds the whole response (its data reads) before yielding the first
|
|
749
|
+
// frame, then compares the gate token across those reads. Nothing escapes
|
|
750
|
+
// before the verification, and the buffered size is bounded by the request's
|
|
751
|
+
// pull limits and the server's inline segment cap. Every request verifies,
|
|
752
|
+
// including a mixed push+pull: a write that already applied when the token
|
|
753
|
+
// changed is durable and replayable under the same commit id / idempotency
|
|
754
|
+
// key, so refusing the request is safe (proved in the serve-gate lane).
|
|
755
|
+
const verified = async function* (): AsyncGenerator<Uint8Array> {
|
|
756
|
+
const buffered: Uint8Array[] = [];
|
|
757
|
+
for await (const chunk of streamResponse(plan, ctx, schema, report)) {
|
|
758
|
+
buffered.push(chunk);
|
|
759
|
+
}
|
|
760
|
+
const after = await ctx.storage.readServeGate(
|
|
761
|
+
ctx.partition,
|
|
762
|
+
schema.version,
|
|
763
|
+
);
|
|
764
|
+
const changed = serveGateTokenChanged(gate, after);
|
|
765
|
+
const afterRefusal = serveGateRefusal(
|
|
766
|
+
after,
|
|
767
|
+
schema.version,
|
|
768
|
+
ctx.checkpoints,
|
|
769
|
+
);
|
|
770
|
+
if (afterRefusal !== undefined) throw serveNotReadyError(afterRefusal);
|
|
771
|
+
if (changed !== undefined) throw serveNotReadyError(changed);
|
|
772
|
+
for (const chunk of buffered) yield chunk;
|
|
773
|
+
};
|
|
774
|
+
if (events === undefined) return verified();
|
|
717
775
|
return instrumentedStream(
|
|
718
|
-
|
|
776
|
+
verified(),
|
|
719
777
|
ctx,
|
|
720
778
|
events,
|
|
721
779
|
plan,
|
package/src/operations.ts
CHANGED
|
@@ -415,22 +415,26 @@ export function registerRemoteQuery<Params>(
|
|
|
415
415
|
'selected SQL has no generated relation plan; regenerate queries',
|
|
416
416
|
);
|
|
417
417
|
}
|
|
418
|
-
await ctx.storage.ensureSchema(schema);
|
|
418
|
+
await ctx.storage.ensureSchema(schema, ctx.checkpoints);
|
|
419
419
|
const prefix = 'SELECT * FROM (';
|
|
420
420
|
let result;
|
|
421
421
|
try {
|
|
422
|
-
result = await ctx.storage.queryAuthoritative(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
422
|
+
result = await ctx.storage.queryAuthoritative(
|
|
423
|
+
ctx.partition,
|
|
424
|
+
{
|
|
425
|
+
plan: {
|
|
426
|
+
sql: `${prefix}${selectedSql}) AS "_syncular_registered_query" LIMIT ?`,
|
|
427
|
+
relations: plan.relations.map((relation) => ({
|
|
428
|
+
...relation,
|
|
429
|
+
start: relation.start + prefix.length,
|
|
430
|
+
end: relation.end + prefix.length,
|
|
431
|
+
})),
|
|
432
|
+
},
|
|
433
|
+
params: [...descriptor.bind(params), options.maxRows + 1],
|
|
434
|
+
tables: descriptor.tables,
|
|
430
435
|
},
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
});
|
|
436
|
+
ctx.checkpoints,
|
|
437
|
+
);
|
|
434
438
|
} catch (error) {
|
|
435
439
|
if (error instanceof SyncError) throw error;
|
|
436
440
|
throw syncError(
|
|
@@ -603,7 +607,7 @@ export function registerRemoteCommand<Input>(
|
|
|
603
607
|
}
|
|
604
608
|
const resolved: ResolvedScopes = { ok: true, allowed };
|
|
605
609
|
const schema = compileSchema(ctx.schema);
|
|
606
|
-
await ctx.storage.ensureSchema(schema);
|
|
610
|
+
await ctx.storage.ensureSchema(schema, ctx.checkpoints);
|
|
607
611
|
const processed = await processPushOperationsWithTrace(
|
|
608
612
|
ctx,
|
|
609
613
|
schema,
|