@nkzw/fate 1.5.0 → 1.5.2

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.
Files changed (179) hide show
  1. package/README.md +5 -7
  2. package/docs/api/classes/FateClient.md +39 -39
  3. package/docs/api/functions/clientRoot.md +1 -1
  4. package/docs/api/functions/createClient.md +1 -1
  5. package/docs/api/functions/createGraphQLTransport.md +1 -1
  6. package/docs/api/functions/createHTTPTransport.md +1 -1
  7. package/docs/api/functions/createTRPCTransport.md +1 -1
  8. package/docs/api/functions/defer.md +1 -1
  9. package/docs/api/functions/getDeferredMetadata.md +1 -1
  10. package/docs/api/functions/getListEntries.md +1 -1
  11. package/docs/api/functions/getSelectionPlan.md +1 -1
  12. package/docs/api/functions/graphqlMutation.md +1 -1
  13. package/docs/api/functions/isDeferred.md +1 -1
  14. package/docs/api/functions/isRecord.md +1 -1
  15. package/docs/api/functions/isViewTag.md +1 -1
  16. package/docs/api/functions/liveConnectionTopic.md +1 -1
  17. package/docs/api/functions/liveEntityTopic.md +1 -1
  18. package/docs/api/functions/liveGlobalConnectionTopic.md +1 -1
  19. package/docs/api/functions/mutation.md +1 -1
  20. package/docs/api/functions/toEntityId.md +1 -1
  21. package/docs/api/functions/view.md +1 -1
  22. package/docs/api/index.md +2 -0
  23. package/docs/api/interfaces/FateThenable.md +3 -3
  24. package/docs/api/interfaces/Persistence.md +1 -19
  25. package/docs/api/interfaces/PersistenceSession.md +10 -10
  26. package/docs/api/interfaces/Transport.md +8 -8
  27. package/docs/api/server/classes/FateRequestError.md +5 -5
  28. package/docs/api/server/drizzle/functions/createDrizzleFate.md +3 -3
  29. package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +1 -1
  30. package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +1 -1
  31. package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +1 -1
  32. package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +1 -1
  33. package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +3 -3
  34. package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +9 -9
  35. package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +1 -1
  36. package/docs/api/server/functions/bindSourceProcedures.md +1 -1
  37. package/docs/api/server/functions/computed.md +1 -1
  38. package/docs/api/server/functions/count.md +1 -1
  39. package/docs/api/server/functions/createFateFetchHandler.md +1 -1
  40. package/docs/api/server/functions/createFateServer.md +1 -1
  41. package/docs/api/server/functions/createHonoFateHandler.md +1 -1
  42. package/docs/api/server/functions/createLiveEventBus.md +1 -1
  43. package/docs/api/server/functions/createNestedSourcePlan.md +1 -1
  44. package/docs/api/server/functions/createResolver.md +1 -1
  45. package/docs/api/server/functions/createSourcePlan.md +1 -1
  46. package/docs/api/server/functions/createSourceProcedures.md +1 -1
  47. package/docs/api/server/functions/dataView.md +1 -1
  48. package/docs/api/server/functions/field.md +1 -1
  49. package/docs/api/server/functions/getNestedSelection.md +1 -1
  50. package/docs/api/server/functions/getScopedArgs.md +1 -1
  51. package/docs/api/server/functions/hasNestedSelection.md +1 -1
  52. package/docs/api/server/functions/list.md +1 -1
  53. package/docs/api/server/functions/refetchSourceById.md +1 -1
  54. package/docs/api/server/functions/resolveSourceById.md +1 -1
  55. package/docs/api/server/functions/resolveSourceByIds.md +1 -1
  56. package/docs/api/server/functions/resolveSourceConnection.md +2 -2
  57. package/docs/api/server/functions/resolver.md +1 -1
  58. package/docs/api/server/functions/toPrismaSelect.md +1 -1
  59. package/docs/api/server/functions/withConnection.md +1 -1
  60. package/docs/api/server/prisma/functions/createPrismaFate.md +3 -3
  61. package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +1 -1
  62. package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +1 -1
  63. package/docs/api/server/prisma/functions/prismaConnectionArgs.md +2 -2
  64. package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +4 -4
  65. package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +1 -1
  66. package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +9 -9
  67. package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +1 -1
  68. package/docs/api/server/type-aliases/ComputedField.md +5 -5
  69. package/docs/api/server/type-aliases/ComputedSelection.md +1 -1
  70. package/docs/api/server/type-aliases/ConnectionItem.md +3 -3
  71. package/docs/api/server/type-aliases/ConnectionPagination.md +5 -5
  72. package/docs/api/server/type-aliases/ConnectionResult.md +3 -3
  73. package/docs/api/server/type-aliases/CountSelection.md +4 -4
  74. package/docs/api/server/type-aliases/CountWhere.md +1 -1
  75. package/docs/api/server/type-aliases/DataViewListOptions.md +2 -2
  76. package/docs/api/server/type-aliases/DataViewOrderBy.md +1 -1
  77. package/docs/api/server/type-aliases/DataViewOrderDirection.md +1 -1
  78. package/docs/api/server/type-aliases/DataViewResult.md +1 -1
  79. package/docs/api/server/type-aliases/Entity.md +1 -1
  80. package/docs/api/server/type-aliases/FateServer.md +5 -5
  81. package/docs/api/server/type-aliases/FateServerManifest.md +1 -1
  82. package/docs/api/server/type-aliases/FieldSelection.md +3 -3
  83. package/docs/api/server/type-aliases/LiveConnectionEventType.md +1 -1
  84. package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +1 -1
  85. package/docs/api/server/type-aliases/LiveEventBus.md +1 -1
  86. package/docs/api/server/type-aliases/LiveEventType.md +1 -1
  87. package/docs/api/server/type-aliases/LiveSourceEvent.md +1 -1
  88. package/docs/api/server/type-aliases/NativeFateAPI.md +4 -4
  89. package/docs/api/server/type-aliases/OrderDirection.md +1 -1
  90. package/docs/api/server/type-aliases/SourceConfig.md +6 -6
  91. package/docs/api/server/type-aliases/SourceDefinition.md +6 -6
  92. package/docs/api/server/type-aliases/SourceOrder.md +1 -1
  93. package/docs/api/server/type-aliases/SourceOrderField.md +3 -3
  94. package/docs/api/server/type-aliases/SourcePlan.md +1 -1
  95. package/docs/api/server/type-aliases/SourcePlanNode.md +1 -1
  96. package/docs/api/server/type-aliases/SourceRegistry.md +1 -1
  97. package/docs/api/server/type-aliases/SourceRelation.md +1 -1
  98. package/docs/api/server/type-aliases/SourceRelationConfig.md +1 -1
  99. package/docs/api/server/variables/byIdInput.md +1 -1
  100. package/docs/api/server/variables/connectionArgs.md +1 -1
  101. package/docs/api/type-aliases/ConnectionMetadata.md +1 -1
  102. package/docs/api/type-aliases/ConnectionRef.md +1 -1
  103. package/docs/api/type-aliases/Deferred.md +1 -1
  104. package/docs/api/type-aliases/DeferredMetadata.md +1 -1
  105. package/docs/api/type-aliases/DeferredSelection.md +1 -1
  106. package/docs/api/type-aliases/DeferredSnapshot.md +1 -1
  107. package/docs/api/type-aliases/Entity.md +2 -2
  108. package/docs/api/type-aliases/EntityId.md +1 -1
  109. package/docs/api/type-aliases/FateDehydratedState.md +1 -1
  110. package/docs/api/type-aliases/FateLiveConnectionEvent.md +1 -1
  111. package/docs/api/type-aliases/FateLiveEvent.md +1 -1
  112. package/docs/api/type-aliases/FateMutations.md +1 -1
  113. package/docs/api/type-aliases/FateOperation.md +1 -1
  114. package/docs/api/type-aliases/FateProtocolRequest.md +1 -1
  115. package/docs/api/type-aliases/FateProtocolResponse.md +1 -1
  116. package/docs/api/type-aliases/FateRecord.md +1 -1
  117. package/docs/api/type-aliases/FateRoots.md +1 -1
  118. package/docs/api/type-aliases/GraphQLMutationDefinition.md +1 -1
  119. package/docs/api/type-aliases/GraphQLMutationInput.md +1 -1
  120. package/docs/api/type-aliases/GraphQLMutationMap.md +1 -1
  121. package/docs/api/type-aliases/GraphQLMutationOutput.md +1 -1
  122. package/docs/api/type-aliases/GraphQLTransportOptions.md +12 -12
  123. package/docs/api/type-aliases/HydrateOptions.md +1 -1
  124. package/docs/api/type-aliases/HydrationLimits.md +1 -1
  125. package/docs/api/type-aliases/InferFateAPI.md +1 -1
  126. package/docs/api/type-aliases/List.md +1 -1
  127. package/docs/api/type-aliases/ListEntry.md +1 -1
  128. package/docs/api/type-aliases/ListItem.md +1 -1
  129. package/docs/api/type-aliases/Mask.md +1 -1
  130. package/docs/api/type-aliases/MutationDefinition.md +1 -1
  131. package/docs/api/type-aliases/MutationEntity.md +1 -1
  132. package/docs/api/type-aliases/MutationIdentifier.md +1 -1
  133. package/docs/api/type-aliases/MutationIdentity.md +1 -1
  134. package/docs/api/type-aliases/MutationInput.md +1 -1
  135. package/docs/api/type-aliases/MutationResult.md +1 -1
  136. package/docs/api/type-aliases/NodesItem.md +1 -1
  137. package/docs/api/type-aliases/Pagination.md +5 -5
  138. package/docs/api/type-aliases/PersistedMutationStatus.md +5 -0
  139. package/docs/api/type-aliases/PersistenceSnapshot.md +2 -2
  140. package/docs/api/type-aliases/Request.md +1 -1
  141. package/docs/api/type-aliases/RequestMode.md +1 -1
  142. package/docs/api/type-aliases/RequestOptions.md +2 -2
  143. package/docs/api/type-aliases/RequestPersistenceOptions.md +5 -0
  144. package/docs/api/type-aliases/RequestResult.md +1 -1
  145. package/docs/api/type-aliases/Selection.md +1 -1
  146. package/docs/api/type-aliases/TypeConfig.md +4 -4
  147. package/docs/api/type-aliases/View.md +1 -1
  148. package/docs/api/type-aliases/ViewData.md +1 -1
  149. package/docs/api/type-aliases/ViewEntity.md +1 -1
  150. package/docs/api/type-aliases/ViewEntityName.md +1 -1
  151. package/docs/api/type-aliases/ViewRef.md +1 -1
  152. package/docs/api/type-aliases/ViewSelection.md +1 -1
  153. package/docs/api/type-aliases/ViewSnapshot.md +1 -1
  154. package/docs/api/type-aliases/ViewTag.md +1 -1
  155. package/docs/api/variables/ConnectionTag.md +1 -1
  156. package/docs/api/variables/DeferTag.md +1 -1
  157. package/docs/api/variables/DeferredTag.md +1 -1
  158. package/docs/guide/persistence.md +5 -7
  159. package/lib/clientStub.d.mts +4 -5
  160. package/lib/{executor-DoiTBiVv.d.mts → executor-oPTYqgQV.d.mts} +1 -1
  161. package/lib/{graphqlTransport-DGLYyRxL.d.mts → graphqlTransport-CGYE4NdD.d.mts} +1 -1
  162. package/lib/graphqlTransport.d.mts +1 -1
  163. package/lib/idempotency-types-DuqJ7KSd.d.mts +16 -0
  164. package/lib/idempotency.d.mts +8 -8
  165. package/lib/index.d.mts +12 -12
  166. package/lib/index.mjs +25 -15
  167. package/lib/{list-BRT1VtiT.d.mts → list-5hrTVoiB.d.mts} +1 -1
  168. package/lib/list.d.mts +1 -1
  169. package/lib/persistence.d.mts +21 -19
  170. package/lib/persistence.mjs +553 -352
  171. package/lib/{record-DITM4zb_.d.mts → record-Dxjvpsep.d.mts} +3 -3
  172. package/lib/server/drizzle.d.mts +14 -13
  173. package/lib/server/prisma.d.mts +14 -13
  174. package/lib/server.d.mts +9 -9
  175. package/lib/{store-CeatPJeK.mjs → store-CakBBzWG.mjs} +47 -11
  176. package/lib/{transport-DqjhjLfC.d.mts → transport-DR0fwleU.d.mts} +29 -16
  177. package/lib/vite.d.mts +3 -3
  178. package/package.json +3 -3
  179. package/lib/idempotency-types-CqA-BFpn.d.mts +0 -15
