@nkzw/fate 1.4.0 → 1.5.1

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 (191) hide show
  1. package/README.md +324 -0
  2. package/docs/api/classes/FateClient.md +51 -199
  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 +5 -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 +6 -1
  23. package/docs/api/interfaces/FateThenable.md +3 -3
  24. package/docs/api/interfaces/Persistence.md +5 -0
  25. package/docs/api/interfaces/PersistenceSession.md +109 -0
  26. package/docs/api/interfaces/Transport.md +46 -7
  27. package/docs/api/server/classes/FateRequestError.md +5 -5
  28. package/docs/api/server/drizzle/functions/createDrizzleFate.md +1 -1
  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 +1 -1
  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 +1 -1
  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 +1 -1
  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 +2 -2
  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 +19 -11
  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 +7 -0
  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 +5 -0
  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/getting-started.md +2 -0
  159. package/docs/guide/persistence.md +321 -0
  160. package/lib/{liveTopics-uVP0LG-Q.mjs → args-CRLIRl_v.mjs} +1 -59
  161. package/lib/{executor-BRudKWDY.d.mts → executor-oPTYqgQV.d.mts} +1 -1
  162. package/lib/{graphqlTransport-BDR1pnUQ.d.mts → graphqlTransport-CGYE4NdD.d.mts} +3 -2
  163. package/lib/{graphqlTransport-DSBVS2Fq.mjs → graphqlTransport-DmheG3AO.mjs} +3 -2
  164. package/lib/graphqlTransport.d.mts +1 -1
  165. package/lib/graphqlTransport.mjs +1 -1
  166. package/lib/hydration-DPZRJgnu.mjs +273 -0
  167. package/lib/idempotency-types-DuqJ7KSd.d.mts +16 -0
  168. package/lib/idempotency.d.mts +26 -0
  169. package/lib/idempotency.mjs +34 -0
  170. package/lib/index.d.mts +5 -5
  171. package/lib/index.mjs +275 -1567
  172. package/lib/{list-Bb1SeABn.d.mts → list-5hrTVoiB.d.mts} +1 -1
  173. package/lib/list.d.mts +1 -1
  174. package/lib/liveTopics-DuFkDZQm.mjs +17 -0
  175. package/lib/persistence.d.mts +40 -0
  176. package/lib/persistence.mjs +1730 -0
  177. package/lib/protocol-DS5sf6sd.mjs +44 -0
  178. package/lib/{record-BTUXkHQU.d.mts → record-Dxjvpsep.d.mts} +8 -4
  179. package/lib/server/drizzle.d.mts +2 -2
  180. package/lib/server/drizzle.mjs +1 -1
  181. package/lib/server/prisma.d.mts +2 -2
  182. package/lib/server/prisma.mjs +1 -6
  183. package/lib/server.d.mts +3 -3
  184. package/lib/server.mjs +19 -7
  185. package/lib/sortObjectKeys-yhuitEQZ.mjs +9 -0
  186. package/lib/{sourceRouter-DSaADXSf.mjs → sourceRouter-51POBlEE.mjs} +9 -6
  187. package/lib/store-CakBBzWG.mjs +1386 -0
  188. package/lib/{transport-Bo1Jgo9R.d.mts → transport-DR0fwleU.d.mts} +607 -412
  189. package/lib/vite.mjs +18 -10
  190. package/package.json +12 -2
  191. package/docs/api/type-aliases/Snapshot.md +0 -7
