@tanstack/db-sqlite-persistence-core 0.4.4 → 0.4.7

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.
@@ -8,3 +8,15 @@ export type SerializedSQLiteBigInt = {
8
8
  };
9
9
  export declare function assertSQLiteBigIntInRange(value: bigint): bigint;
10
10
  export declare function serializeSQLiteBigInt(value: bigint): SerializedSQLiteBigInt;
11
+ type SQLiteTemporalKind = `Temporal.Instant` | `Temporal.PlainDate`;
12
+ export type SerializedSQLiteTemporal = {
13
+ [PERSISTED_TYPE_TAG]: SQLiteTemporalKind;
14
+ [PERSISTED_VALUE_TAG]: string;
15
+ order: string;
16
+ };
17
+ export declare function sqliteTemporalKind(value: unknown): SQLiteTemporalKind | undefined;
18
+ export declare function reviveSQLiteTemporal(kind: SQLiteTemporalKind, value: string): unknown;
19
+ /** Registered prototype methods validate the brand; user toString is ignored. */
20
+ export declare function serializeSQLiteTemporal(value: unknown): SerializedSQLiteTemporal | undefined;
21
+ export declare function sqliteTemporalIdentity(value: SerializedSQLiteTemporal): string;
22
+ export {};
@@ -1,4 +1,4 @@
1
- import { SQLiteBigIntOutOfRangeError } from "./errors.js";
1
+ import { SQLiteBigIntOutOfRangeError, InvalidPersistedCollectionConfigError } from "./errors.js";
2
2
  const SQLITE_BIGINT_MIN = -9223372036854775808n;
3
3
  const SQLITE_BIGINT_MAX = 9223372036854775807n;
4
4
  const PERSISTED_TYPE_TAG = `__tanstack_db_persisted_type__`;
@@ -20,12 +20,63 @@ function serializeSQLiteBigInt(value) {
20
20
  [PERSISTED_VALUE_TAG]: value.toString()
21
21
  };
22
22
  }
23
+ function sqliteTemporalKind(value) {
24
+ if (value === null || typeof value !== `object`) return;
25
+ const tag = value[Symbol.toStringTag];
26
+ if (tag === `Temporal.Instant` || tag === `Temporal.PlainDate`) return tag;
27
+ return void 0;
28
+ }
29
+ function requireSQLiteTemporalConstructor(kind) {
30
+ const name = kind.slice(`Temporal.`.length);
31
+ const constructor = globalThis.Temporal?.[name];
32
+ if (typeof constructor?.from !== `function` || typeof constructor.prototype.toString !== `function`) {
33
+ throw new InvalidPersistedCollectionConfigError(
34
+ `Missing global ${kind} constructor for SQLite persistence`
35
+ );
36
+ }
37
+ return constructor;
38
+ }
39
+ function reviveSQLiteTemporal(kind, value) {
40
+ return requireSQLiteTemporalConstructor(kind).from(value);
41
+ }
42
+ function serializeSQLiteTemporal(value) {
43
+ if (value === null || typeof value !== `object`) return;
44
+ const prototype = Object.getPrototypeOf(value);
45
+ if (prototype === null || prototype === Object.prototype) return;
46
+ const tag = value[Symbol.toStringTag];
47
+ if (typeof tag !== `string` || !tag.startsWith(`Temporal.`)) return;
48
+ const kind = sqliteTemporalKind(value);
49
+ if (!kind)
50
+ throw new InvalidPersistedCollectionConfigError(
51
+ `Unsupported ${tag} value for SQLite persistence`
52
+ );
53
+ const constructor = requireSQLiteTemporalConstructor(kind);
54
+ let text;
55
+ try {
56
+ text = constructor.prototype.toString.call(value);
57
+ } catch {
58
+ throw new InvalidPersistedCollectionConfigError(
59
+ `Invalid ${kind} value for the registered global constructor`
60
+ );
61
+ }
62
+ const native = constructor.from(text);
63
+ const iso = kind === `Temporal.PlainDate` ? native.withCalendar(`iso8601`) : void 0;
64
+ const order = iso ? `${String(iso.year + 1e6).padStart(7, `0`)}${String(iso.month).padStart(2, `0`)}${String(iso.day).padStart(2, `0`)}` : (native.epochNanoseconds + 10000000000000000000000n).toString().padStart(23, `0`);
65
+ return { [PERSISTED_TYPE_TAG]: kind, [PERSISTED_VALUE_TAG]: text, order };
66
+ }
67
+ function sqliteTemporalIdentity(value) {
68
+ return `${value[PERSISTED_TYPE_TAG]}:${value[PERSISTED_VALUE_TAG]}`;
69
+ }
23
70
  export {
24
71
  PERSISTED_TYPE_TAG,
25
72
  PERSISTED_VALUE_TAG,
26
73
  SQLITE_BIGINT_MAX,
27
74
  SQLITE_BIGINT_MIN,
28
75
  assertSQLiteBigIntInRange,
29
- serializeSQLiteBigInt
76
+ reviveSQLiteTemporal,
77
+ serializeSQLiteBigInt,
78
+ serializeSQLiteTemporal,
79
+ sqliteTemporalIdentity,
80
+ sqliteTemporalKind
30
81
  };