@@ -1,8 +1,13 @@
1
+ import { t as isRecord } from "./record-B07VwXd-.mjs";
1
2
  import { i as filterConnectionArgs } from "./args-CRLIRl_v.mjs";
2
3
  import { a as createNodeRef, n as decodeHydrationValue, o as getNodeRefId, r as encodeHydrationValue, s as isNodeRef, t as decodeClientHydrationState } from "./hydration-DPZRJgnu.mjs";
3
- import { i as getErrorStatusCode, m as toEntityId, n as getListKey, o as prepareMutation } from "./store-CeatPJeK.mjs";
4
+ import { i as getErrorStatusCode, m as toEntityId, n as getListKey, o as prepareMutation } from "./store-CakBBzWG.mjs";
4
5
  import { t as FateRequestError } from "./protocol-DS5sf6sd.mjs";
5
6
  //#region src/persistence-codec.ts
7
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
8
+ const isLivePolicy = (value) => isRecord(value) && (value.append === void 0 || value.append === "edge" || value.append === "visible") && (value.prepend === void 0 || value.prepend === "edge" || value.prepend === "visible");
9
+ const isSerializedPlan = (value) => isRecord(value) && Array.isArray(value.args) && value.args.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && isRecord(entry[1]) && typeof entry[1].hash === "string" && isRecord(entry[1].value) && (entry[1].ignoreKeys === void 0 || isStringArray(entry[1].ignoreKeys))) && Array.isArray(value.live) && value.live.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && isLivePolicy(entry[1])) && isStringArray(value.paths);
10
+ const isSerializedRequest = (value) => isRecord(value) && typeof value.key === "string" && Array.isArray(value.items) && value.items.every((item) => isRecord(item) && isSerializedPlan(item.plan) && (item.kind === "list" ? isStringArray(item.nodeRefViewNames) : (item.kind === "node" || item.kind === "nodes" || item.kind === "query") && isStringArray(item.refViewNames)));
6
11
  const serializePlan = (plan) => ({
7
12
  args: [...plan.args].map(([key, value]) => [key, {
8
13
  ...value,
@@ -27,47 +32,71 @@ const encodeRequests = (requests) => encodeHydrationValue(requests.map((request)
27
32
  ..."refViewNames" in item ? { refViewNames: [...item.refViewNames] } : { nodeRefViewNames: [...item.nodeRefViewNames] }
28
33
  }))
29
34
  })));
30
- const decodeRequests = (value) => decodeHydrationValue(value).map((request) => ({
31
- ...request,
32
- items: request.items.map((item) => ({
33
- ...item,
34
- plan: deserializePlan(item.plan),
35
- ..."refViewNames" in item ? { refViewNames: new Set(item.refViewNames) } : { nodeRefViewNames: new Set(item.nodeRefViewNames) }
36
- }))
37
- }));
35
+ const decodeItem = (item) => item.kind === "list" ? {
36
+ ...item,
37
+ nodeRefViewNames: new Set(item.nodeRefViewNames),
38
+ plan: deserializePlan(item.plan)
39
+ } : {
40
+ ...item,
41
+ plan: deserializePlan(item.plan),
42
+ refViewNames: new Set(item.refViewNames)
43
+ };
44
+ const decodeRequests = (value) => {
45
+ const requests = decodeHydrationValue(value);
46
+ if (!Array.isArray(requests) || !requests.every(isSerializedRequest)) throw new Error("fate: Invalid persisted request descriptors.");
47
+ return requests.map((request) => ({
48
+ ...request,
49
+ items: request.items.map(decodeItem)
50
+ }));
51
+ };
52
+ //#endregion
53
+ //#region src/persistence-utils.ts
54
+ const encoder = new TextEncoder();
55
+ const persistenceEntrySize = (key, value) => encoder.encode(JSON.stringify([key, value])).byteLength;
56
+ const yieldPersistenceTask = () => new Promise((resolve) => setTimeout(resolve, 0));
38
57
  //#endregion
39
58
  //#region src/persistence-cache.ts
59
+ const createNodeChanges = () => ({
60
+ listBases: /* @__PURE__ */ new Map(),
61
+ recordFields: /* @__PURE__ */ new Map(),
62
+ referenceChanges: /* @__PURE__ */ new Set(),
63
+ values: /* @__PURE__ */ new Map()
64
+ });
65
+ const decodeStoredValue = (value) => {
66
+ if (value === void 0) return;
67
+ if (!isRecord(value) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Object.hasOwn(value, "value")) throw new Error("fate: Unsupported or corrupt persisted cache data.");
68
+ return value;
69
+ };
40
70
  const mergeCacheUpdate = (key, before, after) => ({
41
71
  fields: after.fields && (!before || before.fields) ? [.../* @__PURE__ */ new Set([...before?.fields ?? [], ...after.fields])] : void 0,
42
72
  previousList: before ? before.previousList : after.previousList,
73
+ referencesChanged: before?.referencesChanged || after.referencesChanged,
43
74
  value: key.startsWith("r:") && before?.value && after.value ? applyRecordUpdate(before.value, after.value, after.fields ? new Set(after.fields) : void 0) : after.value
44
75
  });
