@syncular/server 0.37.0 → 0.39.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/content-encoding.d.ts +24 -23
- package/dist/content-encoding.js +77 -22
- package/dist/d1-storage.js +24 -19
- package/dist/postgres-storage.d.ts +1 -1
- package/dist/postgres-storage.js +240 -74
- package/dist/pull.d.ts +7 -3
- package/dist/pull.js +21 -12
- package/dist/relational-rows.d.ts +49 -23
- package/dist/relational-rows.js +133 -59
- package/dist/schema.d.ts +19 -0
- package/dist/schema.js +22 -0
- package/dist/sqlite-bun-driver.js +1 -1
- package/dist/sqlite-node-driver.d.ts +1 -0
- package/dist/sqlite-node-driver.js +13 -3
- package/dist/sqlite-storage.js +36 -21
- package/dist/storage.d.ts +8 -1
- package/dist/storage.js +3 -0
- package/package.json +2 -2
- package/src/content-encoding.ts +99 -31
- package/src/d1-storage.ts +36 -22
- package/src/postgres-storage.ts +338 -109
- package/src/pull.ts +31 -18
- package/src/relational-rows.ts +182 -66
- package/src/schema.ts +55 -0
- package/src/sqlite-bun-driver.ts +1 -1
- package/src/sqlite-node-driver.ts +18 -3
- package/src/sqlite-storage.ts +64 -34
- package/src/storage.ts +9 -1
package/src/sqlite-storage.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { retainRollbackFailure, rollbackAfterError } from './storage-errors';
|
|
|
2
2
|
import { validateCommitPruneQuery } from './prune';
|
|
3
3
|
import { StorageQueryError } from './storage-errors';
|
|
4
4
|
import type { CommitPruneQuery, CommitPruneResult } from './storage';
|
|
5
|
-
import { serveGateRefusal } from './storage';
|
|
5
|
+
import { PARTITION_TOUCH_INTERVAL_MS, serveGateRefusal } from './storage';
|
|
6
6
|
import { serveNotReadyError } from './readiness';
|
|
7
7
|
/**
|
|
8
8
|
* SQLite server storage over the shared synchronous driver.
|
|
@@ -45,6 +45,9 @@ import {
|
|
|
45
45
|
type StoredColumnLayout,
|
|
46
46
|
scanRowPageSql,
|
|
47
47
|
schemaDdl,
|
|
48
|
+
SQLITE_TABLE_INDEXES_SQL,
|
|
49
|
+
type SqliteTableIndexRow,
|
|
50
|
+
sqliteIndexSignatures,
|
|
48
51
|
selectRowScopesSql,
|
|
49
52
|
selectRowSql,
|
|
50
53
|
selectRowsForRewriteSql,
|
|
@@ -653,10 +656,8 @@ END`);
|
|
|
653
656
|
// rewrite stored rows (payload re-encode for layout changes, and/or
|
|
654
657
|
// projection backfill for flipped-on materialization). One
|
|
655
658
|
// transaction: a failed bump leaves no half-state.
|
|
656
|
-
const layouts = parseLayouts(marker?.layouts);
|
|
657
|
-
const retiredTables = retiredTableNames(schema, layouts);
|
|
658
659
|
const existing = new Map<string, ReadonlySet<string>>();
|
|
659
|
-
const existingIndexes = new Map<string,
|
|
660
|
+
const existingIndexes = new Map<string, ReadonlyMap<string, string>>();
|
|
660
661
|
for (const table of schema.tables.values()) {
|
|
661
662
|
const escapedTableName = table.name.replaceAll('"', '""');
|
|
662
663
|
const columns = this.db
|
|
@@ -666,15 +667,19 @@ END`);
|
|
|
666
667
|
.all();
|
|
667
668
|
if (columns.length > 0) {
|
|
668
669
|
existing.set(table.name, new Set(columns.map((c) => c.name)));
|
|
669
|
-
const indexes = this.db
|
|
670
|
-
.query<{ name: string; origin: string }, []>(
|
|
671
|
-
`PRAGMA index_list("${escapedTableName}")`,
|
|
672
|
-
)
|
|
673
|
-
.all()
|
|
674
|
-
.filter((index) => index.origin === 'c');
|
|
675
670
|
existingIndexes.set(
|
|
676
671
|
table.name,
|
|
677
|
-
new
|
|
672
|
+
new Map(
|
|
673
|
+
Object.entries(
|
|
674
|
+
sqliteIndexSignatures(
|
|
675
|
+
this.db
|
|
676
|
+
.query<SqliteTableIndexRow, [string]>(
|
|
677
|
+
SQLITE_TABLE_INDEXES_SQL,
|
|
678
|
+
)
|
|
679
|
+
.all(table.name),
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
),
|
|
678
683
|
);
|
|
679
684
|
}
|
|
680
685
|
}
|
|
@@ -686,6 +691,25 @@ END`);
|
|
|
686
691
|
await this.#serializeWrite(() => {
|
|
687
692
|
this.db.exec('BEGIN IMMEDIATE');
|
|
688
693
|
try {
|
|
694
|
+
// Another process sharing the database may have bumped between the
|
|
695
|
+
// marker read above and the writer lock; the locked marker decides
|
|
696
|
+
// (SPEC "Schema upgrade writer fence").
|
|
697
|
+
const locked = this.db
|
|
698
|
+
.query<{ schema_version: number; layouts: string }, []>(
|
|
699
|
+
'SELECT schema_version, layouts FROM sync_schema_meta WHERE id=1',
|
|
700
|
+
)
|
|
701
|
+
.get();
|
|
702
|
+
if (locked !== null && locked.schema_version > schema.version) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`stored schema version ${locked.schema_version} is newer than the configured schema (${schema.version}) — refusing to run an older server against a migrated database`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (locked !== null && locked.schema_version === schema.version) {
|
|
708
|
+
this.db.exec('COMMIT');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const layouts = parseLayouts(locked?.layouts);
|
|
712
|
+
const retiredTables = retiredTableNames(schema, layouts);
|
|
689
713
|
for (const tableName of retiredTables) {
|
|
690
714
|
this.db
|
|
691
715
|
.query('DELETE FROM sync_row_scopes WHERE tbl=?')
|
|
@@ -763,28 +787,34 @@ END`);
|
|
|
763
787
|
if (initialLogEpoch.length === 0) {
|
|
764
788
|
throw new Error('initial log epoch must be non-empty');
|
|
765
789
|
}
|
|
766
|
-
this.db
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
790
|
+
const select = this.db.query<
|
|
791
|
+
{
|
|
792
|
+
log_epoch: string;
|
|
793
|
+
epoch_required: number;
|
|
794
|
+
last_authenticated_at_ms: number;
|
|
795
|
+
},
|
|
796
|
+
[string]
|
|
797
|
+
>(
|
|
798
|
+
`SELECT log_epoch, epoch_required, last_authenticated_at_ms
|
|
799
|
+
FROM sync_partition_registry WHERE partition=?`,
|
|
800
|
+
);
|
|
801
|
+
let row = select.get(partition);
|
|
802
|
+
if (
|
|
803
|
+
row === null ||
|
|
804
|
+
authenticatedAtMs - row.last_authenticated_at_ms >=
|
|
805
|
+
PARTITION_TOUCH_INTERVAL_MS
|
|
806
|
+
) {
|
|
807
|
+
this.db
|
|
808
|
+
.query(
|
|
809
|
+
`INSERT INTO sync_partition_registry(
|
|
810
|
+
partition, log_epoch, last_authenticated_at_ms
|
|
811
|
+
) VALUES (?,?,?)
|
|
812
|
+
ON CONFLICT(partition) DO UPDATE SET
|
|
813
|
+
last_authenticated_at_ms=excluded.last_authenticated_at_ms`,
|
|
814
|
+
)
|
|
815
|
+
.run(partition, initialLogEpoch, authenticatedAtMs);
|
|
816
|
+
row = select.get(partition);
|
|
817
|
+
}
|
|
788
818
|
if (row === null)
|
|
789
819
|
throw new Error('partition registry write did not persist');
|
|
790
820
|
return {
|
|
@@ -867,7 +897,7 @@ END`);
|
|
|
867
897
|
oldLayout: readonly StoredColumnLayout[] | undefined,
|
|
868
898
|
): void {
|
|
869
899
|
const select = selectRowsForRewriteSql(table, 'sqlite');
|
|
870
|
-
const update = this.db.query(rewriteRowSql(table
|
|
900
|
+
const update = this.db.query(rewriteRowSql(table));
|
|
871
901
|
const BATCH = 500;
|
|
872
902
|
let afterPartition = '';
|
|
873
903
|
let afterRowId = '';
|
package/src/storage.ts
CHANGED
|
@@ -358,6 +358,9 @@ export interface ClientCursorInfo {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
/** Durable partition identity refreshed after host authentication (§2.1). */
|
|
361
|
+
/** Longest age of `lastAuthenticatedAtMs` that `touchPartition` leaves unwritten. */
|
|
362
|
+
export const PARTITION_TOUCH_INTERVAL_MS = 60_000;
|
|
363
|
+
|
|
361
364
|
export interface PartitionRegistryEntry {
|
|
362
365
|
readonly partition: string;
|
|
363
366
|
readonly logEpoch: string;
|
|
@@ -651,7 +654,12 @@ export interface ServerStorage {
|
|
|
651
654
|
checkpoints?: readonly CheckpointDeclaration[],
|
|
652
655
|
): Promise<void>;
|
|
653
656
|
|
|
654
|
-
/**
|
|
657
|
+
/**
|
|
658
|
+
* Create or refresh the authenticated partition registry row (§2.1). A
|
|
659
|
+
* row whose `lastAuthenticatedAtMs` is less than
|
|
660
|
+
* {@link PARTITION_TOUCH_INTERVAL_MS} behind `authenticatedAtMs` is read
|
|
661
|
+
* back without a write, so a request that only reads takes no write lock.
|
|
662
|
+
*/
|
|
655
663
|
touchPartition(
|
|
656
664
|
partition: string,
|
|
657
665
|
authenticatedAtMs: number,
|