31
82
  //# sourceMappingURL=sqlite-value.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-value.js","sources":["../../src/sqlite-value.ts"],"sourcesContent":["import { SQLiteBigIntOutOfRangeError } from './errors'\n\nexport const SQLITE_BIGINT_MIN = -9_223_372_036_854_775_808n\nexport const SQLITE_BIGINT_MAX = 9_223_372_036_854_775_807n\nexport const PERSISTED_TYPE_TAG = `__tanstack_db_persisted_type__`\nexport const PERSISTED_VALUE_TAG = `value`\n\nexport type SerializedSQLiteBigInt = {\n [PERSISTED_TYPE_TAG]: `bigint`\n [PERSISTED_VALUE_TAG]: string\n}\n\nexport function assertSQLiteBigIntInRange(value: bigint): bigint {\n if (value < SQLITE_BIGINT_MIN || value > SQLITE_BIGINT_MAX) {\n throw new SQLiteBigIntOutOfRangeError(\n value,\n SQLITE_BIGINT_MIN,\n SQLITE_BIGINT_MAX,\n )\n }\n return value\n}\n\nexport function serializeSQLiteBigInt(value: bigint): SerializedSQLiteBigInt {\n assertSQLiteBigIntInRange(value)\n return {\n [PERSISTED_TYPE_TAG]: `bigint`,\n [PERSISTED_VALUE_TAG]: value.toString(),\n }\n}\n"],"names":[],"mappings":";AAEO,MAAM,oBAAoB,CAAC;AAC3B,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAO5B,SAAS,0BAA0B,OAAuB;AAC/D,MAAI,QAAQ,qBAAqB,QAAQ,mBAAmB;AAC1D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;AAEO,SAAS,sBAAsB,OAAuC;AAC3E,4BAA0B,KAAK;AAC/B,SAAO;AAAA,IACL,CAAC,kBAAkB,GAAG;AAAA,IACtB,CAAC,mBAAmB,GAAG,MAAM,SAAA;AAAA,EAAS;AAE1C;"}