@@ -0,0 +1,1730 @@
1
+ import { t as isRecord } from "./record-B07VwXd-.mjs";
2
+ import { i as filterConnectionArgs } from "./args-CRLIRl_v.mjs";
3
+ import { a as createNodeRef, n as decodeHydrationValue, o as getNodeRefId, r as encodeHydrationValue, s as isNodeRef, t as decodeClientHydrationState } from "./hydration-DPZRJgnu.mjs";
4
+ import { i as getErrorStatusCode, m as toEntityId, n as getListKey, o as prepareMutation } from "./store-CakBBzWG.mjs";
5
+ import { t as FateRequestError } from "./protocol-DS5sf6sd.mjs";
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)));
11
+ const serializePlan = (plan) => ({
12
+ args: [...plan.args].map(([key, value]) => [key, {
13
+ ...value,
14
+ ignoreKeys: value.ignoreKeys ? [...value.ignoreKeys] : void 0
15
+ }]),
16
+ live: [...plan.live],
17
+ paths: [...plan.paths]
18
+ });
19
+ const deserializePlan = (value) => ({
20
+ args: new Map(value.args.map(([key, entry]) => [key, {
21
+ ...entry,
22
+ ignoreKeys: entry.ignoreKeys ? new Set(entry.ignoreKeys) : void 0
23
+ }])),
24
+ live: new Map(value.live),
25
+ paths: new Set(value.paths)
26
+ });
27
+ const encodeRequests = (requests) => encodeHydrationValue(requests.map((request) => ({
28
+ ...request,
29
+ items: request.items.map((item) => ({
30
+ ...item,
31
+ plan: serializePlan(item.plan),
32
+ ..."refViewNames" in item ? { refViewNames: [...item.refViewNames] } : { nodeRefViewNames: [...item.nodeRefViewNames] }
33
+ }))
34
+ })));
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));
57
+ //#endregion
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
+ };
70
+ const mergeCacheUpdate = (key, before, after) => ({
71
+ fields: after.fields && (!before || before.fields) ? [.../* @__PURE__ */ new Set([...before?.fields ?? [], ...after.fields])] : void 0,
72
+ previousList: before ? before.previousList : after.previousList,
73
+ referencesChanged: before?.referencesChanged || after.referencesChanged,
74
+ value: key.startsWith("r:") && before?.value && after.value ? applyRecordUpdate(before.value, after.value, after.fields ? new Set(after.fields) : void 0) : after.value
75
+ });
76
+ const withStoredSize = (key, value) => {
77
+ const item = {
78
+ bytes: 0,
79
+ value
80
+ };
81
+ for (;;) {
82
+ const bytes = persistenceEntrySize(key, item);
83
+ if (bytes === item.bytes) return item;
84
+ item.bytes = bytes;
85
+ }
86
+ };
87
+ const stamp = (time) => String(time).padStart(16, "0");
88
+ const recordKey = (id) => `r:${id}`;
89
+ const listKey = (id) => `l:${id}`;
90
+ const queryKey = (id) => `q:${id}`;
91
+ const pathsOverlap = (path, selected) => path === selected || path.startsWith(`${selected}.`) || selected.startsWith(`${path}.`);
92
+ const projectRecord = (value, paths) => {
93
+ const fields = /* @__PURE__ */ new Set([
94
+ "id",
95
+ "__typename",
96
+ ...paths.map((path) => path.split(".")[0])
97
+ ]);
98
+ return {
99
+ paths: value.paths.filter((path) => paths.some((selected) => pathsOverlap(path, selected))),
100
+ record: Object.fromEntries(Object.entries(value.record).filter(([field]) => fields.has(field)))
101
+ };
102
+ };
103
+ const applyRecordUpdate = (before, after, fields) => ({
104
+ paths: [.../* @__PURE__ */ new Set([...before.paths, ...after.paths.filter((path) => !fields || fields.has(path.split(".")[0]))])],
105
+ record: {
106
+ ...before.record,
107
+ ...Object.fromEntries(Object.entries(after.record).filter(([key]) => !fields || fields.has(key)))
108
+ }
109
+ });
110
+ const mergeIds = (before, after, latest) => {
111
+ const old = new Set(before);
112
+ const next = new Set(after);
113
+ let retained = latest.filter((id) => !old.has(id) || next.has(id));
114
+ const present = new Set(retained);
115
+ const oldOrder = before.filter((id) => next.has(id));
116
+ const nextOrder = after.filter((id) => old.has(id));
117
+ if (oldOrder.some((id, index) => id !== nextOrder[index])) {
118
+ let index = 0;
119
+ const reordered = nextOrder.filter((id) => present.has(id));
120
+ retained = retained.map((id) => old.has(id) ? reordered[index++] : id);
121
+ }
122
+ const insertions = /* @__PURE__ */ new Map();
123
+ let anchor;
124
+ for (let index = after.length - 1; index >= 0; index--) {
125
+ const id = after[index];
126
+ if (present.has(id)) anchor = id;
127
+ else if (!old.has(id)) {
128
+ const group = insertions.get(anchor) ?? [];
129
+ group.push(id);
130
+ insertions.set(anchor, group);
131
+ present.add(id);
132
+ }
133
+ }
134
+ return [...retained.flatMap((id) => [...insertions.get(id)?.reverse() ?? [], id]), ...insertions.get(void 0)?.reverse() ?? []];
135
+ };
136
+ const membershipFields = [
137
+ "ids",
138
+ "liveAfterIds",
139
+ "liveBeforeIds",
140
+ "pendingAfterIds",
141
+ "pendingBeforeIds"
142
+ ];
143
+ const applyListUpdate = (latest, before, after) => {
144
+ const previousMembers = new Set(membershipFields.flatMap((field) => before[field] ?? []));
145
+ const latestMembers = new Set(membershipFields.flatMap((field) => latest[field] ?? []));
146
+ const surviving = (ids) => ids.filter((id) => !previousMembers.has(id) || latestMembers.has(id));
147
+ const ids = mergeIds(before.ids, surviving(after.ids), latest.ids);
148
+ const oldCursors = new Map(before.ids.map((id, index) => [id, before.cursors?.[index]]));
149
+ const nextCursors = new Map(after.ids.map((id, index) => [id, after.cursors?.[index]]));
150
+ const cursors = new Map(latest.ids.map((id, index) => [id, latest.cursors?.[index]]));
151
+ for (const [id, cursor] of nextCursors) if (!oldCursors.has(id) || oldCursors.get(id) !== cursor) cursors.set(id, cursor);
152
+ const result = {
153
+ ...latest,
154
+ ids
155
+ };
156
+ if (latest.cursors || after.cursors) result.cursors = ids.map((id) => cursors.get(id));
157
+ for (const field of [
158
+ "liveAfterIds",
159
+ "liveBeforeIds",
160
+ "pendingAfterIds",
161
+ "pendingBeforeIds"
162
+ ]) if (before[field] || after[field] || latest[field]) result[field] = mergeIds(before[field] ?? [], surviving(after[field] ?? []), latest[field] ?? []);
163
+ for (const field of ["backwardPageLimit", "forwardPageLimit"]) if (before[field] !== after[field]) result[field] = after[field];
164
+ if (after.pagination) result.pagination = {
165
+ ...latest.pagination ?? after.pagination,
166
+ ...Object.fromEntries(Object.entries(after.pagination).filter(([key, value]) => value !== before.pagination?.[key]))
167
+ };
168
+ return result;
169
+ };
170
+ /** Independently addressable normalized disk data. No in-memory retention roots. */
171
+ var PersistenceCache = class {
172
+ constructor(client, storage, key, maxAge, maxBytes) {
173
+ this.client = client;
174
+ this.storage = storage;
175
+ this.key = key;
176
+ this.maxAge = maxAge;
177
+ this.maxBytes = maxBytes;
178
+ this.pendingNodes = createNodeChanges();
179
+ this.flushingNodes = createNodeChanges();
180
+ this.recoveryUpdates = /* @__PURE__ */ new Map();
181
+ this.pendingRequests = /* @__PURE__ */ new Map();
182
+ this.suppressed = false;
183
+ this.disposed = false;
184
+ this.work = 0;
185
+ this.journalBytes = 0;
186
+ this.policies = /* @__PURE__ */ new WeakMap();
187
+ this.prefix = `${JSON.stringify([key, "cache-v2"])}:`;
188
+ }
189
+ async initialize() {
190
+ await this.storage.exclusive(`${this.key}:write`, async () => {
191
+ this.assertActive();
192
+ if (await this.read(`${this.prefix}scope`) !== this.client.getPersistenceScope()) {
193
+ await this.clear();
194
+ if (withStoredSize(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes + this.overhead <= this.maxBytes) await this.put([[`${this.prefix}scope`, this.client.getPersistenceScope()]]);
195
+ }
196
+ });
197
+ }
198
+ assertActive() {
199
+ if (this.disposed) throw new Error("fate: Persistence was disposed.");
200
+ }
201
+ async active() {
202
+ this.assertActive();
203
+ const scope = await this.read(`${this.prefix}scope`);
204
+ this.assertActive();
205
+ return scope === this.client.getPersistenceScope();
206
+ }
207
+ restoreBase(state) {
208
+ this.assertActive();
209
+ this.suppressed = true;
210
+ try {
211
+ this.client.restorePersistenceData(state);
212
+ } finally {
213
+ this.suppressed = false;
214
+ }
215
+ }
216
+ /** Save only recovery dependencies, never an unrelated read-cache snapshot. */
217
+ async mutationBase(command, base) {
218
+ const previous = base ? decodeClientHydrationState(base.data).store : void 0;
219
+ const previousRecords = new Map(previous?.records);
220
+ const previousCoverage = new Map(previous?.coverage);
221
+ const previousLists = new Map(previous?.lists);
222
+ const ids = new Set(previousRecords.keys());
223
+ const config = this.client.getTypeConfig(command.entity);
224
+ for (const input of [command.input, command.optimistic]) if (input) try {
225
+ const id = config.getId(input);
226
+ if (id != null) ids.add(toEntityId(command.entity, id));
227
+ } catch {}
228
+ const lists = /* @__PURE__ */ new Set([...previousLists.keys(), ...command.insert !== "none" ? this.client.getPersistenceRootLists(command.entity) : []]);
229
+ for (const id of ids) {
230
+ const owners = this.client.store.getPersistenceOwners(id);
231
+ for (const owner of owners.records) ids.add(owner);
232
+ for (const key of owners.lists) lists.add(key);
233
+ }
234
+ const records = [];
235
+ const coverage = [];
236
+ const visit = (value) => {
237
+ if (isNodeRef(value)) ids.add(getNodeRefId(value));
238
+ else if (Array.isArray(value)) for (const item of value) visit(item);
239
+ else if (value && typeof value === "object") for (const item of Object.values(value)) visit(item);
240
+ };
241
+ for (const id of ids) {
242
+ const key = recordKey(id);
243
+ const saved = previousRecords.get(id);
244
+ const before = saved ? {
245
+ paths: [...previousCoverage.get(id) ?? []],
246
+ record: saved
247
+ } : void 0;
248
+ const memory = this.client.store.readConfirmed(id);
249
+ const disk = memory || before ? void 0 : await this.node(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)
254
+ }) : recovery;
255
+ const incoming = update?.value;
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);
257
+ if (value) {
258
+ records.push([id, value.record]);
259
+ coverage.push([id, value.paths]);
260
+ visit(value.record);
261
+ for (const [field, entry] of Object.entries(value.record)) if (Array.isArray(entry)) for (const [key] of this.client.store.getListsForField(id, field)) lists.add(key);
262
+ }
263
+ await this.step();
264
+ }
265
+ const listValues = [];
266
+ for (const key of lists) {
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);
268
+ if (value) listValues.push([key, value]);
269
+ await this.step();
270
+ }
271
+ return {
272
+ data: encodeHydrationValue({
273
+ rootLists: [[command.entity, [...this.client.getPersistenceRootLists(command.entity)]]],
274
+ rootRequests: [],
275
+ store: {
276
+ coverage,
277
+ lists: listValues,
278
+ records
279
+ }
280
+ }),
281
+ scope: this.client.getPersistenceScope(),
282
+ version: 1
283
+ };
284
+ }
285
+ get overhead() {
286
+ return persistenceEntrySize(`${this.prefix}bytes`, this.maxBytes);
287
+ }
288
+ async step() {
289
+ this.assertActive();
290
+ if (++this.work % 64 === 0) {
291
+ await yieldPersistenceTask();
292
+ this.assertActive();
293
+ }
294
+ }
295
+ nodeKey(key) {
296
+ return `${this.prefix}node:${key}`;
297
+ }
298
+ rootKey(key) {
299
+ return `${this.prefix}root:${key}`;
300
+ }
301
+ async read(key) {
302
+ return (await this.readStored(key))?.value;
303
+ }
304
+ async readStored(key) {
305
+ return decodeStoredValue(await this.storage.read(key));
306
+ }
307
+ async put(entries) {
308
+ let bytes = await this.storage.read(`${this.prefix}bytes`) ?? 0;
309
+ const writes = [];
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;
315
+ if (value === void 0) writes.push([key, void 0]);
316
+ else {
317
+ const item = withStoredSize(key, value);
318
+ bytes += item.bytes;
319
+ writes.push([key, item]);
320
+ }
321
+ await this.step();
322
+ }
323
+ writes.push([`${this.prefix}bytes`, Math.max(0, bytes)]);
324
+ this.assertActive();
325
+ await this.storage.writeBatch(writes);
326
+ }
327
+ get hasChanges() {
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);
338
+ }
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);
343
+ if (this.capturing) {
344
+ const previous = this.capturing.get(id);
345
+ this.capturing.set(id, {
346
+ fields: selected?.size && (!previous || previous.fields) ? [.../* @__PURE__ */ new Set([...previous?.fields ?? [], ...selected])] : void 0,
347
+ previousList: previous ? previous.previousList : change.kind === "list" ? change.previousList : void 0,
348
+ referencesChanged: previous?.referencesChanged || change.kind === "record" && change.referencesChanged,
349
+ value
350
+ });
351
+ }
352
+ return true;
353
+ }
354
+ return false;
355
+ }
356
+ /** A recovered receipt has already been checkpointed by another tab. */
357
+ withoutChanges(commit) {
358
+ this.suppressed = true;
359
+ try {
360
+ commit();
361
+ } finally {
362
+ this.suppressed = false;
363
+ }
364
+ }
365
+ /** Capture only the confirmed writes made by this mutation for crash recovery. */
366
+ captureChanges(commit) {
367
+ const changes = /* @__PURE__ */ new Map();
368
+ this.capturing = changes;
369
+ try {
370
+ commit();
371
+ } finally {
372
+ this.capturing = void 0;
373
+ }
374
+ for (const [key, update] of changes) {
375
+ changes.set(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)
380
+ });
381
+ this.pendingNodes.values.delete(key);
382
+ this.pendingNodes.recordFields.delete(key);
383
+ this.pendingNodes.referenceChanges.delete(key);
384
+ this.pendingNodes.listBases.delete(key);
385
+ }
386
+ return encodeHydrationValue([...changes]);
387
+ }
388
+ replayChanges(updates) {
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
+ }
397
+ }
398
+ fetched(request, maxAge = this.maxAge, fromNetwork = true) {
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.");
400
+ if (!fromNetwork && this.policies.get(request) === maxAge) return;
401
+ this.policies.set(request, maxAge);
402
+ const previous = this.pendingRequests.get(request.key);
403
+ const retain = this.client.retainPersistenceRequest(request);
404
+ previous?.release();
405
+ this.pendingRequests.set(request.key, {
406
+ fetchedAt: fromNetwork ? Date.now() : previous?.fetchedAt,
407
+ maxAge,
408
+ release: retain.dispose,
409
+ request
410
+ });
411
+ }
412
+ async node(key) {
413
+ const node = await this.read(this.nodeKey(key));
414
+ if (!node) return;
415
+ node.claims = Object.fromEntries(Object.entries(node.claims).filter(([, claim]) => claim.expiresAt > Date.now()));
416
+ if (!Object.keys(node.claims).length) return;
417
+ if (key.startsWith("r:")) {
418
+ const paths = Object.values(node.claims).flatMap((claim) => claim.paths);
419
+ node.value = encodeHydrationValue(projectRecord(decodeHydrationValue(node.value), paths));
420
+ }
421
+ return node;
422
+ }
423
+ /** Walk only the requested selection, yielding between small pieces of work. */
424
+ async collect(request, source) {
425
+ const readsMemory = source !== "disk";
426
+ const prefersNetworkResult = source === "network";
427
+ const nodes = /* @__PURE__ */ new Map();
428
+ const visited = /* @__PURE__ */ new Map();
429
+ let complete = true;
430
+ let fetchedAt = Infinity;
431
+ const queue = [];
432
+ const readNode = async (key) => {
433
+ await this.step();
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:")) {
439
+ const stored = await this.node(key);
440
+ if (stored) return applyListUpdate(decodeHydrationValue(stored.value), base, incoming);
441
+ }
442
+ if (incoming && key.startsWith("r:") && this.flushingNodes.recordFields.get(key)) {
443
+ const stored = await this.node(key);
444
+ if (stored) return applyRecordUpdate(decodeHydrationValue(stored.value), incoming, this.flushingNodes.recordFields.get(key));
445
+ }
446
+ return incoming;
447
+ }
448
+ if (prefersNetworkResult) {
449
+ const id = key.slice(2);
450
+ const value = key.startsWith("r:") ? this.client.store.readConfirmed(id) : key.startsWith("l:") ? this.client.store.readConfirmedList(id) : this.client.getPersistenceQuery(id);
451
+ if (value !== void 0) return value;
452
+ if (this.pendingNodes.values.has(key)) return;
453
+ }
454
+ }
455
+ const stored = await this.node(key);
456
+ if (stored) for (const claim of Object.values(stored.claims)) fetchedAt = Math.min(fetchedAt, claim.fetchedAt);
457
+ if (!stored) return readsMemory && key.startsWith("r:") ? this.client.store.readConfirmed(key.slice(2)) : void 0;
458
+ const value = decodeHydrationValue(stored.value);
459
+ if (!readsMemory) return value;
460
+ const deleted = (id) => this.flushingNodes.values.has(recordKey(id)) && this.flushingNodes.values.get(recordKey(id)) === void 0;
461
+ if (key.startsWith("l:")) {
462
+ const list = value;
463
+ return {
464
+ ...list,
465
+ cursors: list.cursors?.filter((_, index) => !deleted(list.ids[index])),
466
+ ids: list.ids.filter((id) => !deleted(id)),
467
+ liveAfterIds: list.liveAfterIds?.filter((id) => !deleted(id)),
468
+ liveBeforeIds: list.liveBeforeIds?.filter((id) => !deleted(id)),
469
+ pendingAfterIds: list.pendingAfterIds?.filter((id) => !deleted(id)),
470
+ pendingBeforeIds: list.pendingBeforeIds?.filter((id) => !deleted(id))
471
+ };
472
+ }
473
+ if (key.startsWith("r:")) {
474
+ const record = value;
475
+ return {
476
+ ...record,
477
+ record: Object.fromEntries(Object.entries(record.record).map(([field, value]) => [field, isNodeRef(value) && deleted(getNodeRefId(value)) ? null : Array.isArray(value) ? value.filter((item) => !isNodeRef(item) || !deleted(getNodeRefId(item))) : value]))
478
+ };
479
+ }
480
+ return typeof value === "string" && deleted(value) ? null : value;
481
+ };
482
+ const addList = async (key, paths, plan, prefix) => {
483
+ const list = await readNode(listKey(key));
484
+ if (!list) {
485
+ complete = false;
486
+ return;
487
+ }
488
+ nodes.set(listKey(key), {
489
+ paths: /* @__PURE__ */ new Set(),
490
+ value: list
491
+ });
492
+ for (const id of [
493
+ ...list.ids,
494
+ ...list.pendingBeforeIds ?? [],
495
+ ...list.pendingAfterIds ?? [],
496
+ ...list.liveBeforeIds ?? [],
497
+ ...list.liveAfterIds ?? []
498
+ ]) queue.push({
499
+ id,
500
+ paths,
501
+ plan,
502
+ prefix
503
+ });
504
+ };
505
+ for (const item of request.items) {
506
+ const paths = [...item.plan.paths];
507
+ if (item.kind === "node" || item.kind === "nodes") for (const id of item.ids) queue.push({
508
+ id: toEntityId(item.type, id),
509
+ paths,
510
+ plan: item.plan,
511
+ prefix: ""
512
+ });
513
+ else if (item.kind === "query") {
514
+ const id = await readNode(queryKey(item.queryKey));
515
+ if (id === void 0) {
516
+ complete = false;
517
+ continue;
518
+ }
519
+ nodes.set(queryKey(item.queryKey), {
520
+ paths: /* @__PURE__ */ new Set(),
521
+ value: id
522
+ });
523
+ if (typeof id === "string") queue.push({
524
+ id,
525
+ paths,
526
+ plan: item.plan,
527
+ prefix: ""
528
+ });
529
+ } else if (item.kind === "list") await addList(item.listKey, paths, item.plan, "");
530
+ }
531
+ for (let index = 0; index < queue.length; index++) {
532
+ const { id, paths, plan, prefix } = queue[index];
533
+ const key = recordKey(id);
534
+ const seen = nodes.get(key);
535
+ const contexts = visited.get(plan) ?? /* @__PURE__ */ new Map();
536
+ visited.set(plan, contexts);
537
+ const context = JSON.stringify([id, prefix]);
538
+ const traversed = contexts.get(context);
539
+ const needed = paths.filter((path) => !traversed?.has(path));
540
+ if (traversed && !needed.length) continue;
541
+ contexts.set(context, /* @__PURE__ */ new Set([...traversed ?? [], ...paths]));
542
+ const record = await readNode(key);
543
+ if (!record) {
544
+ complete = false;
545
+ continue;
546
+ }
547
+ if (paths.some((path) => !record.paths.some((covered) => path === covered || path.startsWith(`${covered}.`)))) complete = false;
548
+ const all = /* @__PURE__ */ new Set([...seen?.paths ?? [], ...paths]);
549
+ nodes.set(key, {
550
+ paths: all,
551
+ value: projectRecord(record, [...all])
552
+ });
553
+ const groups = /* @__PURE__ */ new Map();
554
+ for (const path of needed) {
555
+ const [field, ...rest] = path.split(".");
556
+ const children = groups.get(field) ?? [];
557
+ if (rest.length) children.push(rest.join("."));
558
+ groups.set(field, children);
559
+ }
560
+ const walk = (value, children, childPrefix) => {
561
+ if (isNodeRef(value)) queue.push({
562
+ id: getNodeRefId(value),
563
+ paths: children,
564
+ plan,
565
+ prefix: childPrefix
566
+ });
567
+ else if (Array.isArray(value)) for (const item of value) walk(item, children, childPrefix);
568
+ else if (value && typeof value === "object" && children.length) for (const child of children) {
569
+ const [field, ...rest] = child.split(".");
570
+ walk(value[field], rest.length ? [rest.join(".")] : [], `${childPrefix}.${field}`);
571
+ }
572
+ };
573
+ for (const [field, children] of groups) {
574
+ const childPrefix = prefix ? `${prefix}.${field}` : field;
575
+ const value = record.record[field];
576
+ if (Array.isArray(value)) {
577
+ const nestedKey = getListKey(id, field, plan.args.get(childPrefix)?.hash);
578
+ const list = await readNode(listKey(nestedKey));
579
+ if (list) {
580
+ const projected = nodes.get(key).value;
581
+ projected.record[field] = list.ids.map(createNodeRef);
582
+ await addList(nestedKey, children, plan, childPrefix);
583
+ } else walk(value, children, childPrefix);
584
+ } else walk(value, children, childPrefix);
585
+ }
586
+ }
587
+ return {
588
+ complete,
589
+ fetchedAt,
590
+ nodes
591
+ };
592
+ }
593
+ async restore(request) {
594
+ await this.storage.exclusive(`${this.key}:write`, async () => {
595
+ if (!await this.active()) return;
596
+ const { complete, nodes } = await this.collect(request, "disk");
597
+ if (!complete) return;
598
+ for (const [key, node] of nodes) {
599
+ this.assertActive();
600
+ const id = key.slice(2);
601
+ const state = {
602
+ rootLists: [],
603
+ rootRequests: [],
604
+ store: {
605
+ coverage: [],
606
+ lists: [],
607
+ records: []
608
+ }
609
+ };
610
+ if (key.startsWith("r:")) {
611
+ const value = node.value;
612
+ state.store.records.push([id, value.record]);
613
+ state.store.coverage.push([id, value.paths]);
614
+ } else if (key.startsWith("l:")) {
615
+ state.store.lists.push([id, node.value]);
616
+ for (const item of request.items) if (item.kind === "list" && item.listKey === id && !filterConnectionArgs(item.argsPayload)) state.rootLists.push([item.type, [id]]);
617
+ } else state.rootRequests.push([id, node.value]);
618
+ this.suppressed = true;
619
+ try {
620
+ this.client.restorePersistenceData(state);
621
+ } finally {
622
+ this.suppressed = false;
623
+ }
624
+ await this.step();
625
+ }
626
+ const root = await this.read(this.rootKey(request.key));
627
+ if (root && root.fetchedAt + root.maxAge > Date.now()) {
628
+ const old = `${this.prefix}lru:${stamp(root.usedAt)}:${request.key}`;
629
+ root.usedAt = Date.now();
630
+ await this.put([
631
+ [old, void 0],
632
+ [this.rootKey(request.key), root],
633
+ [`${this.prefix}lru:${stamp(root.usedAt)}:${request.key}`, request.key]
634
+ ]);
635
+ }
636
+ });
637
+ }
638
+ async remove(rootId) {
639
+ const root = await this.read(this.rootKey(rootId));
640
+ if (!root) return;
641
+ let writes = [];
642
+ for (const key of root.nodes) {
643
+ const storageKey = this.nodeKey(key);
644
+ const stored = await this.readStored(storageKey);
645
+ const node = stored?.value;
646
+ if (node) {
647
+ delete node.claims[rootId];
648
+ const live = Object.values(node.claims).filter((claim) => claim.expiresAt > Date.now());
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
+ }
659
+ }
660
+ await this.step();
661
+ }
662
+ if (writes.length) await this.put(writes);
663
+ await this.put([
664
+ [this.rootKey(rootId), void 0],
665
+ [`${this.prefix}lru:${stamp(root.usedAt)}:${rootId}`, void 0],
666
+ [`${this.prefix}expiry:${stamp(root.fetchedAt + root.maxAge)}:${rootId}`, void 0]
667
+ ]);
668
+ }
669
+ async save(request, fetchedAt, maxAge, usedAt = Date.now(), fromNetwork = false) {
670
+ if (!maxAge || fetchedAt + maxAge <= Date.now()) {
671
+ await this.remove(request.key);
672
+ return;
673
+ }
674
+ const { nodes } = await this.collect(request, fromNetwork ? "network" : "memory");
675
+ const root = {
676
+ fetchedAt,
677
+ maxAge,
678
+ nodes: [...nodes.keys()],
679
+ request: encodeRequests([request]),
680
+ usedAt
681
+ };
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;
683
+ for (const [key, node] of nodes) {
684
+ minimum += withStoredSize(this.nodeKey(key), {
685
+ claims: { [request.key]: {
686
+ expiresAt: fetchedAt + maxAge,
687
+ fetchedAt,
688
+ paths: [...node.paths]
689
+ } },
690
+ value: encodeHydrationValue(node.value)
691
+ }).bytes;
692
+ await this.step();
693
+ }
694
+ if (minimum + this.journalBytes + this.overhead > this.maxBytes) {
695
+ await this.remove(request.key);
696
+ return;
697
+ }
698
+ const prepare = async () => {
699
+ const old = await this.read(this.rootKey(request.key));
700
+ const writes = /* @__PURE__ */ new Map();
701
+ if (old) {
702
+ writes.set(`${this.prefix}lru:${stamp(old.usedAt)}:${request.key}`, void 0);
703
+ writes.set(`${this.prefix}expiry:${stamp(old.fetchedAt + old.maxAge)}:${request.key}`, void 0);
704
+ }
705
+ writes.set(this.rootKey(request.key), {
706
+ ...root,
707
+ nodes: [.../* @__PURE__ */ new Set([...root.nodes, ...old?.nodes ?? []])]
708
+ });
709
+ writes.set(`${this.prefix}lru:${stamp(root.usedAt)}:${request.key}`, request.key);
710
+ writes.set(`${this.prefix}expiry:${stamp(fetchedAt + maxAge)}:${request.key}`, request.key);
711
+ for (const key of /* @__PURE__ */ new Set([...nodes.keys(), ...old?.nodes ?? []])) {
712
+ const previous = await this.node(key);
713
+ const next = nodes.get(key);
714
+ const claims = previous?.claims ?? {};
715
+ if (!next) delete claims[request.key];
716
+ else claims[request.key] = {
717
+ expiresAt: fetchedAt + maxAge,
718
+ fetchedAt,
719
+ paths: [...next.paths]
720
+ };
721
+ let value = next?.value;
722
+ if (previous && key.startsWith("r:")) {
723
+ const before = decodeHydrationValue(previous.value);
724
+ const after = next?.value;
725
+ value = projectRecord({
726
+ paths: [.../* @__PURE__ */ new Set([...before.paths, ...after?.paths ?? []])],
727
+ record: {
728
+ ...before.record,
729
+ ...after?.record
730
+ }
731
+ }, Object.values(claims).flatMap((claim) => claim.paths));
732
+ }
733
+ writes.set(this.nodeKey(key), Object.keys(claims).length ? {
734
+ claims,
735
+ value: value !== void 0 ? encodeHydrationValue(value) : previous.value
736
+ } : void 0);
737
+ await this.step();
738
+ }
739
+ return writes;
740
+ };
741
+ let writes = await prepare();
742
+ for (;;) {
743
+ let added = 0;
744
+ for (const [key, value] of writes) {
745
+ const before = await this.readStored(key);
746
+ added += Math.max(0, (value === void 0 ? 0 : withStoredSize(key, value).bytes) - (before?.bytes ?? 0));
747
+ await this.step();
748
+ }
749
+ if ((await this.storage.read(`${this.prefix}bytes`) ?? 0) + added + this.journalBytes + this.overhead <= this.maxBytes) break;
750
+ const candidate = (await this.storage.scan(`${this.prefix}lru:`, void 0, 2)).find((entry) => decodeStoredValue(entry.value)?.value !== request.key);
751
+ if (!candidate) {
752
+ await this.remove(request.key);
753
+ return;
754
+ }
755
+ const id = decodeStoredValue(candidate.value).value;
756
+ if (await this.read(this.rootKey(id))) await this.remove(id);
757
+ else await this.put([[candidate.key, void 0]]);
758
+ writes = await prepare();
759
+ }
760
+ const entries = [...writes];
761
+ for (let index = 0; index < entries.length; index += 64) {
762
+ await this.put(entries.slice(index, index + 64));
763
+ await yieldPersistenceTask();
764
+ }
765
+ await this.put([[this.rootKey(request.key), root]]);
766
+ }
767
+ /** Called under the shared write lock. Expiry/LRU indexes avoid whole-cache scans. */
768
+ async prune(journalBytes) {
769
+ this.journalBytes = journalBytes;
770
+ while (true) {
771
+ const [entry] = await this.storage.scan(`${this.prefix}expiry:`, void 0, 1);
772
+ if (!entry) break;
773
+ const id = decodeStoredValue(entry.value).value;
774
+ const root = await this.read(this.rootKey(id));
775
+ if (root && root.fetchedAt + root.maxAge > Date.now()) break;
776
+ if (root) await this.remove(id);
777
+ else await this.put([[entry.key, void 0]]);
778
+ await this.step();
779
+ }
780
+ while ((await this.storage.read(`${this.prefix}bytes`) ?? 0) + journalBytes + this.overhead > this.maxBytes) {
781
+ const [entry] = await this.storage.scan(`${this.prefix}lru:`, void 0, 1);
782
+ if (!entry) break;
783
+ const id = decodeStoredValue(entry.value).value;
784
+ if (await this.read(this.rootKey(id))) await this.remove(id);
785
+ else await this.put([[entry.key, void 0]]);
786
+ await this.step();
787
+ }
788
+ }
789
+ async fits(journalBytes) {
790
+ return (await this.storage.read(`${this.prefix}bytes`) ?? 0) + journalBytes + this.overhead <= this.maxBytes;
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
+ }
799
+ async flush(journalBytes) {
800
+ if (!await this.active()) {
801
+ this.reset();
802
+ return;
803
+ }
804
+ await this.prune(journalBytes);
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();
812
+ const updates = new Map(recovery);
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,
815
+ previousList: listBases.get(key),
816
+ referencesChanged: referenceChanges.has(key),
817
+ value
818
+ }));
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
+ };
825
+ try {
826
+ const affected = /* @__PURE__ */ new Map();
827
+ for (const key of this.flushingNodes.values.keys()) {
828
+ const node = await this.node(key);
829
+ const incoming = this.flushingNodes.values.get(key);
830
+ if (node && incoming && key.startsWith("r:") && !this.flushingNodes.referenceChanges.has(key)) {
831
+ const before = decodeHydrationValue(node.value);
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
838
+ }
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]]);
852
+ }
853
+ this.journalBytes = journalBytes;
854
+ continue;
855
+ }
856
+ for (const rootId of Object.keys(node?.claims ?? {})) {
857
+ if (pending.has(rootId) || affected.has(rootId)) continue;
858
+ const root = await this.read(this.rootKey(rootId));
859
+ if (root) affected.set(rootId, root);
860
+ }
861
+ await this.step();
862
+ }
863
+ for (const root of affected.values()) await this.save(decodeRequests(root.request)[0], root.fetchedAt, root.maxAge, root.usedAt);
864
+ for (const { fetchedAt, maxAge, request } of pending.values()) if (fetchedAt !== void 0) await this.save(request, fetchedAt, maxAge, Date.now(), true);
865
+ for (const { fetchedAt, maxAge, request } of pending.values()) {
866
+ if (fetchedAt !== void 0) continue;
867
+ if (!maxAge) {
868
+ await this.remove(request.key);
869
+ continue;
870
+ }
871
+ const cached = await this.collect(request, "disk");
872
+ if (cached.complete && Number.isFinite(cached.fetchedAt)) {
873
+ const root = await this.read(this.rootKey(request.key));
874
+ await this.save(request, root?.fetchedAt ?? cached.fetchedAt, maxAge);
875
+ }
876
+ }
877
+ this.flushingNodes.values.clear();
878
+ await this.prune(journalBytes);
879
+ } catch (error) {
880
+ if (!this.disposed) {
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));
888
+ } else if (key.startsWith("r:")) {
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);
892
+ }
893
+ }
894
+ for (const [key, entry] of pending) if (!this.pendingRequests.has(key)) {
895
+ this.pendingRequests.set(key, entry);
896
+ pending.delete(key);
897
+ }
898
+ }
899
+ throw error;
900
+ } finally {
901
+ this.flushingNodes = createNodeChanges();
902
+ for (const entry of pending.values()) entry.release();
903
+ }
904
+ }
905
+ async clear() {
906
+ this.assertActive();
907
+ this.reset();
908
+ while (true) {
909
+ const entries = await this.storage.scan(this.prefix, void 0, 64);
910
+ this.assertActive();
911
+ if (!entries.length) return;
912
+ await this.storage.writeBatch(entries.map(({ key }) => [key, void 0]));
913
+ await yieldPersistenceTask();
914
+ this.assertActive();
915
+ }
916
+ }
917
+ dispose() {
918
+ this.disposed = true;
919
+ this.reset();
920
+ }
921
+ reset() {
922
+ this.recoveryUpdates.clear();
923
+ this.policies = /* @__PURE__ */ new WeakMap();
924
+ for (const entry of this.pendingRequests.values()) entry.release();
925
+ this.pendingRequests.clear();
926
+ this.pendingNodes = createNodeChanges();
927
+ this.flushingNodes = createNodeChanges();
928
+ }
929
+ };
930
+ //#endregion
931
+ //#region src/persistence-journal.ts
932
+ const invalid = () => /* @__PURE__ */ new Error("fate: Unsupported or corrupt persistence data. It has not been overwritten.");
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) => {
942
+ return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => left[key] === right[key]);
943
+ };
944
+ /** All methods run under the account's write lock, including reads across scan pages. */
945
+ var PersistenceJournal = class {
946
+ constructor(storage, key, validate) {
947
+ this.storage = storage;
948
+ this.key = key;
949
+ this.validate = validate;
950
+ this.disposed = false;
951
+ this.entries = /* @__PURE__ */ new Map();
952
+ this.prefix = `${JSON.stringify([key, "journal"])}:`;
953
+ }
954
+ dispose() {
955
+ this.disposed = true;
956
+ this.entries.clear();
957
+ }
958
+ assertActive() {
959
+ if (this.disposed) throw new Error("fate: Persistence was disposed.");
960
+ }
961
+ async load() {
962
+ this.assertActive();
963
+ const value = await this.storage.read(this.key);
964
+ this.assertActive();
965
+ if (value == null) {
966
+ this.header = void 0;
967
+ this.entries.clear();
968
+ } else if (value.version === 1) {
969
+ if (!Array.isArray(value.mutations) || value.mutations.some((entry) => !valid(entry)) || new Set(value.mutations.map(({ id }) => id)).size !== value.mutations.length) throw invalid();
970
+ let checked = 0;
971
+ for (const entry of value.mutations) {
972
+ this.validate?.(entry);
973
+ if (++checked % 64 === 0) {
974
+ await yieldPersistenceTask();
975
+ this.assertActive();
976
+ }
977
+ }
978
+ this.header = void 0;
979
+ this.entries.clear();
980
+ await this.save({ mutations: value.mutations });
981
+ } else if (value.version !== 2 || typeof value.revision !== "string" || !Number.isSafeInteger(value.nextSequence) || value.nextSequence < 0) throw invalid();
982
+ else if (this.header?.revision !== value.revision) {
983
+ const entries = /* @__PURE__ */ new Map();
984
+ let after;
985
+ while (true) {
986
+ const page = await this.storage.scan(this.prefix, after, 64);
987
+ this.assertActive();
988
+ for (const { key, value } of page) {
989
+ const stored = value;
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);
992
+ entries.set(stored.entry.id, {
993
+ ...stored,
994
+ key
995
+ });
996
+ }
997
+ if (page.length < 64) break;
998
+ after = page.at(-1).key;
999
+ await yieldPersistenceTask();
1000
+ }
1001
+ this.assertActive();
1002
+ this.header = value;
1003
+ this.entries = entries;
1004
+ }
1005
+ return { mutations: [...this.entries.values()].map(({ entry }) => ({ ...entry })) };
1006
+ }
1007
+ async prepare(data) {
1008
+ this.assertActive();
1009
+ const header = {
1010
+ nextSequence: this.header?.nextSequence ?? 0,
1011
+ revision: crypto.randomUUID(),
1012
+ version: 2
1013
+ };
1014
+ const entries = /* @__PURE__ */ new Map();
1015
+ const writes = [];
1016
+ let bytes = 0;
1017
+ let count = 0;
1018
+ for (const entry of data.mutations) {
1019
+ const previous = this.entries.get(entry.id);
1020
+ let stored = previous;
1021
+ if (!previous || !hasSameFields(previous.entry, entry)) {
1022
+ const key = previous?.key ?? `${this.prefix}${String(header.nextSequence++).padStart(16, "0")}:${entry.id}`;
1023
+ const value = {
1024
+ bytes: 0,
1025
+ entry
1026
+ };
1027
+ let measured = persistenceEntrySize(key, value);
1028
+ while (measured !== value.bytes) {
1029
+ value.bytes = measured;
1030
+ measured = persistenceEntrySize(key, value);
1031
+ }
1032
+ stored = {
1033
+ ...value,
1034
+ entry: { ...entry },
1035
+ key
1036
+ };
1037
+ writes.push([key, value]);
1038
+ }
1039
+ entries.set(entry.id, stored);
1040
+ bytes += stored.bytes;
1041
+ if (++count % 64 === 0) {
1042
+ await yieldPersistenceTask();
1043
+ this.assertActive();
1044
+ }
1045
+ }
1046
+ for (const [id, { key }] of this.entries) if (!entries.has(id)) writes.push([key, void 0]);
1047
+ bytes += persistenceEntrySize(this.key, header);
1048
+ return {
1049
+ bytes,
1050
+ entries,
1051
+ header,
1052
+ writes
1053
+ };
1054
+ }
1055
+ async commit(plan) {
1056
+ this.assertActive();
1057
+ if (plan.writes.length || !this.header) {
1058
+ await this.storage.writeBatch([...plan.writes, [this.key, plan.header]]);
1059
+ this.assertActive();
1060
+ this.header = plan.header;
1061
+ this.entries = plan.entries;
1062
+ }
1063
+ }
1064
+ async save(data) {
1065
+ await this.commit(await this.prepare(data));
1066
+ }
1067
+ };
1068
+ //#endregion
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.");
1082
+ const encodeCommand = (command) => encodeHydrationValue({
1083
+ ...command,
1084
+ plan: command.plan ? serializePlan(command.plan) : void 0
1085
+ });
1086
+ const decodeCommand = (value) => {
1087
+ const command = decodeHydrationValue(value);
1088
+ if (!command || typeof command.key !== "string" || typeof command.entity !== "string" || ![
1089
+ "after",
1090
+ "before",
1091
+ "none"
1092
+ ].includes(command.insert)) throw new Error("fate: Invalid persisted mutation.");
1093
+ return {
1094
+ ...command,
1095
+ plan: command.plan ? deserializePlan(command.plan) : void 0
1096
+ };
1097
+ };
1098
+ /**
1099
+ * Enables cache persistence and durable delivery through the existing mutation API.
1100
+ * Import this entry point only in applications using persistence. Storage keys must
1101
+ * isolate authenticated accounts/workspaces and match the server's identity scope.
1102
+ */
1103
+ function createPersistence({ key, maxAge = 864e5, maxBytes = 26214400, online = () => typeof navigator === "undefined" || navigator.onLine, retryDelay = 1e3, storage }) {
1104
+ if (!key || key.length > 1024) throw new Error("fate: Persistence requires an account-scoped key of 1–1024 characters.");
1105
+ if (!Number.isFinite(retryDelay) || retryDelay <= 0) throw new Error("fate: retryDelay must be a positive finite number.");
1106
+ 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.");
1107
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error("fate: Persistence maxBytes must be a positive safe integer.");
1108
+ return { attach: (client) => new Session(client, storage, key, online, retryDelay, maxAge, maxBytes) };
1109
+ }
1110
+ var Session = class {
1111
+ constructor(client, storage, key, online, retryDelay, maxAge, maxBytes) {
1112
+ this.client = client;
1113
+ this.storage = storage;
1114
+ this.key = key;
1115
+ this.online = online;
1116
+ this.retryDelay = retryDelay;
1117
+ this.snapshot = {
1118
+ mutations: [],
1119
+ status: "restoring"
1120
+ };
1121
+ this.listeners = /* @__PURE__ */ new Set();
1122
+ this.commands = /* @__PURE__ */ new Map();
1123
+ this.operations = /* @__PURE__ */ new Map();
1124
+ this.mutationAdmission = Promise.resolve();
1125
+ this.cacheChangeRevision = 0;
1126
+ this.baseCheckpointRevision = 0;
1127
+ this.checkpointedCacheUpdates = /* @__PURE__ */ new Set();
1128
+ this.needsCacheRecovery = false;
1129
+ this.needsCheckpoint = false;
1130
+ this.waiters = /* @__PURE__ */ new Map();
1131
+ this.disposed = false;
1132
+ this.draining = false;
1133
+ this.flushRequested = false;
1134
+ this.refreshRequested = false;
1135
+ this.onOnline = () => this.retry();
1136
+ this.getSnapshot = () => this.snapshot;
1137
+ this.subscribe = (listener) => {
1138
+ this.listeners.add(listener);
1139
+ return () => {
1140
+ this.listeners.delete(listener);
1141
+ };
1142
+ };
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();
1146
+ const data = await this.journal.load();
1147
+ const result = await apply(data);
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.");
1151
+ try {
1152
+ await this.cache.prune(bytes);
1153
+ } catch (error) {
1154
+ if (admit) throw error;
1155
+ this.report(error);
1156
+ }
1157
+ if (admit && !await this.cache.fits(bytes)) throw new Error("fate: Persistence size limit exceeded; unconfirmed mutations have been preserved.");
1158
+ this.assertActive();
1159
+ await this.journal.commit(plan);
1160
+ return result;
1161
+ });
1162
+ this.journal = new PersistenceJournal(storage, key, (entry) => {
1163
+ decodeCommand(entry.command);
1164
+ });
1165
+ this.cache = new PersistenceCache(client, storage, key, maxAge, maxBytes);
1166
+ try {
1167
+ client.validatePersistence();
1168
+ } catch (error) {
1169
+ this.mutationError = error instanceof Error ? error : new Error(String(error));
1170
+ this.snapshot = {
1171
+ ...this.snapshot,
1172
+ error: this.mutationError
1173
+ };
1174
+ }
1175
+ this.ready = this.restore().catch((error) => {
1176
+ if (!this.disposed) this.snapshot = {
1177
+ ...this.snapshot,
1178
+ status: "error"
1179
+ };
1180
+ throw error;
1181
+ });
1182
+ this.ready.catch((error) => this.report(error));
1183
+ }
1184
+ command(entry) {
1185
+ let cached = this.commands.get(entry.id);
1186
+ if (cached?.value !== entry.command) {
1187
+ cached = {
1188
+ command: decodeCommand(entry.command),
1189
+ input: decodeHydrationValue(entry.command).input,
1190
+ value: entry.command
1191
+ };
1192
+ this.commands.set(entry.id, cached);
1193
+ }
1194
+ return cached.command;
1195
+ }
1196
+ publish(data) {
1197
+ if (data) this.snapshot = {
1198
+ ...this.snapshot,
1199
+ mutations: data.mutations.filter((entry) => entry.status !== "confirmed").map((entry) => ({
1200
+ error: entry.error?.message,
1201
+ id: entry.id,
1202
+ input: this.commands.get(entry.id).input,
1203
+ name: this.command(entry).key,
1204
+ status: entry.status
1205
+ }))
1206
+ };
1207
+ for (const listener of this.listeners) try {
1208
+ listener();
1209
+ } catch {}
1210
+ }
1211
+ report(error) {
1212
+ if (this.disposed) return;
1213
+ this.snapshot = {
1214
+ ...this.snapshot,
1215
+ error: error instanceof Error ? error : new Error(String(error)),
1216
+ status: this.snapshot.status === "restoring" ? "error" : this.snapshot.status
1217
+ };
1218
+ this.publish();
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
+ }
1232
+ async restore() {
1233
+ const data = await this.loadJournal();
1234
+ await this.cache.initialize();
1235
+ if (this.disposed) return;
1236
+ for (const entry of data.mutations) if (isPendingMutation(entry)) {
1237
+ if (entry.base && entry.base.scope === this.client.getPersistenceScope()) this.cache.restoreBase(decodeClientHydrationState(entry.base.data));
1238
+ }
1239
+ this.snapshot = {
1240
+ error: this.mutationError,
1241
+ mutations: [],
1242
+ status: "ready"
1243
+ };
1244
+ await this.reconcile(data);
1245
+ if (this.needsCacheRecovery) try {
1246
+ await this.flushCache();
1247
+ } catch (error) {
1248
+ this.report(error);
1249
+ }
1250
+ if (this.disposed) return;
1251
+ this.unsubscribe = this.storage.subscribe?.(this.key, () => {
1252
+ this.refresh().catch((error) => this.report(error));
1253
+ });
1254
+ if (typeof globalThis.addEventListener === "function") globalThis.addEventListener("online", this.onOnline);
1255
+ this.retry();
1256
+ }
1257
+ async reconcile(data) {
1258
+ if (this.disposed) return;
1259
+ let processed = 0;
1260
+ for (const entry of data.mutations) {
1261
+ if (++processed % 64 === 0) await yieldPersistenceTask();
1262
+ if (this.disposed) return;
1263
+ this.command(entry);
1264
+ if (entry.status === "confirmed" || entry.status === "failed") {
1265
+ const operation = this.operations.get(entry.id);
1266
+ if (entry.status === "confirmed") {
1267
+ this.needsCheckpoint = true;
1268
+ const result = operation || this.waiters.has(entry.id) ? decodeHydrationValue(entry.result) : void 0;
1269
+ operation?.commit(result);
1270
+ if (entry.cacheUpdates && entry.scope === this.client.getPersistenceScope()) this.needsCacheRecovery = true;
1271
+ this.waiters.get(entry.id)?.resolve(result);
1272
+ } else {
1273
+ operation?.rollback();
1274
+ this.waiters.get(entry.id)?.reject(new FateRequestError("BAD_REQUEST", entry.error?.message ?? "Mutation failed.", { status: entry.error?.status ?? 400 }));
1275
+ }
1276
+ this.operations.delete(entry.id);
1277
+ this.waiters.delete(entry.id);
1278
+ } else if (!this.operations.has(entry.id) && entry.scope === this.client.getPersistenceScope()) {
1279
+ if (entry.base) this.cache.restoreBase(decodeClientHydrationState(entry.base.data));
1280
+ const command = this.command(entry);
1281
+ if (this.client.hasMutation(command.key, command.entity)) this.operations.set(entry.id, this.prepareOperation(command));
1282
+ }
1283
+ }
1284
+ const retained = new Set(data.mutations.map(({ id }) => id));
1285
+ for (const id of this.commands.keys()) if (!retained.has(id)) this.commands.delete(id);
1286
+ this.publish(data);
1287
+ }
1288
+ prepareOperation(command) {
1289
+ const operation = prepareMutation(this.client, command, this.client.getTypeConfig(command.entity), true);
1290
+ const { promise, resolve } = Promise.withResolvers();
1291
+ let cacheUpdates;
1292
+ let commitError;
1293
+ let commitFailed = false;
1294
+ if (operation.entityId) this.client.registerPendingOptimisticMutation(operation.entityId, promise);
1295
+ return {
1296
+ ...operation,
1297
+ commit: (result, persist = true) => {
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
+ }
1307
+ resolve();
1308
+ },
1309
+ getCacheUpdates: () => cacheUpdates,
1310
+ rollback: (persist = true) => {
1311
+ try {
1312
+ if (persist) operation.rollback();
1313
+ else this.cache.withoutChanges(() => operation.rollback());
1314
+ } finally {
1315
+ resolve();
1316
+ }
1317
+ }
1318
+ };
1319
+ }
1320
+ refresh() {
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
+ }
1330
+ this.retry();
1331
+ } finally {
1332
+ this.refreshing = void 0;
1333
+ if (this.refreshRequested && !this.disposed) this.refresh().catch((error) => this.report(error));
1334
+ }
1335
+ }
1336
+ async mutate(command) {
1337
+ if (this.mutationError) throw this.mutationError;
1338
+ const encoded = encodeCommand(command);
1339
+ await this.ready;
1340
+ this.assertActive();
1341
+ const id = crypto.randomUUID();
1342
+ const admission = this.mutationAdmission.then(async () => {
1343
+ if (this.needsCheckpoint) await this.flushCache().catch((error) => this.report(error));
1344
+ return this.updateJournal(async (data) => {
1345
+ await this.reconcile(data);
1346
+ const base = await this.cache.mutationBase(decodeCommand(encoded));
1347
+ data.mutations.push({
1348
+ attempts: 0,
1349
+ base,
1350
+ command: encoded,
1351
+ id,
1352
+ scope: base.scope,
1353
+ status: "queued"
1354
+ });
1355
+ return base;
1356
+ }, true);
1357
+ });
1358
+ this.mutationAdmission = admission.catch(() => {});
1359
+ const base = await admission;
1360
+ this.assertActive();
1361
+ this.cache.restoreBase(decodeClientHydrationState(base.data));
1362
+ const result = new Promise((resolve, reject) => this.waiters.set(id, {
1363
+ reject,
1364
+ resolve
1365
+ }));
1366
+ try {
1367
+ if (!this.operations.has(id)) this.operations.set(id, this.prepareOperation(decodeCommand(encoded)));
1368
+ } catch (error) {
1369
+ await this.fail(id, error);
1370
+ }
1371
+ this.refresh().catch((error) => this.report(error));
1372
+ this.retry();
1373
+ return result;
1374
+ }
1375
+ async restoreRequest(request) {
1376
+ await this.ready;
1377
+ this.assertActive();
1378
+ try {
1379
+ if (this.needsCacheRecovery) await this.flush();
1380
+ await this.cache.restore(request);
1381
+ } catch (error) {
1382
+ if (this.disposed) throw error;
1383
+ this.report(error);
1384
+ }
1385
+ }
1386
+ fetched(request, options) {
1387
+ if (this.disposed) return;
1388
+ this.cache.fetched(request, options?.maxAge);
1389
+ this.changed();
1390
+ }
1391
+ used(request, options) {
1392
+ if (this.disposed) return;
1393
+ this.cache.fetched(request, options?.maxAge, false);
1394
+ this.changed();
1395
+ }
1396
+ changed(change) {
1397
+ if (this.disposed) return;
1398
+ if (this.cache.changedNode(change)) {
1399
+ this.needsCacheRecovery = true;
1400
+ this.cacheChangeRevision++;
1401
+ }
1402
+ if (this.disposed || this.snapshot.status !== "ready") return;
1403
+ if (this.flushing || this.draining) this.flushRequested = true;
1404
+ else this.scheduleSave();
1405
+ }
1406
+ async flush() {
1407
+ clearTimeout(this.saveTimer);
1408
+ this.saveTimer = void 0;
1409
+ await this.ready;
1410
+ await this.flushCache();
1411
+ this.clearError();
1412
+ }
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() {
1473
+ await this.storage.exclusive(`${this.key}:write`, async () => {
1474
+ this.assertActive();
1475
+ const data = await this.journal.load();
1476
+ await this.reconcile(data);
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);
1482
+ this.needsCacheRecovery = this.cache.hasChanges;
1483
+ this.needsCheckpoint = false;
1484
+ });
1485
+ }
1486
+ retry() {
1487
+ if (this.mutationError || this.disposed || this.snapshot.status !== "ready" || this.draining) return;
1488
+ if (this.timer) clearTimeout(this.timer);
1489
+ this.timer = setTimeout(() => {
1490
+ this.timer = void 0;
1491
+ this.drain().catch((error) => {
1492
+ this.report(error);
1493
+ this.scheduleRetry();
1494
+ });
1495
+ }, 0);
1496
+ }
1497
+ scheduleRetry(delay = this.retryDelay) {
1498
+ if (!this.disposed && !this.timer) this.timer = setTimeout(() => {
1499
+ this.timer = void 0;
1500
+ this.retry();
1501
+ }, delay);
1502
+ }
1503
+ async fail(id, error, status) {
1504
+ const failure = error instanceof Error ? error : new Error(String(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: {
1511
+ message: failure.message,
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;
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
+ }));
1587
+ });
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
+ }
1626
+ }
1627
+ async drain() {
1628
+ if (this.draining || this.disposed || !this.online()) return;
1629
+ this.draining = true;
1630
+ try {
1631
+ await this.storage.exclusive(`${this.key}:delivery`, async () => {
1632
+ while (!this.disposed && this.online()) {
1633
+ const data = await this.loadJournal();
1634
+ await this.reconcile(data);
1635
+ if (!await this.recoverRemovedOperations(data)) return;
1636
+ const entry = data.mutations.find((item) => item.status !== "confirmed" && item.status !== "failed");
1637
+ if (!entry) return;
1638
+ if (entry.nextAttemptAt && entry.nextAttemptAt > Date.now()) {
1639
+ this.scheduleRetry(entry.nextAttemptAt - Date.now());
1640
+ return;
1641
+ }
1642
+ const command = this.command(entry);
1643
+ if (entry.scope !== this.client.getPersistenceScope() || !this.client.hasMutation(command.key, command.entity)) {
1644
+ await this.fail(entry.id, new FateRequestError("BAD_REQUEST", "Persisted mutation is incompatible with this client. Its input has been preserved."));
1645
+ continue;
1646
+ }
1647
+ const operation = this.operations.get(entry.id);
1648
+ if (!operation) throw new Error(`fate: Missing restored mutation operation '${entry.id}'.`);
1649
+ const claimed = await this.claim(entry);
1650
+ if (this.disposed) return;
1651
+ if (!claimed) continue;
1652
+ if (!await this.deliver(entry, operation)) return;
1653
+ }
1654
+ });
1655
+ if ((this.needsCheckpoint || this.flushRequested) && !this.disposed) await this.flush().catch((error) => this.report(error));
1656
+ } finally {
1657
+ this.draining = false;
1658
+ }
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
+ }
1676
+ async discard(id) {
1677
+ await this.ready;
1678
+ const outcome = await this.updateJournal((data) => {
1679
+ const entry = data.mutations.find((entry) => entry.id === id);
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.");
1681
+ if (entry?.status === "confirmed") return "confirmed";
1682
+ if (!entry) return "missing";
1683
+ data.mutations = data.mutations.filter((entry) => entry.id !== id);
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") {
1690
+ this.operations.get(id)?.rollback();
1691
+ this.waiters.get(id)?.reject(mutationDiscardedError());
1692
+ this.forgetOperation(id);
1693
+ }
1694
+ await this.refresh();
1695
+ }
1696
+ async clearCache() {
1697
+ await this.ready;
1698
+ this.assertActive();
1699
+ await this.storage.exclusive(`${this.key}:write`, async () => {
1700
+ this.assertActive();
1701
+ await this.cache.clear();
1702
+ });
1703
+ this.assertActive();
1704
+ await this.cache.initialize();
1705
+ this.clearError();
1706
+ }
1707
+ dispose() {
1708
+ if (this.disposed) return;
1709
+ this.disposed = true;
1710
+ this.client.disposePersistence();
1711
+ this.cache.dispose();
1712
+ this.journal.dispose();
1713
+ clearTimeout(this.timer);
1714
+ clearTimeout(this.saveTimer);
1715
+ this.unsubscribe?.();
1716
+ if (typeof globalThis.removeEventListener === "function") globalThis.removeEventListener("online", this.onOnline);
1717
+ for (const operation of this.operations.values()) operation.rollback();
1718
+ this.operations.clear();
1719
+ this.commands.clear();
1720
+ for (const waiter of this.waiters.values()) waiter.reject(/* @__PURE__ */ new Error("fate: Persistence was disposed; pending mutations remain stored."));
1721
+ this.waiters.clear();
1722
+ this.snapshot = {
1723
+ ...this.snapshot,
1724
+ status: "disposed"
1725
+ };
1726
+ this.publish();
1727
+ }
1728
+ };
1729
+ //#endregion
1730
+ export { createPersistence };