@syncular/server 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/d1-storage.js +8 -1
- package/dist/postgres-storage.js +385 -119
- package/dist/push.d.ts +4 -1
- package/dist/push.js +47 -72
- package/dist/relational-rows.d.ts +9 -5
- package/dist/relational-rows.js +29 -11
- package/dist/sqlite-storage.js +8 -7
- package/dist/storage.d.ts +39 -32
- package/package.json +2 -2
- package/src/d1-storage.ts +18 -2
- package/src/postgres-storage.ts +552 -187
- package/src/push.ts +63 -88
- package/src/relational-rows.ts +31 -10
- package/src/sqlite-storage.ts +25 -16
- package/src/storage.ts +48 -36
package/src/push.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { authorizeWrite, renderScopeValue, storedScopesForRow } from './scopes';
|
|
|
48
48
|
import { decodeColumnVersions, encodeColumnVersions } from './relational-rows';
|
|
49
49
|
import type {
|
|
50
50
|
NewChange,
|
|
51
|
+
PreparedPushApply,
|
|
51
52
|
StorageTransaction,
|
|
52
53
|
StoredCommit,
|
|
53
54
|
StoredPushResult,
|
|
@@ -1574,6 +1575,16 @@ export async function processPushCommitWithTrace(
|
|
|
1574
1575
|
frame: PushCommitFrame,
|
|
1575
1576
|
wireSchema: CompiledSchema = schema,
|
|
1576
1577
|
): Promise<ProcessedPushCommit> {
|
|
1578
|
+
// The frame names its rows before the transaction opens, so the admission
|
|
1579
|
+
// read carries the commit's read-ahead and the apply issues no separate
|
|
1580
|
+
// prefetch statement.
|
|
1581
|
+
const targets = new Map<string, string[]>();
|
|
1582
|
+
for (const op of frame.operations) {
|
|
1583
|
+
if (!schema.tables.has(op.table)) continue;
|
|
1584
|
+
const ids = targets.get(op.table) ?? [];
|
|
1585
|
+
ids.push(op.rowId);
|
|
1586
|
+
targets.set(op.table, ids);
|
|
1587
|
+
}
|
|
1577
1588
|
return processPushOperationsWithTrace(
|
|
1578
1589
|
ctx,
|
|
1579
1590
|
schema,
|
|
@@ -1582,6 +1593,9 @@ export async function processPushCommitWithTrace(
|
|
|
1582
1593
|
frame.clientCommitId,
|
|
1583
1594
|
async () => frame.operations,
|
|
1584
1595
|
wireSchema,
|
|
1596
|
+
targets.size === 0
|
|
1597
|
+
? undefined
|
|
1598
|
+
: [...targets].map(([table, rowIds]) => ({ table, rowIds })),
|
|
1585
1599
|
);
|
|
1586
1600
|
}
|
|
1587
1601
|
|
|
@@ -1600,86 +1614,46 @@ export async function processPushOperationsWithTrace(
|
|
|
1600
1614
|
tx: StorageTransaction,
|
|
1601
1615
|
) => Promise<readonly ApplyOperation[]>,
|
|
1602
1616
|
wireSchema: CompiledSchema = schema,
|
|
1617
|
+
prefetchTargets?: readonly {
|
|
1618
|
+
readonly table: string;
|
|
1619
|
+
readonly rowIds: readonly string[];
|
|
1620
|
+
}[],
|
|
1603
1621
|
): Promise<ProcessedPushCommit> {
|
|
1604
|
-
const {
|
|
1605
|
-
let persisted: StoredPushResult | undefined;
|
|
1606
|
-
try {
|
|
1607
|
-
persisted = await storage.getPushResult(
|
|
1608
|
-
partition,
|
|
1609
|
-
clientId,
|
|
1610
|
-
clientCommitId,
|
|
1611
|
-
);
|
|
1612
|
-
} catch (error) {
|
|
1613
|
-
if (
|
|
1614
|
-
error instanceof SyncError &&
|
|
1615
|
-
error.code === 'sync.idempotency_cache_miss'
|
|
1616
|
-
) {
|
|
1617
|
-
// §6.3: answer the retryable cache-miss for this commit rather than
|
|
1618
|
-
// re-applying. Not persisted — a retry may find a readable record.
|
|
1619
|
-
return {
|
|
1620
|
-
frame: idempotencyCacheMissFrame(clientCommitId, error),
|
|
1621
|
-
replayed: false,
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
throw error;
|
|
1625
|
-
}
|
|
1626
|
-
if (persisted !== undefined) {
|
|
1627
|
-
return processedPushCommit(clientCommitId, persisted, true);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1622
|
+
const { partition } = ctx;
|
|
1630
1623
|
const createdAtMs = clockOf(ctx)();
|
|
1631
1624
|
const blobCtx: BlobApplyContext = { store: ctx.blobs, partition };
|
|
1632
1625
|
const crdtMergers = ctx.crdtMergers;
|
|
1633
1626
|
const validators = ctx.validators;
|
|
1634
1627
|
const commitValidator = ctx.commitValidator;
|
|
1635
1628
|
const reactionPlanner = ctx.reactionPlanner;
|
|
1636
|
-
const tx = await storage.begin(partition);
|
|
1637
|
-
const lockPartitionForPush =
|
|
1638
|
-
tx.lockPartitionForPush?.bind(tx) ??
|
|
1639
|
-
tx.lockPartitionForCommitValidation?.bind(tx);
|
|
1640
|
-
const commitRejectedPushResult = tx.commitRejectedPushResult?.bind(tx);
|
|
1629
|
+
const tx = await ctx.storage.begin(partition);
|
|
1641
1630
|
try {
|
|
1642
|
-
if (
|
|
1643
|
-
lockPartitionForPush === undefined ||
|
|
1644
|
-
commitRejectedPushResult === undefined ||
|
|
1645
|
-
(reactionPlanner !== undefined && tx.enqueueReactions === undefined)
|
|
1646
|
-
) {
|
|
1631
|
+
if (reactionPlanner !== undefined && tx.enqueueReactions === undefined) {
|
|
1647
1632
|
throw new Error(
|
|
1648
|
-
'storage transaction does not support
|
|
1633
|
+
'storage transaction does not support configured durable reactions',
|
|
1649
1634
|
);
|
|
1650
1635
|
}
|
|
1651
|
-
|
|
1652
|
-
//
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
await tx.rollback();
|
|
1659
|
-
throw serveNotReadyError(refusal);
|
|
1660
|
-
}
|
|
1661
|
-
// The optimistic lookup above may have raced another delivery. Re-check
|
|
1662
|
-
// only after acquiring partition serialization and before any operation
|
|
1663
|
-
// read, validation, merge, or staged write. The re-check runs on the
|
|
1664
|
-
// transaction's own connection when the backend provides one: a pooled
|
|
1665
|
-
// Postgres client holding the partition lock would otherwise wait for a
|
|
1666
|
-
// second pool slot and deadlock against pushes waiting on the lock. A
|
|
1667
|
-
// racing duplicate's result commits before the lock releases, so the
|
|
1668
|
-
// transaction-scoped read observes it.
|
|
1636
|
+
// The storage takes the partition lock and then reads the admission token
|
|
1637
|
+
// on the transaction's own connection: the RFC 0007 serve gate and the
|
|
1638
|
+
// §2.3 idempotency record, plus any read-ahead the caller already knows.
|
|
1639
|
+
// A racing duplicate's result commits before the lock releases, so the
|
|
1640
|
+
// later read is exact and a duplicate never reruns an operation,
|
|
1641
|
+
// validator, or merge.
|
|
1642
|
+
let prepared: PreparedPushApply;
|
|
1669
1643
|
try {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
return processedPushCommit(clientCommitId, serializedPersisted, true);
|
|
1677
|
-
}
|
|
1644
|
+
prepared = await tx.preparePushApply(
|
|
1645
|
+
clientId,
|
|
1646
|
+
clientCommitId,
|
|
1647
|
+
schema.version,
|
|
1648
|
+
prefetchTargets,
|
|
1649
|
+
);
|
|
1678
1650
|
} catch (error) {
|
|
1679
1651
|
if (
|
|
1680
1652
|
error instanceof SyncError &&
|
|
1681
1653
|
error.code === 'sync.idempotency_cache_miss'
|
|
1682
1654
|
) {
|
|
1655
|
+
// §6.3: answer the retryable cache-miss for this commit rather than
|
|
1656
|
+
// re-applying. Not persisted — a retry may find a readable record.
|
|
1683
1657
|
await tx.rollback();
|
|
1684
1658
|
return {
|
|
1685
1659
|
frame: idempotencyCacheMissFrame(clientCommitId, error),
|
|
@@ -1688,6 +1662,22 @@ export async function processPushOperationsWithTrace(
|
|
|
1688
1662
|
}
|
|
1689
1663
|
throw error;
|
|
1690
1664
|
}
|
|
1665
|
+
// RFC 0007: the gate is evaluated while the partition lock is held, so
|
|
1666
|
+
// the migration check and the write share one transaction and no
|
|
1667
|
+
// migration can interleave in between.
|
|
1668
|
+
const refusal = serveGateRefusal(
|
|
1669
|
+
prepared.gate,
|
|
1670
|
+
schema.version,
|
|
1671
|
+
ctx.checkpoints,
|
|
1672
|
+
);
|
|
1673
|
+
if (refusal !== undefined) {
|
|
1674
|
+
await tx.rollback();
|
|
1675
|
+
throw serveNotReadyError(refusal);
|
|
1676
|
+
}
|
|
1677
|
+
if (prepared.persisted !== undefined) {
|
|
1678
|
+
await tx.rollback();
|
|
1679
|
+
return processedPushCommit(clientCommitId, prepared.persisted, true);
|
|
1680
|
+
}
|
|
1691
1681
|
const results: PushOperationResult[] = [];
|
|
1692
1682
|
const changes: NewChange[] = [];
|
|
1693
1683
|
const validatedOperations: ValidateCommitOperation[] = [];
|
|
@@ -1702,8 +1692,10 @@ export async function processPushOperationsWithTrace(
|
|
|
1702
1692
|
ids.push(op.rowId);
|
|
1703
1693
|
targets.set(op.table, ids);
|
|
1704
1694
|
}
|
|
1705
|
-
|
|
1706
|
-
await tx.prefetchRows(
|
|
1695
|
+
if (targets.size > 0) {
|
|
1696
|
+
await tx.prefetchRows(
|
|
1697
|
+
[...targets].map(([table, rowIds]) => ({ table, rowIds })),
|
|
1698
|
+
);
|
|
1707
1699
|
}
|
|
1708
1700
|
}
|
|
1709
1701
|
for (let opIndex = 0; opIndex < operations.length; opIndex++) {
|
|
@@ -1802,18 +1794,13 @@ export async function processPushOperationsWithTrace(
|
|
|
1802
1794
|
results: [terminated],
|
|
1803
1795
|
});
|
|
1804
1796
|
// Discard candidates and persist the rejection while retaining the same
|
|
1805
|
-
// partition lock. There is no unlock gap in which a duplicate can rerun
|
|
1806
|
-
|
|
1807
|
-
const canonical = await
|
|
1808
|
-
partition,
|
|
1797
|
+
// partition lock. There is no unlock gap in which a duplicate can rerun,
|
|
1798
|
+
// and the finalization returns the canonical outcome it persisted.
|
|
1799
|
+
const canonical = await tx.commitRejectedPushResult(
|
|
1809
1800
|
clientId,
|
|
1810
1801
|
clientCommitId,
|
|
1802
|
+
stored,
|
|
1811
1803
|
);
|
|
1812
|
-
if (canonical === undefined) {
|
|
1813
|
-
throw new Error(
|
|
1814
|
-
'push rejection finalization did not persist an outcome',
|
|
1815
|
-
);
|
|
1816
|
-
}
|
|
1817
1804
|
return processedPushCommit(
|
|
1818
1805
|
clientCommitId,
|
|
1819
1806
|
canonical,
|
|
@@ -1895,24 +1882,12 @@ export async function processPushOperationsWithTrace(
|
|
|
1895
1882
|
},
|
|
1896
1883
|
],
|
|
1897
1884
|
});
|
|
1898
|
-
if (commitRejectedPushResult === undefined) {
|
|
1899
|
-
await tx.rollback();
|
|
1900
|
-
throw new Error(
|
|
1901
|
-
'storage transaction lost atomic push rejection finalization support',
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
1885
|
try {
|
|
1905
|
-
await commitRejectedPushResult(
|
|
1906
|
-
const canonical = await storage.getPushResult(
|
|
1907
|
-
partition,
|
|
1886
|
+
const canonical = await tx.commitRejectedPushResult(
|
|
1908
1887
|
clientId,
|
|
1909
1888
|
clientCommitId,
|
|
1889
|
+
stored,
|
|
1910
1890
|
);
|
|
1911
|
-
if (canonical === undefined) {
|
|
1912
|
-
throw new Error(
|
|
1913
|
-
'push rejection finalization did not persist an outcome',
|
|
1914
|
-
);
|
|
1915
|
-
}
|
|
1916
1891
|
return processedPushCommit(
|
|
1917
1892
|
clientCommitId,
|
|
1918
1893
|
canonical,
|
package/src/relational-rows.ts
CHANGED
|
@@ -807,17 +807,38 @@ export const COMMIT_WINDOW_BATCH_SQL = `SELECT s.i AS sub, c.commit_seq AS commi
|
|
|
807
807
|
ORDER BY s.i, c.commit_seq, ch.idx`;
|
|
808
808
|
|
|
809
809
|
/**
|
|
810
|
-
* PostgreSQL push read-ahead
|
|
811
|
-
*
|
|
812
|
-
*
|
|
810
|
+
* PostgreSQL push read-ahead across every table a commit targets, in one
|
|
811
|
+
* statement: each requested row id with its stored row (NULL columns when
|
|
812
|
+
* absent, `selectRowSql` shape) and its §5 delete tombstone. Params:
|
|
813
|
+
* [partition, specs as JSON text], `specs` = `{tbl, ids: string[]}` records.
|
|
813
814
|
*/
|
|
814
|
-
export function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
815
|
+
export function prefetchRowsBatchSql(
|
|
816
|
+
tables: readonly CompiledTable[],
|
|
817
|
+
params: { readonly partition: string; readonly specs: string } = {
|
|
818
|
+
partition: '$1',
|
|
819
|
+
specs: '$2',
|
|
820
|
+
},
|
|
821
|
+
): string {
|
|
822
|
+
// One branch per table: a lateral join cannot switch its relation per
|
|
823
|
+
// record, and the union keeps the whole read-ahead one round trip.
|
|
824
|
+
const projection = tables
|
|
825
|
+
.map((table) => {
|
|
826
|
+
const quoted = quoteIdent(table.name);
|
|
827
|
+
return `SELECT ids.tbl AS tbl, ids.id AS lookup_id, r.${quoteIdent(SYNC_ROW_ID_COLUMN)} AS row_id, r.${quoteIdent(SYNC_VERSION_COLUMN)} AS server_version, r.${quoteIdent(SYNC_SCOPES_COLUMN)} AS scopes, r.${quoteIdent(SYNC_PAYLOAD_COLUMN)} AS payload, r.${quoteIdent(SYNC_COLUMN_VERSIONS_COLUMN)} AS column_versions, t.commit_seq AS tombstone_seq
|
|
828
|
+
FROM ids
|
|
829
|
+
LEFT JOIN ${quoted} r
|
|
830
|
+
ON r.${quoteIdent(SYNC_PARTITION_COLUMN)}=${params.partition} AND r.${quoteIdent(SYNC_ROW_ID_COLUMN)}=ids.id
|
|
831
|
+
LEFT JOIN sync_tombstones t
|
|
832
|
+
ON t.partition=${params.partition} AND t.tbl=ids.tbl AND t.row_id=ids.id
|
|
833
|
+
WHERE ids.tbl='${table.name.replaceAll("'", "''")}'`;
|
|
834
|
+
})
|
|
835
|
+
.join('\n UNION ALL ');
|
|
836
|
+
return `WITH ids AS (
|
|
837
|
+
SELECT spec.tbl AS tbl, entry.id AS id
|
|
838
|
+
FROM jsonb_to_recordset(${params.specs}::text::jsonb) AS spec(tbl text, ids jsonb)
|
|
839
|
+
CROSS JOIN LATERAL jsonb_array_elements_text(spec.ids) AS entry(id)
|
|
840
|
+
)
|
|
841
|
+
${projection}`;
|
|
821
842
|
}
|
|
822
843
|
|
|
823
844
|
/**
|
package/src/sqlite-storage.ts
CHANGED
|
@@ -101,6 +101,7 @@ import type {
|
|
|
101
101
|
IndexRowScanQuery,
|
|
102
102
|
NewCommit,
|
|
103
103
|
NewReaction,
|
|
104
|
+
PreparedPushApply,
|
|
104
105
|
PartitionRegistryEntry,
|
|
105
106
|
PrunedReactionCounts,
|
|
106
107
|
ReactionClaimQuery,
|
|
@@ -255,20 +256,6 @@ class SqliteTransaction implements StorageTransaction {
|
|
|
255
256
|
.run(this.#partition, table, rowId);
|
|
256
257
|
}
|
|
257
258
|
|
|
258
|
-
getPushResult(
|
|
259
|
-
clientId: string,
|
|
260
|
-
clientCommitId: string,
|
|
261
|
-
): Promise<StoredPushResult | undefined> {
|
|
262
|
-
this.#assertOpen();
|
|
263
|
-
// One shared SQLite connection: this read runs inside this
|
|
264
|
-
// transaction's BEGIN IMMEDIATE.
|
|
265
|
-
return this.#storage.getPushResult(
|
|
266
|
-
this.#partition,
|
|
267
|
-
clientId,
|
|
268
|
-
clientCommitId,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
259
|
scanRows(query: RowScanQuery): Promise<StoredRow[]> {
|
|
273
260
|
this.#assertOpen();
|
|
274
261
|
return this.#storage.scanRows(this.#partition, query);
|
|
@@ -288,11 +275,30 @@ class SqliteTransaction implements StorageTransaction {
|
|
|
288
275
|
return this.#storage.runAuthoritativeQuery(this.#partition, query);
|
|
289
276
|
}
|
|
290
277
|
|
|
291
|
-
async
|
|
278
|
+
async preparePushApply(
|
|
279
|
+
clientId: string,
|
|
280
|
+
clientCommitId: string,
|
|
281
|
+
runningSchemaVersion: number,
|
|
282
|
+
_targets: readonly {
|
|
283
|
+
readonly table: string;
|
|
284
|
+
readonly rowIds: readonly string[];
|
|
285
|
+
}[] = [],
|
|
286
|
+
): Promise<PreparedPushApply> {
|
|
292
287
|
this.#assertOpen();
|
|
293
288
|
// BEGIN IMMEDIATE in the constructor already owns SQLite's writer lock.
|
|
294
289
|
this.#storage.db.exec('SAVEPOINT syncular_push_candidate');
|
|
295
290
|
this.#pushApplySavepoint = true;
|
|
291
|
+
return {
|
|
292
|
+
gate: await this.#storage.readServeGate(
|
|
293
|
+
this.#partition,
|
|
294
|
+
runningSchemaVersion,
|
|
295
|
+
),
|
|
296
|
+
persisted: await this.#storage.getPushResult(
|
|
297
|
+
this.#partition,
|
|
298
|
+
clientId,
|
|
299
|
+
clientCommitId,
|
|
300
|
+
),
|
|
301
|
+
};
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
async advanceCheckpoint(
|
|
@@ -324,7 +330,7 @@ class SqliteTransaction implements StorageTransaction {
|
|
|
324
330
|
clientId: string,
|
|
325
331
|
clientCommitId: string,
|
|
326
332
|
result: StoredPushResult,
|
|
327
|
-
): Promise<
|
|
333
|
+
): Promise<StoredPushResult> {
|
|
328
334
|
this.#assertOpen();
|
|
329
335
|
if (!this.#pushApplySavepoint) {
|
|
330
336
|
throw new Error('push rejection requires its apply savepoint');
|
|
@@ -334,6 +340,9 @@ class SqliteTransaction implements StorageTransaction {
|
|
|
334
340
|
this.#pushApplySavepoint = false;
|
|
335
341
|
await this.putPushResult(clientId, clientCommitId, result);
|
|
336
342
|
await this.commit();
|
|
343
|
+
// The idempotency re-check found no record under the writer lock, so the
|
|
344
|
+
// insert above is the canonical outcome.
|
|
345
|
+
return result;
|
|
337
346
|
}
|
|
338
347
|
|
|
339
348
|
async upsertRow(
|
package/src/storage.ts
CHANGED
|
@@ -74,6 +74,18 @@ export interface ServeGate {
|
|
|
74
74
|
}[];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The push apply's admission token: the partition lock, the serve gate, and
|
|
79
|
+
* the idempotency record read in one round trip. A storage with per-statement
|
|
80
|
+
* network cost answers all three with one statement (see
|
|
81
|
+
* `PostgresServerStorage`); the lock is held from this call through commit.
|
|
82
|
+
*/
|
|
83
|
+
export interface PreparedPushApply {
|
|
84
|
+
readonly gate: ServeGate;
|
|
85
|
+
/** The already-persisted outcome for this key, or `undefined` on a miss. */
|
|
86
|
+
readonly persisted: StoredPushResult | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
/** Why `serveGate` refuses, or `undefined` when this request may serve. */
|
|
78
90
|
export type ServeGateRefusal =
|
|
79
91
|
| {
|
|
@@ -547,12 +559,17 @@ export interface StorageTransaction {
|
|
|
547
559
|
getRow(table: string, rowId: string): Promise<StoredRow | undefined>;
|
|
548
560
|
/**
|
|
549
561
|
* Optional read-ahead: the push layer names every row a commit's
|
|
550
|
-
* operations target,
|
|
551
|
-
* per-statement network cost reads them in one statement
|
|
552
|
-
* following `getRow` calls from that snapshot
|
|
553
|
-
* this transaction.
|
|
562
|
+
* operations target, across every table, before it applies them. A storage
|
|
563
|
+
* with per-statement network cost reads them in one statement (one branch
|
|
564
|
+
* per table) and answers the following `getRow` calls from that snapshot
|
|
565
|
+
* until the row is written in this transaction.
|
|
554
566
|
*/
|
|
555
|
-
prefetchRows?(
|
|
567
|
+
prefetchRows?(
|
|
568
|
+
targets: readonly {
|
|
569
|
+
readonly table: string;
|
|
570
|
+
readonly rowIds: readonly string[];
|
|
571
|
+
}[],
|
|
572
|
+
): Promise<void>;
|
|
556
573
|
/**
|
|
557
574
|
* §5 delete-precedence tombstone read: the `commit_seq` of the newest
|
|
558
575
|
* applied delete of this row still within the pruning horizon, or
|
|
@@ -567,20 +584,6 @@ export interface StorageTransaction {
|
|
|
567
584
|
* over an absent row; a no-op when no tombstone exists.
|
|
568
585
|
*/
|
|
569
586
|
clearTombstone(table: string, rowId: string): Promise<void>;
|
|
570
|
-
/**
|
|
571
|
-
* Optional transaction-scoped idempotency lookup (§2.3) with the same
|
|
572
|
-
* semantics as `ServerStorage.getPushResult`, including the
|
|
573
|
-
* `sync.idempotency_cache_miss` throw. The push layer prefers it for the
|
|
574
|
-
* post-serialization duplicate re-check so the read runs on the
|
|
575
|
-
* transaction's own connection — a pooled Postgres client that holds the
|
|
576
|
-
* partition lock must never wait for a second pool slot mid-push. In-tree
|
|
577
|
-
* SQLite/Postgres/D1 adapters implement it; a custom adapter that omits it
|
|
578
|
-
* keeps the pool-level read.
|
|
579
|
-
*/
|
|
580
|
-
getPushResult?(
|
|
581
|
-
clientId: string,
|
|
582
|
-
clientCommitId: string,
|
|
583
|
-
): Promise<StoredPushResult | undefined>;
|
|
584
587
|
/**
|
|
585
588
|
* Optional candidate-state scan used only by whole-commit validation.
|
|
586
589
|
* In-tree SQLite/Postgres/D1 backends implement it with read-your-own-writes
|
|
@@ -610,28 +613,37 @@ export interface StorageTransaction {
|
|
|
610
613
|
): Promise<AuthoritativeQueryResult>;
|
|
611
614
|
/**
|
|
612
615
|
* Serialize every push apply for this partition before any operation read,
|
|
613
|
-
* validation, merge, or write
|
|
614
|
-
*
|
|
616
|
+
* validation, merge, or write, and read the admission token in the same
|
|
617
|
+
* round trip: the RFC 0007 serve gate and the §2.3 idempotency record for
|
|
618
|
+
* `clientCommitId`. The lock is retained through terminal-result commit,
|
|
619
|
+
* so the idempotency read is exact and a racing duplicate cannot rerun.
|
|
620
|
+
* `targets` are the rows the commit is known to touch before its operations
|
|
621
|
+
* are built; a storage with per-statement network cost folds that read-ahead
|
|
622
|
+
* into the admission read and answers the following `getRow` calls from it.
|
|
615
623
|
* Missing support fails closed before an app-row mutation.
|
|
616
624
|
*/
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
625
|
+
preparePushApply(
|
|
626
|
+
clientId: string,
|
|
627
|
+
clientCommitId: string,
|
|
628
|
+
runningSchemaVersion: number,
|
|
629
|
+
targets?: readonly {
|
|
630
|
+
readonly table: string;
|
|
631
|
+
readonly rowIds: readonly string[];
|
|
632
|
+
}[],
|
|
633
|
+
): Promise<PreparedPushApply>;
|
|
624
634
|
/**
|
|
625
635
|
* Rejection finalization while the push-apply serialization lock is still
|
|
626
636
|
* held: discard every candidate write, persist the rejected idempotency
|
|
627
|
-
* result, and finish the transaction atomically.
|
|
628
|
-
*
|
|
637
|
+
* result, and finish the transaction atomically. Returns the persisted
|
|
638
|
+
* canonical outcome, so the caller never re-reads it. Required for every
|
|
639
|
+
* push so a concurrent duplicate cannot rerun operations, validators, or
|
|
640
|
+
* merges.
|
|
629
641
|
*/
|
|
630
|
-
commitRejectedPushResult
|
|
642
|
+
commitRejectedPushResult(
|
|
631
643
|
clientId: string,
|
|
632
644
|
clientCommitId: string,
|
|
633
645
|
result: StoredPushResult,
|
|
634
|
-
): Promise<
|
|
646
|
+
): Promise<StoredPushResult>;
|
|
635
647
|
upsertRow(
|
|
636
648
|
table: string,
|
|
637
649
|
row: StoredRow,
|
|
@@ -744,10 +756,10 @@ export interface ServerStorage {
|
|
|
744
756
|
* same command races a push on Postgres and a SQLite-only test conceals it.
|
|
745
757
|
*
|
|
746
758
|
* A trusted command must therefore run through `registerRemoteCommand`,
|
|
747
|
-
* which builds after `
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
759
|
+
* which builds after `preparePushApply` and shares the committed
|
|
760
|
+
* transaction, or call `preparePushApply` before its first authority,
|
|
761
|
+
* version, or replay read. SSP2 request handling does this for you; only
|
|
762
|
+
* host-authored commands choose.
|
|
751
763
|
*/
|
|
752
764
|
begin(partition: string): Promise<StorageTransaction>;
|
|
753
765
|
|