@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/dist/realtime.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the `sync` wake-up (§8.3).
|
|
15
15
|
*/
|
|
16
16
|
import { DecodeError, decodeMessage, encodeMessage, encodePresenceError, encodePresenceFanout, MessageStreamScanner, PROTOCOL_WIRE_VERSION, parseRealtimePresencePublish, REALTIME_TAG_DELTA, REALTIME_TAG_ROUND, } from '@syncular/core';
|
|
17
|
-
import { REMOTE_COMMAND_CLIENT_ID_PREFIX, RESOLVER_OUTAGE } from './context.js';
|
|
17
|
+
import { REMOTE_COMMAND_CLIENT_ID_PREFIX, RESOLVER_OUTAGE, touchAuthenticatedPartition, } from './context.js';
|
|
18
18
|
import { SyncError, syncError } from './errors.js';
|
|
19
19
|
import { emitEvent } from './events.js';
|
|
20
20
|
import { createSyncResponseStream } from './handler.js';
|
|
@@ -120,6 +120,9 @@ export class RealtimeSession {
|
|
|
120
120
|
partition;
|
|
121
121
|
actorId;
|
|
122
122
|
clientId;
|
|
123
|
+
logEpoch;
|
|
124
|
+
/** Response/delta layout selected by the most recent socket round. */
|
|
125
|
+
wireVersion;
|
|
123
126
|
/** Highest contiguously applied commitSeq acknowledged by the client. */
|
|
124
127
|
cursor;
|
|
125
128
|
/** Suppress deltas until the client catches up via pull + ack (§8.2). */
|
|
@@ -144,11 +147,13 @@ export class RealtimeSession {
|
|
|
144
147
|
/** §8.6.4 rate cap: per-scope-key last-fanout time + a pending latest
|
|
145
148
|
* document coalesced while over the cap. */
|
|
146
149
|
#presenceRate = new Map();
|
|
147
|
-
constructor(hub, options, registrations, cursor, latestSeq, clock, maxDeltaBytes, storage, events) {
|
|
150
|
+
constructor(hub, options, registrations, cursor, latestSeq, clock, maxDeltaBytes, storage, logEpoch, wireVersion, events) {
|
|
148
151
|
this.sessionId = crypto.randomUUID();
|
|
149
152
|
this.partition = options.partition;
|
|
150
153
|
this.actorId = options.actorId;
|
|
151
154
|
this.clientId = options.clientId;
|
|
155
|
+
this.logEpoch = logEpoch;
|
|
156
|
+
this.wireVersion = wireVersion;
|
|
152
157
|
this.cursor = cursor;
|
|
153
158
|
this.lastKnownSeq = latestSeq;
|
|
154
159
|
this.wakePending = cursor < latestSeq;
|
|
@@ -376,6 +381,10 @@ export class RealtimeSession {
|
|
|
376
381
|
this.#activeRound = undefined;
|
|
377
382
|
};
|
|
378
383
|
try {
|
|
384
|
+
const requestedWireVersion = (requestBytes[4] ?? 0) | ((requestBytes[5] ?? 0) << 8);
|
|
385
|
+
if (requestedWireVersion === 1 || requestedWireVersion === 2) {
|
|
386
|
+
this.wireVersion = requestedWireVersion;
|
|
387
|
+
}
|
|
379
388
|
let stream;
|
|
380
389
|
try {
|
|
381
390
|
// §8.7: the round's clientId must match the connection's —
|
|
@@ -398,7 +407,7 @@ export class RealtimeSession {
|
|
|
398
407
|
? error
|
|
399
408
|
: syncError(error.code, error.message);
|
|
400
409
|
finishRound(); // END is in this one chunk
|
|
401
|
-
await this.#sendRoundChunk(errorResponseBytes(sync));
|
|
410
|
+
await this.#sendRoundChunk(errorResponseBytes(sync, this.wireVersion, this.logEpoch));
|
|
402
411
|
return;
|
|
403
412
|
}
|
|
404
413
|
throw error;
|
|
@@ -550,7 +559,14 @@ export class RealtimeSession {
|
|
|
550
559
|
this.sendWake('catchup-required');
|
|
551
560
|
return;
|
|
552
561
|
}
|
|
553
|
-
const frames = [
|
|
562
|
+
const frames = [
|
|
563
|
+
{
|
|
564
|
+
type: 'RESP_HEADER',
|
|
565
|
+
...(this.wireVersion >= 2
|
|
566
|
+
? { logEpoch: this.logEpoch, resetRequired: false }
|
|
567
|
+
: {}),
|
|
568
|
+
},
|
|
569
|
+
];
|
|
554
570
|
for (const section of sections) {
|
|
555
571
|
frames.push({
|
|
556
572
|
type: 'SUB_START',
|
|
@@ -571,7 +587,7 @@ export class RealtimeSession {
|
|
|
571
587
|
frames.push({ type: 'SUB_END', nextCursor: commit.commitSeq });
|
|
572
588
|
}
|
|
573
589
|
const bytes = encodeMessage({
|
|
574
|
-
wireVersion:
|
|
590
|
+
wireVersion: this.wireVersion,
|
|
575
591
|
msgKind: 'response',
|
|
576
592
|
frames,
|
|
577
593
|
});
|
|
@@ -770,6 +786,11 @@ export class RealtimeHub {
|
|
|
770
786
|
async connect(options) {
|
|
771
787
|
const { storage } = this.#config;
|
|
772
788
|
const clock = this.#config.clock ?? Date.now;
|
|
789
|
+
const registry = await touchAuthenticatedPartition({
|
|
790
|
+
...this.#config,
|
|
791
|
+
partition: options.partition,
|
|
792
|
+
actorId: options.actorId,
|
|
793
|
+
});
|
|
773
794
|
if (options.clientId.startsWith(REMOTE_COMMAND_CLIENT_ID_PREFIX)) {
|
|
774
795
|
throw syncError('sync.invalid_client_id', 'clientId uses a reserved server-command namespace (§1.5)');
|
|
775
796
|
}
|
|
@@ -780,7 +801,7 @@ export class RealtimeHub {
|
|
|
780
801
|
const registrations = await this.loadRegistrations(options.partition, options.actorId, options.clientId);
|
|
781
802
|
const latestSeq = await storage.getMaxCommitSeq(options.partition);
|
|
782
803
|
const cursor = record?.cursor ?? -1;
|
|
783
|
-
const session = new RealtimeSession(this, options, registrations, cursor, latestSeq, clock, this.#config.maxDeltaBytes ?? DEFAULT_MAX_DELTA_BYTES, storage, this.#config.events);
|
|
804
|
+
const session = new RealtimeSession(this, options, registrations, cursor, latestSeq, clock, this.#config.maxDeltaBytes ?? DEFAULT_MAX_DELTA_BYTES, storage, registry.logEpoch, record?.wireVersion ?? PROTOCOL_WIRE_VERSION, this.#config.events);
|
|
784
805
|
this.#sessions.add(session);
|
|
785
806
|
const helloResult = options.send(JSON.stringify({
|
|
786
807
|
event: 'hello',
|
|
@@ -860,12 +881,15 @@ export function createRealtimeHub(config) {
|
|
|
860
881
|
/** §8.7 failures: the socket has no HTTP status surface, so a
|
|
861
882
|
* request-level failure becomes a minimal RESP_HEADER/ERROR/END
|
|
862
883
|
* response message delivered as the round's response stream. */
|
|
863
|
-
function errorResponseBytes(error) {
|
|
884
|
+
function errorResponseBytes(error, wireVersion, logEpoch) {
|
|
864
885
|
return encodeMessage({
|
|
865
|
-
wireVersion
|
|
886
|
+
wireVersion,
|
|
866
887
|
msgKind: 'response',
|
|
867
888
|
frames: [
|
|
868
|
-
{
|
|
889
|
+
{
|
|
890
|
+
type: 'RESP_HEADER',
|
|
891
|
+
...(wireVersion >= 2 ? { logEpoch, resetRequired: false } : {}),
|
|
892
|
+
},
|
|
869
893
|
{
|
|
870
894
|
type: 'ERROR',
|
|
871
895
|
code: error.code,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PartitionRegistryEntry, ServerStorage } from './storage.js';
|
|
2
|
+
export interface RotatePartitionLogEpochOptions {
|
|
3
|
+
readonly storage: ServerStorage;
|
|
4
|
+
readonly partition: string;
|
|
5
|
+
readonly nowMs?: number;
|
|
6
|
+
/** Deterministic operator/test override. Defaults to a random UUID. */
|
|
7
|
+
readonly logEpoch?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fence clients from a prior authoritative database timeline after restore.
|
|
11
|
+
* Call while traffic and realtime delivery remain stopped (§2.1).
|
|
12
|
+
*/
|
|
13
|
+
export declare function rotatePartitionLogEpoch(options: RotatePartitionLogEpochOptions): Promise<PartitionRegistryEntry>;
|
package/dist/restore.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fence clients from a prior authoritative database timeline after restore.
|
|
3
|
+
* Call while traffic and realtime delivery remain stopped (§2.1).
|
|
4
|
+
*/
|
|
5
|
+
export function rotatePartitionLogEpoch(options) {
|
|
6
|
+
if (options.partition.length === 0) {
|
|
7
|
+
throw new TypeError('partition must be non-empty');
|
|
8
|
+
}
|
|
9
|
+
const logEpoch = options.logEpoch ?? crypto.randomUUID();
|
|
10
|
+
if (logEpoch.length === 0)
|
|
11
|
+
throw new TypeError('logEpoch must be non-empty');
|
|
12
|
+
return options.storage.rotatePartitionLogEpoch(options.partition, logEpoch, options.nowMs ?? Date.now());
|
|
13
|
+
}
|
package/dist/s3-segment-store.js
CHANGED
|
@@ -70,7 +70,13 @@ function parseRecordJson(json, source) {
|
|
|
70
70
|
throw new Error(`S3SegmentStore: corrupt record in ${source}`);
|
|
71
71
|
}
|
|
72
72
|
const r = parsed;
|
|
73
|
-
const strings = [
|
|
73
|
+
const strings = [
|
|
74
|
+
'segmentId',
|
|
75
|
+
'partition',
|
|
76
|
+
'logEpoch',
|
|
77
|
+
'table',
|
|
78
|
+
'scopeDigest',
|
|
79
|
+
];
|
|
74
80
|
const numbers = [
|
|
75
81
|
'schemaVersion',
|
|
76
82
|
'asOfCommitSeq',
|
|
@@ -99,6 +105,7 @@ function parseRecordJson(json, source) {
|
|
|
99
105
|
return {
|
|
100
106
|
segmentId: r.segmentId,
|
|
101
107
|
partition: r.partition,
|
|
108
|
+
logEpoch: r.logEpoch,
|
|
102
109
|
table: r.table,
|
|
103
110
|
schemaVersion: r.schemaVersion,
|
|
104
111
|
mediaType: r.mediaType,
|
|
@@ -136,6 +143,7 @@ export class S3SegmentStore {
|
|
|
136
143
|
async #findKeyFor(key) {
|
|
137
144
|
const canonical = JSON.stringify([
|
|
138
145
|
key.partition,
|
|
146
|
+
key.logEpoch,
|
|
139
147
|
key.table,
|
|
140
148
|
key.schemaVersion,
|
|
141
149
|
key.mediaType,
|
|
@@ -304,6 +312,7 @@ export class S3SegmentStore {
|
|
|
304
312
|
return undefined;
|
|
305
313
|
const record = parseRecordJson(await response.text(), 'reuse pointer');
|
|
306
314
|
if (record.partition !== key.partition ||
|
|
315
|
+
record.logEpoch !== key.logEpoch ||
|
|
307
316
|
record.table !== key.table ||
|
|
308
317
|
record.schemaVersion !== key.schemaVersion ||
|
|
309
318
|
record.mediaType !== key.mediaType ||
|
package/dist/seed.js
CHANGED
|
@@ -105,8 +105,42 @@ export async function seedMutations(config, target, mutations) {
|
|
|
105
105
|
payload: encodeRow(table.columns, values),
|
|
106
106
|
};
|
|
107
107
|
});
|
|
108
|
+
const requestContext = {
|
|
109
|
+
...config,
|
|
110
|
+
partition: target.partition,
|
|
111
|
+
actorId: target.actorId,
|
|
112
|
+
};
|
|
113
|
+
const acquisition = decodeMessage(await handleSyncRequest(encodeMessage({
|
|
114
|
+
wireVersion: PROTOCOL_WIRE_VERSION,
|
|
115
|
+
msgKind: 'request',
|
|
116
|
+
frames: [
|
|
117
|
+
{
|
|
118
|
+
type: 'REQ_HEADER',
|
|
119
|
+
clientId,
|
|
120
|
+
schemaVersion: config.schema.version,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'PULL_HEADER',
|
|
124
|
+
limitCommits: 0,
|
|
125
|
+
limitSnapshotRows: 0,
|
|
126
|
+
maxSnapshotPages: 0,
|
|
127
|
+
accept: 0b0011,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}), requestContext));
|
|
131
|
+
const acquisitionHeader = acquisition.frames[0];
|
|
132
|
+
if (acquisition.msgKind !== 'response' ||
|
|
133
|
+
acquisitionHeader?.type !== 'RESP_HEADER' ||
|
|
134
|
+
acquisitionHeader.logEpoch === undefined) {
|
|
135
|
+
throw new SyncError('sync.invalid_request', 'seedMutations: epoch acquisition returned an invalid response');
|
|
136
|
+
}
|
|
108
137
|
const frames = [
|
|
109
|
-
{
|
|
138
|
+
{
|
|
139
|
+
type: 'REQ_HEADER',
|
|
140
|
+
clientId,
|
|
141
|
+
schemaVersion: config.schema.version,
|
|
142
|
+
logEpoch: acquisitionHeader.logEpoch,
|
|
143
|
+
},
|
|
110
144
|
{ type: 'PUSH_COMMIT', clientCommitId, operations },
|
|
111
145
|
// A pull that asks for nothing: the round exists for its push half.
|
|
112
146
|
{
|
|
@@ -132,9 +166,7 @@ export async function seedMutations(config, target, mutations) {
|
|
|
132
166
|
msgKind: 'request',
|
|
133
167
|
frames,
|
|
134
168
|
}), {
|
|
135
|
-
...
|
|
136
|
-
partition: target.partition,
|
|
137
|
-
actorId: target.actorId,
|
|
169
|
+
...requestContext,
|
|
138
170
|
events: config.events === undefined
|
|
139
171
|
? capture
|
|
140
172
|
: composeEvents(config.events, capture),
|
package/dist/segment-download.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clockOf, RESOLVER_OUTAGE } from './context.js';
|
|
1
|
+
import { clockOf, RESOLVER_OUTAGE, touchAuthenticatedPartition, } from './context.js';
|
|
2
2
|
import { SyncError, syncError } from './errors.js';
|
|
3
3
|
import { emitEvent } from './events.js';
|
|
4
4
|
import { compileSchema } from './schema.js';
|
|
@@ -60,8 +60,11 @@ export async function handleSegmentDownload(ctx, request) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
async function downloadSegment(ctx, request) {
|
|
63
|
+
const registry = await touchAuthenticatedPartition(ctx);
|
|
63
64
|
const entry = await ctx.segments.get(request.segmentId);
|
|
64
|
-
if (entry === undefined ||
|
|
65
|
+
if (entry === undefined ||
|
|
66
|
+
entry.record.partition !== ctx.partition ||
|
|
67
|
+
entry.record.logEpoch !== registry.logEpoch) {
|
|
65
68
|
throw syncError('sync.not_found', 'unknown segment (§5.5)');
|
|
66
69
|
}
|
|
67
70
|
if (entry.record.expiresAtMs <= clockOf(ctx)()) {
|
package/dist/segment-store.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare const DEFAULT_SEGMENT_TTL_MS: number;
|
|
2
2
|
export interface SegmentMetadata {
|
|
3
3
|
readonly partition: string;
|
|
4
|
+
/** Partition log continuity that produced these bytes (§2.1). */
|
|
5
|
+
readonly logEpoch: string;
|
|
4
6
|
readonly table: string;
|
|
5
7
|
readonly schemaVersion: number;
|
|
6
8
|
readonly mediaType: 'rows' | 'sqlite';
|
|
@@ -23,6 +25,7 @@ export interface SegmentRecord extends SegmentMetadata {
|
|
|
23
25
|
*/
|
|
24
26
|
export interface SegmentFindKey {
|
|
25
27
|
readonly partition: string;
|
|
28
|
+
readonly logEpoch: string;
|
|
26
29
|
readonly table: string;
|
|
27
30
|
readonly schemaVersion: number;
|
|
28
31
|
readonly mediaType: 'rows' | 'sqlite';
|
package/dist/segment-store.js
CHANGED
|
@@ -35,6 +35,7 @@ export class MemorySegmentStore {
|
|
|
35
35
|
async find(key, nowMs) {
|
|
36
36
|
for (const { record } of this.#entries.values()) {
|
|
37
37
|
if (record.partition === key.partition &&
|
|
38
|
+
record.logEpoch === key.logEpoch &&
|
|
38
39
|
record.table === key.table &&
|
|
39
40
|
record.schemaVersion === key.schemaVersion &&
|
|
40
41
|
record.mediaType === key.mediaType &&
|
|
@@ -2,7 +2,8 @@ import { Database } from 'bun:sqlite';
|
|
|
2
2
|
import type { SqliteDatabase, SqliteRunResult, SqliteStatement, SqliteValue } from './sqlite-driver.js';
|
|
3
3
|
export declare class BunSqliteDatabase implements SqliteDatabase {
|
|
4
4
|
readonly native: Database;
|
|
5
|
-
constructor(
|
|
5
|
+
constructor(value?: string | Database);
|
|
6
|
+
static deserialize(bytes: Uint8Array): BunSqliteDatabase;
|
|
6
7
|
exec(sql: string): void;
|
|
7
8
|
run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
|
|
8
9
|
query<Row, Params extends readonly SqliteValue[]>(sql: string): SqliteStatement<Row, Params>;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
export class BunSqliteDatabase {
|
|
3
3
|
native;
|
|
4
|
-
constructor(
|
|
5
|
-
this.native = new Database(
|
|
4
|
+
constructor(value = ':memory:') {
|
|
5
|
+
this.native = typeof value === 'string' ? new Database(value) : value;
|
|
6
|
+
}
|
|
7
|
+
static deserialize(bytes) {
|
|
8
|
+
return new BunSqliteDatabase(Database.deserialize(bytes));
|
|
6
9
|
}
|
|
7
10
|
exec(sql) {
|
|
8
11
|
this.native.exec(sql);
|
package/dist/sqlite-bun.js
CHANGED
|
@@ -27,10 +27,10 @@ export class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
|
27
27
|
super(database(value), options);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
export const buildSqliteImage = (input) => {
|
|
30
|
+
export const buildSqliteImage = async (input) => {
|
|
31
31
|
const db = new BunSqliteDatabase();
|
|
32
32
|
try {
|
|
33
|
-
writeSqliteImage(db, input);
|
|
33
|
+
await writeSqliteImage(db, input);
|
|
34
34
|
return db.serialize();
|
|
35
35
|
}
|
|
36
36
|
finally {
|
package/dist/sqlite-dialect.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type { StoredChange, StoredCommit, StoredPushResult, StoredRow } from './
|
|
|
30
30
|
* the Postgres storage documents (§3.1, performance-by-
|
|
31
31
|
* construction).
|
|
32
32
|
*/
|
|
33
|
-
export declare const SQLITE_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq INTEGER NOT NULL DEFAULT 0,\n horizon_seq INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op INTEGER NOT NULL,\n row_version INTEGER, scopes TEXT NOT NULL, payload BLOB,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result TEXT NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,\n available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,\n last_failure TEXT,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n";
|
|
33
|
+
export declare const SQLITE_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq INTEGER NOT NULL DEFAULT 0,\n horizon_seq INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_partition_registry(\n partition TEXT PRIMARY KEY,\n log_epoch TEXT NOT NULL,\n epoch_required INTEGER NOT NULL DEFAULT 0,\n last_authenticated_at_ms INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op INTEGER NOT NULL,\n row_version INTEGER, scopes TEXT NOT NULL, payload BLOB,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result TEXT NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,\n available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,\n last_failure TEXT,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n wire_version INTEGER NOT NULL DEFAULT 1,\n cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n";
|
|
34
34
|
/** Split the DDL into individual statements (D1 applies them one by one). */
|
|
35
35
|
export declare function sqliteDdlStatements(): string[];
|
|
36
36
|
/** `?,?,…` for an `IN (…)` clause of `count` positional parameters. */
|
package/dist/sqlite-dialect.js
CHANGED
|
@@ -18,6 +18,12 @@ CREATE TABLE IF NOT EXISTS sync_partitions(
|
|
|
18
18
|
max_commit_seq INTEGER NOT NULL DEFAULT 0,
|
|
19
19
|
horizon_seq INTEGER NOT NULL DEFAULT 0
|
|
20
20
|
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS sync_partition_registry(
|
|
22
|
+
partition TEXT PRIMARY KEY,
|
|
23
|
+
log_epoch TEXT NOT NULL,
|
|
24
|
+
epoch_required INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
last_authenticated_at_ms INTEGER NOT NULL
|
|
26
|
+
);
|
|
21
27
|
CREATE TABLE IF NOT EXISTS sync_row_scopes(
|
|
22
28
|
partition TEXT NOT NULL, tbl TEXT NOT NULL,
|
|
23
29
|
var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,
|
|
@@ -71,6 +77,7 @@ CREATE INDEX IF NOT EXISTS sync_reactions_dead_letter
|
|
|
71
77
|
ON sync_reactions(partition, status, available_at_ms, idempotency_key);
|
|
72
78
|
CREATE TABLE IF NOT EXISTS sync_clients(
|
|
73
79
|
partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,
|
|
80
|
+
wire_version INTEGER NOT NULL DEFAULT 1,
|
|
74
81
|
cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,
|
|
75
82
|
updated_at_ms INTEGER NOT NULL,
|
|
76
83
|
PRIMARY KEY(partition, client_id)
|
package/dist/sqlite-image.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface SqliteImageInput {
|
|
|
10
10
|
readonly schemaVersion: number;
|
|
11
11
|
readonly asOfCommitSeq: number;
|
|
12
12
|
readonly scopeDigest: string;
|
|
13
|
-
readonly
|
|
13
|
+
readonly rowBatches: AsyncIterable<readonly StoredRow[]> | Iterable<readonly StoredRow[]>;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* The §5.3 image-builder capability, injected through
|
|
@@ -20,6 +20,6 @@ export interface SqliteImageInput {
|
|
|
20
20
|
* on the pull path. A Bun or Node host passes `buildSqliteImage`; a Workers
|
|
21
21
|
* host omits it and serves the rows lane.
|
|
22
22
|
*/
|
|
23
|
-
export type SqliteImageBuilder = (input: SqliteImageInput) => Uint8Array
|
|
23
|
+
export type SqliteImageBuilder = (input: SqliteImageInput) => Promise<Uint8Array>;
|
|
24
24
|
/** Populate a §5.3 image database for a whole-table snapshot. */
|
|
25
|
-
export declare function writeSqliteImage(db: SqliteDatabase, input: SqliteImageInput): void
|
|
25
|
+
export declare function writeSqliteImage(db: SqliteDatabase, input: SqliteImageInput): Promise<void>;
|
package/dist/sqlite-image.js
CHANGED
|
@@ -45,8 +45,8 @@ function toSql(value) {
|
|
|
45
45
|
return value;
|
|
46
46
|
}
|
|
47
47
|
/** Populate a §5.3 image database for a whole-table snapshot. */
|
|
48
|
-
export function writeSqliteImage(db, input) {
|
|
49
|
-
const { table,
|
|
48
|
+
export async function writeSqliteImage(db, input) {
|
|
49
|
+
const { table, rowBatches } = input;
|
|
50
50
|
const primaryKey = table.columns[table.primaryKeyIndex]?.name;
|
|
51
51
|
const columnDefs = table.columns.map((column) => {
|
|
52
52
|
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
@@ -59,7 +59,6 @@ export function writeSqliteImage(db, input) {
|
|
|
59
59
|
format INTEGER NOT NULL, "table" TEXT NOT NULL,
|
|
60
60
|
"schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
|
|
61
61
|
"scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`);
|
|
62
|
-
db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(table.name, input.schemaVersion, input.asOfCommitSeq, input.scopeDigest, rows.length);
|
|
63
62
|
const names = [
|
|
64
63
|
...table.columns.map((column) => quoteIdent(column.name)),
|
|
65
64
|
quoteIdent(IMAGE_VERSION_COLUMN),
|
|
@@ -68,10 +67,15 @@ export function writeSqliteImage(db, input) {
|
|
|
68
67
|
VALUES (${names.map(() => '?').join(', ')})`);
|
|
69
68
|
db.exec('BEGIN');
|
|
70
69
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let rowCount = 0;
|
|
71
|
+
for await (const rows of rowBatches) {
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const values = decodeRow(table.columns, row.payload);
|
|
74
|
+
insert.run(...values.map(toSql), row.serverVersion);
|
|
75
|
+
}
|
|
76
|
+
rowCount += rows.length;
|
|
74
77
|
}
|
|
78
|
+
db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(table.name, input.schemaVersion, input.asOfCommitSeq, input.scopeDigest, rowCount);
|
|
75
79
|
db.exec('COMMIT');
|
|
76
80
|
}
|
|
77
81
|
catch (error) {
|
package/dist/sqlite-node.js
CHANGED
|
@@ -30,13 +30,13 @@ export class SqliteLeaseStore extends SharedSqliteLeaseStore {
|
|
|
30
30
|
super(database(value), options);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
export const buildSqliteImage = (input) => {
|
|
33
|
+
export const buildSqliteImage = async (input) => {
|
|
34
34
|
const directory = mkdtempSync(join(tmpdir(), 'syncular-server-image-'));
|
|
35
35
|
const path = join(directory, 'segment.db');
|
|
36
36
|
const db = new NodeSqliteDatabase(path);
|
|
37
37
|
try {
|
|
38
38
|
try {
|
|
39
|
-
writeSqliteImage(db, input);
|
|
39
|
+
await writeSqliteImage(db, input);
|
|
40
40
|
}
|
|
41
41
|
finally {
|
|
42
42
|
db.close();
|
|
@@ -15,6 +15,7 @@ export class SqliteSegmentStore {
|
|
|
15
15
|
this.db.exec(`
|
|
16
16
|
CREATE TABLE IF NOT EXISTS sync_segments(
|
|
17
17
|
segment_id TEXT PRIMARY KEY, partition TEXT NOT NULL,
|
|
18
|
+
log_epoch TEXT NOT NULL,
|
|
18
19
|
tbl TEXT NOT NULL, schema_version INTEGER NOT NULL,
|
|
19
20
|
media_type TEXT NOT NULL, scope_digest TEXT NOT NULL,
|
|
20
21
|
as_of_commit_seq INTEGER NOT NULL, row_count INTEGER NOT NULL,
|
|
@@ -23,6 +24,12 @@ export class SqliteSegmentStore {
|
|
|
23
24
|
expires_at_ms INTEGER NOT NULL, bytes BLOB NOT NULL
|
|
24
25
|
);
|
|
25
26
|
`);
|
|
27
|
+
const columns = this.db
|
|
28
|
+
.query('PRAGMA table_info(sync_segments)')
|
|
29
|
+
.all();
|
|
30
|
+
if (!columns.some((column) => column.name === 'log_epoch')) {
|
|
31
|
+
this.db.exec("ALTER TABLE sync_segments ADD COLUMN log_epoch TEXT NOT NULL DEFAULT ''");
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
34
|
async put(metadata, bytes, nowMs) {
|
|
28
35
|
const segmentId = await segmentIdFor(bytes);
|
|
@@ -35,11 +42,11 @@ export class SqliteSegmentStore {
|
|
|
35
42
|
};
|
|
36
43
|
this.db
|
|
37
44
|
.query(`INSERT OR REPLACE INTO sync_segments(
|
|
38
|
-
segment_id, partition, tbl, schema_version, media_type,
|
|
45
|
+
segment_id, partition, log_epoch, tbl, schema_version, media_type,
|
|
39
46
|
scope_digest, as_of_commit_seq, row_count, row_cursor,
|
|
40
47
|
next_row_cursor, byte_length, created_at_ms, expires_at_ms, bytes
|
|
41
|
-
) VALUES (
|
|
42
|
-
.run(record.segmentId, record.partition, record.table, record.schemaVersion, record.mediaType, record.scopeDigest, record.asOfCommitSeq, record.rowCount, record.rowCursor, record.nextRowCursor, record.byteLength, record.createdAtMs, record.expiresAtMs, bytes);
|
|
48
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
|
49
|
+
.run(record.segmentId, record.partition, record.logEpoch, record.table, record.schemaVersion, record.mediaType, record.scopeDigest, record.asOfCommitSeq, record.rowCount, record.rowCursor, record.nextRowCursor, record.byteLength, record.createdAtMs, record.expiresAtMs, bytes);
|
|
43
50
|
return record;
|
|
44
51
|
}
|
|
45
52
|
async get(segmentId) {
|
|
@@ -52,6 +59,7 @@ export class SqliteSegmentStore {
|
|
|
52
59
|
record: {
|
|
53
60
|
segmentId: row.segment_id,
|
|
54
61
|
partition: row.partition,
|
|
62
|
+
logEpoch: row.log_epoch,
|
|
55
63
|
table: row.tbl,
|
|
56
64
|
schemaVersion: row.schema_version,
|
|
57
65
|
mediaType: row.media_type === 'sqlite' ? 'sqlite' : 'rows',
|
|
@@ -72,16 +80,17 @@ export class SqliteSegmentStore {
|
|
|
72
80
|
.query(`SELECT segment_id, row_count, next_row_cursor, byte_length,
|
|
73
81
|
created_at_ms, expires_at_ms
|
|
74
82
|
FROM sync_segments
|
|
75
|
-
WHERE partition=? AND tbl=? AND schema_version=? AND media_type=?
|
|
83
|
+
WHERE partition=? AND log_epoch=? AND tbl=? AND schema_version=? AND media_type=?
|
|
76
84
|
AND scope_digest=? AND as_of_commit_seq=? AND row_cursor IS NULL
|
|
77
85
|
AND expires_at_ms > ?
|
|
78
86
|
LIMIT 1`)
|
|
79
|
-
.get(key.partition, key.table, key.schemaVersion, key.mediaType, key.scopeDigest, key.asOfCommitSeq, nowMs);
|
|
87
|
+
.get(key.partition, key.logEpoch, key.table, key.schemaVersion, key.mediaType, key.scopeDigest, key.asOfCommitSeq, nowMs);
|
|
80
88
|
if (row === null)
|
|
81
89
|
return undefined;
|
|
82
90
|
return {
|
|
83
91
|
segmentId: row.segment_id,
|
|
84
92
|
partition: key.partition,
|
|
93
|
+
logEpoch: key.logEpoch,
|
|
85
94
|
table: key.table,
|
|
86
95
|
schemaVersion: key.schemaVersion,
|
|
87
96
|
mediaType: key.mediaType,
|
package/dist/sqlite-storage.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { CommitPruneQuery, CommitPruneResult } from './storage.js';
|
|
1
2
|
import type { CompiledSchema, CompiledTable } from './schema.js';
|
|
2
3
|
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
3
|
-
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';
|
|
4
|
+
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';
|
|
4
5
|
export declare class SqliteServerStorage implements ServerStorage {
|
|
5
6
|
#private;
|
|
6
7
|
readonly db: SqliteDatabase;
|
|
@@ -8,14 +9,18 @@ export declare class SqliteServerStorage implements ServerStorage {
|
|
|
8
9
|
/** Resolve a table's compiled schema; row operations require `ensureSchema`. */
|
|
9
10
|
table(name: string): CompiledTable;
|
|
10
11
|
ensureSchema(schema: CompiledSchema): Promise<void>;
|
|
12
|
+
touchPartition(partition: string, authenticatedAtMs: number, initialLogEpoch: string): Promise<PartitionRegistryEntry>;
|
|
13
|
+
rotatePartitionLogEpoch(partition: string, logEpoch: string, authenticatedAtMs: number): Promise<PartitionRegistryEntry>;
|
|
14
|
+
listPartitionRegistry(): Promise<PartitionRegistryEntry[]>;
|
|
11
15
|
begin(partition: string): Promise<StorageTransaction>;
|
|
12
16
|
/** Internal: write a row + refresh its scope-index entries. */
|
|
13
17
|
writeRow(partition: string, table: string, row: StoredRow): void;
|
|
14
18
|
getMaxCommitSeq(partition: string): Promise<number>;
|
|
15
19
|
queryAuthoritative(partition: string, query: AuthoritativeQueryRequest): Promise<AuthoritativeQueryResult>;
|
|
20
|
+
getPartitionLogEpoch(partition: string): Promise<string | undefined>;
|
|
16
21
|
getHorizonSeq(partition: string): Promise<number>;
|
|
17
22
|
setHorizonSeq(partition: string, seq: number): Promise<void>;
|
|
18
|
-
pruneCommitsThrough(partition: string,
|
|
23
|
+
pruneCommitsThrough(partition: string, query: CommitPruneQuery): Promise<CommitPruneResult>;
|
|
19
24
|
getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise<number>;
|
|
20
25
|
getRow(partition: string, table: string, rowId: string): Promise<StoredRow | undefined>;
|
|
21
26
|
getPushResult(partition: string, clientId: string, clientCommitId: string): Promise<StoredPushResult | undefined>;
|
|
@@ -32,6 +37,7 @@ export declare class SqliteServerStorage implements ServerStorage {
|
|
|
32
37
|
scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise<StoredRow[]>;
|
|
33
38
|
getClientRecord(partition: string, clientId: string): Promise<ClientRecord | undefined>;
|
|
34
39
|
putClientRecord(partition: string, record: ClientRecord): Promise<void>;
|
|
40
|
+
getActiveClientCursorFloor(partition: string, cutoffMs: number): Promise<number | null>;
|
|
35
41
|
listClientCursors(partition: string): Promise<ClientCursorInfo[]>;
|
|
36
42
|
listRowsReferencingBlob(partition: string, blobId: string): Promise<{
|
|
37
43
|
readonly table: string;
|