45
- const encoder = new TextEncoder();
46
- const sized = (key, value) => {
76
+ const withStoredSize = (key, value) => {
47
77
  const item = {
48
78
  bytes: 0,
49
79
  value
50
80
  };
51
81
  for (;;) {
52
- const bytes = encoder.encode(JSON.stringify([key, item])).byteLength;
82
+ const bytes = persistenceEntrySize(key, item);
53
83
  if (bytes === item.bytes) return item;
54
84
  item.bytes = bytes;
55
85
  }
56
86
  };
57
- const yieldTask$1 = () => new Promise((resolve) => setTimeout(resolve, 0));
58
87
  const stamp = (time) => String(time).padStart(16, "0");
59
88
  const recordKey = (id) => `r:${id}`;
60
89
  const listKey = (id) => `l:${id}`;
61
90
  const queryKey = (id) => `q:${id}`;
62
- const matches = (path, selected) => path === selected || path.startsWith(`${selected}.`) || selected.startsWith(`${path}.`);
63
- const project = (value, paths) => {
91
+ const pathsOverlap = (path, selected) => path === selected || path.startsWith(`${selected}.`) || selected.startsWith(`${path}.`);
92
+ const projectRecord = (value, paths) => {
64
93
  const fields = /* @__PURE__ */ new Set([
65
94
  "id",
66
95
  "__typename",
67
96
  ...paths.map((path) => path.split(".")[0])
68
97
  ]);
69
98
  return {
70
- paths: value.paths.filter((path) => paths.some((selected) => matches(path, selected))),
99
+ paths: value.paths.filter((path) => paths.some((selected) => pathsOverlap(path, selected))),
71
100
  record: Object.fromEntries(Object.entries(value.record).filter(([field]) => fields.has(field)))
72
101
  };
73
102
  };
@@ -138,12 +167,6 @@ const applyListUpdate = (latest, before, after) => {
138
167
  };
139
168
  return result;
140
169
  };
141
- const references = (value) => {
142
- if (isNodeRef(value)) return JSON.stringify(["ref", getNodeRefId(value)]);
143
- if (Array.isArray(value)) return JSON.stringify(value.map(references));
144
- if (value && typeof value === "object") return JSON.stringify(Object.entries(value).map(([key, entry]) => [key, references(entry)]));
145
- return "";
146
- };
147
170
  /** Independently addressable normalized disk data. No in-memory retention roots. */
148
171
  var PersistenceCache = class {
149
172
  constructor(client, storage, key, maxAge, maxBytes) {
@@ -152,14 +175,10 @@ var PersistenceCache = class {
152
175
  this.key = key;
153
176
  this.maxAge = maxAge;
154
177
  this.maxBytes = maxBytes;
155
- this.changed = /* @__PURE__ */ new Map();
156
- this.updates = /* @__PURE__ */ new Map();
157
- this.listBases = /* @__PURE__ */ new Map();
158
- this.updateListBases = /* @__PURE__ */ new Map();
159
- this.fields = /* @__PURE__ */ new Map();
160
- this.updateFields = /* @__PURE__ */ new Map();
161
- this.recoveryChanges = /* @__PURE__ */ new Map();
162
- this.pending = /* @__PURE__ */ new Map();
178
+ this.pendingNodes = createNodeChanges();
179
+ this.flushingNodes = createNodeChanges();
180
+ this.recoveryUpdates = /* @__PURE__ */ new Map();
181
+ this.pendingRequests = /* @__PURE__ */ new Map();
163
182
  this.suppressed = false;
164
183
  this.disposed = false;
165
184
  this.work = 0;
@@ -172,7 +191,7 @@ var PersistenceCache = class {
172
191
  this.assertActive();
173
192
  if (await this.read(`${this.prefix}scope`) !== this.client.getPersistenceScope()) {
174
193
  await this.clear();
175
- if (sized(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes + this.overhead <= this.maxBytes) await this.put([[`${this.prefix}scope`, this.client.getPersistenceScope()]]);
194
+ if (withStoredSize(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes + this.overhead <= this.maxBytes) await this.put([[`${this.prefix}scope`, this.client.getPersistenceScope()]]);
176
195
  }
177
196
  });
178
197
  }
@@ -228,10 +247,10 @@ var PersistenceCache = class {
228
247
  } : void 0;
229
248
  const memory = this.client.store.readConfirmed(id);
230
249
  const disk = memory || before ? void 0 : await this.node(key);
231
- const recovery = this.recoveryChanges.get(key);
232
- const update = this.changed.has(key) ? mergeCacheUpdate(key, recovery, {
233
- fields: this.fields.get(key) ? [...this.fields.get(key)] : void 0,
234
- value: this.changed.get(key)
250
+ const recovery = this.recoveryUpdates.get(key);
251
+ const update = this.pendingNodes.values.has(key) ? mergeCacheUpdate(key, recovery, {
252
+ fields: this.pendingNodes.recordFields.get(key) ? [...this.pendingNodes.recordFields.get(key)] : void 0,
253
+ value: this.pendingNodes.values.get(key)
235
254
  }) : recovery;
236
255
  const incoming = update?.value;
237
256
  const value = update ? incoming && (before ? applyRecordUpdate(before, incoming, update.fields ? new Set(update.fields) : void 0) : incoming) : before ?? memory ?? (disk ? decodeHydrationValue(disk.value) : void 0);
@@ -245,7 +264,7 @@ var PersistenceCache = class {
245
264
  }
246
265
  const listValues = [];
247
266
  for (const key of lists) {
248
- const value = this.changed.has(listKey(key)) ? this.changed.get(listKey(key)) : this.recoveryChanges.has(listKey(key)) ? this.recoveryChanges.get(listKey(key)).value : previousLists.get(key) ?? this.client.store.readConfirmedList(key);
267
+ const value = this.pendingNodes.values.has(listKey(key)) ? this.pendingNodes.values.get(listKey(key)) : this.recoveryUpdates.has(listKey(key)) ? this.recoveryUpdates.get(listKey(key)).value : previousLists.get(key) ?? this.client.store.readConfirmedList(key);
249
268
  if (value) listValues.push([key, value]);
250
269
  await this.step();
251
270
  }
@@ -264,12 +283,12 @@ var PersistenceCache = class {
264
283
  };
265
284
  }
266
285
  get overhead() {
267
- return encoder.encode(JSON.stringify([`${this.prefix}bytes`, this.maxBytes])).byteLength;
286
+ return persistenceEntrySize(`${this.prefix}bytes`, this.maxBytes);
268
287
  }
269
288
  async step() {
270
289
  this.assertActive();
271
290
  if (++this.work % 64 === 0) {
272
- await yieldTask$1();
291
+ await yieldPersistenceTask();
273
292
  this.assertActive();
274
293
  }
275
294
  }
@@ -280,17 +299,22 @@ var PersistenceCache = class {
280
299
  return `${this.prefix}root:${key}`;
281
300
  }
282
301
  async read(key) {
283
- return (await this.storage.read(key))?.value;
302
+ return (await this.readStored(key))?.value;
303
+ }
304
+ async readStored(key) {
305
+ return decodeStoredValue(await this.storage.read(key));
284
306
  }
285
307
  async put(entries) {
286
308
  let bytes = await this.storage.read(`${this.prefix}bytes`) ?? 0;
287
309
  const writes = [];
288
- for (const [key, value] of new Map(entries)) {
289
- const previous = await this.storage.read(key);
290
- bytes -= previous?.bytes ?? 0;
310
+ const unique = /* @__PURE__ */ new Map();
311
+ for (const [key, value, previousBytes] of entries) unique.set(key, [value, previousBytes]);
312
+ for (const [key, [value, knownPreviousBytes]] of unique) {
313
+ const previousBytes = knownPreviousBytes ?? (await this.readStored(key))?.bytes ?? 0;
314
+ bytes -= previousBytes;
291
315
  if (value === void 0) writes.push([key, void 0]);
292
316
  else {
293
- const item = sized(key, value);
317
+ const item = withStoredSize(key, value);
294
318
  bytes += item.bytes;
295
319
  writes.push([key, item]);
296
320
  }
@@ -301,25 +325,27 @@ var PersistenceCache = class {
301
325
  await this.storage.writeBatch(writes);
302
326
  }
303
327
  get hasChanges() {
304
- return this.changed.size > 0 || this.recoveryChanges.size > 0;
305
- }
306
- changedNode(kind, key, paths, previousList) {
307
- if (!this.suppressed && key) {
308
- const selected = paths ? new Set([...paths].map((path) => path.split(".")[0])) : void 0;
309
- if (kind === "record") {
310
- const id = recordKey(key);
311
- const previous = this.fields.get(id);
312
- this.fields.set(id, selected?.size && (!this.fields.has(id) || previous) ? /* @__PURE__ */ new Set([...previous ?? [], ...selected]) : void 0);
328
+ return this.pendingNodes.values.size > 0 || this.recoveryUpdates.size > 0;
329
+ }
330
+ changedNode(change) {
331
+ if (!this.suppressed && change) {
332
+ const selected = change.kind === "record" && change.paths ? new Set([...change.paths].map((path) => path.split(".")[0])) : void 0;
333
+ if (change.kind === "record") {
334
+ const id = recordKey(change.key);
335
+ const previous = this.pendingNodes.recordFields.get(id);
336
+ this.pendingNodes.recordFields.set(id, selected?.size && (!this.pendingNodes.recordFields.has(id) || previous) ? /* @__PURE__ */ new Set([...previous ?? [], ...selected]) : void 0);
337
+ if (change.referencesChanged) this.pendingNodes.referenceChanges.add(id);
313
338
  }
314
- const id = kind === "record" ? recordKey(key) : listKey(key);
315
- const value = kind === "record" ? this.client.store.readConfirmed(key) : this.client.store.readConfirmedList(key);
316
- if (kind === "list" && !this.changed.has(id)) this.listBases.set(id, previousList);
317
- this.changed.set(id, value);
339
+ const id = change.kind === "record" ? recordKey(change.key) : listKey(change.key);
340
+ const value = change.kind === "record" ? this.client.store.readConfirmed(change.key) : this.client.store.readConfirmedList(change.key);
341
+ if (change.kind === "list" && !this.pendingNodes.values.has(id)) this.pendingNodes.listBases.set(id, change.previousList);
342
+ this.pendingNodes.values.set(id, value);
318
343
  if (this.capturing) {
319
344
  const previous = this.capturing.get(id);
320
345
  this.capturing.set(id, {
321
346
  fields: selected?.size && (!previous || previous.fields) ? [.../* @__PURE__ */ new Set([...previous?.fields ?? [], ...selected])] : void 0,
322
- previousList: previous ? previous.previousList : previousList,
347
+ previousList: previous ? previous.previousList : change.kind === "list" ? change.previousList : void 0,
348
+ referencesChanged: previous?.referencesChanged || change.kind === "record" && change.referencesChanged,
323
349
  value
324
350
  });
325
351
  }
@@ -347,28 +373,36 @@ var PersistenceCache = class {
347
373
  }
348
374
  for (const [key, update] of changes) {
349
375
  changes.set(key, {
350
- fields: this.fields.get(key) ? [...this.fields.get(key)] : void 0,
351
- previousList: this.listBases.has(key) ? this.listBases.get(key) : update.previousList,
352
- value: this.changed.get(key)
376
+ fields: this.pendingNodes.recordFields.get(key) ? [...this.pendingNodes.recordFields.get(key)] : void 0,
377
+ previousList: this.pendingNodes.listBases.has(key) ? this.pendingNodes.listBases.get(key) : update.previousList,
378
+ referencesChanged: this.pendingNodes.referenceChanges.has(key),
379
+ value: this.pendingNodes.values.get(key)
353
380
  });
354
- this.changed.delete(key);
355
- this.fields.delete(key);
356
- this.listBases.delete(key);
381
+ this.pendingNodes.values.delete(key);
382
+ this.pendingNodes.recordFields.delete(key);
383
+ this.pendingNodes.referenceChanges.delete(key);
384
+ this.pendingNodes.listBases.delete(key);
357
385
  }
358
386
  return encodeHydrationValue([...changes]);
359
387
  }
360
388
  replayChanges(updates) {
361
- this.recoveryChanges.clear();
362
- for (const encoded of updates) for (const [key, update] of decodeHydrationValue(encoded)) this.recoveryChanges.set(key, mergeCacheUpdate(key, this.recoveryChanges.get(key), update));
389
+ this.recoveryUpdates.clear();
390
+ for (const encoded of updates) for (const [key, update] of decodeHydrationValue(encoded)) {
391
+ const compatible = key.startsWith("r:") && update.referencesChanged === void 0 ? {
392
+ ...update,
393
+ referencesChanged: true
394
+ } : update;
395
+ this.recoveryUpdates.set(key, mergeCacheUpdate(key, this.recoveryUpdates.get(key), compatible));
396
+ }
363
397
  }
364
398
  fetched(request, maxAge = this.maxAge, fromNetwork = true) {
365
399
  if (!Number.isSafeInteger(maxAge) || maxAge < 0 || maxAge > Number.MAX_SAFE_INTEGER - Date.now()) throw new Error("fate: Persistence maxAge must be a non-negative finite duration.");
366
400
  if (!fromNetwork && this.policies.get(request) === maxAge) return;
367
401
  this.policies.set(request, maxAge);
368
- const previous = this.pending.get(request.key);
402
+ const previous = this.pendingRequests.get(request.key);
369
403
  const retain = this.client.retainPersistenceRequest(request);
370
404
  previous?.release();
371
- this.pending.set(request.key, {
405
+ this.pendingRequests.set(request.key, {
372
406
  fetchedAt: fromNetwork ? Date.now() : previous?.fetchedAt,
373
407
  maxAge,
374
408
  release: retain.dispose,
@@ -382,46 +416,48 @@ var PersistenceCache = class {
382
416
  if (!Object.keys(node.claims).length) return;
383
417
  if (key.startsWith("r:")) {
384
418
  const paths = Object.values(node.claims).flatMap((claim) => claim.paths);
385
- node.value = encodeHydrationValue(project(decodeHydrationValue(node.value), paths));
419
+ node.value = encodeHydrationValue(projectRecord(decodeHydrationValue(node.value), paths));
386
420
  }
387
421
  return node;
388
422
  }
389
423
  /** Walk only the requested selection, yielding between small pieces of work. */
390
- async collect(request, memory, fromNetwork = false) {
424
+ async collect(request, source) {
425
+ const readsMemory = source !== "disk";
426
+ const prefersNetworkResult = source === "network";
391
427
  const nodes = /* @__PURE__ */ new Map();
392
428
  const visited = /* @__PURE__ */ new Map();
393
429
  let complete = true;
394
430
  let fetchedAt = Infinity;
395
431
  const queue = [];
396
- const read = async (key) => {
432
+ const readNode = async (key) => {
397
433
  await this.step();
398
- if (memory) {
399
- if (this.updates.has(key)) {
400
- const incoming = this.updates.get(key);
401
- const base = this.updateListBases.get(key);
402
- if (!fromNetwork && incoming && base && key.startsWith("l:")) {
434
+ if (readsMemory) {
435
+ if (this.flushingNodes.values.has(key)) {
436
+ const incoming = this.flushingNodes.values.get(key);
437
+ const base = this.flushingNodes.listBases.get(key);
438
+ if (!prefersNetworkResult && incoming && base && key.startsWith("l:")) {
403
439
  const stored = await this.node(key);
404
440
  if (stored) return applyListUpdate(decodeHydrationValue(stored.value), base, incoming);
405
441
  }
406
- if (incoming && key.startsWith("r:") && this.updateFields.get(key)) {
442
+ if (incoming && key.startsWith("r:") && this.flushingNodes.recordFields.get(key)) {
407
443
  const stored = await this.node(key);
408
- if (stored) return applyRecordUpdate(decodeHydrationValue(stored.value), incoming, this.updateFields.get(key));
444
+ if (stored) return applyRecordUpdate(decodeHydrationValue(stored.value), incoming, this.flushingNodes.recordFields.get(key));
409
445
  }
410
446
  return incoming;
411
447
  }
412
- if (fromNetwork) {
448
+ if (prefersNetworkResult) {
413
449
  const id = key.slice(2);
414
450
  const value = key.startsWith("r:") ? this.client.store.readConfirmed(id) : key.startsWith("l:") ? this.client.store.readConfirmedList(id) : this.client.getPersistenceQuery(id);
415
451
  if (value !== void 0) return value;
416
- if (this.changed.has(key)) return;
452
+ if (this.pendingNodes.values.has(key)) return;
417
453
  }
418
454
  }
419
455
  const stored = await this.node(key);
420
456
  if (stored) for (const claim of Object.values(stored.claims)) fetchedAt = Math.min(fetchedAt, claim.fetchedAt);
421
- if (!stored) return memory && key.startsWith("r:") ? this.client.store.readConfirmed(key.slice(2)) : void 0;
457
+ if (!stored) return readsMemory && key.startsWith("r:") ? this.client.store.readConfirmed(key.slice(2)) : void 0;
422
458
  const value = decodeHydrationValue(stored.value);
423
- if (!memory) return value;
424
- const deleted = (id) => this.updates.has(recordKey(id)) && this.updates.get(recordKey(id)) === void 0;
459
+ if (!readsMemory) return value;
460
+ const deleted = (id) => this.flushingNodes.values.has(recordKey(id)) && this.flushingNodes.values.get(recordKey(id)) === void 0;
425
461
  if (key.startsWith("l:")) {
426
462
  const list = value;
427
463
  return {
@@ -444,7 +480,7 @@ var PersistenceCache = class {
444
480
  return typeof value === "string" && deleted(value) ? null : value;
445
481
  };
446
482
  const addList = async (key, paths, plan, prefix) => {
447
- const list = await read(listKey(key));
483
+ const list = await readNode(listKey(key));
448
484
  if (!list) {
449
485
  complete = false;
450
486
  return;
@@ -475,7 +511,7 @@ var PersistenceCache = class {
475
511
  prefix: ""
476
512
  });
477
513
  else if (item.kind === "query") {
478
- const id = await read(queryKey(item.queryKey));
514
+ const id = await readNode(queryKey(item.queryKey));
479
515
  if (id === void 0) {
480
516
  complete = false;
481
517
  continue;
@@ -503,16 +539,16 @@ var PersistenceCache = class {
503
539
  const needed = paths.filter((path) => !traversed?.has(path));
504
540
  if (traversed && !needed.length) continue;
505
541
  contexts.set(context, /* @__PURE__ */ new Set([...traversed ?? [], ...paths]));
506
- const source = await read(key);
507
- if (!source) {
542
+ const record = await readNode(key);
543
+ if (!record) {
508
544
  complete = false;
509
545
  continue;
510
546
  }
511
- if (paths.some((path) => !source.paths.some((covered) => path === covered || path.startsWith(`${covered}.`)))) complete = false;
547
+ if (paths.some((path) => !record.paths.some((covered) => path === covered || path.startsWith(`${covered}.`)))) complete = false;
512
548
  const all = /* @__PURE__ */ new Set([...seen?.paths ?? [], ...paths]);
513
549
  nodes.set(key, {
514
550
  paths: all,
515
- value: project(source, [...all])
551
+ value: projectRecord(record, [...all])
516
552
  });
517
553
  const groups = /* @__PURE__ */ new Map();
518
554
  for (const path of needed) {
@@ -536,10 +572,10 @@ var PersistenceCache = class {
536
572
  };
537
573
  for (const [field, children] of groups) {
538
574
  const childPrefix = prefix ? `${prefix}.${field}` : field;
539
- const value = source.record[field];
575
+ const value = record.record[field];
540
576
  if (Array.isArray(value)) {
541
577
  const nestedKey = getListKey(id, field, plan.args.get(childPrefix)?.hash);
542
- const list = await read(listKey(nestedKey));
578
+ const list = await readNode(listKey(nestedKey));
543
579
  if (list) {
544
580
  const projected = nodes.get(key).value;
545
581
  projected.record[field] = list.ids.map(createNodeRef);
@@ -557,7 +593,7 @@ var PersistenceCache = class {
557
593
  async restore(request) {
558
594
  await this.storage.exclusive(`${this.key}:write`, async () => {
559
595
  if (!await this.active()) return;
560
- const { complete, nodes } = await this.collect(request, false);
596
+ const { complete, nodes } = await this.collect(request, "disk");
561
597
  if (!complete) return;
562
598
  for (const [key, node] of nodes) {
563
599
  this.assertActive();
@@ -602,16 +638,28 @@ var PersistenceCache = class {
602
638
  async remove(rootId) {
603
639
  const root = await this.read(this.rootKey(rootId));
604
640
  if (!root) return;
641
+ let writes = [];
605
642
  for (const key of root.nodes) {
606
- const node = await this.read(this.nodeKey(key));
643
+ const storageKey = this.nodeKey(key);
644
+ const stored = await this.readStored(storageKey);
645
+ const node = stored?.value;
607
646
  if (node) {
608
647
  delete node.claims[rootId];
609
648
  const live = Object.values(node.claims).filter((claim) => claim.expiresAt > Date.now());
610
- if (key.startsWith("r:") && live.length) node.value = encodeHydrationValue(project(decodeHydrationValue(node.value), live.flatMap((claim) => claim.paths)));
611
- await this.put([[this.nodeKey(key), live.length ? node : void 0]]);
649
+ if (key.startsWith("r:") && live.length) node.value = encodeHydrationValue(projectRecord(decodeHydrationValue(node.value), live.flatMap((claim) => claim.paths)));
650
+ writes.push([
651
+ storageKey,
652
+ live.length ? node : void 0,
653
+ stored.bytes
654
+ ]);
655
+ if (writes.length === 64) {
656
+ await this.put(writes);
657
+ writes = [];
658
+ }
612
659
  }
613
660
  await this.step();
614
661
  }
662
+ if (writes.length) await this.put(writes);
615
663
  await this.put([
616
664
  [this.rootKey(rootId), void 0],
617
665
  [`${this.prefix}lru:${stamp(root.usedAt)}:${rootId}`, void 0],
@@ -623,7 +671,7 @@ var PersistenceCache = class {
623
671
  await this.remove(request.key);
624
672
  return;
625
673
  }
626
- const { nodes } = await this.collect(request, true, fromNetwork);
674
+ const { nodes } = await this.collect(request, fromNetwork ? "network" : "memory");
627
675
  const root = {
628
676
  fetchedAt,
629
677
  maxAge,
@@ -631,9 +679,9 @@ var PersistenceCache = class {
631
679
  request: encodeRequests([request]),
632
680
  usedAt
633
681
  };
634
- let minimum = sized(this.rootKey(request.key), root).bytes + sized(`${this.prefix}lru:${stamp(usedAt)}:${request.key}`, request.key).bytes + sized(`${this.prefix}expiry:${stamp(fetchedAt + maxAge)}:${request.key}`, request.key).bytes + sized(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes;
682
+ let minimum = withStoredSize(this.rootKey(request.key), root).bytes + withStoredSize(`${this.prefix}lru:${stamp(usedAt)}:${request.key}`, request.key).bytes + withStoredSize(`${this.prefix}expiry:${stamp(fetchedAt + maxAge)}:${request.key}`, request.key).bytes + withStoredSize(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes;
635
683
  for (const [key, node] of nodes) {
636
- minimum += sized(this.nodeKey(key), {
684
+ minimum += withStoredSize(this.nodeKey(key), {
637
685
  claims: { [request.key]: {
638
686
  expiresAt: fetchedAt + maxAge,
639
687
  fetchedAt,
@@ -674,7 +722,7 @@ var PersistenceCache = class {
674
722
  if (previous && key.startsWith("r:")) {
675
723
  const before = decodeHydrationValue(previous.value);
676
724
  const after = next?.value;
677
- value = project({
725
+ value = projectRecord({
678
726
  paths: [.../* @__PURE__ */ new Set([...before.paths, ...after?.paths ?? []])],
679
727
  record: {
680
728
  ...before.record,
@@ -694,17 +742,17 @@ var PersistenceCache = class {
694
742
  for (;;) {
695
743
  let added = 0;
696
744
  for (const [key, value] of writes) {
697
- const before = await this.storage.read(key);
698
- added += Math.max(0, (value === void 0 ? 0 : sized(key, value).bytes) - (before?.bytes ?? 0));
745
+ const before = await this.readStored(key);
746
+ added += Math.max(0, (value === void 0 ? 0 : withStoredSize(key, value).bytes) - (before?.bytes ?? 0));
699
747
  await this.step();
700
748
  }
701
749
  if ((await this.storage.read(`${this.prefix}bytes`) ?? 0) + added + this.journalBytes + this.overhead <= this.maxBytes) break;
702
- const candidate = (await this.storage.scan(`${this.prefix}lru:`, void 0, 2)).find((entry) => entry.value.value !== request.key);
750
+ const candidate = (await this.storage.scan(`${this.prefix}lru:`, void 0, 2)).find((entry) => decodeStoredValue(entry.value)?.value !== request.key);
703
751
  if (!candidate) {
704
752
  await this.remove(request.key);
705
753
  return;
706
754
  }
707
- const id = candidate.value.value;
755
+ const id = decodeStoredValue(candidate.value).value;
708
756
  if (await this.read(this.rootKey(id))) await this.remove(id);
709
757
  else await this.put([[candidate.key, void 0]]);
710
758
  writes = await prepare();
@@ -712,7 +760,7 @@ var PersistenceCache = class {
712
760
  const entries = [...writes];
713
761
  for (let index = 0; index < entries.length; index += 64) {
714
762
  await this.put(entries.slice(index, index + 64));
715
- await yieldTask$1();
763
+ await yieldPersistenceTask();
716
764
  }
717
765
  await this.put([[this.rootKey(request.key), root]]);
718
766
  }
@@ -722,7 +770,7 @@ var PersistenceCache = class {
722
770
  while (true) {
723
771
  const [entry] = await this.storage.scan(`${this.prefix}expiry:`, void 0, 1);
724
772
  if (!entry) break;
725
- const id = entry.value.value;
773
+ const id = decodeStoredValue(entry.value).value;
726
774
  const root = await this.read(this.rootKey(id));
727
775
  if (root && root.fetchedAt + root.maxAge > Date.now()) break;
728
776
  if (root) await this.remove(id);
@@ -732,7 +780,7 @@ var PersistenceCache = class {
732
780
  while ((await this.storage.read(`${this.prefix}bytes`) ?? 0) + journalBytes + this.overhead > this.maxBytes) {
733
781
  const [entry] = await this.storage.scan(`${this.prefix}lru:`, void 0, 1);
734
782
  if (!entry) break;
735
- const id = entry.value.value;
783
+ const id = decodeStoredValue(entry.value).value;
736
784
  if (await this.read(this.rootKey(id))) await this.remove(id);
737
785
  else await this.put([[entry.key, void 0]]);
738
786
  await this.step();
@@ -741,66 +789,69 @@ var PersistenceCache = class {
741
789
  async fits(journalBytes) {
742
790
  return (await this.storage.read(`${this.prefix}bytes`) ?? 0) + journalBytes + this.overhead <= this.maxBytes;
743
791
  }
792
+ /** Check fixed cache metadata before evicting any saved requests for a new journal entry. */
793
+ async fitsWithoutEviction(journalBytes) {
794
+ this.assertActive();
795
+ const scope = await this.storage.read(`${this.prefix}scope`);
796
+ this.assertActive();
797
+ return (scope?.bytes ?? 0) + journalBytes + this.overhead <= this.maxBytes;
798
+ }
744
799
  async flush(journalBytes) {
745
800
  if (!await this.active()) {
746
801
  this.reset();
747
802
  return;
748
803
  }
749
804
  await this.prune(journalBytes);
750
- const pending = this.pending;
751
- this.pending = /* @__PURE__ */ new Map();
752
- const changes = this.changed;
753
- const fields = this.fields;
754
- const listBases = this.listBases;
755
- this.listBases = /* @__PURE__ */ new Map();
756
- const recovery = this.recoveryChanges;
757
- this.recoveryChanges = /* @__PURE__ */ new Map();
805
+ const pending = this.pendingRequests;
806
+ this.pendingRequests = /* @__PURE__ */ new Map();
807
+ const pendingNodes = this.pendingNodes;
808
+ this.pendingNodes = createNodeChanges();
809
+ const { listBases, recordFields, referenceChanges, values } = pendingNodes;
810
+ const recovery = this.recoveryUpdates;
811
+ this.recoveryUpdates = /* @__PURE__ */ new Map();
758
812
  const updates = new Map(recovery);
759
- for (const [key, value] of changes) updates.set(key, mergeCacheUpdate(key, updates.get(key), {
760
- fields: fields.get(key) ? [...fields.get(key)] : void 0,
813
+ for (const [key, value] of values) updates.set(key, mergeCacheUpdate(key, updates.get(key), {
814
+ fields: recordFields.get(key) ? [...recordFields.get(key)] : void 0,
761
815
  previousList: listBases.get(key),
816
+ referencesChanged: referenceChanges.has(key),
762
817
  value
763
818
  }));
764
- this.updates = new Map([...updates].map(([key, update]) => [key, update.value]));
765
- this.changed = /* @__PURE__ */ new Map();
766
- this.updateListBases = new Map([...updates].map(([key, update]) => [key, update.previousList]));
767
- this.updateFields = new Map([...updates].map(([key, update]) => [key, update.fields ? new Set(update.fields) : void 0]));
768
- this.fields = /* @__PURE__ */ new Map();
819
+ this.flushingNodes = {
820
+ listBases: new Map([...updates].map(([key, update]) => [key, update.previousList])),
821
+ recordFields: new Map([...updates].map(([key, update]) => [key, update.fields ? new Set(update.fields) : void 0])),
822
+ referenceChanges: new Set([...updates].filter(([, update]) => update.referencesChanged).map(([key]) => key)),
823
+ values: new Map([...updates].map(([key, update]) => [key, update.value]))
824
+ };
769
825
  try {
770
826
  const affected = /* @__PURE__ */ new Map();
771
- for (const key of this.updates.keys()) {
827
+ for (const key of this.flushingNodes.values.keys()) {
772
828
  const node = await this.node(key);
773
- const incoming = this.updates.get(key);
774
- if (node && incoming && key.startsWith("r:")) {
829
+ const incoming = this.flushingNodes.values.get(key);
830
+ if (node && incoming && key.startsWith("r:") && !this.flushingNodes.referenceChanges.has(key)) {
775
831
  const before = decodeHydrationValue(node.value);
776
- const after = applyRecordUpdate(before, incoming, this.updateFields.get(key));
777
- if (!Object.keys({
778
- ...before.record,
779
- ...after.record
780
- }).some((field) => references(before.record[field]) !== references(after.record[field]))) {
781
- const value = encodeHydrationValue(project({
782
- paths: [.../* @__PURE__ */ new Set([...before.paths, ...after.paths])],
783
- record: {
784
- ...before.record,
785
- ...after.record
786
- }
787
- }, Object.values(node.claims).flatMap((claim) => claim.paths)));
788
- const next = {
789
- ...node,
790
- value
791
- };
792
- const beforeSize = (await this.storage.read(this.nodeKey(key))).bytes;
793
- const growth = Math.max(0, sized(this.nodeKey(key), next).bytes - beforeSize);
794
- await this.prune(journalBytes + growth);
795
- const retained = await this.node(key);
796
- if (retained) {
797
- next.claims = retained.claims;
798
- next.value = encodeHydrationValue(project(decodeHydrationValue(value), Object.values(retained.claims).flatMap((claim) => claim.paths)));
799
- await this.put([[this.nodeKey(key), next]]);
832
+ const after = applyRecordUpdate(before, incoming, this.flushingNodes.recordFields.get(key));
833
+ const value = encodeHydrationValue(projectRecord({
834
+ paths: [.../* @__PURE__ */ new Set([...before.paths, ...after.paths])],
835
+ record: {
836
+ ...before.record,
837
+ ...after.record
800
838
  }
801
- this.journalBytes = journalBytes;
802
- continue;
839
+ }, Object.values(node.claims).flatMap((claim) => claim.paths)));
840
+ const next = {
841
+ ...node,
842
+ value
843
+ };
844
+ const beforeSize = (await this.readStored(this.nodeKey(key))).bytes;
845
+ const growth = Math.max(0, withStoredSize(this.nodeKey(key), next).bytes - beforeSize);
846
+ await this.prune(journalBytes + growth);
847
+ const retained = await this.node(key);
848
+ if (retained) {
849
+ next.claims = retained.claims;
850
+ next.value = encodeHydrationValue(projectRecord(decodeHydrationValue(value), Object.values(retained.claims).flatMap((claim) => claim.paths)));
851
+ await this.put([[this.nodeKey(key), next]]);
803
852
  }
853
+ this.journalBytes = journalBytes;
854
+ continue;
804
855
  }
805
856
  for (const rootId of Object.keys(node?.claims ?? {})) {
806
857
  if (pending.has(rootId) || affected.has(rootId)) continue;
@@ -817,48 +868,50 @@ var PersistenceCache = class {
817
868
  await this.remove(request.key);
818
869
  continue;
819
870
  }
820
- const cached = await this.collect(request, false);
871
+ const cached = await this.collect(request, "disk");
821
872
  if (cached.complete && Number.isFinite(cached.fetchedAt)) {
822
873
  const root = await this.read(this.rootKey(request.key));
823
874
  await this.save(request, root?.fetchedAt ?? cached.fetchedAt, maxAge);
824
875
  }
825
876
  }
826
- this.updates.clear();
877
+ this.flushingNodes.values.clear();
827
878
  await this.prune(journalBytes);
828
879
  } catch (error) {
829
880
  if (!this.disposed) {
830
- for (const [key, update] of recovery) this.recoveryChanges.set(key, this.recoveryChanges.has(key) ? mergeCacheUpdate(key, update, this.recoveryChanges.get(key)) : update);
831
- for (const [key, value] of changes) {
832
- if (key.startsWith("l:")) this.listBases.set(key, listBases.get(key));
833
- if (!this.changed.has(key)) {
834
- this.changed.set(key, value);
835
- this.fields.set(key, fields.get(key));
881
+ for (const [key, update] of recovery) this.recoveryUpdates.set(key, this.recoveryUpdates.has(key) ? mergeCacheUpdate(key, update, this.recoveryUpdates.get(key)) : update);
882
+ for (const [key, value] of values) {
883
+ if (referenceChanges.has(key)) this.pendingNodes.referenceChanges.add(key);
884
+ if (key.startsWith("l:")) this.pendingNodes.listBases.set(key, listBases.get(key));
885
+ if (!this.pendingNodes.values.has(key)) {
886
+ this.pendingNodes.values.set(key, value);
887
+ this.pendingNodes.recordFields.set(key, recordFields.get(key));
836
888
  } else if (key.startsWith("r:")) {
837
- const before = fields.get(key);
838
- const after = this.fields.get(key);
839
- this.fields.set(key, before && after ? /* @__PURE__ */ new Set([...before, ...after]) : void 0);
889
+ const before = recordFields.get(key);
890
+ const after = this.pendingNodes.recordFields.get(key);
891
+ this.pendingNodes.recordFields.set(key, before && after ? /* @__PURE__ */ new Set([...before, ...after]) : void 0);
840
892
  }
841
893
  }
842
- for (const [key, entry] of pending) if (!this.pending.has(key)) {
843
- this.pending.set(key, entry);
894
+ for (const [key, entry] of pending) if (!this.pendingRequests.has(key)) {
895
+ this.pendingRequests.set(key, entry);
844
896
  pending.delete(key);
845
897
  }
846
898
  }
847
899
  throw error;
848
900
  } finally {
849
- this.updates.clear();
850
- this.updateFields.clear();
851
- this.updateListBases.clear();
901
+ this.flushingNodes = createNodeChanges();
852
902
  for (const entry of pending.values()) entry.release();
853
903
  }
854
904
  }
855
905
  async clear() {
906
+ this.assertActive();
856
907
  this.reset();
857
908
  while (true) {
858
909
  const entries = await this.storage.scan(this.prefix, void 0, 64);
910
+ this.assertActive();
859
911
  if (!entries.length) return;
860
912
  await this.storage.writeBatch(entries.map(({ key }) => [key, void 0]));
861
- await yieldTask$1();
913
+ await yieldPersistenceTask();
914
+ this.assertActive();
862
915
  }
863
916
  }
864
917
  dispose() {
@@ -866,32 +919,28 @@ var PersistenceCache = class {
866
919
  this.reset();
867
920
  }
868
921
  reset() {
869
- this.recoveryChanges.clear();
922
+ this.recoveryUpdates.clear();
870
923
  this.policies = /* @__PURE__ */ new WeakMap();
871
- for (const entry of this.pending.values()) entry.release();
872
- this.pending.clear();
873
- this.changed.clear();
874
- this.updates.clear();
875
- this.fields.clear();
876
- this.updateFields.clear();
877
- this.listBases.clear();
878
- this.updateListBases.clear();
924
+ for (const entry of this.pendingRequests.values()) entry.release();
925
+ this.pendingRequests.clear();
926
+ this.pendingNodes = createNodeChanges();
927
+ this.flushingNodes = createNodeChanges();
879
928
  }
880
929
  };
881
930
  //#endregion
882
931
  //#region src/persistence-journal.ts
883
932
  const invalid = () => /* @__PURE__ */ new Error("fate: Unsupported or corrupt persistence data. It has not been overwritten.");
884
- const valid = (entry) => entry && typeof entry.id === "string" && typeof entry.scope === "string" && [
885
- "queued",
886
- "sending",
887
- "failed",
888
- "confirmed"
889
- ].includes(entry.status) && Number.isInteger(entry.attempts) && entry.attempts >= 0;
890
- const same = (left, right) => {
933
+ const valid = (value) => {
934
+ if (!value || typeof value !== "object") return false;
935
+ const entry = value;
936
+ if (typeof entry.id !== "string" || typeof entry.scope !== "string" || !Number.isInteger(entry.attempts) || entry.attempts < 0) return false;
937
+ if (entry.status === "failed") return !!entry.error && typeof entry.error === "object" && typeof entry.error.message === "string";
938
+ if (entry.status === "confirmed") return "cacheUpdates" in entry && "result" in entry;
939
+ return entry.status === "queued" || entry.status === "sending";
940
+ };
941
+ const hasSameFields = (left, right) => {
891
942
  return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => left[key] === right[key]);
892
943
  };
893
- const size = (key, value) => new TextEncoder().encode(JSON.stringify([key, value])).byteLength;
894
- const yieldTask = () => new Promise((resolve) => setTimeout(resolve, 0));
895
944
  /** All methods run under the account's write lock, including reads across scan pages. */
896
945
  var PersistenceJournal = class {
897
946
  constructor(storage, key, validate) {
@@ -905,7 +954,6 @@ var PersistenceJournal = class {
905
954
  dispose() {
906
955
  this.disposed = true;
907
956
  this.entries.clear();
908
- this.prepared = void 0;
909
957
  }
910
958
  assertActive() {
911
959
  if (this.disposed) throw new Error("fate: Persistence was disposed.");
@@ -923,7 +971,7 @@ var PersistenceJournal = class {
923
971
  for (const entry of value.mutations) {
924
972
  this.validate?.(entry);
925
973
  if (++checked % 64 === 0) {
926
- await yieldTask();
974
+ await yieldPersistenceTask();
927
975
  this.assertActive();
928
976
  }
929
977
  }
@@ -940,6 +988,7 @@ var PersistenceJournal = class {
940
988
  for (const { key, value } of page) {
941
989
  const stored = value;
942
990
  if (!stored || !valid(stored.entry) || !Number.isSafeInteger(stored.bytes) || stored.bytes <= 0 || entries.has(stored.entry.id)) throw invalid();
991
+ this.validate?.(stored.entry);
943
992
  entries.set(stored.entry.id, {
944
993
  ...stored,
945
994
  key
@@ -947,16 +996,15 @@ var PersistenceJournal = class {
947
996
  }
948
997
  if (page.length < 64) break;
949
998
  after = page.at(-1).key;
950
- await yieldTask();
999
+ await yieldPersistenceTask();
951
1000
  }
952
1001
  this.assertActive();
953
1002
  this.header = value;
954
1003
  this.entries = entries;
955
1004
  }
956
- this.prepared = void 0;
957
1005
  return { mutations: [...this.entries.values()].map(({ entry }) => ({ ...entry })) };
958
1006
  }
959
- async measure(data) {
1007
+ async prepare(data) {
960
1008
  this.assertActive();
961
1009
  const header = {
962
1010
  nextSequence: this.header?.nextSequence ?? 0,
@@ -970,16 +1018,16 @@ var PersistenceJournal = class {
970
1018
  for (const entry of data.mutations) {
971
1019
  const previous = this.entries.get(entry.id);
972
1020
  let stored = previous;
973
- if (!previous || !same(previous.entry, entry)) {
1021
+ if (!previous || !hasSameFields(previous.entry, entry)) {
974
1022
  const key = previous?.key ?? `${this.prefix}${String(header.nextSequence++).padStart(16, "0")}:${entry.id}`;
975
1023
  const value = {
976
1024
  bytes: 0,
977
1025
  entry
978
1026
  };
979
- let measured = size(key, value);
1027
+ let measured = persistenceEntrySize(key, value);
980
1028
  while (measured !== value.bytes) {
981
1029
  value.bytes = measured;
982
- measured = size(key, value);
1030
+ measured = persistenceEntrySize(key, value);
983
1031
  }
984
1032
  stored = {
985
1033
  ...value,
@@ -991,37 +1039,46 @@ var PersistenceJournal = class {
991
1039
  entries.set(entry.id, stored);
992
1040
  bytes += stored.bytes;
993
1041
  if (++count % 64 === 0) {
994
- await yieldTask();
1042
+ await yieldPersistenceTask();
995
1043
  this.assertActive();
996
1044
  }
997
1045
  }
998
1046
  for (const [id, { key }] of this.entries) if (!entries.has(id)) writes.push([key, void 0]);
999
- bytes += size(this.key, header);
1000
- this.prepared = {
1047
+ bytes += persistenceEntrySize(this.key, header);
1048
+ return {
1001
1049
  bytes,
1002
- data,
1003
1050
  entries,
1004
1051
  header,
1005
1052
  writes
1006
1053
  };
1007
- return bytes;
1008
1054
  }
1009
- async save(data) {
1010
- this.assertActive();
1011
- if (this.prepared?.data !== data) await this.measure(data);
1055
+ async commit(plan) {
1012
1056
  this.assertActive();
1013
- const prepared = this.prepared;
1014
- if (prepared.writes.length || !this.header) {
1015
- await this.storage.writeBatch([...prepared.writes, [this.key, prepared.header]]);
1057
+ if (plan.writes.length || !this.header) {
1058
+ await this.storage.writeBatch([...plan.writes, [this.key, plan.header]]);
1016
1059
  this.assertActive();
1017
- this.header = prepared.header;
1018
- this.entries = prepared.entries;
1060
+ this.header = plan.header;
1061
+ this.entries = plan.entries;
1019
1062
  }
1020
- this.prepared = void 0;
1063
+ }
1064
+ async save(data) {
1065
+ await this.commit(await this.prepare(data));
1021
1066
  }
1022
1067
  };
1023
1068
  //#endregion
1024
1069
  //#region src/persistence.ts
1070
+ const isPendingMutation = (entry) => entry.status === "queued" || entry.status === "sending";
1071
+ const replaceMutation = (data, id, replace) => {
1072
+ const index = data.mutations.findIndex((entry) => entry.id === id);
1073
+ if (index >= 0) data.mutations[index] = replace(data.mutations[index]);
1074
+ };
1075
+ const isTerminalDeliveryError = (status) => status !== void 0 && status >= 400 && status < 500 && ![
1076
+ 401,
1077
+ 403,
1078
+ 408,
1079
+ 429
1080
+ ].includes(status);
1081
+ const mutationDiscardedError = () => /* @__PURE__ */ new Error("fate: Mutation was discarded.");
1025
1082
  const encodeCommand = (command) => encodeHydrationValue({
1026
1083
  ...command,
1027
1084
  plan: command.plan ? serializePlan(command.plan) : void 0
@@ -1057,7 +1114,6 @@ var Session = class {
1057
1114
  this.key = key;
1058
1115
  this.online = online;
1059
1116
  this.retryDelay = retryDelay;
1060
- this.maxBytes = maxBytes;
1061
1117
  this.snapshot = {
1062
1118
  mutations: [],
1063
1119
  status: "restoring"
@@ -1066,11 +1122,16 @@ var Session = class {
1066
1122
  this.commands = /* @__PURE__ */ new Map();
1067
1123
  this.operations = /* @__PURE__ */ new Map();
1068
1124
  this.mutationAdmission = Promise.resolve();
1125
+ this.cacheChangeRevision = 0;
1126
+ this.baseCheckpointRevision = 0;
1127
+ this.checkpointedCacheUpdates = /* @__PURE__ */ new Set();
1069
1128
  this.needsCacheRecovery = false;
1070
1129
  this.needsCheckpoint = false;
1071
1130
  this.waiters = /* @__PURE__ */ new Map();
1072
1131
  this.disposed = false;
1073
1132
  this.draining = false;
1133
+ this.flushRequested = false;
1134
+ this.refreshRequested = false;
1074
1135
  this.onOnline = () => this.retry();
1075
1136
  this.getSnapshot = () => this.snapshot;
1076
1137
  this.subscribe = (listener) => {
@@ -1079,13 +1140,14 @@ var Session = class {
1079
1140
  this.listeners.delete(listener);
1080
1141
  };
1081
1142
  };
1082
- this.load = () => this.storage.exclusive(`${this.key}:write`, () => this.journal.load());
1083
- this.update = (apply, admit = false) => this.storage.exclusive(`${this.key}:write`, async () => {
1084
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1143
+ this.loadJournal = () => this.storage.exclusive(`${this.key}:write`, () => this.journal.load());
1144
+ this.updateJournal = (apply, admit = false) => this.storage.exclusive(`${this.key}:write`, async () => {
1145
+ this.assertActive();
1085
1146
  const data = await this.journal.load();
1086
1147
  const result = await apply(data);
1087
- const bytes = await this.journal.measure(data);
1088
- if (admit && bytes > this.maxBytes) throw new Error("fate: Persistence size limit exceeded; unconfirmed mutations have been preserved.");
1148
+ const plan = await this.journal.prepare(data);
1149
+ const { bytes } = plan;
1150
+ if (admit && !await this.cache.fitsWithoutEviction(bytes)) throw new Error("fate: Persistence size limit exceeded; unconfirmed mutations have been preserved.");
1089
1151
  try {
1090
1152
  await this.cache.prune(bytes);
1091
1153
  } catch (error) {
@@ -1093,8 +1155,8 @@ var Session = class {
1093
1155
  this.report(error);
1094
1156
  }
1095
1157
  if (admit && !await this.cache.fits(bytes)) throw new Error("fate: Persistence size limit exceeded; unconfirmed mutations have been preserved.");
1096
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1097
- await this.journal.save(data);
1158
+ this.assertActive();
1159
+ await this.journal.commit(plan);
1098
1160
  return result;
1099
1161
  });
1100
1162
  this.journal = new PersistenceJournal(storage, key, (entry) => {
@@ -1155,11 +1217,23 @@ var Session = class {
1155
1217
  };
1156
1218
  this.publish();
1157
1219
  }
1220
+ clearError() {
1221
+ if (!this.mutationError && this.snapshot.error) {
1222
+ this.snapshot = {
1223
+ ...this.snapshot,
1224
+ error: void 0
1225
+ };
1226
+ this.publish();
1227
+ }
1228
+ }
1229
+ assertActive() {
1230
+ if (this.disposed) throw new Error("fate: Persistence was disposed.");
1231
+ }
1158
1232
  async restore() {
1159
- const data = await this.load();
1233
+ const data = await this.loadJournal();
1160
1234
  await this.cache.initialize();
1161
1235
  if (this.disposed) return;
1162
- for (const entry of data.mutations) if (entry.status === "queued" || entry.status === "sending") {
1236
+ for (const entry of data.mutations) if (isPendingMutation(entry)) {
1163
1237
  if (entry.base && entry.base.scope === this.client.getPersistenceScope()) this.cache.restoreBase(decodeClientHydrationState(entry.base.data));
1164
1238
  }
1165
1239
  this.snapshot = {
@@ -1184,7 +1258,7 @@ var Session = class {
1184
1258
  if (this.disposed) return;
1185
1259
  let processed = 0;
1186
1260
  for (const entry of data.mutations) {
1187
- if (++processed % 64 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
1261
+ if (++processed % 64 === 0) await yieldPersistenceTask();
1188
1262
  if (this.disposed) return;
1189
1263
  this.command(entry);
1190
1264
  if (entry.status === "confirmed" || entry.status === "failed") {
@@ -1215,12 +1289,21 @@ var Session = class {
1215
1289
  const operation = prepareMutation(this.client, command, this.client.getTypeConfig(command.entity), true);
1216
1290
  const { promise, resolve } = Promise.withResolvers();
1217
1291
  let cacheUpdates;
1292
+ let commitError;
1293
+ let commitFailed = false;
1218
1294
  if (operation.entityId) this.client.registerPendingOptimisticMutation(operation.entityId, promise);
1219
1295
  return {
1220
1296
  ...operation,
1221
1297
  commit: (result, persist = true) => {
1222
- if (persist) cacheUpdates ??= this.cache.captureChanges(() => operation.commit(result));
1223
- else this.cache.withoutChanges(() => operation.commit(result));
1298
+ if (commitFailed) throw commitError;
1299
+ try {
1300
+ if (persist) cacheUpdates ??= this.cache.captureChanges(() => operation.commit(result));
1301
+ else this.cache.withoutChanges(() => operation.commit(result));
1302
+ } catch (error) {
1303
+ commitFailed = true;
1304
+ commitError = error;
1305
+ throw error;
1306
+ }
1224
1307
  resolve();
1225
1308
  },
1226
1309
  getCacheUpdates: () => cacheUpdates,
@@ -1235,22 +1318,30 @@ var Session = class {
1235
1318
  };
1236
1319
  }
1237
1320
  refresh() {
1238
- return this.refreshing ??= this.load().then(async (data) => {
1239
- await this.reconcile(data);
1321
+ this.refreshRequested = true;
1322
+ return this.refreshing ??= this.drainRefreshes();
1323
+ }
1324
+ async drainRefreshes() {
1325
+ try {
1326
+ while (this.refreshRequested && !this.disposed) {
1327
+ this.refreshRequested = false;
1328
+ await this.reconcile(await this.loadJournal());
1329
+ }
1240
1330
  this.retry();
1241
- }).finally(() => {
1331
+ } finally {
1242
1332
  this.refreshing = void 0;
1243
- });
1333
+ if (this.refreshRequested && !this.disposed) this.refresh().catch((error) => this.report(error));
1334
+ }
1244
1335
  }
1245
1336
  async mutate(command) {
1246
1337
  if (this.mutationError) throw this.mutationError;
1247
1338
  const encoded = encodeCommand(command);
1248
1339
  await this.ready;
1249
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1340
+ this.assertActive();
1250
1341
  const id = crypto.randomUUID();
1251
1342
  const admission = this.mutationAdmission.then(async () => {
1252
1343
  if (this.needsCheckpoint) await this.flushCache().catch((error) => this.report(error));
1253
- return this.update(async (data) => {
1344
+ return this.updateJournal(async (data) => {
1254
1345
  await this.reconcile(data);
1255
1346
  const base = await this.cache.mutationBase(decodeCommand(encoded));
1256
1347
  data.mutations.push({
@@ -1266,7 +1357,7 @@ var Session = class {
1266
1357
  });
1267
1358
  this.mutationAdmission = admission.catch(() => {});
1268
1359
  const base = await admission;
1269
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1360
+ this.assertActive();
1270
1361
  this.cache.restoreBase(decodeClientHydrationState(base.data));
1271
1362
  const result = new Promise((resolve, reject) => this.waiters.set(id, {
1272
1363
  reject,
@@ -1283,7 +1374,7 @@ var Session = class {
1283
1374
  }
1284
1375
  async restoreRequest(request) {
1285
1376
  await this.ready;
1286
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1377
+ this.assertActive();
1287
1378
  try {
1288
1379
  if (this.needsCacheRecovery) await this.flush();
1289
1380
  await this.cache.restore(request);
@@ -1302,41 +1393,92 @@ var Session = class {
1302
1393
  this.cache.fetched(request, options?.maxAge, false);
1303
1394
  this.changed();
1304
1395
  }
1305
- changed(kind, key, paths, previousList) {
1396
+ changed(change) {
1306
1397
  if (this.disposed) return;
1307
- if (this.cache.changedNode(kind, key, paths, previousList)) this.needsCacheRecovery = true;
1398
+ if (this.cache.changedNode(change)) {
1399
+ this.needsCacheRecovery = true;
1400
+ this.cacheChangeRevision++;
1401
+ }
1308
1402
  if (this.disposed || this.snapshot.status !== "ready") return;
1309
- if (!this.saveTimer) this.saveTimer = setTimeout(() => {
1310
- this.saveTimer = void 0;
1311
- this.flush().catch((error) => this.report(error));
1312
- }, 25);
1403
+ if (this.flushing || this.draining) this.flushRequested = true;
1404
+ else this.scheduleSave();
1313
1405
  }
1314
1406
  async flush() {
1407
+ clearTimeout(this.saveTimer);
1408
+ this.saveTimer = void 0;
1315
1409
  await this.ready;
1316
1410
  await this.flushCache();
1411
+ this.clearError();
1317
1412
  }
1318
- async flushCache() {
1319
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1413
+ scheduleSave() {
1414
+ if (!this.saveTimer && !this.disposed) this.saveTimer = setTimeout(() => {
1415
+ this.saveTimer = void 0;
1416
+ this.flush().catch((error) => this.report(error));
1417
+ }, 25);
1418
+ }
1419
+ confirmedCacheEntries(data) {
1420
+ const scope = this.client.getPersistenceScope();
1421
+ return data.mutations.flatMap((entry) => entry.status === "confirmed" && entry.scope === scope ? [[entry.id, entry.cacheUpdates]] : []);
1422
+ }
1423
+ confirmedCacheUpdates(data) {
1424
+ return this.confirmedCacheEntries(data).map(([, updates]) => updates);
1425
+ }
1426
+ async checkpointPendingBases(data) {
1427
+ const revision = this.cacheChangeRevision;
1428
+ const cacheUpdateIds = this.confirmedCacheEntries(data).map(([id]) => id);
1429
+ if (revision === this.baseCheckpointRevision && cacheUpdateIds.every((id) => this.checkpointedCacheUpdates.has(id))) return;
1430
+ const scope = this.client.getPersistenceScope();
1431
+ if (!data.mutations.some((entry) => isPendingMutation(entry) && entry.scope === scope)) {
1432
+ this.baseCheckpointRevision = revision;
1433
+ for (const id of cacheUpdateIds) this.checkpointedCacheUpdates.add(id);
1434
+ return;
1435
+ }
1436
+ for (const entry of data.mutations) if (isPendingMutation(entry) && entry.scope === scope) entry.base = await this.cache.mutationBase(this.command(entry), entry.base);
1437
+ const plan = await this.journal.prepare(data);
1438
+ await this.cache.prune(plan.bytes);
1439
+ this.assertActive();
1440
+ await this.journal.commit(plan);
1441
+ this.baseCheckpointRevision = revision;
1442
+ for (const id of cacheUpdateIds) this.checkpointedCacheUpdates.add(id);
1443
+ }
1444
+ async removeConfirmedEntries(data) {
1445
+ const confirmedIds = new Set(data.mutations.filter((entry) => entry.status === "confirmed").map(({ id }) => id));
1446
+ if (!confirmedIds.size) return;
1447
+ data.mutations = data.mutations.filter(({ id }) => !confirmedIds.has(id));
1448
+ await this.journal.commit(await this.journal.prepare(data));
1449
+ for (const id of confirmedIds) this.checkpointedCacheUpdates.delete(id);
1450
+ }
1451
+ flushCache() {
1452
+ this.assertActive();
1453
+ this.flushRequested = true;
1454
+ if (!this.flushing) {
1455
+ const flushing = this.drainCacheFlushes();
1456
+ this.flushing = flushing;
1457
+ flushing.then(() => this.finishCacheFlush(flushing), () => this.finishCacheFlush(flushing));
1458
+ }
1459
+ return this.flushing;
1460
+ }
1461
+ finishCacheFlush(flushing) {
1462
+ if (this.flushing !== flushing) return;
1463
+ this.flushing = void 0;
1464
+ if (this.flushRequested && !this.disposed) this.scheduleSave();
1465
+ }
1466
+ async drainCacheFlushes() {
1467
+ while (this.flushRequested && !this.disposed) {
1468
+ this.flushRequested = false;
1469
+ await this.flushCacheOnce();
1470
+ }
1471
+ }
1472
+ async flushCacheOnce() {
1320
1473
  await this.storage.exclusive(`${this.key}:write`, async () => {
1321
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1474
+ this.assertActive();
1322
1475
  const data = await this.journal.load();
1323
1476
  await this.reconcile(data);
1324
- this.cache.replayChanges(data.mutations.flatMap((entry) => entry.cacheUpdates && entry.scope === this.client.getPersistenceScope() ? [entry.cacheUpdates] : []));
1325
- if (data.mutations.some((entry) => entry.status === "queued" || entry.status === "sending")) {
1326
- for (const entry of data.mutations) if ((entry.status === "queued" || entry.status === "sending") && entry.scope === this.client.getPersistenceScope()) entry.base = await this.cache.mutationBase(this.command(entry), entry.base);
1327
- await this.cache.prune(await this.journal.measure(data));
1328
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1329
- await this.journal.save(data);
1330
- }
1331
- await this.cache.flush(await this.journal.measure(data));
1332
- if (this.disposed) throw new Error("fate: Persistence was disposed.");
1333
- const confirmed = data.mutations.filter((entry) => entry.status === "confirmed");
1334
- if (confirmed.length) {
1335
- const ids = new Set(confirmed.map(({ id }) => id));
1336
- data.mutations = data.mutations.filter(({ id }) => !ids.has(id));
1337
- await this.journal.measure(data);
1338
- await this.journal.save(data);
1339
- }
1477
+ this.cache.replayChanges(this.confirmedCacheUpdates(data));
1478
+ await this.checkpointPendingBases(data);
1479
+ await this.cache.flush((await this.journal.prepare(data)).bytes);
1480
+ this.assertActive();
1481
+ await this.removeConfirmedEntries(data);
1340
1482
  this.needsCacheRecovery = this.cache.hasChanges;
1341
1483
  this.needsCheckpoint = false;
1342
1484
  });
@@ -1358,20 +1500,129 @@ var Session = class {
1358
1500
  this.retry();
1359
1501
  }, delay);
1360
1502
  }
1361
- async fail(id, error) {
1503
+ async fail(id, error, status) {
1362
1504
  const failure = error instanceof Error ? error : new Error(String(error));
1363
- await this.update((data) => {
1364
- const entry = data.mutations.find((item) => item.id === id);
1365
- if (entry) {
1366
- entry.status = "failed";
1367
- delete entry.base;
1368
- entry.error = {
1505
+ status ??= getErrorStatusCode(failure);
1506
+ await this.updateJournal((data) => {
1507
+ replaceMutation(data, id, (entry) => ({
1508
+ attempts: entry.attempts,
1509
+ command: entry.command,
1510
+ error: {
1369
1511
  message: failure.message,
1370
- status: getErrorStatusCode(failure)
1371
- };
1512
+ status
1513
+ },
1514
+ id: entry.id,
1515
+ scope: entry.scope,
1516
+ status: "failed"
1517
+ }));
1518
+ });
1519
+ await this.reconcile(await this.loadJournal());
1520
+ }
1521
+ forgetOperation(id) {
1522
+ this.operations.delete(id);
1523
+ this.waiters.delete(id);
1524
+ }
1525
+ /** Resolve an operation whose journal entry another client already removed. */
1526
+ async recoverOperation(id, operation) {
1527
+ try {
1528
+ const result = await operation.execute({
1529
+ id,
1530
+ replayOnly: true,
1531
+ scope: this.key
1532
+ });
1533
+ if (this.disposed) return;
1534
+ operation.commit(result, false);
1535
+ this.waiters.get(id)?.resolve(result);
1536
+ } catch (error) {
1537
+ if (this.disposed) return;
1538
+ if (!(error instanceof Error) || getErrorStatusCode(error) !== 404) throw error;
1539
+ operation.rollback(false);
1540
+ this.waiters.get(id)?.reject(mutationDiscardedError());
1541
+ }
1542
+ this.forgetOperation(id);
1543
+ }
1544
+ async recoverRemovedOperations(data) {
1545
+ const retained = new Set(data.mutations.map(({ id }) => id));
1546
+ for (const [id, operation] of this.operations) {
1547
+ if (retained.has(id)) continue;
1548
+ try {
1549
+ await this.recoverOperation(id, operation);
1550
+ } catch (error) {
1551
+ this.report(error);
1552
+ this.scheduleRetry();
1553
+ return false;
1372
1554
  }
1555
+ if (this.disposed) return false;
1556
+ }
1557
+ return true;
1558
+ }
1559
+ async claim(entry) {
1560
+ const data = await this.updateJournal((data) => {
1561
+ const current = data.mutations.find((item) => item.id === entry.id);
1562
+ if (!current || !isPendingMutation(current)) return;
1563
+ replaceMutation(data, entry.id, () => ({
1564
+ ...current,
1565
+ attempts: current.attempts + 1,
1566
+ status: "sending"
1567
+ }));
1568
+ return data;
1569
+ });
1570
+ if (!data) return false;
1571
+ await this.reconcile(data);
1572
+ return true;
1573
+ }
1574
+ async confirm(entry, operation, encodedResult) {
1575
+ const cacheUpdates = operation.getCacheUpdates();
1576
+ if (!cacheUpdates) throw new Error("fate: Mutation confirmation did not capture its cache updates.");
1577
+ await this.updateJournal((data) => {
1578
+ replaceMutation(data, entry.id, (current) => ({
1579
+ attempts: current.attempts,
1580
+ cacheUpdates,
1581
+ command: current.command,
1582
+ id: current.id,
1583
+ result: encodedResult,
1584
+ scope: current.scope,
1585
+ status: "confirmed"
1586
+ }));
1373
1587
  });
1374
- await this.reconcile(await this.load());
1588
+ await this.reconcile(await this.loadJournal());
1589
+ }
1590
+ /** Deliver one claimed entry. False means the queue must pause for a retry. */
1591
+ async deliver(entry, operation) {
1592
+ let result;
1593
+ try {
1594
+ result = await operation.execute({
1595
+ id: entry.id,
1596
+ scope: this.key
1597
+ });
1598
+ } catch (error) {
1599
+ if (this.disposed) return false;
1600
+ const status = error instanceof Error ? getErrorStatusCode(error) : void 0;
1601
+ if (isTerminalDeliveryError(status)) {
1602
+ await this.fail(entry.id, error);
1603
+ return true;
1604
+ }
1605
+ await this.queueRetry(entry.id, error, status);
1606
+ return false;
1607
+ }
1608
+ if (this.disposed) return false;
1609
+ let encodedResult;
1610
+ try {
1611
+ encodedResult = encodeHydrationValue(result);
1612
+ operation.commit(result);
1613
+ } catch (error) {
1614
+ await this.fail(entry.id, error, 500);
1615
+ return true;
1616
+ }
1617
+ try {
1618
+ await this.confirm(entry, operation, encodedResult);
1619
+ return true;
1620
+ } catch (error) {
1621
+ if (this.disposed) return false;
1622
+ const status = error instanceof Error ? getErrorStatusCode(error) : void 0;
1623
+ await this.queueRetry(entry.id, error, status);
1624
+ return false;
1625
+ }
1375
1626
  }
1376
1627
  async drain() {
1377
1628
  if (this.draining || this.disposed || !this.online()) return;
@@ -1379,33 +1630,9 @@ var Session = class {
1379
1630
  try {
1380
1631
  await this.storage.exclusive(`${this.key}:delivery`, async () => {
1381
1632
  while (!this.disposed && this.online()) {
1382
- const data = await this.load();
1633
+ const data = await this.loadJournal();
1383
1634
  await this.reconcile(data);
1384
- const retained = new Set(data.mutations.map(({ id }) => id));
1385
- for (const [id, operation] of this.operations) {
1386
- if (retained.has(id)) continue;
1387
- try {
1388
- const result = await operation.execute({
1389
- id,
1390
- replayOnly: true,
1391
- scope: this.key
1392
- });
1393
- if (this.disposed) return;
1394
- operation.commit(result, false);
1395
- this.waiters.get(id)?.resolve(result);
1396
- } catch (error) {
1397
- if (this.disposed) return;
1398
- if (!(error instanceof Error) || getErrorStatusCode(error) !== 404) {
1399
- this.report(error);
1400
- this.scheduleRetry();
1401
- return;
1402
- }
1403
- operation.rollback(false);
1404
- this.waiters.get(id)?.reject(/* @__PURE__ */ new Error("fate: Mutation was discarded."));
1405
- }
1406
- this.operations.delete(id);
1407
- this.waiters.delete(id);
1408
- }
1635
+ if (!await this.recoverRemovedOperations(data)) return;
1409
1636
  const entry = data.mutations.find((item) => item.status !== "confirmed" && item.status !== "failed");
1410
1637
  if (!entry) return;
1411
1638
  if (entry.nextAttemptAt && entry.nextAttemptAt > Date.now()) {
@@ -1418,90 +1645,64 @@ var Session = class {
1418
1645
  continue;
1419
1646
  }
1420
1647
  const operation = this.operations.get(entry.id);
1421
- const claimed = await this.update((latest) => {
1422
- const item = latest.mutations.find((item) => item.id === entry.id);
1423
- if (!item) return false;
1424
- item.status = "sending";
1425
- item.attempts += 1;
1426
- return true;
1427
- });
1648
+ if (!operation) throw new Error(`fate: Missing restored mutation operation '${entry.id}'.`);
1649
+ const claimed = await this.claim(entry);
1428
1650
  if (this.disposed) return;
1429
1651
  if (!claimed) continue;
1430
- try {
1431
- const result = await operation.execute({
1432
- id: entry.id,
1433
- scope: this.key
1434
- });
1435
- if (this.disposed) return;
1436
- const encoded = encodeHydrationValue(result);
1437
- operation.commit(result);
1438
- await this.update(async (latest) => {
1439
- this.cache.replayChanges([operation.getCacheUpdates()]);
1440
- const item = latest.mutations.find((item) => item.id === entry.id);
1441
- if (item) {
1442
- item.status = "confirmed";
1443
- delete item.base;
1444
- item.result = encoded;
1445
- item.cacheUpdates = operation.getCacheUpdates();
1446
- item.error = void 0;
1447
- }
1448
- for (const pending of latest.mutations) if ((pending.status === "queued" || pending.status === "sending") && pending.scope === this.client.getPersistenceScope()) pending.base = await this.cache.mutationBase(decodeCommand(pending.command), pending.base);
1449
- });
1450
- await this.reconcile(await this.load());
1451
- await this.flush().catch((error) => this.report(error));
1452
- } catch (error) {
1453
- if (this.disposed) return;
1454
- const status = error instanceof Error ? getErrorStatusCode(error) : void 0;
1455
- if (status && status >= 400 && status < 500 && ![
1456
- 401,
1457
- 403,
1458
- 408,
1459
- 429
1460
- ].includes(status)) {
1461
- await this.fail(entry.id, error);
1462
- continue;
1463
- }
1464
- await this.update((latest) => {
1465
- const item = latest.mutations.find((item) => item.id === entry.id);
1466
- if (item && item.status !== "confirmed") {
1467
- item.status = "queued";
1468
- item.nextAttemptAt = Date.now() + Math.min(3e4, this.retryDelay * 2 ** Math.min(item.attempts - 1, 10));
1469
- item.error = {
1470
- message: error instanceof Error ? error.message : String(error),
1471
- status
1472
- };
1473
- }
1474
- });
1475
- await this.reconcile(await this.load());
1476
- this.scheduleRetry();
1477
- return;
1478
- }
1652
+ if (!await this.deliver(entry, operation)) return;
1479
1653
  }
1480
1654
  });
1655
+ if ((this.needsCheckpoint || this.flushRequested) && !this.disposed) await this.flush().catch((error) => this.report(error));
1481
1656
  } finally {
1482
1657
  this.draining = false;
1483
1658
  }
1484
1659
  }
1660
+ async queueRetry(id, error, status) {
1661
+ await this.updateJournal((latest) => {
1662
+ const item = latest.mutations.find((item) => item.id === id);
1663
+ if (item && isPendingMutation(item)) replaceMutation(latest, id, () => ({
1664
+ ...item,
1665
+ error: {
1666
+ message: error instanceof Error ? error.message : String(error),
1667
+ status
1668
+ },
1669
+ nextAttemptAt: Date.now() + Math.min(3e4, this.retryDelay * 2 ** Math.min(item.attempts - 1, 10)),
1670
+ status: "queued"
1671
+ }));
1672
+ });
1673
+ await this.reconcile(await this.loadJournal());
1674
+ this.scheduleRetry();
1675
+ }
1485
1676
  async discard(id) {
1486
1677
  await this.ready;
1487
- if (await this.update((data) => {
1678
+ const outcome = await this.updateJournal((data) => {
1488
1679
  const entry = data.mutations.find((entry) => entry.id === id);
1489
1680
  if (entry && entry.attempts > 0 && entry.status !== "failed" && entry.status !== "confirmed") throw new Error("fate: An attempted mutation may have committed remotely and cannot be discarded until its outcome is known.");
1490
- if (entry?.status === "confirmed") return;
1681
+ if (entry?.status === "confirmed") return "confirmed";
1682
+ if (!entry) return "missing";
1491
1683
  data.mutations = data.mutations.filter((entry) => entry.id !== id);
1492
- return true;
1493
- })) {
1684
+ return "discarded";
1685
+ });
1686
+ if (outcome === "missing") {
1687
+ const operation = this.operations.get(id);
1688
+ if (operation) await this.recoverOperation(id, operation);
1689
+ } else if (outcome === "discarded") {
1494
1690
  this.operations.get(id)?.rollback();
1495
- this.operations.delete(id);
1496
- this.waiters.get(id)?.reject(/* @__PURE__ */ new Error("fate: Mutation was discarded."));
1497
- this.waiters.delete(id);
1691
+ this.waiters.get(id)?.reject(mutationDiscardedError());
1692
+ this.forgetOperation(id);
1498
1693
  }
1499
1694
  await this.refresh();
1500
1695
  }
1501
1696
  async clearCache() {
1502
1697
  await this.ready;
1503
- await this.storage.exclusive(`${this.key}:write`, () => this.cache.clear());
1698
+ this.assertActive();
1699
+ await this.storage.exclusive(`${this.key}:write`, async () => {
1700
+ this.assertActive();
1701
+ await this.cache.clear();
1702
+ });
1703
+ this.assertActive();
1504
1704
  await this.cache.initialize();
1705
+ this.clearError();
1505
1706
  }
1506
1707
  dispose() {
1507
1708
  if (this.disposed) return;