@syncular/server 0.30.16 → 0.32.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/README.md +196 -76
- package/dist/admin.d.ts +394 -50
- package/dist/admin.js +714 -82
- package/dist/context.d.ts +16 -6
- package/dist/d1-storage.d.ts +6 -5
- package/dist/d1-storage.js +148 -27
- package/dist/errors.d.ts +17 -4
- package/dist/errors.js +31 -6
- package/dist/events-ring.d.ts +12 -2
- package/dist/events-ring.js +27 -2
- package/dist/events.d.ts +21 -3
- package/dist/handler.js +19 -6
- package/dist/postgres-storage.d.ts +6 -5
- package/dist/postgres-storage.js +102 -18
- package/dist/pull.js +3 -26
- package/dist/realtime.d.ts +18 -1
- package/dist/realtime.js +31 -3
- package/dist/sqlite-image.d.ts +1 -1
- package/dist/sqlite-storage.d.ts +6 -5
- package/dist/sqlite-storage.js +115 -18
- package/dist/storage.d.ts +58 -7
- package/package.json +2 -2
- package/src/admin.ts +1215 -120
- package/src/context.ts +16 -6
- package/src/d1-storage.ts +210 -31
- package/src/errors.ts +45 -5
- package/src/events-ring.ts +31 -2
- package/src/events.ts +27 -4
- package/src/handler.ts +20 -6
- package/src/postgres-storage.ts +173 -28
- package/src/pull.ts +3 -29
- package/src/realtime.ts +58 -4
- package/src/sqlite-image.ts +1 -1
- package/src/sqlite-storage.ts +181 -26
- package/src/storage.ts +72 -9
package/dist/postgres-storage.js
CHANGED
|
@@ -5,7 +5,7 @@ import { serveNotReadyError } from './readiness.js';
|
|
|
5
5
|
import { bindAuthoritativePartition, postgresPlaceholders, prepareAuthoritativeQuery, } from './authoritative-query.js';
|
|
6
6
|
import { syncError } from './errors.js';
|
|
7
7
|
import { asBytes, asNumber, } from './pg-executor.js';
|
|
8
|
-
import { assertPhysicalColumns, assertStoredLayouts, COMMIT_WINDOW_BATCH_SQL, LATEST_SCOPED_CHANGE_BATCH_SQL, deleteRowSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_POSTGRES, scanRowBatchSql, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, prefetchRowsSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
|
|
8
|
+
import { assertPhysicalColumns, assertStoredLayouts, COMMIT_WINDOW_BATCH_SQL, LATEST_SCOPED_CHANGE_BATCH_SQL, deleteRowSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, quoteIdent, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_POSTGRES, SYNC_COLUMN_VERSIONS_COLUMN, SYNC_PARTITION_COLUMN, SYNC_PAYLOAD_COLUMN, SYNC_ROW_ID_COLUMN, SYNC_SCOPES_COLUMN, SYNC_VERSION_COLUMN, scanRowBatchSql, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, prefetchRowsSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
|
|
9
9
|
import { matchesEffective } from './scopes.js';
|
|
10
10
|
import { isPostgresConstraintError, StorageConstraintError, } from './storage-errors.js';
|
|
11
11
|
import { assertScopeIndexedScan, resolveIndexRowScan } from './storage-query.js';
|
|
@@ -1659,17 +1659,45 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
|
|
|
1659
1659
|
}));
|
|
1660
1660
|
}
|
|
1661
1661
|
async listCommitMetadata(partition, query) {
|
|
1662
|
+
// Newest-first window, resumed by `afterSeq` (exclusive lower bound) and
|
|
1663
|
+
// ended by `beforeSeq` (exclusive upper bound) when paging older. An
|
|
1664
|
+
// omitted bound is the largest safe commitSeq, so one statement serves
|
|
1665
|
+
// both the bounded and unbounded read.
|
|
1666
|
+
const beforeSeq = query.beforeSeq ?? Number.MAX_SAFE_INTEGER;
|
|
1662
1667
|
const { rows } = query.table
|
|
1663
1668
|
? await this.#exec.query(`SELECT c.commit_seq, c.client_id, c.client_commit_id, c.actor_id, c.created_at_ms
|
|
1664
1669
|
FROM sync_commits c
|
|
1665
|
-
WHERE c.partition=$1 AND c.commit_seq>$2
|
|
1670
|
+
WHERE c.partition=$1 AND c.commit_seq>$2 AND c.commit_seq<$3
|
|
1666
1671
|
AND EXISTS (SELECT 1 FROM sync_changes ch
|
|
1667
|
-
WHERE ch.partition=c.partition AND ch.commit_seq=c.commit_seq AND ch.tbl=$
|
|
1668
|
-
ORDER BY c.commit_seq DESC LIMIT $
|
|
1672
|
+
WHERE ch.partition=c.partition AND ch.commit_seq=c.commit_seq AND ch.tbl=$4)
|
|
1673
|
+
ORDER BY c.commit_seq DESC LIMIT $5`, [partition, query.afterSeq, beforeSeq, query.table, query.limit])
|
|
1669
1674
|
: await this.#exec.query(`SELECT commit_seq, client_id, client_commit_id, actor_id, created_at_ms
|
|
1670
1675
|
FROM sync_commits
|
|
1671
|
-
WHERE partition=$1 AND commit_seq>$2
|
|
1672
|
-
ORDER BY commit_seq DESC LIMIT $
|
|
1676
|
+
WHERE partition=$1 AND commit_seq>$2 AND commit_seq<$3
|
|
1677
|
+
ORDER BY commit_seq DESC LIMIT $4`, [partition, query.afterSeq, beforeSeq, query.limit]);
|
|
1678
|
+
// `changeSample`: one extra statement for the whole page — the first
|
|
1679
|
+
// `sample` changes (change-index order) of every listed commit.
|
|
1680
|
+
const sampleCount = query.changeSample !== undefined &&
|
|
1681
|
+
Number.isInteger(query.changeSample) &&
|
|
1682
|
+
query.changeSample > 0
|
|
1683
|
+
? query.changeSample
|
|
1684
|
+
: 0;
|
|
1685
|
+
const samples = new Map();
|
|
1686
|
+
if (sampleCount > 0 && rows.length > 0) {
|
|
1687
|
+
const seqs = rows.map((row) => asNumber(row.commit_seq));
|
|
1688
|
+
const { rows: changeRows } = await this.#exec.query(`SELECT commit_seq, tbl, row_id, op FROM sync_changes
|
|
1689
|
+
WHERE partition=$1 AND commit_seq IN (${seqs.map((_, index) => `$${index + 2}`).join(',')}) AND idx<$${seqs.length + 2}
|
|
1690
|
+
ORDER BY commit_seq, idx`, [partition, ...seqs, sampleCount]);
|
|
1691
|
+
for (const change of changeRows) {
|
|
1692
|
+
const sample = samples.get(asNumber(change.commit_seq)) ?? [];
|
|
1693
|
+
sample.push({
|
|
1694
|
+
table: change.tbl,
|
|
1695
|
+
rowId: change.row_id,
|
|
1696
|
+
op: asNumber(change.op) === 1 ? 'upsert' : 'delete',
|
|
1697
|
+
});
|
|
1698
|
+
samples.set(asNumber(change.commit_seq), sample);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1673
1701
|
const out = [];
|
|
1674
1702
|
for (const row of rows) {
|
|
1675
1703
|
const commitSeq = asNumber(row.commit_seq);
|
|
@@ -1682,10 +1710,78 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
|
|
|
1682
1710
|
createdAtMs: asNumber(row.created_at_ms),
|
|
1683
1711
|
changeCount: changes.rows.reduce((sum, c) => sum + asNumber(c.n), 0),
|
|
1684
1712
|
tables: changes.rows.map((c) => c.tbl),
|
|
1713
|
+
...(sampleCount > 0 ? { sample: samples.get(commitSeq) ?? [] } : {}),
|
|
1685
1714
|
});
|
|
1686
1715
|
}
|
|
1687
1716
|
return out;
|
|
1688
1717
|
}
|
|
1718
|
+
async getCommit(partition, commitSeq) {
|
|
1719
|
+
const { rows } = await this.#exec.query('SELECT client_id, client_commit_id, actor_id, created_at_ms FROM sync_commits WHERE partition=$1 AND commit_seq=$2', [partition, commitSeq]);
|
|
1720
|
+
const meta = rows[0];
|
|
1721
|
+
if (meta === undefined)
|
|
1722
|
+
return undefined;
|
|
1723
|
+
const { rows: changeRows } = await this.#exec.query('SELECT tbl, row_id, op, row_version, scopes, payload FROM sync_changes WHERE partition=$1 AND commit_seq=$2 ORDER BY idx', [partition, commitSeq]);
|
|
1724
|
+
const changes = changeRows.map(toStoredChange);
|
|
1725
|
+
return {
|
|
1726
|
+
commitSeq,
|
|
1727
|
+
clientId: meta.client_id,
|
|
1728
|
+
clientCommitId: meta.client_commit_id,
|
|
1729
|
+
actorId: meta.actor_id,
|
|
1730
|
+
createdAtMs: asNumber(meta.created_at_ms),
|
|
1731
|
+
changeCount: changes.length,
|
|
1732
|
+
tables: [...new Set(changes.map((change) => change.table))].sort(),
|
|
1733
|
+
changes,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
async listRowChanges(partition, query) {
|
|
1737
|
+
// Retained-log read for one row: no dedicated index (the write path pays
|
|
1738
|
+
// nothing for it), bounded by retention (§4.6).
|
|
1739
|
+
const { rows } = await this.#exec.query(`SELECT ch.commit_seq, ch.op, ch.row_version, c.actor_id, c.client_id, c.created_at_ms
|
|
1740
|
+
FROM sync_changes ch
|
|
1741
|
+
JOIN sync_commits c
|
|
1742
|
+
ON c.partition=ch.partition AND c.commit_seq=ch.commit_seq
|
|
1743
|
+
WHERE ch.partition=$1 AND ch.tbl=$2 AND ch.row_id=$3
|
|
1744
|
+
ORDER BY ch.commit_seq DESC, ch.idx DESC LIMIT $4`, [partition, query.table, query.rowId, query.limit]);
|
|
1745
|
+
return rows.map((row) => ({
|
|
1746
|
+
commitSeq: asNumber(row.commit_seq),
|
|
1747
|
+
op: asNumber(row.op) === 1 ? 'upsert' : 'delete',
|
|
1748
|
+
...(row.row_version === null || row.row_version === undefined
|
|
1749
|
+
? {}
|
|
1750
|
+
: { rowVersion: asNumber(row.row_version) }),
|
|
1751
|
+
actorId: row.actor_id,
|
|
1752
|
+
clientId: row.client_id,
|
|
1753
|
+
createdAtMs: asNumber(row.created_at_ms),
|
|
1754
|
+
}));
|
|
1755
|
+
}
|
|
1756
|
+
async listRows(partition, query) {
|
|
1757
|
+
// Keyset page over the row table's (partition, rowId) primary key: an
|
|
1758
|
+
// index range read, no scope filter, no per-row lookup.
|
|
1759
|
+
const { rows } = await this.#exec.query(`SELECT ${quoteIdent(SYNC_ROW_ID_COLUMN)} AS row_id, ${quoteIdent(SYNC_VERSION_COLUMN)} AS server_version, ${quoteIdent(SYNC_SCOPES_COLUMN)} AS scopes, ${quoteIdent(SYNC_PAYLOAD_COLUMN)} AS payload, ${quoteIdent(SYNC_COLUMN_VERSIONS_COLUMN)} AS column_versions
|
|
1760
|
+
FROM ${quoteIdent(this.table(query.table).name)}
|
|
1761
|
+
WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=$1 AND ${quoteIdent(SYNC_ROW_ID_COLUMN)}>$2
|
|
1762
|
+
ORDER BY ${quoteIdent(SYNC_ROW_ID_COLUMN)} LIMIT $3`, [partition, query.afterRowId ?? '', query.limit]);
|
|
1763
|
+
return rows.map(toStoredRow);
|
|
1764
|
+
}
|
|
1765
|
+
async countRows(partition, table) {
|
|
1766
|
+
const { rows } = await this.#exec.query(`SELECT count(*) AS n FROM ${quoteIdent(this.table(table).name)} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=$1`, [partition]);
|
|
1767
|
+
return rows[0] === undefined ? 0 : asNumber(rows[0].n);
|
|
1768
|
+
}
|
|
1769
|
+
async listPushResults(partition, clientCommitId) {
|
|
1770
|
+
// Retained outcomes for one clientCommitId: no dedicated index, ordered
|
|
1771
|
+
// by client so the read is deterministic.
|
|
1772
|
+
const { rows } = await this.#exec.query('SELECT client_id, result FROM sync_push_results WHERE partition=$1 AND client_commit_id=$2 ORDER BY client_id', [partition, clientCommitId]);
|
|
1773
|
+
return rows.map((row) => {
|
|
1774
|
+
try {
|
|
1775
|
+
return {
|
|
1776
|
+
clientId: row.client_id,
|
|
1777
|
+
result: deserializePushResult(asJson(row.result)),
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
throw syncError('sync.idempotency_cache_miss', 'persisted push result unreadable (§6.3)');
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1689
1785
|
async scopeActivity(partition, query) {
|
|
1690
1786
|
const { rows } = await this.#exec.query(`SELECT DISTINCT commit_seq, tbl FROM sync_change_scopes
|
|
1691
1787
|
WHERE partition=$1 AND var=$2 AND value=$3
|
|
@@ -1708,18 +1804,6 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
|
|
|
1708
1804
|
}
|
|
1709
1805
|
return out;
|
|
1710
1806
|
}
|
|
1711
|
-
async getRowScopes(partition, table, rowId) {
|
|
1712
|
-
const { rows } = await this.#exec.query(selectRowScopesSql(this.table(table), 'postgres'), [partition, rowId]);
|
|
1713
|
-
const row = rows[0];
|
|
1714
|
-
if (row === undefined)
|
|
1715
|
-
return undefined;
|
|
1716
|
-
return {
|
|
1717
|
-
serverVersion: asNumber(row.server_version),
|
|
1718
|
-
scopes: (typeof row.scopes === 'string'
|
|
1719
|
-
? JSON.parse(row.scopes)
|
|
1720
|
-
: row.scopes),
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
1807
|
async listPartitions() {
|
|
1724
1808
|
return (await this.listPartitionRegistry()).map((entry) => entry.partition);
|
|
1725
1809
|
}
|
package/dist/pull.js
CHANGED
|
@@ -10,24 +10,6 @@ import { issueSegmentUrl } from './signed-url.js';
|
|
|
10
10
|
import { projectRowPayload, projectRowValues } from './schema-window.js';
|
|
11
11
|
// One artifact build per owning storage pair and complete immutable identity.
|
|
12
12
|
const imageBuilds = new WeakMap();
|
|
13
|
-
/**
|
|
14
|
-
* Resolve the §5.3 image builder: the host-injected one if present, else the
|
|
15
|
-
* in-tree `buildSqliteImage` on a Bun runtime (dynamic import so `bun:sqlite`
|
|
16
|
-
* is never a static dep of the neutral core), else `undefined` (rows lane).
|
|
17
|
-
* Memoized so the dynamic import happens at most once per process.
|
|
18
|
-
*/
|
|
19
|
-
let cachedDefaultBuilder;
|
|
20
|
-
async function resolveImageBuilder(ctx) {
|
|
21
|
-
if (ctx.sqliteImageBuilder !== undefined)
|
|
22
|
-
return ctx.sqliteImageBuilder;
|
|
23
|
-
if (cachedDefaultBuilder === undefined) {
|
|
24
|
-
const hasBun = globalThis.Bun !== undefined;
|
|
25
|
-
cachedDefaultBuilder = hasBun
|
|
26
|
-
? (await import('./sqlite-bun.js')).buildSqliteImage
|
|
27
|
-
: null;
|
|
28
|
-
}
|
|
29
|
-
return cachedDefaultBuilder ?? undefined;
|
|
30
|
-
}
|
|
31
13
|
/** §4.2 accept bitmask. */
|
|
32
14
|
export const ACCEPT_INLINE_ROWS = 1 << 0;
|
|
33
15
|
export const ACCEPT_EXTERNAL_ROWS = 1 << 1;
|
|
@@ -176,14 +158,9 @@ async function* sqliteImageSegment(ctx, schema, limits, plan, asOf, digest, trac
|
|
|
176
158
|
// The probe is the rows lane's first page, read by `prepareSections`.
|
|
177
159
|
if (probe.length <= limits.limitSnapshotRows)
|
|
178
160
|
return false;
|
|
179
|
-
// §5.3:
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
// import so `bun:sqlite` is never a static dependency of the pull path
|
|
183
|
-
// (runtime neutrality is enforced by test/runtime-neutrality.test.ts). On
|
|
184
|
-
// Workers/edge (no `Bun`) this yields the rows lane for a bit-2 client — a
|
|
185
|
-
// support floor, not a fallback (§5.3: sqlite is an *accept*, not a demand).
|
|
186
|
-
const buildImage = await resolveImageBuilder(ctx);
|
|
161
|
+
// §5.3: hosts opt into image construction with a runtime-specific builder.
|
|
162
|
+
// Without one, matching stored images remain usable and cold pulls use rows.
|
|
163
|
+
const buildImage = ctx.sqliteImageBuilder;
|
|
187
164
|
if (buildImage === undefined)
|
|
188
165
|
return false;
|
|
189
166
|
let stores = imageBuilds.get(storage);
|
package/dist/realtime.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type PresenceKind, type ScopeMap, type WakeReason } from '@syncular/core';
|
|
20
20
|
import type { SyncRequestContext, SyncServerConfig } from './context.js';
|
|
21
|
-
import { type SyncularErrorHandler } from './errors.js';
|
|
21
|
+
import { type SyncularErrorHandler, type SyncularErrorMapper } from './errors.js';
|
|
22
22
|
import { type SyncularServerEvents } from './events.js';
|
|
23
23
|
import { type CompiledTable } from './schema.js';
|
|
24
24
|
import type { ServerStorage, StoredCommit } from './storage.js';
|
|
@@ -169,13 +169,28 @@ export declare class RealtimeSession {
|
|
|
169
169
|
deliverCommit(commit: StoredCommit): void;
|
|
170
170
|
close(): void;
|
|
171
171
|
}
|
|
172
|
+
/** One open realtime socket. */
|
|
173
|
+
export interface RealtimeConnection {
|
|
174
|
+
readonly sessionId: string;
|
|
175
|
+
readonly clientId: string;
|
|
176
|
+
readonly actorId: string;
|
|
177
|
+
readonly openedAtMs: number;
|
|
178
|
+
}
|
|
172
179
|
export declare class RealtimeHub {
|
|
173
180
|
#private;
|
|
174
181
|
/** §8.6 presence registry — ephemeral, in-memory, never persisted. */
|
|
175
182
|
readonly presence: PresenceRegistry;
|
|
176
183
|
constructor(config: RealtimeHubConfig);
|
|
177
184
|
projectPayload(registration: Registration, payload: Uint8Array): Uint8Array;
|
|
185
|
+
/**
|
|
186
|
+
* Compose `sink` into the hub's `events`, so socket rounds and sessions
|
|
187
|
+
* opened afterwards report to it as well. `createSyncularHono` uses it to
|
|
188
|
+
* feed the admin console's event ring.
|
|
189
|
+
*/
|
|
190
|
+
addEventSink(sink: SyncularServerEvents): void;
|
|
178
191
|
get sessionCount(): number;
|
|
192
|
+
/** The partition's open sockets, for the admin console. */
|
|
193
|
+
connections(partition: string): RealtimeConnection[];
|
|
179
194
|
/** §8.6.2 published-document size cap (bytes). */
|
|
180
195
|
get maxPresenceBytes(): number;
|
|
181
196
|
/** §8.6.4 presence rate cap (ms); 0 = off (reference default). */
|
|
@@ -204,6 +219,8 @@ export declare class RealtimeHub {
|
|
|
204
219
|
}): SyncRequestContext;
|
|
205
220
|
/** The host's error reporter (`SyncServerConfig.onError`), if any. */
|
|
206
221
|
get onError(): SyncularErrorHandler | undefined;
|
|
222
|
+
/** The host's adapter error mapper (`SyncServerConfig.mapError`), if any. */
|
|
223
|
+
get mapError(): SyncularErrorMapper | undefined;
|
|
207
224
|
requestContext(session: RealtimeSession): SyncRequestContext;
|
|
208
225
|
/**
|
|
209
226
|
* Register a connected socket (§8.1): load the client's last pull's
|
package/dist/realtime.js
CHANGED
|
@@ -20,6 +20,7 @@ import { DecodeError, decodeMessage, encodeMessage, encodePresenceError, encodeP
|
|
|
20
20
|
import { REMOTE_COMMAND_CLIENT_ID_PREFIX, RESOLVER_OUTAGE, touchAuthenticatedPartition, } from './context.js';
|
|
21
21
|
import { adapterSyncError, reportError, SyncError, syncError, } from './errors.js';
|
|
22
22
|
import { emitEvent } from './events.js';
|
|
23
|
+
import { composeEvents } from './events-ring.js';
|
|
23
24
|
import { createSyncResponseStream } from './handler.js';
|
|
24
25
|
import { compileSchema, } from './schema.js';
|
|
25
26
|
import { schemaWindowOf, projectRowPayload } from './schema-window.js';
|
|
@@ -436,7 +437,7 @@ export class RealtimeSession {
|
|
|
436
437
|
// `sync.internal_error`, exactly like the HTTP adapter's 500.
|
|
437
438
|
const sync = error instanceof DecodeError
|
|
438
439
|
? syncError(error.code, error.message)
|
|
439
|
-
: adapterSyncError(error, this.#hub.onError, 'realtime');
|
|
440
|
+
: adapterSyncError(error, (error, context) => this.#hub.onError?.(error, context), 'realtime', (error, context) => this.#hub.mapError?.(error, context));
|
|
440
441
|
finishRound(); // END is in this one chunk
|
|
441
442
|
await this.#sendRoundChunk(errorResponseBytes(sync, this.wireVersion, this.logEpoch));
|
|
442
443
|
return;
|
|
@@ -460,7 +461,7 @@ export class RealtimeSession {
|
|
|
460
461
|
// the §8.7 violation path.
|
|
461
462
|
const sync = error instanceof DecodeError
|
|
462
463
|
? syncError(error.code, error.message)
|
|
463
|
-
: adapterSyncError(error, this.#hub.onError, 'realtime');
|
|
464
|
+
: adapterSyncError(error, (error, context) => this.#hub.onError?.(error, context), 'realtime', (error, context) => this.#hub.mapError?.(error, context));
|
|
464
465
|
finishRound();
|
|
465
466
|
await this.#sendRoundChunk(errorResponseBytes(sync, this.wireVersion, this.logEpoch));
|
|
466
467
|
return;
|
|
@@ -476,7 +477,7 @@ export class RealtimeSession {
|
|
|
476
477
|
catch (error) {
|
|
477
478
|
// A host failure mid-stream leaves the byte stream unfinishable —
|
|
478
479
|
// fail loud, drop the connection (§8.7 / §1.4 abort rule).
|
|
479
|
-
reportError(error, this.#hub.onError, 'realtime');
|
|
480
|
+
reportError(error, (error, context) => this.#hub.onError?.(error, context), 'realtime');
|
|
480
481
|
this.#violation('sync round failed mid-stream');
|
|
481
482
|
}
|
|
482
483
|
finally {
|
|
@@ -714,9 +715,32 @@ export class RealtimeHub {
|
|
|
714
715
|
projectPayload(registration, payload) {
|
|
715
716
|
return projectRowPayload(this.#schema.tables.get(registration.table), registration.codec, payload);
|
|
716
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Compose `sink` into the hub's `events`, so socket rounds and sessions
|
|
720
|
+
* opened afterwards report to it as well. `createSyncularHono` uses it to
|
|
721
|
+
* feed the admin console's event ring.
|
|
722
|
+
*/
|
|
723
|
+
addEventSink(sink) {
|
|
724
|
+
const events = this.#config.events;
|
|
725
|
+
this.#config = {
|
|
726
|
+
...this.#config,
|
|
727
|
+
events: events === undefined ? sink : composeEvents(events, sink),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
717
730
|
get sessionCount() {
|
|
718
731
|
return this.#sessions.size;
|
|
719
732
|
}
|
|
733
|
+
/** The partition's open sockets, for the admin console. */
|
|
734
|
+
connections(partition) {
|
|
735
|
+
return [...this.#sessions]
|
|
736
|
+
.filter((session) => session.partition === partition)
|
|
737
|
+
.map((session) => ({
|
|
738
|
+
sessionId: session.sessionId,
|
|
739
|
+
clientId: session.clientId,
|
|
740
|
+
actorId: session.actorId,
|
|
741
|
+
openedAtMs: session.openedAtMs,
|
|
742
|
+
}));
|
|
743
|
+
}
|
|
720
744
|
/** §8.6.2 published-document size cap (bytes). */
|
|
721
745
|
get maxPresenceBytes() {
|
|
722
746
|
return this.#config.maxPresenceBytes ?? DEFAULT_MAX_PRESENCE_BYTES;
|
|
@@ -876,6 +900,10 @@ export class RealtimeHub {
|
|
|
876
900
|
get onError() {
|
|
877
901
|
return this.#config.onError;
|
|
878
902
|
}
|
|
903
|
+
/** The host's adapter error mapper (`SyncServerConfig.mapError`), if any. */
|
|
904
|
+
get mapError() {
|
|
905
|
+
return this.#config.mapError;
|
|
906
|
+
}
|
|
879
907
|
requestContext(session) {
|
|
880
908
|
return this.requestContextFor(session);
|
|
881
909
|
}
|
package/dist/sqlite-image.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface SqliteImageInput {
|
|
|
18
18
|
* a real SQLite engine, which is not available on every runtime. The core
|
|
19
19
|
* takes the builder as an optional capability rather than importing a driver
|
|
20
20
|
* on the pull path. A Bun or Node host passes `buildSqliteImage`; a Workers
|
|
21
|
-
* host omits it and serves the rows lane.
|
|
21
|
+
* host omits it and serves matching stored images or the rows lane.
|
|
22
22
|
*/
|
|
23
23
|
export type SqliteImageBuilder = (input: SqliteImageInput) => Promise<Uint8Array>;
|
|
24
24
|
/** Populate a §5.3 image database for a whole-table snapshot. */
|
package/dist/sqlite-storage.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { CommitPruneQuery, CommitPruneResult } from './storage.js';
|
|
|
2
2
|
import type { ScopeMap } from '@syncular/core';
|
|
3
3
|
import type { CompiledSchema, CompiledTable } from './schema.js';
|
|
4
4
|
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
5
|
-
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, CheckpointDeclaration, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PartitionRegistryEntry, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, ServeGate, StorageTransaction, StoredCommit, StoredCheckpoint, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
5
|
+
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, CheckpointDeclaration, ClientCursorInfo, ClientPushResult, ClientRecord, CommitDetail, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PartitionRegistryEntry, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowChangeRecord, RowChangesQuery, RowListQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, ServeGate, StorageTransaction, StoredCommit, StoredCheckpoint, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
6
6
|
export declare class SqliteServerStorage implements ServerStorage {
|
|
7
7
|
#private;
|
|
8
8
|
readonly db: SqliteDatabase;
|
|
@@ -69,10 +69,11 @@ export declare class SqliteServerStorage implements ServerStorage {
|
|
|
69
69
|
listReferencedBlobIds(partition: string): Promise<string[]>;
|
|
70
70
|
listClientRecords(partition: string): Promise<ClientRecord[]>;
|
|
71
71
|
listCommitMetadata(partition: string, query: CommitMetadataQuery): Promise<CommitMetadata[]>;
|
|
72
|
+
getCommit(partition: string, commitSeq: number): Promise<CommitDetail | undefined>;
|
|
73
|
+
listRowChanges(partition: string, query: RowChangesQuery): Promise<RowChangeRecord[]>;
|
|
74
|
+
listRows(partition: string, query: RowListQuery): Promise<StoredRow[]>;
|
|
75
|
+
countRows(partition: string, table: string): Promise<number>;
|
|
76
|
+
listPushResults(partition: string, clientCommitId: string): Promise<ClientPushResult[]>;
|
|
72
77
|
scopeActivity(partition: string, query: ScopeActivityQuery): Promise<ScopeCommitActivity[]>;
|
|
73
|
-
getRowScopes(partition: string, table: string, rowId: string): Promise<{
|
|
74
|
-
serverVersion: number;
|
|
75
|
-
scopes: Record<string, string>;
|
|
76
|
-
} | undefined>;
|
|
77
78
|
listPartitions(): Promise<string[]>;
|
|
78
79
|
}
|
package/dist/sqlite-storage.js
CHANGED
|
@@ -13,9 +13,9 @@ import { serveNotReadyError } from './readiness.js';
|
|
|
13
13
|
*/
|
|
14
14
|
import { bindAuthoritativePartition, prepareAuthoritativeQuery, } from './authoritative-query.js';
|
|
15
15
|
import { syncError } from './errors.js';
|
|
16
|
-
import { assertPhysicalColumns, assertStoredLayouts, commitWindowPageSql, latestScopedChangeSeqSql, deleteRowSql, deleteSqliteRowScopesSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
|
|
16
|
+
import { assertPhysicalColumns, assertStoredLayouts, commitWindowPageSql, latestScopedChangeSeqSql, deleteRowSql, deleteSqliteRowScopesSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, quoteIdent, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_SQLITE, SYNC_COLUMN_VERSIONS_COLUMN, SYNC_PARTITION_COLUMN, SYNC_PAYLOAD_COLUMN, SYNC_ROW_ID_COLUMN, SYNC_SCOPES_COLUMN, SYNC_VERSION_COLUMN, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
|
|
17
17
|
import { matchesEffective } from './scopes.js';
|
|
18
|
-
import { collectCommitWindowPage, deserializePushResult, placeholders, SQLITE_DDL, serializePushResult, toStoredRow, } from './sqlite-dialect.js';
|
|
18
|
+
import { collectCommitWindowPage, deserializePushResult, placeholders, SQLITE_DDL, serializePushResult, toStoredChange, toStoredRow, } from './sqlite-dialect.js';
|
|
19
19
|
import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
|
|
20
20
|
import { isSqliteConstraintError, StorageConstraintError, } from './storage-errors.js';
|
|
21
21
|
import { assertScopeIndexedScan, resolveIndexRowScan } from './storage-query.js';
|
|
@@ -1218,22 +1218,51 @@ END`);
|
|
|
1218
1218
|
}));
|
|
1219
1219
|
}
|
|
1220
1220
|
async listCommitMetadata(partition, query) {
|
|
1221
|
-
// Newest-first window, resumed by `afterSeq` (exclusive lower bound)
|
|
1221
|
+
// Newest-first window, resumed by `afterSeq` (exclusive lower bound) and
|
|
1222
|
+
// ended by `beforeSeq` (exclusive upper bound) when paging older. An
|
|
1223
|
+
// omitted bound is the largest safe commitSeq, so one statement serves
|
|
1224
|
+
// both the bounded and unbounded read.
|
|
1225
|
+
const beforeSeq = query.beforeSeq ?? Number.MAX_SAFE_INTEGER;
|
|
1222
1226
|
const rows = query.table
|
|
1223
1227
|
? this.db
|
|
1224
1228
|
.query(`SELECT c.commit_seq, c.client_id, c.client_commit_id, c.actor_id, c.created_at_ms
|
|
1225
1229
|
FROM sync_commits c
|
|
1226
|
-
WHERE c.partition=? AND c.commit_seq>?
|
|
1230
|
+
WHERE c.partition=? AND c.commit_seq>? AND c.commit_seq<?
|
|
1227
1231
|
AND EXISTS (SELECT 1 FROM sync_changes ch
|
|
1228
1232
|
WHERE ch.partition=c.partition AND ch.commit_seq=c.commit_seq AND ch.tbl=?)
|
|
1229
1233
|
ORDER BY c.commit_seq DESC LIMIT ?`)
|
|
1230
|
-
.all(partition, query.afterSeq, query.table, query.limit)
|
|
1234
|
+
.all(partition, query.afterSeq, beforeSeq, query.table, query.limit)
|
|
1231
1235
|
: this.db
|
|
1232
1236
|
.query(`SELECT commit_seq, client_id, client_commit_id, actor_id, created_at_ms
|
|
1233
1237
|
FROM sync_commits
|
|
1234
|
-
WHERE partition=? AND commit_seq>?
|
|
1238
|
+
WHERE partition=? AND commit_seq>? AND commit_seq<?
|
|
1235
1239
|
ORDER BY commit_seq DESC LIMIT ?`)
|
|
1236
|
-
.all(partition, query.afterSeq, query.limit);
|
|
1240
|
+
.all(partition, query.afterSeq, beforeSeq, query.limit);
|
|
1241
|
+
// `changeSample`: one extra statement for the whole page — the first
|
|
1242
|
+
// `sample` changes (change-index order) of every listed commit.
|
|
1243
|
+
const sampleCount = query.changeSample !== undefined &&
|
|
1244
|
+
Number.isInteger(query.changeSample) &&
|
|
1245
|
+
query.changeSample > 0
|
|
1246
|
+
? query.changeSample
|
|
1247
|
+
: 0;
|
|
1248
|
+
const samples = new Map();
|
|
1249
|
+
if (sampleCount > 0 && rows.length > 0) {
|
|
1250
|
+
const seqs = rows.map((row) => row.commit_seq);
|
|
1251
|
+
const records = this.db
|
|
1252
|
+
.query(`SELECT commit_seq, tbl, row_id, op FROM sync_changes
|
|
1253
|
+
WHERE partition=? AND commit_seq IN (${placeholders(seqs.length)}) AND idx<?
|
|
1254
|
+
ORDER BY commit_seq, idx`)
|
|
1255
|
+
.all(partition, ...seqs, sampleCount);
|
|
1256
|
+
for (const record of records) {
|
|
1257
|
+
const sample = samples.get(record.commit_seq) ?? [];
|
|
1258
|
+
sample.push({
|
|
1259
|
+
table: record.tbl,
|
|
1260
|
+
rowId: record.row_id,
|
|
1261
|
+
op: record.op === 1 ? 'upsert' : 'delete',
|
|
1262
|
+
});
|
|
1263
|
+
samples.set(record.commit_seq, sample);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1237
1266
|
return rows.map((row) => {
|
|
1238
1267
|
const changes = this.db
|
|
1239
1268
|
.query('SELECT tbl, count(*) AS n FROM sync_changes WHERE partition=? AND commit_seq=? GROUP BY tbl')
|
|
@@ -1246,9 +1275,88 @@ END`);
|
|
|
1246
1275
|
createdAtMs: row.created_at_ms,
|
|
1247
1276
|
changeCount: changes.reduce((sum, c) => sum + c.n, 0),
|
|
1248
1277
|
tables: changes.map((c) => c.tbl),
|
|
1278
|
+
...(sampleCount > 0
|
|
1279
|
+
? { sample: samples.get(row.commit_seq) ?? [] }
|
|
1280
|
+
: {}),
|
|
1249
1281
|
};
|
|
1250
1282
|
});
|
|
1251
1283
|
}
|
|
1284
|
+
async getCommit(partition, commitSeq) {
|
|
1285
|
+
const meta = this.db
|
|
1286
|
+
.query('SELECT client_id, client_commit_id, actor_id, created_at_ms FROM sync_commits WHERE partition=? AND commit_seq=?')
|
|
1287
|
+
.get(partition, commitSeq);
|
|
1288
|
+
if (meta === null)
|
|
1289
|
+
return undefined;
|
|
1290
|
+
const changes = this.db
|
|
1291
|
+
.query('SELECT tbl, row_id, op, row_version, scopes, payload FROM sync_changes WHERE partition=? AND commit_seq=? ORDER BY idx')
|
|
1292
|
+
.all(partition, commitSeq)
|
|
1293
|
+
.map(toStoredChange);
|
|
1294
|
+
return {
|
|
1295
|
+
commitSeq,
|
|
1296
|
+
clientId: meta.client_id,
|
|
1297
|
+
clientCommitId: meta.client_commit_id,
|
|
1298
|
+
actorId: meta.actor_id,
|
|
1299
|
+
createdAtMs: meta.created_at_ms,
|
|
1300
|
+
changeCount: changes.length,
|
|
1301
|
+
tables: [...new Set(changes.map((change) => change.table))].sort(),
|
|
1302
|
+
changes,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
async listRowChanges(partition, query) {
|
|
1306
|
+
// Retained-log read for one row: no dedicated index (the write path pays
|
|
1307
|
+
// nothing for it), bounded by retention (§4.6).
|
|
1308
|
+
return this.db
|
|
1309
|
+
.query(`SELECT ch.commit_seq, ch.op, ch.row_version, c.actor_id, c.client_id, c.created_at_ms
|
|
1310
|
+
FROM sync_changes ch
|
|
1311
|
+
JOIN sync_commits c
|
|
1312
|
+
ON c.partition=ch.partition AND c.commit_seq=ch.commit_seq
|
|
1313
|
+
WHERE ch.partition=? AND ch.tbl=? AND ch.row_id=?
|
|
1314
|
+
ORDER BY ch.commit_seq DESC, ch.idx DESC LIMIT ?`)
|
|
1315
|
+
.all(partition, query.table, query.rowId, query.limit)
|
|
1316
|
+
.map((row) => ({
|
|
1317
|
+
commitSeq: row.commit_seq,
|
|
1318
|
+
op: row.op === 1 ? 'upsert' : 'delete',
|
|
1319
|
+
...(row.row_version !== null ? { rowVersion: row.row_version } : {}),
|
|
1320
|
+
actorId: row.actor_id,
|
|
1321
|
+
clientId: row.client_id,
|
|
1322
|
+
createdAtMs: row.created_at_ms,
|
|
1323
|
+
}));
|
|
1324
|
+
}
|
|
1325
|
+
async listRows(partition, query) {
|
|
1326
|
+
// Keyset page over the row table's (partition, rowId) primary key: an
|
|
1327
|
+
// index range read, no scope filter, no per-row lookup.
|
|
1328
|
+
return this.db
|
|
1329
|
+
.query(`SELECT ${quoteIdent(SYNC_ROW_ID_COLUMN)} AS row_id, ${quoteIdent(SYNC_VERSION_COLUMN)} AS server_version, ${quoteIdent(SYNC_SCOPES_COLUMN)} AS scopes, ${quoteIdent(SYNC_PAYLOAD_COLUMN)} AS payload, ${quoteIdent(SYNC_COLUMN_VERSIONS_COLUMN)} AS column_versions
|
|
1330
|
+
FROM ${quoteIdent(this.table(query.table).name)}
|
|
1331
|
+
WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=? AND ${quoteIdent(SYNC_ROW_ID_COLUMN)}>?
|
|
1332
|
+
ORDER BY ${quoteIdent(SYNC_ROW_ID_COLUMN)} LIMIT ?`)
|
|
1333
|
+
.all(partition, query.afterRowId ?? '', query.limit)
|
|
1334
|
+
.map(toStoredRow);
|
|
1335
|
+
}
|
|
1336
|
+
async countRows(partition, table) {
|
|
1337
|
+
const record = this.db
|
|
1338
|
+
.query(`SELECT count(*) AS n FROM ${quoteIdent(this.table(table).name)} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=?`)
|
|
1339
|
+
.get(partition);
|
|
1340
|
+
return record?.n ?? 0;
|
|
1341
|
+
}
|
|
1342
|
+
async listPushResults(partition, clientCommitId) {
|
|
1343
|
+
// Retained outcomes for one clientCommitId: no dedicated index, ordered
|
|
1344
|
+
// by client so the read is deterministic.
|
|
1345
|
+
return this.db
|
|
1346
|
+
.query('SELECT client_id, result FROM sync_push_results WHERE partition=? AND client_commit_id=? ORDER BY client_id')
|
|
1347
|
+
.all(partition, clientCommitId)
|
|
1348
|
+
.map((record) => {
|
|
1349
|
+
try {
|
|
1350
|
+
return {
|
|
1351
|
+
clientId: record.client_id,
|
|
1352
|
+
result: deserializePushResult(record.result),
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
throw syncError('sync.idempotency_cache_miss', 'persisted push result unreadable (§6.3)');
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1252
1360
|
async scopeActivity(partition, query) {
|
|
1253
1361
|
// Candidate commits via the change-scope index (§3.1) — never a scan.
|
|
1254
1362
|
const rows = this.db
|
|
@@ -1276,17 +1384,6 @@ END`);
|
|
|
1276
1384
|
}
|
|
1277
1385
|
return out;
|
|
1278
1386
|
}
|
|
1279
|
-
async getRowScopes(partition, table, rowId) {
|
|
1280
|
-
const record = this.db
|
|
1281
|
-
.query(selectRowScopesSql(this.table(table), 'sqlite'))
|
|
1282
|
-
.get(partition, rowId);
|
|
1283
|
-
if (record === null)
|
|
1284
|
-
return undefined;
|
|
1285
|
-
return {
|
|
1286
|
-
serverVersion: record.server_version,
|
|
1287
|
-
scopes: JSON.parse(record.scopes),
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
1387
|
async listPartitions() {
|
|
1291
1388
|
return (await this.listPartitionRegistry()).map((entry) => entry.partition);
|
|
1292
1389
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -303,13 +303,55 @@ export interface CommitMetadata {
|
|
|
303
303
|
readonly createdAtMs: number;
|
|
304
304
|
readonly changeCount: number;
|
|
305
305
|
readonly tables: readonly string[];
|
|
306
|
+
/** The first `changeSample` changes in commit order, when requested. */
|
|
307
|
+
readonly sample?: readonly CommitChangeSummary[];
|
|
308
|
+
}
|
|
309
|
+
/** One change without its payload, for commit lists. */
|
|
310
|
+
export interface CommitChangeSummary {
|
|
311
|
+
readonly table: string;
|
|
312
|
+
readonly rowId: string;
|
|
313
|
+
readonly op: 'upsert' | 'delete';
|
|
306
314
|
}
|
|
307
315
|
export interface CommitMetadataQuery {
|
|
308
|
-
/** Window `afterSeq < commitSeq
|
|
316
|
+
/** Window lower bound, exclusive: `afterSeq < commitSeq`. */
|
|
309
317
|
readonly afterSeq: number;
|
|
318
|
+
/** Window upper bound, exclusive: `commitSeq < beforeSeq` (older pages). */
|
|
319
|
+
readonly beforeSeq?: number;
|
|
320
|
+
/** Newest first, at most `limit` commits. */
|
|
310
321
|
readonly limit: number;
|
|
311
322
|
/** Restrict to commits that touched this table (via the change index). */
|
|
312
323
|
readonly table?: string;
|
|
324
|
+
/** Attach the first N changes of each commit as `sample`. */
|
|
325
|
+
readonly changeSample?: number;
|
|
326
|
+
}
|
|
327
|
+
export interface RowListQuery {
|
|
328
|
+
readonly table: string;
|
|
329
|
+
/** Resume after this rowId (exclusive); `null` = start of table. */
|
|
330
|
+
readonly afterRowId: string | null;
|
|
331
|
+
readonly limit: number;
|
|
332
|
+
}
|
|
333
|
+
/** A stored push outcome with the client that sent it. */
|
|
334
|
+
export interface ClientPushResult {
|
|
335
|
+
readonly clientId: string;
|
|
336
|
+
readonly result: StoredPushResult;
|
|
337
|
+
}
|
|
338
|
+
/** One retained commit with its changes, for the admin commit drill-down. */
|
|
339
|
+
export interface CommitDetail extends CommitMetadata {
|
|
340
|
+
readonly changes: readonly StoredChange[];
|
|
341
|
+
}
|
|
342
|
+
/** One retained change to a single row, for the admin row history. */
|
|
343
|
+
export interface RowChangeRecord {
|
|
344
|
+
readonly commitSeq: number;
|
|
345
|
+
readonly op: 'upsert' | 'delete';
|
|
346
|
+
readonly rowVersion?: number;
|
|
347
|
+
readonly actorId: string;
|
|
348
|
+
readonly clientId: string;
|
|
349
|
+
readonly createdAtMs: number;
|
|
350
|
+
}
|
|
351
|
+
export interface RowChangesQuery {
|
|
352
|
+
readonly table: string;
|
|
353
|
+
readonly rowId: string;
|
|
354
|
+
readonly limit: number;
|
|
313
355
|
}
|
|
314
356
|
/** One recent commit touching a scope key, for the admin scope activity view. */
|
|
315
357
|
export interface ScopeCommitActivity {
|
|
@@ -671,18 +713,27 @@ export interface ServerStorage {
|
|
|
671
713
|
* touched tables), the newest-oriented window an operator inspects.
|
|
672
714
|
* `scopeActivity`: the recent commits touching one scope key, via the
|
|
673
715
|
* change scope index (never a log scan).
|
|
674
|
-
* `
|
|
675
|
-
*
|
|
716
|
+
* `getCommit`: one retained commit with its changes and payloads;
|
|
717
|
+
* `undefined` when the commit does not exist or was pruned.
|
|
718
|
+
* `listRowChanges`: the retained changes to one row, newest first. It
|
|
719
|
+
* reads the partition's retained log without a dedicated index, so the
|
|
720
|
+
* write path pays nothing for it and its cost is bounded by retention.
|
|
721
|
+
* `listRows`: one table's rows ordered by rowId, without a scope filter.
|
|
722
|
+
* `countRows`: one table's row count.
|
|
723
|
+
* `listPushResults`: every stored idempotency outcome for one
|
|
724
|
+
* clientCommitId across the partition's clients, scanning the retained
|
|
725
|
+
* outcomes without a dedicated index.
|
|
676
726
|
* `listPartitions`: the partition-only compatibility view of
|
|
677
727
|
* `listPartitionRegistry`, sorted.
|
|
678
728
|
*/
|
|
679
729
|
listClientRecords?(partition: string): Promise<ClientRecord[]>;
|
|
680
730
|
listCommitMetadata?(partition: string, query: CommitMetadataQuery): Promise<CommitMetadata[]>;
|
|
681
731
|
scopeActivity?(partition: string, query: ScopeActivityQuery): Promise<ScopeCommitActivity[]>;
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
732
|
+
getCommit?(partition: string, commitSeq: number): Promise<CommitDetail | undefined>;
|
|
733
|
+
listRowChanges?(partition: string, query: RowChangesQuery): Promise<RowChangeRecord[]>;
|
|
734
|
+
listRows?(partition: string, query: RowListQuery): Promise<StoredRow[]>;
|
|
735
|
+
countRows?(partition: string, table: string): Promise<number>;
|
|
736
|
+
listPushResults?(partition: string, clientCommitId: string): Promise<ClientPushResult[]>;
|
|
686
737
|
listPartitions(): Promise<string[]>;
|
|
687
738
|
}
|
|
688
739
|
/** A row referencing a blob, with the scopes needed to authorize download. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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.32.0"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@electric-sql/pglite": "^0.5.4"
|