1
+ {"version":3,"file":"sqlite-value.js","sources":["../../src/sqlite-value.ts"],"sourcesContent":["import {\n InvalidPersistedCollectionConfigError,\n SQLiteBigIntOutOfRangeError,\n} from './errors'\n\nexport const SQLITE_BIGINT_MIN = -9_223_372_036_854_775_808n\nexport const SQLITE_BIGINT_MAX = 9_223_372_036_854_775_807n\nexport const PERSISTED_TYPE_TAG = `__tanstack_db_persisted_type__`\nexport const PERSISTED_VALUE_TAG = `value`\n\nexport type SerializedSQLiteBigInt = {\n [PERSISTED_TYPE_TAG]: `bigint`\n [PERSISTED_VALUE_TAG]: string\n}\n\nexport function assertSQLiteBigIntInRange(value: bigint): bigint {\n if (value < SQLITE_BIGINT_MIN || value > SQLITE_BIGINT_MAX) {\n throw new SQLiteBigIntOutOfRangeError(\n value,\n SQLITE_BIGINT_MIN,\n SQLITE_BIGINT_MAX,\n )\n }\n return value\n}\n\nexport function serializeSQLiteBigInt(value: bigint): SerializedSQLiteBigInt {\n assertSQLiteBigIntInRange(value)\n return {\n [PERSISTED_TYPE_TAG]: `bigint`,\n [PERSISTED_VALUE_TAG]: value.toString(),\n }\n}\n\ntype SQLiteTemporalKind = `Temporal.Instant` | `Temporal.PlainDate`\ntype SQLiteTemporal = {\n epochNanoseconds: bigint\n year: number\n month: number\n day: number\n withCalendar: (calendar: string) => SQLiteTemporal\n}\ntype TemporalConstructor = {\n from: (value: string) => SQLiteTemporal\n prototype: { toString: () => string }\n}\n\nexport type SerializedSQLiteTemporal = {\n [PERSISTED_TYPE_TAG]: SQLiteTemporalKind\n [PERSISTED_VALUE_TAG]: string\n order: string\n}\n\nexport function sqliteTemporalKind(\n value: unknown,\n): SQLiteTemporalKind | undefined {\n if (value === null || typeof value !== `object`) return\n const tag = (value as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]\n if (tag === `Temporal.Instant` || tag === `Temporal.PlainDate`) return tag\n return undefined\n}\n\nfunction requireSQLiteTemporalConstructor(\n kind: SQLiteTemporalKind,\n): TemporalConstructor {\n const name = kind.slice(`Temporal.`.length)\n const constructor = (\n globalThis as { Temporal?: Record<string, TemporalConstructor> }\n ).Temporal?.[name]\n if (\n typeof constructor?.from !== `function` ||\n typeof constructor.prototype.toString !== `function`\n ) {\n throw new InvalidPersistedCollectionConfigError(\n `Missing global ${kind} constructor for SQLite persistence`,\n )\n }\n return constructor\n}\n\nexport function reviveSQLiteTemporal(\n kind: SQLiteTemporalKind,\n value: string,\n): unknown {\n return requireSQLiteTemporalConstructor(kind).from(value)\n}\n\n/** Registered prototype methods validate the brand; user toString is ignored. */\nexport function serializeSQLiteTemporal(\n value: unknown,\n): SerializedSQLiteTemporal | undefined {\n if (value === null || typeof value !== `object`) return\n const prototype = Object.getPrototypeOf(value)\n if (prototype === null || prototype === Object.prototype) return\n const tag = (value as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]\n if (typeof tag !== `string` || !tag.startsWith(`Temporal.`)) return\n const kind = sqliteTemporalKind(value)\n if (!kind)\n throw new InvalidPersistedCollectionConfigError(\n `Unsupported ${tag} value for SQLite persistence`,\n )\n const constructor = requireSQLiteTemporalConstructor(kind)\n let text: string\n try {\n text = constructor.prototype.toString.call(value)\n } catch {\n throw new InvalidPersistedCollectionConfigError(\n `Invalid ${kind} value for the registered global constructor`,\n )\n }\n const native = constructor.from(text)\n const iso =\n kind === `Temporal.PlainDate` ? native.withCalendar(`iso8601`) : undefined\n // Text keys avoid both floating-point precision loss and SQLite's int64 cap.\n const order = iso\n ? `${String(iso.year + 1_000_000).padStart(7, `0`)}${String(iso.month).padStart(2, `0`)}${String(iso.day).padStart(2, `0`)}`\n : (native.epochNanoseconds + 10_000_000_000_000_000_000_000n)\n .toString()\n .padStart(23, `0`)\n return { [PERSISTED_TYPE_TAG]: kind, [PERSISTED_VALUE_TAG]: text, order }\n}\n\nexport function sqliteTemporalIdentity(\n value: SerializedSQLiteTemporal,\n): string {\n return `${value[PERSISTED_TYPE_TAG]}:${value[PERSISTED_VALUE_TAG]}`\n}\n"],"names":[],"mappings":";AAKO,MAAM,oBAAoB,CAAC;AAC3B,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAO5B,SAAS,0BAA0B,OAAuB;AAC/D,MAAI,QAAQ,qBAAqB,QAAQ,mBAAmB;AAC1D,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AACA,SAAO;AACT;AAEO,SAAS,sBAAsB,OAAuC;AAC3E,4BAA0B,KAAK;AAC/B,SAAO;AAAA,IACL,CAAC,kBAAkB,GAAG;AAAA,IACtB,CAAC,mBAAmB,GAAG,MAAM,SAAA;AAAA,EAAS;AAE1C;AAqBO,SAAS,mBACd,OACgC;AAChC,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU;AACjD,QAAM,MAAO,MAA6C,OAAO,WAAW;AAC5E,MAAI,QAAQ,sBAAsB,QAAQ,qBAAsB,QAAO;AACvE,SAAO;AACT;AAEA,SAAS,iCACP,MACqB;AACrB,QAAM,OAAO,KAAK,MAAM,YAAY,MAAM;AAC1C,QAAM,cACJ,WACA,WAAW,IAAI;AACjB,MACE,OAAO,aAAa,SAAS,cAC7B,OAAO,YAAY,UAAU,aAAa,YAC1C;AACA,UAAM,IAAI;AAAA,MACR,kBAAkB,IAAI;AAAA,IAAA;AAAA,EAE1B;AACA,SAAO;AACT;AAEO,SAAS,qBACd,MACA,OACS;AACT,SAAO,iCAAiC,IAAI,EAAE,KAAK,KAAK;AAC1D;AAGO,SAAS,wBACd,OACsC;AACtC,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU;AACjD,QAAM,YAAY,OAAO,eAAe,KAAK;AAC7C,MAAI,cAAc,QAAQ,cAAc,OAAO,UAAW;AAC1D,QAAM,MAAO,MAA6C,OAAO,WAAW;AAC5E,MAAI,OAAO,QAAQ,YAAY,CAAC,IAAI,WAAW,WAAW,EAAG;AAC7D,QAAM,OAAO,mBAAmB,KAAK;AACrC,MAAI,CAAC;AACH,UAAM,IAAI;AAAA,MACR,eAAe,GAAG;AAAA,IAAA;AAEtB,QAAM,cAAc,iCAAiC,IAAI;AACzD,MAAI;AACJ,MAAI;AACF,WAAO,YAAY,UAAU,SAAS,KAAK,KAAK;AAAA,EAClD,QAAQ;AACN,UAAM,IAAI;AAAA,MACR,WAAW,IAAI;AAAA,IAAA;AAAA,EAEnB;AACA,QAAM,SAAS,YAAY,KAAK,IAAI;AACpC,QAAM,MACJ,SAAS,uBAAuB,OAAO,aAAa,SAAS,IAAI;AAEnE,QAAM,QAAQ,MACV,GAAG,OAAO,IAAI,OAAO,GAAS,EAAE,SAAS,GAAG,GAAG,CAAC,GAAG,OAAO,IAAI,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,GAAG,OAAO,IAAI,GAAG,EAAE,SAAS,GAAG,GAAG,CAAC,MACvH,OAAO,mBAAmB,0BACxB,WACA,SAAS,IAAI,GAAG;AACvB,SAAO,EAAE,CAAC,kBAAkB,GAAG,MAAM,CAAC,mBAAmB,GAAG,MAAM,MAAA;AACpE;AAEO,SAAS,uBACd,OACQ;AACR,SAAO,GAAG,MAAM,kBAAkB,CAAC,IAAI,MAAM,mBAAmB,CAAC;AACnE;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db-sqlite-persistence-core",
3
- "version": "0.4.4",
3
+ "version": "0.4.7",
4
4
  "description": "SQLite persisted collection core for TanStack DB",
5
5
  "author": "TanStack Team",
6
6
  "license": "MIT",
@@ -50,13 +50,14 @@
50
50
  ],
