@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.
- package/README.md +5 -7
- package/docs/api/classes/FateClient.md +39 -39
- package/docs/api/functions/clientRoot.md +1 -1
- package/docs/api/functions/createClient.md +1 -1
- package/docs/api/functions/createGraphQLTransport.md +1 -1
- package/docs/api/functions/createHTTPTransport.md +1 -1
- package/docs/api/functions/createTRPCTransport.md +1 -1
- package/docs/api/functions/defer.md +1 -1
- package/docs/api/functions/getDeferredMetadata.md +1 -1
- package/docs/api/functions/getListEntries.md +1 -1
- package/docs/api/functions/getSelectionPlan.md +1 -1
- package/docs/api/functions/graphqlMutation.md +1 -1
- package/docs/api/functions/isDeferred.md +1 -1
- package/docs/api/functions/isRecord.md +1 -1
- package/docs/api/functions/isViewTag.md +1 -1
- package/docs/api/functions/liveConnectionTopic.md +1 -1
- package/docs/api/functions/liveEntityTopic.md +1 -1
- package/docs/api/functions/liveGlobalConnectionTopic.md +1 -1
- package/docs/api/functions/mutation.md +1 -1
- package/docs/api/functions/toEntityId.md +1 -1
- package/docs/api/functions/view.md +1 -1
- package/docs/api/index.md +2 -0
- package/docs/api/interfaces/FateThenable.md +3 -3
- package/docs/api/interfaces/Persistence.md +1 -19
- package/docs/api/interfaces/PersistenceSession.md +10 -10
- package/docs/api/interfaces/Transport.md +8 -8
- package/docs/api/server/classes/FateRequestError.md +5 -5
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +3 -3
- package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +1 -1
- package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +3 -3
- package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +9 -9
- package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +1 -1
- package/docs/api/server/functions/bindSourceProcedures.md +1 -1
- package/docs/api/server/functions/computed.md +1 -1
- package/docs/api/server/functions/count.md +1 -1
- package/docs/api/server/functions/createFateFetchHandler.md +1 -1
- package/docs/api/server/functions/createFateServer.md +1 -1
- package/docs/api/server/functions/createHonoFateHandler.md +1 -1
- package/docs/api/server/functions/createLiveEventBus.md +1 -1
- package/docs/api/server/functions/createNestedSourcePlan.md +1 -1
- package/docs/api/server/functions/createResolver.md +1 -1
- package/docs/api/server/functions/createSourcePlan.md +1 -1
- package/docs/api/server/functions/createSourceProcedures.md +1 -1
- package/docs/api/server/functions/dataView.md +1 -1
- package/docs/api/server/functions/field.md +1 -1
- package/docs/api/server/functions/getNestedSelection.md +1 -1
- package/docs/api/server/functions/getScopedArgs.md +1 -1
- package/docs/api/server/functions/hasNestedSelection.md +1 -1
- package/docs/api/server/functions/list.md +1 -1
- package/docs/api/server/functions/refetchSourceById.md +1 -1
- package/docs/api/server/functions/resolveSourceById.md +1 -1
- package/docs/api/server/functions/resolveSourceByIds.md +1 -1
- package/docs/api/server/functions/resolveSourceConnection.md +2 -2
- package/docs/api/server/functions/resolver.md +1 -1
- package/docs/api/server/functions/toPrismaSelect.md +1 -1
- package/docs/api/server/functions/withConnection.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaFate.md +3 -3
- package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +1 -1
- package/docs/api/server/prisma/functions/prismaConnectionArgs.md +2 -2
- package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +4 -4
- package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +1 -1
- package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +9 -9
- package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +1 -1
- package/docs/api/server/type-aliases/ComputedField.md +5 -5
- package/docs/api/server/type-aliases/ComputedSelection.md +1 -1
- package/docs/api/server/type-aliases/ConnectionItem.md +3 -3
- package/docs/api/server/type-aliases/ConnectionPagination.md +5 -5
- package/docs/api/server/type-aliases/ConnectionResult.md +3 -3
- package/docs/api/server/type-aliases/CountSelection.md +4 -4
- package/docs/api/server/type-aliases/CountWhere.md +1 -1
- package/docs/api/server/type-aliases/DataViewListOptions.md +2 -2
- package/docs/api/server/type-aliases/DataViewOrderBy.md +1 -1
- package/docs/api/server/type-aliases/DataViewOrderDirection.md +1 -1
- package/docs/api/server/type-aliases/DataViewResult.md +1 -1
- package/docs/api/server/type-aliases/Entity.md +1 -1
- package/docs/api/server/type-aliases/FateServer.md +5 -5
- package/docs/api/server/type-aliases/FateServerManifest.md +1 -1
- package/docs/api/server/type-aliases/FieldSelection.md +3 -3
- package/docs/api/server/type-aliases/LiveConnectionEventType.md +1 -1
- package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +1 -1
- package/docs/api/server/type-aliases/LiveEventBus.md +1 -1
- package/docs/api/server/type-aliases/LiveEventType.md +1 -1
- package/docs/api/server/type-aliases/LiveSourceEvent.md +1 -1
- package/docs/api/server/type-aliases/NativeFateAPI.md +4 -4
- package/docs/api/server/type-aliases/OrderDirection.md +1 -1
- package/docs/api/server/type-aliases/SourceConfig.md +6 -6
- package/docs/api/server/type-aliases/SourceDefinition.md +6 -6
- package/docs/api/server/type-aliases/SourceOrder.md +1 -1
- package/docs/api/server/type-aliases/SourceOrderField.md +3 -3
- package/docs/api/server/type-aliases/SourcePlan.md +1 -1
- package/docs/api/server/type-aliases/SourcePlanNode.md +1 -1
- package/docs/api/server/type-aliases/SourceRegistry.md +1 -1
- package/docs/api/server/type-aliases/SourceRelation.md +1 -1
- package/docs/api/server/type-aliases/SourceRelationConfig.md +1 -1
- package/docs/api/server/variables/byIdInput.md +1 -1
- package/docs/api/server/variables/connectionArgs.md +1 -1
- package/docs/api/type-aliases/ConnectionMetadata.md +1 -1
- package/docs/api/type-aliases/ConnectionRef.md +1 -1
- package/docs/api/type-aliases/Deferred.md +1 -1
- package/docs/api/type-aliases/DeferredMetadata.md +1 -1
- package/docs/api/type-aliases/DeferredSelection.md +1 -1
- package/docs/api/type-aliases/DeferredSnapshot.md +1 -1
- package/docs/api/type-aliases/Entity.md +2 -2
- package/docs/api/type-aliases/EntityId.md +1 -1
- package/docs/api/type-aliases/FateDehydratedState.md +1 -1
- package/docs/api/type-aliases/FateLiveConnectionEvent.md +1 -1
- package/docs/api/type-aliases/FateLiveEvent.md +1 -1
- package/docs/api/type-aliases/FateMutations.md +1 -1
- package/docs/api/type-aliases/FateOperation.md +1 -1
- package/docs/api/type-aliases/FateProtocolRequest.md +1 -1
- package/docs/api/type-aliases/FateProtocolResponse.md +1 -1
- package/docs/api/type-aliases/FateRecord.md +1 -1
- package/docs/api/type-aliases/FateRoots.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationDefinition.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationInput.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationMap.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationOutput.md +1 -1
- package/docs/api/type-aliases/GraphQLTransportOptions.md +12 -12
- package/docs/api/type-aliases/HydrateOptions.md +1 -1
- package/docs/api/type-aliases/HydrationLimits.md +1 -1
- package/docs/api/type-aliases/InferFateAPI.md +1 -1
- package/docs/api/type-aliases/List.md +1 -1
- package/docs/api/type-aliases/ListEntry.md +1 -1
- package/docs/api/type-aliases/ListItem.md +1 -1
- package/docs/api/type-aliases/Mask.md +1 -1
- package/docs/api/type-aliases/MutationDefinition.md +1 -1
- package/docs/api/type-aliases/MutationEntity.md +1 -1
- package/docs/api/type-aliases/MutationIdentifier.md +1 -1
- package/docs/api/type-aliases/MutationIdentity.md +1 -1
- package/docs/api/type-aliases/MutationInput.md +1 -1
- package/docs/api/type-aliases/MutationResult.md +1 -1
- package/docs/api/type-aliases/NodesItem.md +1 -1
- package/docs/api/type-aliases/Pagination.md +5 -5
- package/docs/api/type-aliases/PersistedMutationStatus.md +5 -0
- package/docs/api/type-aliases/PersistenceSnapshot.md +2 -2
- package/docs/api/type-aliases/Request.md +1 -1
- package/docs/api/type-aliases/RequestMode.md +1 -1
- package/docs/api/type-aliases/RequestOptions.md +2 -2
- package/docs/api/type-aliases/RequestPersistenceOptions.md +5 -0
- package/docs/api/type-aliases/RequestResult.md +1 -1
- package/docs/api/type-aliases/Selection.md +1 -1
- package/docs/api/type-aliases/TypeConfig.md +4 -4
- package/docs/api/type-aliases/View.md +1 -1
- package/docs/api/type-aliases/ViewData.md +1 -1
- package/docs/api/type-aliases/ViewEntity.md +1 -1
- package/docs/api/type-aliases/ViewEntityName.md +1 -1
- package/docs/api/type-aliases/ViewRef.md +1 -1
- package/docs/api/type-aliases/ViewSelection.md +1 -1
- package/docs/api/type-aliases/ViewSnapshot.md +1 -1
- package/docs/api/type-aliases/ViewTag.md +1 -1
- package/docs/api/variables/ConnectionTag.md +1 -1
- package/docs/api/variables/DeferTag.md +1 -1
- package/docs/api/variables/DeferredTag.md +1 -1
- package/docs/guide/persistence.md +5 -7
- package/lib/clientStub.d.mts +4 -5
- package/lib/{executor-DoiTBiVv.d.mts → executor-oPTYqgQV.d.mts} +1 -1
- package/lib/{graphqlTransport-DGLYyRxL.d.mts → graphqlTransport-CGYE4NdD.d.mts} +1 -1
- package/lib/graphqlTransport.d.mts +1 -1
- package/lib/idempotency-types-DuqJ7KSd.d.mts +16 -0
- package/lib/idempotency.d.mts +8 -8
- package/lib/index.d.mts +12 -12
- package/lib/index.mjs +25 -15
- package/lib/{list-BRT1VtiT.d.mts → list-5hrTVoiB.d.mts} +1 -1
- package/lib/list.d.mts +1 -1
- package/lib/persistence.d.mts +21 -19
- package/lib/persistence.mjs +553 -352
- package/lib/{record-DITM4zb_.d.mts → record-Dxjvpsep.d.mts} +3 -3
- package/lib/server/drizzle.d.mts +14 -13
- package/lib/server/prisma.d.mts +14 -13
- package/lib/server.d.mts +9 -9
- package/lib/{store-CeatPJeK.mjs → store-CakBBzWG.mjs} +47 -11
- package/lib/{transport-DqjhjLfC.d.mts → transport-DR0fwleU.d.mts} +29 -16
- package/lib/vite.d.mts +3 -3
- package/package.json +3 -3
- package/lib/idempotency-types-CqA-BFpn.d.mts +0 -15
package/lib/persistence.mjs
CHANGED
|
@@ -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-
|
|
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
|
|
31
|
-
...
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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 =
|
|
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
|
|
63
|
-
const
|
|
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) =>
|
|
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.
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
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 (
|
|
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.
|
|
232
|
-
const update = this.
|
|
233
|
-
fields: this.
|
|
234
|
-
value: this.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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 =
|
|
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.
|
|
305
|
-
}
|
|
306
|
-
changedNode(
|
|
307
|
-
if (!this.suppressed &&
|
|
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.
|
|
312
|
-
this.
|
|
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.
|
|
317
|
-
this.
|
|
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.
|
|
351
|
-
previousList: this.listBases.has(key) ? this.listBases.get(key) : update.previousList,
|
|
352
|
-
|
|
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.
|
|
355
|
-
this.
|
|
356
|
-
this.
|
|
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.
|
|
362
|
-
for (const encoded of updates) for (const [key, update] of decodeHydrationValue(encoded))
|
|
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.
|
|
402
|
+
const previous = this.pendingRequests.get(request.key);
|
|
369
403
|
const retain = this.client.retainPersistenceRequest(request);
|
|
370
404
|
previous?.release();
|
|
371
|
-
this.
|
|
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(
|
|
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,
|
|
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
|
|
432
|
+
const readNode = async (key) => {
|
|
397
433
|
await this.step();
|
|
398
|
-
if (
|
|
399
|
-
if (this.
|
|
400
|
-
const incoming = this.
|
|
401
|
-
const base = this.
|
|
402
|
-
if (!
|
|
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.
|
|
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.
|
|
444
|
+
if (stored) return applyRecordUpdate(decodeHydrationValue(stored.value), incoming, this.flushingNodes.recordFields.get(key));
|
|
409
445
|
}
|
|
410
446
|
return incoming;
|
|
411
447
|
}
|
|
412
|
-
if (
|
|
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.
|
|
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
|
|
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 (!
|
|
424
|
-
const deleted = (id) => this.
|
|
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
|
|
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
|
|
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
|
|
507
|
-
if (!
|
|
542
|
+
const record = await readNode(key);
|
|
543
|
+
if (!record) {
|
|
508
544
|
complete = false;
|
|
509
545
|
continue;
|
|
510
546
|
}
|
|
511
|
-
if (paths.some((path) => !
|
|
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:
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
611
|
-
|
|
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,
|
|
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 =
|
|
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 +=
|
|
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 =
|
|
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.
|
|
698
|
-
added += Math.max(0, (value === void 0 ? 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
|
|
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
|
|
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.
|
|
751
|
-
this.
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
const listBases =
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
760
|
-
fields:
|
|
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.
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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.
|
|
827
|
+
for (const key of this.flushingNodes.values.keys()) {
|
|
772
828
|
const node = await this.node(key);
|
|
773
|
-
const incoming = this.
|
|
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.
|
|
777
|
-
|
|
778
|
-
...before.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
831
|
-
for (const [key, value] of
|
|
832
|
-
if (
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
this.
|
|
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 =
|
|
838
|
-
const after = this.
|
|
839
|
-
this.
|
|
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.
|
|
843
|
-
this.
|
|
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.
|
|
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
|
|
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.
|
|
922
|
+
this.recoveryUpdates.clear();
|
|
870
923
|
this.policies = /* @__PURE__ */ new WeakMap();
|
|
871
|
-
for (const entry of this.
|
|
872
|
-
this.
|
|
873
|
-
this.
|
|
874
|
-
this.
|
|
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 = (
|
|
885
|
-
"
|
|
886
|
-
|
|
887
|
-
"
|
|
888
|
-
"
|
|
889
|
-
|
|
890
|
-
|
|
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
|
|
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
|
|
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
|
|
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 || !
|
|
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 =
|
|
1027
|
+
let measured = persistenceEntrySize(key, value);
|
|
980
1028
|
while (measured !== value.bytes) {
|
|
981
1029
|
value.bytes = measured;
|
|
982
|
-
measured =
|
|
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
|
|
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 +=
|
|
1000
|
-
|
|
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
|
|
1010
|
-
this.assertActive();
|
|
1011
|
-
if (this.prepared?.data !== data) await this.measure(data);
|
|
1055
|
+
async commit(plan) {
|
|
1012
1056
|
this.assertActive();
|
|
1013
|
-
|
|
1014
|
-
|
|
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 =
|
|
1018
|
-
this.entries =
|
|
1060
|
+
this.header = plan.header;
|
|
1061
|
+
this.entries = plan.entries;
|
|
1019
1062
|
}
|
|
1020
|
-
|
|
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.
|
|
1083
|
-
this.
|
|
1084
|
-
|
|
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
|
|
1088
|
-
|
|
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
|
-
|
|
1097
|
-
await this.journal.
|
|
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.
|
|
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
|
|
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
|
|
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 (
|
|
1223
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1396
|
+
changed(change) {
|
|
1306
1397
|
if (this.disposed) return;
|
|
1307
|
-
if (this.cache.changedNode(
|
|
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 (
|
|
1310
|
-
|
|
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
|
-
|
|
1319
|
-
if (this.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
|
-
|
|
1474
|
+
this.assertActive();
|
|
1322
1475
|
const data = await this.journal.load();
|
|
1323
1476
|
await this.reconcile(data);
|
|
1324
|
-
this.cache.replayChanges(
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
entry.
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
|
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.
|
|
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.
|
|
1633
|
+
const data = await this.loadJournal();
|
|
1383
1634
|
await this.reconcile(data);
|
|
1384
|
-
|
|
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
|
-
|
|
1422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
1496
|
-
this.
|
|
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
|
-
|
|
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;
|