@syncular/server 0.42.2 → 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 +14 -4
- package/dist/operations.d.ts +5 -0
- package/dist/operations.js +4 -4
- package/dist/postgres-storage.js +386 -104
- package/dist/push.d.ts +18 -1
- package/dist/push.js +247 -132
- package/dist/relational-rows.d.ts +9 -5
- package/dist/relational-rows.js +29 -11
- package/dist/sqlite-storage.js +14 -10
- package/dist/storage.d.ts +46 -32
- package/dist/storage.js +26 -0
- package/package.json +2 -2
- package/src/d1-storage.ts +30 -5
- package/src/operations.ts +12 -7
- package/src/postgres-storage.ts +553 -166
- package/src/push.ts +358 -151
- package/src/relational-rows.ts +31 -10
- package/src/sqlite-storage.ts +33 -18
- package/src/storage.ts +75 -36
package/dist/d1-storage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { validateCommitPruneQuery } from './prune.js';
|
|
3
3
|
import { StorageQueryError } from './storage-errors.js';
|
|
4
|
-
import { clientRoundState, PARTITION_TOUCH_INTERVAL_MS } from './storage.js';
|
|
4
|
+
import { clientRoundState, PARTITION_TOUCH_INTERVAL_MS, rowsLiveAfterDelete, } from './storage.js';
|
|
5
5
|
/**
|
|
6
6
|
* Cloudflare D1 server storage for Workers deployments.
|
|
7
7
|
*
|
|
@@ -308,7 +308,7 @@ class D1Transaction {
|
|
|
308
308
|
}
|
|
309
309
|
return queryAuthoritativeOn(this.#db, this.#partition, this.#prepareQuery(query));
|
|
310
310
|
}
|
|
311
|
-
async
|
|
311
|
+
async preparePushApply(clientId, clientCommitId, runningSchemaVersion, _targets = []) {
|
|
312
312
|
this.#assertOpen();
|
|
313
313
|
if (!this.#pushApplySerialized) {
|
|
314
314
|
throw new Error('D1 push apply requires externally serialized partition writes');
|
|
@@ -316,6 +316,10 @@ class D1Transaction {
|
|
|
316
316
|
// D1 has no interactive lock. The caller explicitly asserted that every
|
|
317
317
|
// write for this partition is already serialized (normally by its DO).
|
|
318
318
|
this.#pushApplyCheckpoint = this.#buffer.length;
|
|
319
|
+
return {
|
|
320
|
+
gate: await this.readServeGate(runningSchemaVersion),
|
|
321
|
+
persisted: await this.getPushResult(clientId, clientCommitId),
|
|
322
|
+
};
|
|
319
323
|
}
|
|
320
324
|
advanceCheckpoint(_name, _ownerEpoch, _watermark, _observedRows, _nowMs) {
|
|
321
325
|
// D1 installs no fence and declares no checkpoints in this release.
|
|
@@ -345,6 +349,9 @@ class D1Transaction {
|
|
|
345
349
|
this.#applicationOpIndexes.clear();
|
|
346
350
|
await this.putPushResult(clientId, clientCommitId, result);
|
|
347
351
|
await this.commit();
|
|
352
|
+
// The idempotency re-check found no record under the serialized apply, so
|
|
353
|
+
// the insert above is the canonical outcome.
|
|
354
|
+
return result;
|
|
348
355
|
}
|
|
349
356
|
async #assertNoUniqueCollision(table, row, opIndex) {
|
|
350
357
|
if (!table.materialize)
|
|
@@ -480,6 +487,7 @@ class D1Transaction {
|
|
|
480
487
|
commit.actorId,
|
|
481
488
|
commit.createdAtMs,
|
|
482
489
|
]);
|
|
490
|
+
const liveAfterDelete = rowsLiveAfterDelete(commit.changes);
|
|
483
491
|
commit.changes.forEach((change, idx) => {
|
|
484
492
|
this.#buffer_('INSERT INTO sync_changes(partition, commit_seq, idx, tbl, row_id, op, row_version, scopes, payload) VALUES (?,?,?,?,?,?,?,?,?)', [
|
|
485
493
|
p,
|
|
@@ -492,9 +500,11 @@ class D1Transaction {
|
|
|
492
500
|
JSON.stringify(change.scopes),
|
|
493
501
|
change.payload ?? null,
|
|
494
502
|
]);
|
|
495
|
-
if (change.op === 'delete'
|
|
503
|
+
if (change.op === 'delete' &&
|
|
504
|
+
!liveAfterDelete.has(`${change.table}\u0000${change.rowId}`)) {
|
|
496
505
|
// §5 delete precedence: every applied delete leaves a tombstone,
|
|
497
|
-
// pruned with the commit log (§4.6).
|
|
506
|
+
// pruned with the commit log (§4.6). A delete that the same commit
|
|
507
|
+
// follows with an upsert is a §2.2 scope move, not a deletion.
|
|
498
508
|
this.#buffer_(`INSERT INTO sync_tombstones(partition, tbl, row_id, commit_seq) VALUES (?,?,?,?)
|
|
499
509
|
ON CONFLICT(partition, tbl, row_id) DO UPDATE SET commit_seq=excluded.commit_seq`, [p, change.table, change.rowId, commitSeq]);
|
|
500
510
|
}
|
package/dist/operations.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ export type CommandMutation = {
|
|
|
51
51
|
readonly table: string;
|
|
52
52
|
readonly op: 'upsert';
|
|
53
53
|
readonly values: Readonly<Record<string, RowValue>>;
|
|
54
|
+
} | {
|
|
55
|
+
readonly table: string;
|
|
56
|
+
readonly op: 'rescope';
|
|
57
|
+
/** Full row; only its scope columns may differ from the stored row. */
|
|
58
|
+
readonly values: Readonly<Record<string, RowValue>>;
|
|
54
59
|
} | {
|
|
55
60
|
readonly table: string;
|
|
56
61
|
readonly op: 'delete';
|
package/dist/operations.js
CHANGED
|
@@ -256,23 +256,23 @@ function commandOperations(mutations, schema) {
|
|
|
256
256
|
const supplied = new Set(Object.keys(mutation.values));
|
|
257
257
|
for (const name of supplied) {
|
|
258
258
|
if (!table.columnIndex.has(name)) {
|
|
259
|
-
throw syncError('operation.invalid_request', 'command upsert has an unknown column');
|
|
259
|
+
throw syncError('operation.invalid_request', 'command upsert or rescope has an unknown column');
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
const values = table.columns.map((column) => {
|
|
263
263
|
if (!supplied.has(column.name)) {
|
|
264
|
-
throw syncError('operation.invalid_request', 'command upsert must provide a full row');
|
|
264
|
+
throw syncError('operation.invalid_request', 'command upsert or rescope must provide a full row');
|
|
265
265
|
}
|
|
266
266
|
return mutation.values[column.name] ?? null;
|
|
267
267
|
});
|
|
268
268
|
const rowId = renderScopeValue(values[table.primaryKeyIndex]);
|
|
269
269
|
if (rowId === undefined) {
|
|
270
|
-
throw syncError('operation.invalid_request', 'command upsert requires a non-empty primary key');
|
|
270
|
+
throw syncError('operation.invalid_request', 'command upsert or rescope requires a non-empty primary key');
|
|
271
271
|
}
|
|
272
272
|
return {
|
|
273
273
|
table: table.name,
|
|
274
274
|
rowId,
|
|
275
|
-
op:
|
|
275
|
+
op: mutation.op,
|
|
276
276
|
payload: encodeSparseRow(table.columns, table.primaryKeyIndex, values),
|
|
277
277
|
};
|
|
278
278
|
});
|