@syncular/server 0.36.0 → 0.38.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.
@@ -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,
@@ -656,7 +659,7 @@ END`);
656
659
  const layouts = parseLayouts(marker?.layouts);
657
660
  const retiredTables = retiredTableNames(schema, layouts);
658
661
  const existing = new Map<string, ReadonlySet<string>>();
659
- const existingIndexes = new Map<string, ReadonlySet<string>>();
662
+ const existingIndexes = new Map<string, ReadonlyMap<string, string>>();
660
663
  for (const table of schema.tables.values()) {
661
664
  const escapedTableName = table.name.replaceAll('"', '""');
662
665
  const columns = this.db
@@ -666,15 +669,19 @@ END`);
666
669
  .all();
667
670
  if (columns.length > 0) {
668
671
  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
672
  existingIndexes.set(
676
673
  table.name,
677
- new Set(indexes.map((index) => index.name)),
674
+ new Map(
675
+ Object.entries(
676
+ sqliteIndexSignatures(
677
+ this.db
678
+ .query<SqliteTableIndexRow, [string]>(
679
+ SQLITE_TABLE_INDEXES_SQL,
680
+ )
681
+ .all(table.name),
682
+ ),
683
+ ),
684
+ ),
678
685
  );
679
686
  }
680
687
  }
@@ -763,28 +770,34 @@ END`);
763
770
  if (initialLogEpoch.length === 0) {
764
771
  throw new Error('initial log epoch must be non-empty');
765
772
  }
766
- this.db
767
- .query(
768
- `INSERT INTO sync_partition_registry(
769
- partition, log_epoch, last_authenticated_at_ms
770
- ) VALUES (?,?,?)
771
- ON CONFLICT(partition) DO UPDATE SET
772
- last_authenticated_at_ms=excluded.last_authenticated_at_ms`,
773
- )
774
- .run(partition, initialLogEpoch, authenticatedAtMs);
775
- const row = this.db
776
- .query<
777
- {
778
- log_epoch: string;
779
- epoch_required: number;
780
- last_authenticated_at_ms: number;
781
- },
782
- [string]
783
- >(
784
- `SELECT log_epoch, epoch_required, last_authenticated_at_ms
785
- FROM sync_partition_registry WHERE partition=?`,
786
- )
787
- .get(partition);
773
+ const select = this.db.query<
774
+ {
775
+ log_epoch: string;
776
+ epoch_required: number;
777
+ last_authenticated_at_ms: number;
778
+ },
779
+ [string]
780
+ >(
781
+ `SELECT log_epoch, epoch_required, last_authenticated_at_ms
782
+ FROM sync_partition_registry WHERE partition=?`,
783
+ );
784
+ let row = select.get(partition);
785
+ if (
786
+ row === null ||
787
+ authenticatedAtMs - row.last_authenticated_at_ms >=
788
+ PARTITION_TOUCH_INTERVAL_MS
789
+ ) {
790
+ this.db
791
+ .query(
792
+ `INSERT INTO sync_partition_registry(
793
+ partition, log_epoch, last_authenticated_at_ms
794
+ ) VALUES (?,?,?)
795
+ ON CONFLICT(partition) DO UPDATE SET
796
+ last_authenticated_at_ms=excluded.last_authenticated_at_ms`,
797
+ )
798
+ .run(partition, initialLogEpoch, authenticatedAtMs);
799
+ row = select.get(partition);
800
+ }
788
801
  if (row === null)
789
802
  throw new Error('partition registry write did not persist');
790
803
  return {
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
- /** Create or refresh the authenticated partition registry row (§2.1). */
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,