@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/README.md
CHANGED
|
@@ -15,6 +15,11 @@ registry, command mutations use the ordinary serialized push path, and
|
|
|
15
15
|
specified in [`docs/REMOTE.md`](../../docs/REMOTE.md) and the practical setup is
|
|
16
16
|
in the [remote operations guide](https://syncular.dev/guide-remote-operations/).
|
|
17
17
|
|
|
18
|
+
Remote query registration requires generated `relationPlans` for each selected
|
|
19
|
+
SQL statement. Run `syncular generate` before upgrading existing query modules.
|
|
20
|
+
The server uses those boundaries to bind every physical table occurrence to
|
|
21
|
+
the authenticated partition, including quoted self joins and CTE bodies.
|
|
22
|
+
|
|
18
23
|
Application intent belongs in immutable domain event rows written in the same
|
|
19
24
|
commit as the state change. `SyncularServerEvents` below remains operational
|
|
20
25
|
telemetry. See the [domain event guide](https://syncular.dev/guide-domain-events/).
|
|
@@ -105,6 +110,12 @@ Log the cause for operators and stop startup. Do not catch readiness errors in
|
|
|
105
110
|
authentication or convert them to a 401; request-time schema checks are only a
|
|
106
111
|
defensive fallback.
|
|
107
112
|
|
|
113
|
+
After restoring an authoritative database, keep traffic stopped and call
|
|
114
|
+
`rotatePartitionLogEpoch({ storage, partition })` for every restored
|
|
115
|
+
partition. The rotation clears stale client cursors and requires version 2
|
|
116
|
+
clients to reset their server-derived rows while preserving the outbox. Follow
|
|
117
|
+
the complete [backup and restore runbook](https://syncular.dev/server-backup-restore/).
|
|
118
|
+
|
|
108
119
|
## Write validators and recovery metadata
|
|
109
120
|
|
|
110
121
|
`validators` is the server-authoritative seam for row business rules that
|
|
@@ -507,7 +518,7 @@ by design:
|
|
|
507
518
|
| `horizonStatus(partition)` | `{maxCommitSeq, horizonSeq, retainedCommits, activeCursorFloor, recommendedHorizonSeq, recommendation}` — the horizon a prune pass would reach now (§4.6) + a coarse `up-to-date` / `prune-recommended`. |
|
|
508
519
|
| `segmentStats()` / `blobStats(partition)` / `stats(partition)` | Counts/bytes where the stores expose them (segments split rows/sqlite). `undefined` when a store omits `stats()`. |
|
|
509
520
|
| `metrics(partition, {windowMs?, buckets?})` | Ring-derived request/push health over a trailing window (default 5 min): request count/rate, error share, p50/p95 duration, push applied/rejected/conflicted, and per-bucket counts for a sparkline. Zero new server state; all zeros when no ring is wired. |
|
|
510
|
-
| `
|
|
521
|
+
| `listPartitionRegistry()` / `partitionsOverview()` | Every authenticated partition; the registry carries log continuity and last-authenticated time, while the overview adds retained backlog, client counts, and the prune recommendation. |
|
|
511
522
|
| `events({type?, sinceMs?, clientId?, actorId?, limit?})` | The ring tail, newest first. Empty when no ring is wired (`hasEventStream` reports which). |
|
|
512
523
|
| `subscribeEvents(listener)` | Live events as they land in the ring; returns the unsubscribe function, `undefined` when no ring is wired. |
|
|
513
524
|
|
|
@@ -921,10 +932,22 @@ at or below it. Nothing prunes automatically — the host schedules it.
|
|
|
921
932
|
|
|
922
933
|
**When to run.** A periodic job per partition — hourly to daily is the
|
|
923
934
|
sensible range; there is no benefit below the granularity of your
|
|
924
|
-
`activeWindowMs`.
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
935
|
+
`activeWindowMs`. Pass `events` to get `prune.completed` per pass.
|
|
936
|
+
|
|
937
|
+
Pruning verifies the captured log epoch and updates the horizon together with
|
|
938
|
+
commit/change/scope deletion in one transaction. Concurrent passes cannot
|
|
939
|
+
lower the horizon. A retry cleans up eligible records even when the horizon
|
|
940
|
+
already covers them. A restore invalidates a pending pass with
|
|
941
|
+
`sync.storage.prune_epoch_mismatch`; recompute its retention inputs.
|
|
942
|
+
Unregistered partitions cannot be pruned. D1 maintenance enters the owning
|
|
943
|
+
Durable Object's existing write queue through
|
|
944
|
+
`SyncularRealtimeHost.pruneCommitLog`.
|
|
945
|
+
|
|
946
|
+
Custom storage adapters must add `getPartitionLogEpoch(partition)` and replace
|
|
947
|
+
`pruneCommitsThrough(partition, seq)` with
|
|
948
|
+
`pruneCommitsThrough(partition, { logEpoch, throughSeq })`, returning
|
|
949
|
+
`{ previousHorizonSeq, horizonSeq, removedCommits }` from the transaction.
|
|
950
|
+
`setHorizonSeq` remains monotonic and is no longer used by the pruning helper.
|
|
928
951
|
|
|
929
952
|
**The retention floors (§4.6, encoded in `RetentionPolicy`).** The
|
|
930
953
|
horizon never advances past `min(cursor)` of *active* clients — clients
|
|
@@ -1143,3 +1166,23 @@ deterministic in-process sqlite loopback):
|
|
|
1143
1166
|
```sh
|
|
1144
1167
|
SYNCULAR_PG_URL=postgres://user:pass@localhost:5432/db bun run bench
|
|
1145
1168
|
```
|
|
1169
|
+
|
|
1170
|
+
Custom storage adapters must implement
|
|
1171
|
+
`getActiveClientCursorFloor(partition, cutoffMs)`. Return the minimum cursor
|
|
1172
|
+
whose `updatedAtMs >= cutoffMs`, or `null` when no client qualifies. Preserve
|
|
1173
|
+
negative bootstrap cursors. Pruning and admin horizon status use this scalar
|
|
1174
|
+
aggregate; `listClientCursors` remains the explicit listing interface.
|
|
1175
|
+
|
|
1176
|
+
SQLite image builders now return `Promise<Uint8Array>` and receive
|
|
1177
|
+
`rowBatches`, an iterable or async iterable of row arrays. Replace custom
|
|
1178
|
+
builders' `input.rows` loop with `for await (const rows of input.rowBatches)`,
|
|
1179
|
+
insert each batch into the dedicated image database, and count rows during
|
|
1180
|
+
consumption. Write the final row count into `_syncular_segment` before
|
|
1181
|
+
serialization. Await `buildSqliteImage(input)` when calling the built-in
|
|
1182
|
+
Bun or Node builder directly.
|
|
1183
|
+
|
|
1184
|
+
The server shares in-flight builds for the same storage pair and artifact
|
|
1185
|
+
identity after authorization. Sharing is local to one process. Signed URL
|
|
1186
|
+
grants remain per request. The first eligibility probe has at most
|
|
1187
|
+
`limitSnapshotRows + 1` rows; subsequent builder batches have at most 5,000
|
|
1188
|
+
rows. The image database and serialized output still consume memory.
|
package/dist/admin.d.ts
CHANGED
|
@@ -223,11 +223,7 @@ export declare class SyncularAdmin {
|
|
|
223
223
|
readonly windowMs?: number;
|
|
224
224
|
readonly buckets?: number;
|
|
225
225
|
}): AdminMetrics;
|
|
226
|
-
/**
|
|
227
|
-
* Every partition the storage knows (commit log + client records) — the
|
|
228
|
-
* fleet-view backing and the console's partition picker. Fails loud when
|
|
229
|
-
* the backend omits the optional `listPartitions`.
|
|
230
|
-
*/
|
|
226
|
+
/** Every authenticated partition in the storage-backed registry. */
|
|
231
227
|
listPartitions(): Promise<string[]>;
|
|
232
228
|
/**
|
|
233
229
|
* The fleet view: one row per known partition — retained backlog, client
|
package/dist/admin.js
CHANGED
|
@@ -173,11 +173,7 @@ export class SyncularAdmin {
|
|
|
173
173
|
const nowMs = this.#clock();
|
|
174
174
|
const maxCommitSeq = await this.#storage.getMaxCommitSeq(partition);
|
|
175
175
|
const horizonSeq = await this.#storage.getHorizonSeq(partition);
|
|
176
|
-
const
|
|
177
|
-
const activeCursors = cursors
|
|
178
|
-
.filter((c) => c.updatedAtMs >= nowMs - this.#retention.activeWindowMs)
|
|
179
|
-
.map((c) => c.cursor);
|
|
180
|
-
const activeCursorFloor = activeCursors.length > 0 ? Math.min(...activeCursors) : null;
|
|
176
|
+
const activeCursorFloor = await this.#storage.getActiveClientCursorFloor(partition, nowMs - this.#retention.activeWindowMs);
|
|
181
177
|
const cursorFloor = activeCursorFloor ?? Number.MAX_SAFE_INTEGER;
|
|
182
178
|
const forcedSeq = await this.#storage.getCommitSeqBefore(partition, nowMs - this.#retention.ageForceMs);
|
|
183
179
|
const retainFloor = maxCommitSeq - this.#retention.minRetainedCommits;
|
|
@@ -283,14 +279,9 @@ export class SyncularAdmin {
|
|
|
283
279
|
buckets: { widthMs, counts, errors },
|
|
284
280
|
};
|
|
285
281
|
}
|
|
286
|
-
/**
|
|
287
|
-
* Every partition the storage knows (commit log + client records) — the
|
|
288
|
-
* fleet-view backing and the console's partition picker. Fails loud when
|
|
289
|
-
* the backend omits the optional `listPartitions`.
|
|
290
|
-
*/
|
|
282
|
+
/** Every authenticated partition in the storage-backed registry. */
|
|
291
283
|
async listPartitions() {
|
|
292
|
-
|
|
293
|
-
return list();
|
|
284
|
+
return (await this.#storage.listPartitionRegistry()).map((entry) => entry.partition);
|
|
294
285
|
}
|
|
295
286
|
/**
|
|
296
287
|
* The fleet view: one row per known partition — retained backlog, client
|
|
@@ -9,12 +9,20 @@ export interface BoundAuthoritativeQuery {
|
|
|
9
9
|
readonly params: readonly AuthoritativeQueryValue[];
|
|
10
10
|
}
|
|
11
11
|
declare const PARTITION_BIND: unique symbol;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/** Compiler-proven occurrences in the exact generated positional statement. */
|
|
13
|
+
export interface AuthoritativeRelationPlan {
|
|
14
|
+
readonly sql: string;
|
|
15
|
+
readonly relations: readonly {
|
|
16
|
+
readonly table: string;
|
|
17
|
+
readonly start: number;
|
|
18
|
+
readonly end: number;
|
|
19
|
+
readonly alias?: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
/** Validate trusted generated metadata before registration or storage execution. */
|
|
23
|
+
export declare function validateAuthoritativeRelationPlan(plan: AuthoritativeRelationPlan, declaredTables: readonly string[]): void;
|
|
24
|
+
/** Bind every compiler-proven table occurrence to the authenticated partition. */
|
|
25
|
+
export declare function prepareAuthoritativeQuery(plan: AuthoritativeRelationPlan, params: readonly AuthoritativeQueryValue[], declaredTables: readonly string[], tables: ReadonlyMap<string, CompiledTable>): PreparedAuthoritativeQuery;
|
|
18
26
|
export declare function bindAuthoritativePartition(prepared: PreparedAuthoritativeQuery, partition: string): BoundAuthoritativeQuery;
|
|
19
27
|
export declare function postgresPlaceholders(sql: string): string;
|
|
20
28
|
export {};
|
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
import { quoteIdent, SYNC_PARTITION_COLUMN } from './relational-rows.js';
|
|
2
2
|
const PARTITION_BIND = Symbol('syncular.authoritative_partition');
|
|
3
|
-
const RESERVED_ALIAS = new Set([
|
|
4
|
-
'on',
|
|
5
|
-
'where',
|
|
6
|
-
'group',
|
|
7
|
-
'order',
|
|
8
|
-
'inner',
|
|
9
|
-
'left',
|
|
10
|
-
'right',
|
|
11
|
-
'full',
|
|
12
|
-
'outer',
|
|
13
|
-
'natural',
|
|
14
|
-
'join',
|
|
15
|
-
'cross',
|
|
16
|
-
'using',
|
|
17
|
-
'limit',
|
|
18
|
-
'having',
|
|
19
|
-
]);
|
|
20
|
-
const IDENT = '[A-Za-z_][A-Za-z0-9_]*';
|
|
21
|
-
const TABLE_REF_RE = new RegExp(`\\b(FROM|(?:NATURAL\\s+)?(?:(?:LEFT|RIGHT|FULL)(?:\\s+OUTER)?|INNER|CROSS)?\\s*JOIN)\\s+((?:\\(\\s*)*)(${IDENT})(?:\\s+(?:AS\\s+)?((?!(?:${[...RESERVED_ALIAS].join('|')})\\b)${IDENT}))?`, 'gi');
|
|
22
3
|
function protectedSqlEnd(sql, index) {
|
|
23
4
|
const char = sql[index];
|
|
24
5
|
const next = sql[index + 1];
|
|
@@ -48,97 +29,94 @@ function protectedSqlEnd(sql, index) {
|
|
|
48
29
|
}
|
|
49
30
|
return undefined;
|
|
50
31
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
out += sql[index];
|
|
58
|
-
else
|
|
59
|
-
out += sql.slice(index, end).replace(/[^\n]/g, ' ');
|
|
60
|
-
index = end ?? index + 1;
|
|
32
|
+
/** Validate trusted generated metadata before registration or storage execution. */
|
|
33
|
+
export function validateAuthoritativeRelationPlan(plan, declaredTables) {
|
|
34
|
+
if (plan === undefined ||
|
|
35
|
+
typeof plan.sql !== 'string' ||
|
|
36
|
+
!Array.isArray(plan.relations)) {
|
|
37
|
+
throw new Error('registered query requires generated relation plans; regenerate queries');
|
|
61
38
|
}
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Turn generated local SQL into a partition-local authoritative statement.
|
|
66
|
-
* Only relations declared by the generated descriptor are rewritten. Values
|
|
67
|
-
* remain parameters; request data is never interpolated into SQL.
|
|
68
|
-
*/
|
|
69
|
-
export function prepareAuthoritativeQuery(sql, params, declaredTables, tables) {
|
|
70
|
-
if (maskedSql(sql).includes(';'))
|
|
71
|
-
throw new Error('registered query must be one SELECT');
|
|
39
|
+
const sql = plan.sql;
|
|
72
40
|
const declared = new Set(declaredTables);
|
|
73
|
-
const masked = maskedSql(sql);
|
|
74
|
-
const replacements = [];
|
|
75
41
|
const found = new Set();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
42
|
+
let previousEnd = 0;
|
|
43
|
+
for (const relation of plan.relations) {
|
|
44
|
+
if (!Number.isSafeInteger(relation.start) ||
|
|
45
|
+
!Number.isSafeInteger(relation.end) ||
|
|
46
|
+
relation.start < previousEnd ||
|
|
47
|
+
relation.end <= relation.start ||
|
|
48
|
+
relation.end > sql.length) {
|
|
49
|
+
throw new Error('registered query relation boundaries do not match its SQL; regenerate queries');
|
|
83
50
|
}
|
|
84
|
-
|
|
85
|
-
|
|
51
|
+
previousEnd = relation.end;
|
|
52
|
+
if (!declared.has(relation.table)) {
|
|
53
|
+
throw new Error('registered query table metadata does not match its SQL');
|
|
86
54
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
55
|
+
const spelling = sql.slice(relation.start, relation.end);
|
|
56
|
+
const quote = spelling[0];
|
|
57
|
+
const name = quote === '['
|
|
58
|
+
? spelling.slice(1, -1)
|
|
59
|
+
: quote === '"' || quote === '`'
|
|
60
|
+
? spelling
|
|
61
|
+
.slice(1, -1)
|
|
62
|
+
.split(quote + quote)
|
|
63
|
+
.join(quote)
|
|
64
|
+
: spelling;
|
|
65
|
+
if (name.toLowerCase() !== relation.table.toLowerCase()) {
|
|
66
|
+
throw new Error('registered query relation name does not match its SQL; regenerate queries');
|
|
90
67
|
}
|
|
91
|
-
|
|
92
|
-
const afterOperator = match[1].length;
|
|
93
|
-
const relative = match[0]
|
|
94
|
-
.toLowerCase()
|
|
95
|
-
.indexOf(rawTable.toLowerCase(), afterOperator);
|
|
96
|
-
const start = matchStart + relative;
|
|
97
|
-
const projection = table.columns.map((column) => quoteIdent(column.name));
|
|
98
|
-
replacements.push({
|
|
99
|
-
start,
|
|
100
|
-
end: start + rawTable.length,
|
|
101
|
-
text: `(SELECT ${projection.join(', ')} FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=/*syncular_partition*/?)${alias === undefined ? ` AS ${quoteIdent(table.name)}` : ''}`,
|
|
102
|
-
});
|
|
103
|
-
found.add(table.name);
|
|
68
|
+
found.add(relation.table);
|
|
104
69
|
}
|
|
105
70
|
if (found.size !== declared.size ||
|
|
106
71
|
[...declared].some((table) => !found.has(table))) {
|
|
107
72
|
throw new Error('registered query table metadata does not match its SQL');
|
|
108
73
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
74
|
+
}
|
|
75
|
+
/** Bind every compiler-proven table occurrence to the authenticated partition. */
|
|
76
|
+
export function prepareAuthoritativeQuery(plan, params, declaredTables, tables) {
|
|
77
|
+
validateAuthoritativeRelationPlan(plan, declaredTables);
|
|
78
|
+
const sql = plan.sql;
|
|
79
|
+
const replacements = plan.relations.map((relation) => {
|
|
80
|
+
const table = tables.get(relation.table);
|
|
81
|
+
if (table === undefined)
|
|
82
|
+
throw new Error('registered query targets an unknown table');
|
|
83
|
+
if (!table.materialize)
|
|
84
|
+
throw new Error('registered query targets a non-materialized table');
|
|
85
|
+
return {
|
|
86
|
+
start: relation.start,
|
|
87
|
+
end: relation.end,
|
|
88
|
+
text: `(SELECT ${table.columns.map((column) => quoteIdent(column.name)).join(', ')} FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=?)${relation.alias === undefined ? ` AS ${quoteIdent(table.name)}` : ''}`,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
116
91
|
const bound = [];
|
|
117
92
|
let anonymousIndex = 0;
|
|
118
93
|
let rendered = '';
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
94
|
+
let nextRelation = 0;
|
|
95
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
96
|
+
const replacement = replacements[nextRelation];
|
|
97
|
+
if (replacement?.start === index) {
|
|
98
|
+
rendered += replacement.text;
|
|
122
99
|
bound.push(PARTITION_BIND);
|
|
123
|
-
index
|
|
100
|
+
index = replacement.end - 1;
|
|
101
|
+
nextRelation += 1;
|
|
124
102
|
continue;
|
|
125
103
|
}
|
|
126
|
-
const protectedEnd = protectedSqlEnd(
|
|
104
|
+
const protectedEnd = protectedSqlEnd(sql, index);
|
|
127
105
|
if (protectedEnd !== undefined) {
|
|
128
|
-
rendered +=
|
|
106
|
+
rendered += sql.slice(index, protectedEnd);
|
|
129
107
|
index = protectedEnd - 1;
|
|
130
108
|
continue;
|
|
131
109
|
}
|
|
132
|
-
const char =
|
|
110
|
+
const char = sql[index];
|
|
133
111
|
if (char !== '?') {
|
|
134
112
|
rendered += char;
|
|
135
113
|
continue;
|
|
136
114
|
}
|
|
137
115
|
let end = index + 1;
|
|
138
|
-
while (end <
|
|
116
|
+
while (end < sql.length && /[0-9]/.test(sql[end])) {
|
|
139
117
|
end += 1;
|
|
140
118
|
}
|
|
141
|
-
const numbered =
|
|
119
|
+
const numbered = sql.slice(index + 1, end);
|
|
142
120
|
const parameterIndex = numbered.length > 0
|
|
143
121
|
? Number.parseInt(numbered, 10) - 1
|
|
144
122
|
: anonymousIndex++;
|
package/dist/blob-handlers.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* adapters map these onto `PUT/GET <mount>/blobs/{blobId}`.
|
|
8
8
|
*/
|
|
9
9
|
import { blobIdFor, isBlobId } from './blob-store.js';
|
|
10
|
-
import { clockOf, RESOLVER_OUTAGE } from './context.js';
|
|
10
|
+
import { clockOf, RESOLVER_OUTAGE, touchAuthenticatedPartition, } from './context.js';
|
|
11
11
|
import { SyncError, syncError } from './errors.js';
|
|
12
12
|
import { emitEvent } from './events.js';
|
|
13
13
|
import { compileSchema } from './schema.js';
|
|
@@ -16,6 +16,7 @@ import { issueBlobUploadUrl, issueBlobUrl } from './signed-url.js';
|
|
|
16
16
|
const DEFAULT_MAX_BLOB_BYTES = 64 * 1024 * 1024;
|
|
17
17
|
/** `PUT <mount>/blobs/{blobId}` (§5.9.3). Host auth is the adapter's job. */
|
|
18
18
|
export async function handleBlobUpload(ctx, request) {
|
|
19
|
+
await touchAuthenticatedPartition(ctx);
|
|
19
20
|
const store = ctx.blobs;
|
|
20
21
|
if (store === undefined) {
|
|
21
22
|
throw syncError('blob.not_found', 'this server has no blob store (§5.9)');
|
|
@@ -50,6 +51,7 @@ export async function handleBlobUpload(ctx, request) {
|
|
|
50
51
|
* reference index passes the §3.4 scope check for the actor.
|
|
51
52
|
*/
|
|
52
53
|
export async function handleBlobDownload(ctx, blobId) {
|
|
54
|
+
await touchAuthenticatedPartition(ctx);
|
|
53
55
|
const events = ctx.events;
|
|
54
56
|
if (events === undefined)
|
|
55
57
|
return downloadBlob(ctx, blobId);
|
|
@@ -213,6 +215,7 @@ async function downloadBlob(ctx, blobId) {
|
|
|
213
215
|
* result when no presigned-upload store is configured (client streams direct).
|
|
214
216
|
*/
|
|
215
217
|
export async function handleBlobUploadGrant(ctx, request) {
|
|
218
|
+
await touchAuthenticatedPartition(ctx);
|
|
216
219
|
const store = ctx.blobs;
|
|
217
220
|
if (store === undefined) {
|
|
218
221
|
throw syncError('blob.not_found', 'this server has no blob store (§5.9)');
|
package/dist/context.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { ServerSchema } from './schema.js';
|
|
|
14
14
|
import type { SegmentStore } from './segment-store.js';
|
|
15
15
|
import type { BlobPresignConfig, BlobUploadPresignConfig, SegmentUrlConfig } from './signed-url.js';
|
|
16
16
|
import type { SqliteImageBuilder } from './sqlite-image.js';
|
|
17
|
-
import type { ServerStorage, StoredCommit } from './storage.js';
|
|
17
|
+
import type { PartitionRegistryEntry, ServerStorage, StoredCommit } from './storage.js';
|
|
18
18
|
import type { CommitValidator, ValidatorRegistry } from './validate.js';
|
|
19
19
|
/** SSP2 body content type (§1.1). */
|
|
20
20
|
export declare const SSP2_CONTENT_TYPE = "application/vnd.syncular.sync.v2";
|
|
@@ -166,3 +166,5 @@ export interface SyncRequestContext extends SyncServerConfig {
|
|
|
166
166
|
}
|
|
167
167
|
export declare function clockOf(ctx: SyncServerConfig): () => number;
|
|
168
168
|
export declare function limitsOf(ctx: SyncServerConfig): ServerLimits;
|
|
169
|
+
/** Refresh the registry after host authentication and return log continuity. */
|
|
170
|
+
export declare function touchAuthenticatedPartition(ctx: SyncRequestContext): Promise<PartitionRegistryEntry>;
|
package/dist/context.js
CHANGED
|
@@ -21,3 +21,7 @@ export function clockOf(ctx) {
|
|
|
21
21
|
export function limitsOf(ctx) {
|
|
22
22
|
return { ...DEFAULT_LIMITS, ...ctx.limits };
|
|
23
23
|
}
|
|
24
|
+
/** Refresh the registry after host authentication and return log continuity. */
|
|
25
|
+
export function touchAuthenticatedPartition(ctx) {
|
|
26
|
+
return ctx.storage.touchPartition(ctx.partition, clockOf(ctx)(), crypto.randomUUID());
|
|
27
|
+
}
|
package/dist/d1-storage.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { CommitPruneQuery, CommitPruneResult } from './storage.js';
|
|
1
2
|
import type { CompiledSchema, CompiledTable } from './schema.js';
|
|
2
|
-
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
3
|
+
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PartitionRegistryEntry, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
3
4
|
export interface D1PreparedStatement {
|
|
4
5
|
bind(...values: unknown[]): D1PreparedStatement;
|
|
5
6
|
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
@@ -36,12 +37,16 @@ export declare class D1ServerStorage implements ServerStorage {
|
|
|
36
37
|
/** Resolve a table's compiled schema; row operations require `ensureSchema`. */
|
|
37
38
|
table(name: string): CompiledTable;
|
|
38
39
|
ensureSchema(schema: CompiledSchema): Promise<void>;
|
|
40
|
+
touchPartition(partition: string, authenticatedAtMs: number, initialLogEpoch: string): Promise<PartitionRegistryEntry>;
|
|
41
|
+
rotatePartitionLogEpoch(partition: string, logEpoch: string, authenticatedAtMs: number): Promise<PartitionRegistryEntry>;
|
|
42
|
+
listPartitionRegistry(): Promise<PartitionRegistryEntry[]>;
|
|
39
43
|
begin(partition: string): Promise<StorageTransaction>;
|
|
40
44
|
getMaxCommitSeq(partition: string): Promise<number>;
|
|
41
45
|
queryAuthoritative(partition: string, query: AuthoritativeQueryRequest): Promise<AuthoritativeQueryResult>;
|
|
46
|
+
getPartitionLogEpoch(partition: string): Promise<string | undefined>;
|
|
42
47
|
getHorizonSeq(partition: string): Promise<number>;
|
|
43
48
|
setHorizonSeq(partition: string, seq: number): Promise<void>;
|
|
44
|
-
pruneCommitsThrough(partition: string,
|
|
49
|
+
pruneCommitsThrough(partition: string, query: CommitPruneQuery): Promise<CommitPruneResult>;
|
|
45
50
|
getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise<number>;
|
|
46
51
|
getRow(partition: string, table: string, rowId: string): Promise<StoredRow | undefined>;
|
|
47
52
|
getPushResult(partition: string, clientId: string, clientCommitId: string): Promise<StoredPushResult | undefined>;
|
|
@@ -58,6 +63,7 @@ export declare class D1ServerStorage implements ServerStorage {
|
|
|
58
63
|
scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise<StoredRow[]>;
|
|
59
64
|
getClientRecord(partition: string, clientId: string): Promise<ClientRecord | undefined>;
|
|
60
65
|
putClientRecord(partition: string, record: ClientRecord): Promise<void>;
|
|
66
|
+
getActiveClientCursorFloor(partition: string, cutoffMs: number): Promise<number | null>;
|
|
61
67
|
listClientCursors(partition: string): Promise<ClientCursorInfo[]>;
|
|
62
68
|
listRowsReferencingBlob(partition: string, blobId: string): Promise<{
|
|
63
69
|
readonly table: string;
|