51
51
  "dependencies": {
52
52
  "@standard-schema/spec": "^1.1.0",
53
- "@tanstack/db": "0.12.0"
53
+ "@tanstack/db": "0.12.3"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "typescript": ">=4.7"
57
57
  },
58
58
  "devDependencies": {
59
- "@vitest/coverage-istanbul": "^3.2.4"
59
+ "@vitest/coverage-istanbul": "^3.2.4",
60
+ "temporal-polyfill": "^0.3.0"
60
61
  },
61
62
  "scripts": {
62
63
  "build": "vite build",
package/src/persisted.ts CHANGED
@@ -24,10 +24,11 @@ import {
24
24
  PersistedCollectionDurabilityError,
25
25
  toPersistedCollectionDurabilityError,
26
26
  } from './errors'
27
- import { serializeSQLiteBigInt } from './sqlite-value'
27
+ import { serializeSQLiteBigInt, serializeSQLiteTemporal } from './sqlite-value'
28
28
  import {
29
29
  toProcessLocalLoadSubsetOptions,
30
30
  toTransportedLoadSubsetOptions,
31
+ validateRemoteSubsetOptions,
31
32
  } from './remote-subset-wire'
32
33
  import {
33
34
  reportRemoteSubsetOwnerError,
@@ -543,6 +544,12 @@ export interface PersistedCollectionCoordinator {
543
544
  collectionId: string,
544
545
  mutations: Array<PersistedMutationEnvelope>,
545
546
  ) => Promise<ApplyLocalMutationsResponse>
547
+ /**
548
+ * Applies a committed transaction through the supplied adapter when present.
549
+ * Wrappers must forward this leader-local loan: scheduling it again can wait
550
+ * on the hydration that is awaiting this call. Never serialize or retain the
551
+ * loan after the call; ordinary work uses the registered collection adapter.
552
+ */
546
553
  requestApplyCommittedTx: (
547
554
  collectionId: string,
548
555
  tx: PersistedTx,
@@ -887,8 +894,9 @@ export class SingleProcessCoordinator implements PersistedCollectionCoordinator
887
894
  public async requestApplyCommittedTx(
888
895
  collectionId: string,
889
896
  tx: PersistedTx,
897
+ scopedAdapter?: HydrationPersistenceAdapter,
890
898
  ): Promise<ApplyCommittedTxResponse> {
891
- const adapter = this.collectionAdapters.get(collectionId)
899
+ const adapter = scopedAdapter ?? this.collectionAdapters.get(collectionId)
892
900
  if (!adapter) {
893
901
  throw new InvalidPersistedCollectionConfigError(
894
902
  `SingleProcessCoordinator has no persistence adapter configured for collection "${collectionId}"`,
@@ -1152,6 +1160,9 @@ function toStableSerializable(value: unknown): unknown {
1152
1160
  return undefined
1153
1161
  }
1154
1162
 
1163
+ const temporal = serializeSQLiteTemporal(value)
1164
+ if (temporal) return temporal
1165
+
1155
1166
  if (value instanceof Date) {
1156
1167
  return value.toISOString()
1157
1168
  }
@@ -1730,7 +1741,6 @@ class PersistedCollectionRuntime<
1730
1741
  await this.hydrateSubsetUnsafe(
1731
1742
  baseline,
1732
1743
  {
1733
- requestRemoteEnsure: false,
1734
1744
  lifecycleGeneration,
1735
1745
  bindKeySetEvidence: true,
1736
1746
  },
@@ -1866,6 +1876,8 @@ class PersistedCollectionRuntime<
1866
1876
  const truncateGeneration = this.sourceTruncateGeneration
1867
1877
  const routeRemoteDemandDuringHydration =
1868
1878
  this.canRouteRemoteDemandThroughCoordinator()
1879
+ // Wire admission failures are permanent input errors, not transport retries.
1880
+ if (routeRemoteDemandDuringHydration) validateRemoteSubsetOptions(options)
1869
1881
  this.activeSubsets.set(subsetKey, options)
1870
1882
  const appliedCursor = this.appliedReceiptSequence
1871
1883
  try {
@@ -1875,9 +1887,6 @@ class PersistedCollectionRuntime<
1875
1887
  this.hydrateSubsetUnsafe(
1876
1888
  options,
1877
1889
  {
1878
- requestRemoteEnsure:
1879
- this.mode === `sync-present` &&
1880
- !routeRemoteDemandDuringHydration,
1881
1890
  lifecycleGeneration,
1882
1891
  requestLocalLoadFailure: true,
1883
1892
  rejectBufferedReplayFailure: true,
@@ -1896,6 +1905,14 @@ class PersistedCollectionRuntime<
1896
1905
  })
1897
1906
  if (lifecycleGeneration !== this.lifecycleGeneration) return
1898
1907
  await this.waitForAppliedReceiptsAfter(appliedCursor)
1908
+ // Leadership can change while hydration waits. Validate newly remote
1909
+ // demand inside the admission cleanup boundary, before retry can own it.
1910
+ if (
1911
+ !routeRemoteDemandDuringHydration &&
1912
+ this.canRouteRemoteDemandThroughCoordinator()
1913
+ ) {
1914
+ validateRemoteSubsetOptions(options)
1915
+ }
1899
1916
  } catch (error) {
1900
1917
  if (this.activeSubsets.get(subsetKey) === options) {
1901
1918
  this.activeSubsets.delete(subsetKey)
@@ -2056,7 +2073,6 @@ class PersistedCollectionRuntime<
2056
2073
  this.hydrateSubsetUnsafe(
2057
2074
  options,
2058
2075
  {
2059
- requestRemoteEnsure: false,
2060
2076
  lifecycleGeneration,
2061
2077
  rejectBufferedReplayFailure: true,
2062
2078
  },
@@ -2329,7 +2345,6 @@ class PersistedCollectionRuntime<
2329
2345
  private async hydrateSubsetUnsafe(
2330
2346
  options: LoadSubsetOptions,
2331
2347
  config: {
2332
- requestRemoteEnsure: boolean
2333
2348
  lifecycleGeneration: number
2334
2349
  bindKeySetEvidence?: boolean
2335
2350
  requestLocalLoadFailure?: boolean
@@ -2390,10 +2405,6 @@ class PersistedCollectionRuntime<
2390
2405
  if (config.lifecycleGeneration !== this.lifecycleGeneration) return
2391
2406
  replayFailure = await this.flushQueuedHydrationTransactionsUnsafe(adapter)
2392
2407
  if (config.lifecycleGeneration !== this.lifecycleGeneration) return
2393
-
2394
- if (config.requestRemoteEnsure && !replayFailure) {
2395
- this.queueRemoteSubsetEnsure(options)
2396
- }
2397
2408
  } catch (error) {
2398
2409
  if (config.requestLocalLoadFailure && !rowsLoaded) {
2399
2410
  // Keep admitting source work to the hydration queue while recovery
@@ -3262,6 +3273,9 @@ class PersistedCollectionRuntime<
3262
3273
  const subsetKey = this.getSubsetKey(options)
3263
3274
  if (this.activeSubsets.get(subsetKey) !== options) return
3264
3275
 
3276
+ // A previously local acquisition can become remote after an ownership
3277
+ // change. Recovery must reject permanent wire errors before retry owns it.
3278
+ validateRemoteSubsetOptions(options)
3265
3279
  this.pendingRemoteSubsetEnsures.set(subsetKey, options)
3266
3280
  void this.flushPendingRemoteSubsetEnsures()
3267
3281
  }
@@ -3808,9 +3822,8 @@ class PersistedCollectionRuntime<
3808
3822
  indexMetadata: CollectionIndexMetadata,
3809
3823
  adapter: HydrationPersistenceAdapter,
3810
3824
  ): Promise<boolean> {
3811
- const spec = this.buildPersistedIndexSpec(indexMetadata)
3812
-
3813
3825
  try {
3826
+ const spec = this.buildPersistedIndexSpec(indexMetadata)
3814
3827
  await adapter.ensureIndex(
3815
3828
  this.collectionId,
3816
3829
  indexMetadata.signature,
@@ -3827,9 +3840,8 @@ class PersistedCollectionRuntime<
3827
3840
  indexMetadata: CollectionIndexMetadata,
3828
3841
  completedLocally: boolean,
3829
3842
  ): Promise<void> {
3830
- const spec = this.buildPersistedIndexSpec(indexMetadata)
3831
-
3832
3843
  try {
3844
+ const spec = this.buildPersistedIndexSpec(indexMetadata)
3833
3845
  await this.persistence.coordinator.requestEnsurePersistedIndex(
3834
3846
  this.collectionId,
3835
3847
  indexMetadata.signature,
@@ -4590,6 +4602,9 @@ function createWrappedSyncConfig<
4590
4602
  return visibleAfterDurable(openTransaction, applied)
4591
4603
  }
4592
4604
 
4605
+ // Admission is complete. Only queued hydration replay remains tied
4606
+ // to its sequence; retain hydrationContext for row metadata ownership.
4607
+ openTransaction.hydrationSequence = undefined
4593
4608
  let applied: Promise<void>
4594
4609
  try {
4595
4610
  applied = runtime.applyHydrationBufferedTransaction(transaction)