@nkzw/fate 1.3.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -0
- package/docs/api/classes/FateClient.md +51 -199
- 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 +5 -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 +4 -1
- package/docs/api/interfaces/FateThenable.md +3 -3
- package/docs/api/interfaces/Persistence.md +23 -0
- package/docs/api/interfaces/PersistenceSession.md +109 -0
- package/docs/api/interfaces/Transport.md +46 -7
- package/docs/api/server/classes/FateRequestError.md +5 -5
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +2 -2
- 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 +19 -11
- 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 +7 -0
- 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/PersistenceSnapshot.md +5 -0
- 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/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/getting-started.md +2 -0
- package/docs/guide/persistence.md +323 -0
- package/lib/{liveTopics-uVP0LG-Q.mjs → args-CRLIRl_v.mjs} +1 -59
- package/lib/cli.d.mts +1 -1
- package/lib/{executor-C8HNogYG.d.mts → executor-DoiTBiVv.d.mts} +18 -65
- package/lib/{graphqlTransport-DskuXeUP.d.mts → graphqlTransport-DGLYyRxL.d.mts} +4 -14
- package/lib/{graphqlTransport-DSBVS2Fq.mjs → graphqlTransport-DmheG3AO.mjs} +3 -2
- package/lib/graphqlTransport.d.mts +1 -1
- package/lib/graphqlTransport.mjs +1 -1
- package/lib/hydration-DPZRJgnu.mjs +273 -0
- package/lib/idempotency-types-CqA-BFpn.d.mts +15 -0
- package/lib/idempotency.d.mts +26 -0
- package/lib/idempotency.mjs +34 -0
- package/lib/index.d.mts +6 -15
- package/lib/index.mjs +560 -1774
- package/lib/{list-sI2UPhHc.d.mts → list-BRT1VtiT.d.mts} +1 -2
- package/lib/list.d.mts +1 -1
- package/lib/liveTopics-DuFkDZQm.mjs +17 -0
- package/lib/persistence.d.mts +38 -0
- package/lib/persistence.mjs +1529 -0
- package/lib/protocol-DS5sf6sd.mjs +44 -0
- package/lib/{record-CZyyeLa3.d.mts → record-DITM4zb_.d.mts} +14 -19
- package/lib/server/drizzle.d.mts +45 -114
- package/lib/server/drizzle.mjs +2 -2
- package/lib/server/prisma.d.mts +52 -127
- package/lib/server/prisma.mjs +9 -8
- package/lib/server.d.mts +4 -16
- package/lib/server.mjs +21 -35
- package/lib/sortObjectKeys-yhuitEQZ.mjs +9 -0
- package/lib/{sourceRouter-DSaADXSf.mjs → sourceRouter-51POBlEE.mjs} +9 -6
- package/lib/store-CeatPJeK.mjs +1350 -0
- package/lib/{transport-DhaVVq4P.d.mts → transport-DqjhjLfC.d.mts} +672 -517
- package/lib/vite.d.mts +0 -1
- package/lib/vite.mjs +19 -11
- package/package.json +15 -5
- package/docs/api/type-aliases/Snapshot.md +0 -7
|
@@ -0,0 +1,1529 @@
|
|
|
1
|
+
import { i as filterConnectionArgs } from "./args-CRLIRl_v.mjs";
|
|
2
|
+
import { a as createNodeRef, n as decodeHydrationValue, o as getNodeRefId, r as encodeHydrationValue, s as isNodeRef, t as decodeClientHydrationState } from "./hydration-DPZRJgnu.mjs";
|
|
3
|
+
import { i as getErrorStatusCode, m as toEntityId, n as getListKey, o as prepareMutation } from "./store-CeatPJeK.mjs";
|
|
4
|
+
import { t as FateRequestError } from "./protocol-DS5sf6sd.mjs";
|
|
5
|
+
//#region src/persistence-codec.ts
|
|
6
|
+
const serializePlan = (plan) => ({
|
|
7
|
+
args: [...plan.args].map(([key, value]) => [key, {
|
|
8
|
+
...value,
|
|
9
|
+
ignoreKeys: value.ignoreKeys ? [...value.ignoreKeys] : void 0
|
|
10
|
+
}]),
|
|
11
|
+
live: [...plan.live],
|
|
12
|
+
paths: [...plan.paths]
|
|
13
|
+
});
|
|
14
|
+
const deserializePlan = (value) => ({
|
|
15
|
+
args: new Map(value.args.map(([key, entry]) => [key, {
|
|
16
|
+
...entry,
|
|
17
|
+
ignoreKeys: entry.ignoreKeys ? new Set(entry.ignoreKeys) : void 0
|
|
18
|
+
}])),
|
|
19
|
+
live: new Map(value.live),
|
|
20
|
+
paths: new Set(value.paths)
|
|
21
|
+
});
|
|
22
|
+
const encodeRequests = (requests) => encodeHydrationValue(requests.map((request) => ({
|
|
23
|
+
...request,
|
|
24
|
+
items: request.items.map((item) => ({
|
|
25
|
+
...item,
|
|
26
|
+
plan: serializePlan(item.plan),
|
|
27
|
+
..."refViewNames" in item ? { refViewNames: [...item.refViewNames] } : { nodeRefViewNames: [...item.nodeRefViewNames] }
|
|
28
|
+
}))
|
|
29
|
+
})));
|
|
30
|
+
const decodeRequests = (value) => decodeHydrationValue(value).map((request) => ({
|
|
31
|
+
...request,
|
|
32
|
+
items: request.items.map((item) => ({
|
|
33
|
+
...item,
|
|
34
|
+
plan: deserializePlan(item.plan),
|
|
35
|
+
..."refViewNames" in item ? { refViewNames: new Set(item.refViewNames) } : { nodeRefViewNames: new Set(item.nodeRefViewNames) }
|
|
36
|
+
}))
|
|
37
|
+
}));
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/persistence-cache.ts
|
|
40
|
+
const mergeCacheUpdate = (key, before, after) => ({
|
|
41
|
+
fields: after.fields && (!before || before.fields) ? [.../* @__PURE__ */ new Set([...before?.fields ?? [], ...after.fields])] : void 0,
|
|
42
|
+
previousList: before ? before.previousList : after.previousList,
|
|
43
|
+
value: key.startsWith("r:") && before?.value && after.value ? applyRecordUpdate(before.value, after.value, after.fields ? new Set(after.fields) : void 0) : after.value
|
|
44
|
+
});
|
|
45
|
+
const encoder = new TextEncoder();
|
|
46
|
+
const sized = (key, value) => {
|
|
47
|
+
const item = {
|
|
48
|
+
bytes: 0,
|
|
49
|
+
value
|
|
50
|
+
};
|
|
51
|
+
for (;;) {
|
|
52
|
+
const bytes = encoder.encode(JSON.stringify([key, item])).byteLength;
|
|
53
|
+
if (bytes === item.bytes) return item;
|
|
54
|
+
item.bytes = bytes;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const yieldTask$1 = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
58
|
+
const stamp = (time) => String(time).padStart(16, "0");
|
|
59
|
+
const recordKey = (id) => `r:${id}`;
|
|
60
|
+
const listKey = (id) => `l:${id}`;
|
|
61
|
+
const queryKey = (id) => `q:${id}`;
|
|
62
|
+
const matches = (path, selected) => path === selected || path.startsWith(`${selected}.`) || selected.startsWith(`${path}.`);
|
|
63
|
+
const project = (value, paths) => {
|
|
64
|
+
const fields = /* @__PURE__ */ new Set([
|
|
65
|
+
"id",
|
|
66
|
+
"__typename",
|
|
67
|
+
...paths.map((path) => path.split(".")[0])
|
|
68
|
+
]);
|
|
69
|
+
return {
|
|
70
|
+
paths: value.paths.filter((path) => paths.some((selected) => matches(path, selected))),
|
|
71
|
+
record: Object.fromEntries(Object.entries(value.record).filter(([field]) => fields.has(field)))
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const applyRecordUpdate = (before, after, fields) => ({
|
|
75
|
+
paths: [.../* @__PURE__ */ new Set([...before.paths, ...after.paths.filter((path) => !fields || fields.has(path.split(".")[0]))])],
|
|
76
|
+
record: {
|
|
77
|
+
...before.record,
|
|
78
|
+
...Object.fromEntries(Object.entries(after.record).filter(([key]) => !fields || fields.has(key)))
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const mergeIds = (before, after, latest) => {
|
|
82
|
+
const old = new Set(before);
|
|
83
|
+
const next = new Set(after);
|
|
84
|
+
let retained = latest.filter((id) => !old.has(id) || next.has(id));
|
|
85
|
+
const present = new Set(retained);
|
|
86
|
+
const oldOrder = before.filter((id) => next.has(id));
|
|
87
|
+
const nextOrder = after.filter((id) => old.has(id));
|
|
88
|
+
if (oldOrder.some((id, index) => id !== nextOrder[index])) {
|
|
89
|
+
let index = 0;
|
|
90
|
+
const reordered = nextOrder.filter((id) => present.has(id));
|
|
91
|
+
retained = retained.map((id) => old.has(id) ? reordered[index++] : id);
|
|
92
|
+
}
|
|
93
|
+
const insertions = /* @__PURE__ */ new Map();
|
|
94
|
+
let anchor;
|
|
95
|
+
for (let index = after.length - 1; index >= 0; index--) {
|
|
96
|
+
const id = after[index];
|
|
97
|
+
if (present.has(id)) anchor = id;
|
|
98
|
+
else if (!old.has(id)) {
|
|
99
|
+
const group = insertions.get(anchor) ?? [];
|
|
100
|
+
group.push(id);
|
|
101
|
+
insertions.set(anchor, group);
|
|
102
|
+
present.add(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...retained.flatMap((id) => [...insertions.get(id)?.reverse() ?? [], id]), ...insertions.get(void 0)?.reverse() ?? []];
|
|
106
|
+
};
|
|
107
|
+
const membershipFields = [
|
|
108
|
+
"ids",
|
|
109
|
+
"liveAfterIds",
|
|
110
|
+
"liveBeforeIds",
|
|
111
|
+
"pendingAfterIds",
|
|
112
|
+
"pendingBeforeIds"
|
|
113
|
+
];
|
|
114
|
+
const applyListUpdate = (latest, before, after) => {
|
|
115
|
+
const previousMembers = new Set(membershipFields.flatMap((field) => before[field] ?? []));
|
|
116
|
+
const latestMembers = new Set(membershipFields.flatMap((field) => latest[field] ?? []));
|
|
117
|
+
const surviving = (ids) => ids.filter((id) => !previousMembers.has(id) || latestMembers.has(id));
|
|
118
|
+
const ids = mergeIds(before.ids, surviving(after.ids), latest.ids);
|
|
119
|
+
const oldCursors = new Map(before.ids.map((id, index) => [id, before.cursors?.[index]]));
|
|
120
|
+
const nextCursors = new Map(after.ids.map((id, index) => [id, after.cursors?.[index]]));
|
|
121
|
+
const cursors = new Map(latest.ids.map((id, index) => [id, latest.cursors?.[index]]));
|
|
122
|
+
for (const [id, cursor] of nextCursors) if (!oldCursors.has(id) || oldCursors.get(id) !== cursor) cursors.set(id, cursor);
|
|
123
|
+
const result = {
|
|
124
|
+
...latest,
|
|
125
|
+
ids
|
|
126
|
+
};
|
|
127
|
+
if (latest.cursors || after.cursors) result.cursors = ids.map((id) => cursors.get(id));
|
|
128
|
+
for (const field of [
|
|
129
|
+
"liveAfterIds",
|
|
130
|
+
"liveBeforeIds",
|
|
131
|
+
"pendingAfterIds",
|
|
132
|
+
"pendingBeforeIds"
|
|
133
|
+
]) if (before[field] || after[field] || latest[field]) result[field] = mergeIds(before[field] ?? [], surviving(after[field] ?? []), latest[field] ?? []);
|
|
134
|
+
for (const field of ["backwardPageLimit", "forwardPageLimit"]) if (before[field] !== after[field]) result[field] = after[field];
|
|
135
|
+
if (after.pagination) result.pagination = {
|
|
136
|
+
...latest.pagination ?? after.pagination,
|
|
137
|
+
...Object.fromEntries(Object.entries(after.pagination).filter(([key, value]) => value !== before.pagination?.[key]))
|
|
138
|
+
};
|
|
139
|
+
return result;
|
|
140
|
+
};
|
|
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
|
+
/** Independently addressable normalized disk data. No in-memory retention roots. */
|
|
148
|
+
var PersistenceCache = class {
|
|
149
|
+
constructor(client, storage, key, maxAge, maxBytes) {
|
|
150
|
+
this.client = client;
|
|
151
|
+
this.storage = storage;
|
|
152
|
+
this.key = key;
|
|
153
|
+
this.maxAge = maxAge;
|
|
154
|
+
this.maxBytes = maxBytes;
|
|
155
|
+
this.changed = /* @__PURE__ */ new Map();
|
|
156
|
+
this.updates = /* @__PURE__ */ new Map();
|
|
157
|
+
this.listBases = /* @__PURE__ */ new Map();
|
|
158
|
+
this.updateListBases = /* @__PURE__ */ new Map();
|
|
159
|
+
this.fields = /* @__PURE__ */ new Map();
|
|
160
|
+
this.updateFields = /* @__PURE__ */ new Map();
|
|
161
|
+
this.recoveryChanges = /* @__PURE__ */ new Map();
|
|
162
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
163
|
+
this.suppressed = false;
|
|
164
|
+
this.disposed = false;
|
|
165
|
+
this.work = 0;
|
|
166
|
+
this.journalBytes = 0;
|
|
167
|
+
this.policies = /* @__PURE__ */ new WeakMap();
|
|
168
|
+
this.prefix = `${JSON.stringify([key, "cache-v2"])}:`;
|
|
169
|
+
}
|
|
170
|
+
async initialize() {
|
|
171
|
+
await this.storage.exclusive(`${this.key}:write`, async () => {
|
|
172
|
+
this.assertActive();
|
|
173
|
+
if (await this.read(`${this.prefix}scope`) !== this.client.getPersistenceScope()) {
|
|
174
|
+
await this.clear();
|
|
175
|
+
if (sized(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes + this.overhead <= this.maxBytes) await this.put([[`${this.prefix}scope`, this.client.getPersistenceScope()]]);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
assertActive() {
|
|
180
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
181
|
+
}
|
|
182
|
+
async active() {
|
|
183
|
+
this.assertActive();
|
|
184
|
+
const scope = await this.read(`${this.prefix}scope`);
|
|
185
|
+
this.assertActive();
|
|
186
|
+
return scope === this.client.getPersistenceScope();
|
|
187
|
+
}
|
|
188
|
+
restoreBase(state) {
|
|
189
|
+
this.assertActive();
|
|
190
|
+
this.suppressed = true;
|
|
191
|
+
try {
|
|
192
|
+
this.client.restorePersistenceData(state);
|
|
193
|
+
} finally {
|
|
194
|
+
this.suppressed = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Save only recovery dependencies, never an unrelated read-cache snapshot. */
|
|
198
|
+
async mutationBase(command, base) {
|
|
199
|
+
const previous = base ? decodeClientHydrationState(base.data).store : void 0;
|
|
200
|
+
const previousRecords = new Map(previous?.records);
|
|
201
|
+
const previousCoverage = new Map(previous?.coverage);
|
|
202
|
+
const previousLists = new Map(previous?.lists);
|
|
203
|
+
const ids = new Set(previousRecords.keys());
|
|
204
|
+
const config = this.client.getTypeConfig(command.entity);
|
|
205
|
+
for (const input of [command.input, command.optimistic]) if (input) try {
|
|
206
|
+
const id = config.getId(input);
|
|
207
|
+
if (id != null) ids.add(toEntityId(command.entity, id));
|
|
208
|
+
} catch {}
|
|
209
|
+
const lists = /* @__PURE__ */ new Set([...previousLists.keys(), ...command.insert !== "none" ? this.client.getPersistenceRootLists(command.entity) : []]);
|
|
210
|
+
for (const id of ids) {
|
|
211
|
+
const owners = this.client.store.getPersistenceOwners(id);
|
|
212
|
+
for (const owner of owners.records) ids.add(owner);
|
|
213
|
+
for (const key of owners.lists) lists.add(key);
|
|
214
|
+
}
|
|
215
|
+
const records = [];
|
|
216
|
+
const coverage = [];
|
|
217
|
+
const visit = (value) => {
|
|
218
|
+
if (isNodeRef(value)) ids.add(getNodeRefId(value));
|
|
219
|
+
else if (Array.isArray(value)) for (const item of value) visit(item);
|
|
220
|
+
else if (value && typeof value === "object") for (const item of Object.values(value)) visit(item);
|
|
221
|
+
};
|
|
222
|
+
for (const id of ids) {
|
|
223
|
+
const key = recordKey(id);
|
|
224
|
+
const saved = previousRecords.get(id);
|
|
225
|
+
const before = saved ? {
|
|
226
|
+
paths: [...previousCoverage.get(id) ?? []],
|
|
227
|
+
record: saved
|
|
228
|
+
} : void 0;
|
|
229
|
+
const memory = this.client.store.readConfirmed(id);
|
|
230
|
+
const disk = memory || before ? void 0 : await this.node(key);
|
|
231
|
+
const recovery = this.recoveryChanges.get(key);
|
|
232
|
+
const update = this.changed.has(key) ? mergeCacheUpdate(key, recovery, {
|
|
233
|
+
fields: this.fields.get(key) ? [...this.fields.get(key)] : void 0,
|
|
234
|
+
value: this.changed.get(key)
|
|
235
|
+
}) : recovery;
|
|
236
|
+
const incoming = update?.value;
|
|
237
|
+
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);
|
|
238
|
+
if (value) {
|
|
239
|
+
records.push([id, value.record]);
|
|
240
|
+
coverage.push([id, value.paths]);
|
|
241
|
+
visit(value.record);
|
|
242
|
+
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);
|
|
243
|
+
}
|
|
244
|
+
await this.step();
|
|
245
|
+
}
|
|
246
|
+
const listValues = [];
|
|
247
|
+
for (const key of lists) {
|
|
248
|
+
const value = this.changed.has(listKey(key)) ? this.changed.get(listKey(key)) : this.recoveryChanges.has(listKey(key)) ? this.recoveryChanges.get(listKey(key)).value : previousLists.get(key) ?? this.client.store.readConfirmedList(key);
|
|
249
|
+
if (value) listValues.push([key, value]);
|
|
250
|
+
await this.step();
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
data: encodeHydrationValue({
|
|
254
|
+
rootLists: [[command.entity, [...this.client.getPersistenceRootLists(command.entity)]]],
|
|
255
|
+
rootRequests: [],
|
|
256
|
+
store: {
|
|
257
|
+
coverage,
|
|
258
|
+
lists: listValues,
|
|
259
|
+
records
|
|
260
|
+
}
|
|
261
|
+
}),
|
|
262
|
+
scope: this.client.getPersistenceScope(),
|
|
263
|
+
version: 1
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
get overhead() {
|
|
267
|
+
return encoder.encode(JSON.stringify([`${this.prefix}bytes`, this.maxBytes])).byteLength;
|
|
268
|
+
}
|
|
269
|
+
async step() {
|
|
270
|
+
this.assertActive();
|
|
271
|
+
if (++this.work % 64 === 0) {
|
|
272
|
+
await yieldTask$1();
|
|
273
|
+
this.assertActive();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
nodeKey(key) {
|
|
277
|
+
return `${this.prefix}node:${key}`;
|
|
278
|
+
}
|
|
279
|
+
rootKey(key) {
|
|
280
|
+
return `${this.prefix}root:${key}`;
|
|
281
|
+
}
|
|
282
|
+
async read(key) {
|
|
283
|
+
return (await this.storage.read(key))?.value;
|
|
284
|
+
}
|
|
285
|
+
async put(entries) {
|
|
286
|
+
let bytes = await this.storage.read(`${this.prefix}bytes`) ?? 0;
|
|
287
|
+
const writes = [];
|
|
288
|
+
for (const [key, value] of new Map(entries)) {
|
|
289
|
+
const previous = await this.storage.read(key);
|
|
290
|
+
bytes -= previous?.bytes ?? 0;
|
|
291
|
+
if (value === void 0) writes.push([key, void 0]);
|
|
292
|
+
else {
|
|
293
|
+
const item = sized(key, value);
|
|
294
|
+
bytes += item.bytes;
|
|
295
|
+
writes.push([key, item]);
|
|
296
|
+
}
|
|
297
|
+
await this.step();
|
|
298
|
+
}
|
|
299
|
+
writes.push([`${this.prefix}bytes`, Math.max(0, bytes)]);
|
|
300
|
+
this.assertActive();
|
|
301
|
+
await this.storage.writeBatch(writes);
|
|
302
|
+
}
|
|
303
|
+
get hasChanges() {
|
|
304
|
+
return this.changed.size > 0 || this.recoveryChanges.size > 0;
|
|
305
|
+
}
|
|
306
|
+
changedNode(kind, key, paths, previousList) {
|
|
307
|
+
if (!this.suppressed && key) {
|
|
308
|
+
const selected = paths ? new Set([...paths].map((path) => path.split(".")[0])) : void 0;
|
|
309
|
+
if (kind === "record") {
|
|
310
|
+
const id = recordKey(key);
|
|
311
|
+
const previous = this.fields.get(id);
|
|
312
|
+
this.fields.set(id, selected?.size && (!this.fields.has(id) || previous) ? /* @__PURE__ */ new Set([...previous ?? [], ...selected]) : void 0);
|
|
313
|
+
}
|
|
314
|
+
const id = kind === "record" ? recordKey(key) : listKey(key);
|
|
315
|
+
const value = kind === "record" ? this.client.store.readConfirmed(key) : this.client.store.readConfirmedList(key);
|
|
316
|
+
if (kind === "list" && !this.changed.has(id)) this.listBases.set(id, previousList);
|
|
317
|
+
this.changed.set(id, value);
|
|
318
|
+
if (this.capturing) {
|
|
319
|
+
const previous = this.capturing.get(id);
|
|
320
|
+
this.capturing.set(id, {
|
|
321
|
+
fields: selected?.size && (!previous || previous.fields) ? [.../* @__PURE__ */ new Set([...previous?.fields ?? [], ...selected])] : void 0,
|
|
322
|
+
previousList: previous ? previous.previousList : previousList,
|
|
323
|
+
value
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
/** A recovered receipt has already been checkpointed by another tab. */
|
|
331
|
+
withoutChanges(commit) {
|
|
332
|
+
this.suppressed = true;
|
|
333
|
+
try {
|
|
334
|
+
commit();
|
|
335
|
+
} finally {
|
|
336
|
+
this.suppressed = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Capture only the confirmed writes made by this mutation for crash recovery. */
|
|
340
|
+
captureChanges(commit) {
|
|
341
|
+
const changes = /* @__PURE__ */ new Map();
|
|
342
|
+
this.capturing = changes;
|
|
343
|
+
try {
|
|
344
|
+
commit();
|
|
345
|
+
} finally {
|
|
346
|
+
this.capturing = void 0;
|
|
347
|
+
}
|
|
348
|
+
for (const [key, update] of changes) {
|
|
349
|
+
changes.set(key, {
|
|
350
|
+
fields: this.fields.get(key) ? [...this.fields.get(key)] : void 0,
|
|
351
|
+
previousList: this.listBases.has(key) ? this.listBases.get(key) : update.previousList,
|
|
352
|
+
value: this.changed.get(key)
|
|
353
|
+
});
|
|
354
|
+
this.changed.delete(key);
|
|
355
|
+
this.fields.delete(key);
|
|
356
|
+
this.listBases.delete(key);
|
|
357
|
+
}
|
|
358
|
+
return encodeHydrationValue([...changes]);
|
|
359
|
+
}
|
|
360
|
+
replayChanges(updates) {
|
|
361
|
+
this.recoveryChanges.clear();
|
|
362
|
+
for (const encoded of updates) for (const [key, update] of decodeHydrationValue(encoded)) this.recoveryChanges.set(key, mergeCacheUpdate(key, this.recoveryChanges.get(key), update));
|
|
363
|
+
}
|
|
364
|
+
fetched(request, maxAge = this.maxAge, fromNetwork = true) {
|
|
365
|
+
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
|
+
if (!fromNetwork && this.policies.get(request) === maxAge) return;
|
|
367
|
+
this.policies.set(request, maxAge);
|
|
368
|
+
const previous = this.pending.get(request.key);
|
|
369
|
+
const retain = this.client.retainPersistenceRequest(request);
|
|
370
|
+
previous?.release();
|
|
371
|
+
this.pending.set(request.key, {
|
|
372
|
+
fetchedAt: fromNetwork ? Date.now() : previous?.fetchedAt,
|
|
373
|
+
maxAge,
|
|
374
|
+
release: retain.dispose,
|
|
375
|
+
request
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async node(key) {
|
|
379
|
+
const node = await this.read(this.nodeKey(key));
|
|
380
|
+
if (!node) return;
|
|
381
|
+
node.claims = Object.fromEntries(Object.entries(node.claims).filter(([, claim]) => claim.expiresAt > Date.now()));
|
|
382
|
+
if (!Object.keys(node.claims).length) return;
|
|
383
|
+
if (key.startsWith("r:")) {
|
|
384
|
+
const paths = Object.values(node.claims).flatMap((claim) => claim.paths);
|
|
385
|
+
node.value = encodeHydrationValue(project(decodeHydrationValue(node.value), paths));
|
|
386
|
+
}
|
|
387
|
+
return node;
|
|
388
|
+
}
|
|
389
|
+
/** Walk only the requested selection, yielding between small pieces of work. */
|
|
390
|
+
async collect(request, memory, fromNetwork = false) {
|
|
391
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
392
|
+
const visited = /* @__PURE__ */ new Map();
|
|
393
|
+
let complete = true;
|
|
394
|
+
let fetchedAt = Infinity;
|
|
395
|
+
const queue = [];
|
|
396
|
+
const read = async (key) => {
|
|
397
|
+
await this.step();
|
|
398
|
+
if (memory) {
|
|
399
|
+
if (this.updates.has(key)) {
|
|
400
|
+
const incoming = this.updates.get(key);
|
|
401
|
+
const base = this.updateListBases.get(key);
|
|
402
|
+
if (!fromNetwork && incoming && base && key.startsWith("l:")) {
|
|
403
|
+
const stored = await this.node(key);
|
|
404
|
+
if (stored) return applyListUpdate(decodeHydrationValue(stored.value), base, incoming);
|
|
405
|
+
}
|
|
406
|
+
if (incoming && key.startsWith("r:") && this.updateFields.get(key)) {
|
|
407
|
+
const stored = await this.node(key);
|
|
408
|
+
if (stored) return applyRecordUpdate(decodeHydrationValue(stored.value), incoming, this.updateFields.get(key));
|
|
409
|
+
}
|
|
410
|
+
return incoming;
|
|
411
|
+
}
|
|
412
|
+
if (fromNetwork) {
|
|
413
|
+
const id = key.slice(2);
|
|
414
|
+
const value = key.startsWith("r:") ? this.client.store.readConfirmed(id) : key.startsWith("l:") ? this.client.store.readConfirmedList(id) : this.client.getPersistenceQuery(id);
|
|
415
|
+
if (value !== void 0) return value;
|
|
416
|
+
if (this.changed.has(key)) return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const stored = await this.node(key);
|
|
420
|
+
if (stored) for (const claim of Object.values(stored.claims)) fetchedAt = Math.min(fetchedAt, claim.fetchedAt);
|
|
421
|
+
if (!stored) return memory && key.startsWith("r:") ? this.client.store.readConfirmed(key.slice(2)) : void 0;
|
|
422
|
+
const value = decodeHydrationValue(stored.value);
|
|
423
|
+
if (!memory) return value;
|
|
424
|
+
const deleted = (id) => this.updates.has(recordKey(id)) && this.updates.get(recordKey(id)) === void 0;
|
|
425
|
+
if (key.startsWith("l:")) {
|
|
426
|
+
const list = value;
|
|
427
|
+
return {
|
|
428
|
+
...list,
|
|
429
|
+
cursors: list.cursors?.filter((_, index) => !deleted(list.ids[index])),
|
|
430
|
+
ids: list.ids.filter((id) => !deleted(id)),
|
|
431
|
+
liveAfterIds: list.liveAfterIds?.filter((id) => !deleted(id)),
|
|
432
|
+
liveBeforeIds: list.liveBeforeIds?.filter((id) => !deleted(id)),
|
|
433
|
+
pendingAfterIds: list.pendingAfterIds?.filter((id) => !deleted(id)),
|
|
434
|
+
pendingBeforeIds: list.pendingBeforeIds?.filter((id) => !deleted(id))
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (key.startsWith("r:")) {
|
|
438
|
+
const record = value;
|
|
439
|
+
return {
|
|
440
|
+
...record,
|
|
441
|
+
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]))
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return typeof value === "string" && deleted(value) ? null : value;
|
|
445
|
+
};
|
|
446
|
+
const addList = async (key, paths, plan, prefix) => {
|
|
447
|
+
const list = await read(listKey(key));
|
|
448
|
+
if (!list) {
|
|
449
|
+
complete = false;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
nodes.set(listKey(key), {
|
|
453
|
+
paths: /* @__PURE__ */ new Set(),
|
|
454
|
+
value: list
|
|
455
|
+
});
|
|
456
|
+
for (const id of [
|
|
457
|
+
...list.ids,
|
|
458
|
+
...list.pendingBeforeIds ?? [],
|
|
459
|
+
...list.pendingAfterIds ?? [],
|
|
460
|
+
...list.liveBeforeIds ?? [],
|
|
461
|
+
...list.liveAfterIds ?? []
|
|
462
|
+
]) queue.push({
|
|
463
|
+
id,
|
|
464
|
+
paths,
|
|
465
|
+
plan,
|
|
466
|
+
prefix
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
for (const item of request.items) {
|
|
470
|
+
const paths = [...item.plan.paths];
|
|
471
|
+
if (item.kind === "node" || item.kind === "nodes") for (const id of item.ids) queue.push({
|
|
472
|
+
id: toEntityId(item.type, id),
|
|
473
|
+
paths,
|
|
474
|
+
plan: item.plan,
|
|
475
|
+
prefix: ""
|
|
476
|
+
});
|
|
477
|
+
else if (item.kind === "query") {
|
|
478
|
+
const id = await read(queryKey(item.queryKey));
|
|
479
|
+
if (id === void 0) {
|
|
480
|
+
complete = false;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
nodes.set(queryKey(item.queryKey), {
|
|
484
|
+
paths: /* @__PURE__ */ new Set(),
|
|
485
|
+
value: id
|
|
486
|
+
});
|
|
487
|
+
if (typeof id === "string") queue.push({
|
|
488
|
+
id,
|
|
489
|
+
paths,
|
|
490
|
+
plan: item.plan,
|
|
491
|
+
prefix: ""
|
|
492
|
+
});
|
|
493
|
+
} else if (item.kind === "list") await addList(item.listKey, paths, item.plan, "");
|
|
494
|
+
}
|
|
495
|
+
for (let index = 0; index < queue.length; index++) {
|
|
496
|
+
const { id, paths, plan, prefix } = queue[index];
|
|
497
|
+
const key = recordKey(id);
|
|
498
|
+
const seen = nodes.get(key);
|
|
499
|
+
const contexts = visited.get(plan) ?? /* @__PURE__ */ new Map();
|
|
500
|
+
visited.set(plan, contexts);
|
|
501
|
+
const context = JSON.stringify([id, prefix]);
|
|
502
|
+
const traversed = contexts.get(context);
|
|
503
|
+
const needed = paths.filter((path) => !traversed?.has(path));
|
|
504
|
+
if (traversed && !needed.length) continue;
|
|
505
|
+
contexts.set(context, /* @__PURE__ */ new Set([...traversed ?? [], ...paths]));
|
|
506
|
+
const source = await read(key);
|
|
507
|
+
if (!source) {
|
|
508
|
+
complete = false;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (paths.some((path) => !source.paths.some((covered) => path === covered || path.startsWith(`${covered}.`)))) complete = false;
|
|
512
|
+
const all = /* @__PURE__ */ new Set([...seen?.paths ?? [], ...paths]);
|
|
513
|
+
nodes.set(key, {
|
|
514
|
+
paths: all,
|
|
515
|
+
value: project(source, [...all])
|
|
516
|
+
});
|
|
517
|
+
const groups = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const path of needed) {
|
|
519
|
+
const [field, ...rest] = path.split(".");
|
|
520
|
+
const children = groups.get(field) ?? [];
|
|
521
|
+
if (rest.length) children.push(rest.join("."));
|
|
522
|
+
groups.set(field, children);
|
|
523
|
+
}
|
|
524
|
+
const walk = (value, children, childPrefix) => {
|
|
525
|
+
if (isNodeRef(value)) queue.push({
|
|
526
|
+
id: getNodeRefId(value),
|
|
527
|
+
paths: children,
|
|
528
|
+
plan,
|
|
529
|
+
prefix: childPrefix
|
|
530
|
+
});
|
|
531
|
+
else if (Array.isArray(value)) for (const item of value) walk(item, children, childPrefix);
|
|
532
|
+
else if (value && typeof value === "object" && children.length) for (const child of children) {
|
|
533
|
+
const [field, ...rest] = child.split(".");
|
|
534
|
+
walk(value[field], rest.length ? [rest.join(".")] : [], `${childPrefix}.${field}`);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
for (const [field, children] of groups) {
|
|
538
|
+
const childPrefix = prefix ? `${prefix}.${field}` : field;
|
|
539
|
+
const value = source.record[field];
|
|
540
|
+
if (Array.isArray(value)) {
|
|
541
|
+
const nestedKey = getListKey(id, field, plan.args.get(childPrefix)?.hash);
|
|
542
|
+
const list = await read(listKey(nestedKey));
|
|
543
|
+
if (list) {
|
|
544
|
+
const projected = nodes.get(key).value;
|
|
545
|
+
projected.record[field] = list.ids.map(createNodeRef);
|
|
546
|
+
await addList(nestedKey, children, plan, childPrefix);
|
|
547
|
+
} else walk(value, children, childPrefix);
|
|
548
|
+
} else walk(value, children, childPrefix);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
complete,
|
|
553
|
+
fetchedAt,
|
|
554
|
+
nodes
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async restore(request) {
|
|
558
|
+
await this.storage.exclusive(`${this.key}:write`, async () => {
|
|
559
|
+
if (!await this.active()) return;
|
|
560
|
+
const { complete, nodes } = await this.collect(request, false);
|
|
561
|
+
if (!complete) return;
|
|
562
|
+
for (const [key, node] of nodes) {
|
|
563
|
+
this.assertActive();
|
|
564
|
+
const id = key.slice(2);
|
|
565
|
+
const state = {
|
|
566
|
+
rootLists: [],
|
|
567
|
+
rootRequests: [],
|
|
568
|
+
store: {
|
|
569
|
+
coverage: [],
|
|
570
|
+
lists: [],
|
|
571
|
+
records: []
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
if (key.startsWith("r:")) {
|
|
575
|
+
const value = node.value;
|
|
576
|
+
state.store.records.push([id, value.record]);
|
|
577
|
+
state.store.coverage.push([id, value.paths]);
|
|
578
|
+
} else if (key.startsWith("l:")) {
|
|
579
|
+
state.store.lists.push([id, node.value]);
|
|
580
|
+
for (const item of request.items) if (item.kind === "list" && item.listKey === id && !filterConnectionArgs(item.argsPayload)) state.rootLists.push([item.type, [id]]);
|
|
581
|
+
} else state.rootRequests.push([id, node.value]);
|
|
582
|
+
this.suppressed = true;
|
|
583
|
+
try {
|
|
584
|
+
this.client.restorePersistenceData(state);
|
|
585
|
+
} finally {
|
|
586
|
+
this.suppressed = false;
|
|
587
|
+
}
|
|
588
|
+
await this.step();
|
|
589
|
+
}
|
|
590
|
+
const root = await this.read(this.rootKey(request.key));
|
|
591
|
+
if (root && root.fetchedAt + root.maxAge > Date.now()) {
|
|
592
|
+
const old = `${this.prefix}lru:${stamp(root.usedAt)}:${request.key}`;
|
|
593
|
+
root.usedAt = Date.now();
|
|
594
|
+
await this.put([
|
|
595
|
+
[old, void 0],
|
|
596
|
+
[this.rootKey(request.key), root],
|
|
597
|
+
[`${this.prefix}lru:${stamp(root.usedAt)}:${request.key}`, request.key]
|
|
598
|
+
]);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
async remove(rootId) {
|
|
603
|
+
const root = await this.read(this.rootKey(rootId));
|
|
604
|
+
if (!root) return;
|
|
605
|
+
for (const key of root.nodes) {
|
|
606
|
+
const node = await this.read(this.nodeKey(key));
|
|
607
|
+
if (node) {
|
|
608
|
+
delete node.claims[rootId];
|
|
609
|
+
const live = Object.values(node.claims).filter((claim) => claim.expiresAt > Date.now());
|
|
610
|
+
if (key.startsWith("r:") && live.length) node.value = encodeHydrationValue(project(decodeHydrationValue(node.value), live.flatMap((claim) => claim.paths)));
|
|
611
|
+
await this.put([[this.nodeKey(key), live.length ? node : void 0]]);
|
|
612
|
+
}
|
|
613
|
+
await this.step();
|
|
614
|
+
}
|
|
615
|
+
await this.put([
|
|
616
|
+
[this.rootKey(rootId), void 0],
|
|
617
|
+
[`${this.prefix}lru:${stamp(root.usedAt)}:${rootId}`, void 0],
|
|
618
|
+
[`${this.prefix}expiry:${stamp(root.fetchedAt + root.maxAge)}:${rootId}`, void 0]
|
|
619
|
+
]);
|
|
620
|
+
}
|
|
621
|
+
async save(request, fetchedAt, maxAge, usedAt = Date.now(), fromNetwork = false) {
|
|
622
|
+
if (!maxAge || fetchedAt + maxAge <= Date.now()) {
|
|
623
|
+
await this.remove(request.key);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const { nodes } = await this.collect(request, true, fromNetwork);
|
|
627
|
+
const root = {
|
|
628
|
+
fetchedAt,
|
|
629
|
+
maxAge,
|
|
630
|
+
nodes: [...nodes.keys()],
|
|
631
|
+
request: encodeRequests([request]),
|
|
632
|
+
usedAt
|
|
633
|
+
};
|
|
634
|
+
let minimum = sized(this.rootKey(request.key), root).bytes + sized(`${this.prefix}lru:${stamp(usedAt)}:${request.key}`, request.key).bytes + sized(`${this.prefix}expiry:${stamp(fetchedAt + maxAge)}:${request.key}`, request.key).bytes + sized(`${this.prefix}scope`, this.client.getPersistenceScope()).bytes;
|
|
635
|
+
for (const [key, node] of nodes) {
|
|
636
|
+
minimum += sized(this.nodeKey(key), {
|
|
637
|
+
claims: { [request.key]: {
|
|
638
|
+
expiresAt: fetchedAt + maxAge,
|
|
639
|
+
fetchedAt,
|
|
640
|
+
paths: [...node.paths]
|
|
641
|
+
} },
|
|
642
|
+
value: encodeHydrationValue(node.value)
|
|
643
|
+
}).bytes;
|
|
644
|
+
await this.step();
|
|
645
|
+
}
|
|
646
|
+
if (minimum + this.journalBytes + this.overhead > this.maxBytes) {
|
|
647
|
+
await this.remove(request.key);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const prepare = async () => {
|
|
651
|
+
const old = await this.read(this.rootKey(request.key));
|
|
652
|
+
const writes = /* @__PURE__ */ new Map();
|
|
653
|
+
if (old) {
|
|
654
|
+
writes.set(`${this.prefix}lru:${stamp(old.usedAt)}:${request.key}`, void 0);
|
|
655
|
+
writes.set(`${this.prefix}expiry:${stamp(old.fetchedAt + old.maxAge)}:${request.key}`, void 0);
|
|
656
|
+
}
|
|
657
|
+
writes.set(this.rootKey(request.key), {
|
|
658
|
+
...root,
|
|
659
|
+
nodes: [.../* @__PURE__ */ new Set([...root.nodes, ...old?.nodes ?? []])]
|
|
660
|
+
});
|
|
661
|
+
writes.set(`${this.prefix}lru:${stamp(root.usedAt)}:${request.key}`, request.key);
|
|
662
|
+
writes.set(`${this.prefix}expiry:${stamp(fetchedAt + maxAge)}:${request.key}`, request.key);
|
|
663
|
+
for (const key of /* @__PURE__ */ new Set([...nodes.keys(), ...old?.nodes ?? []])) {
|
|
664
|
+
const previous = await this.node(key);
|
|
665
|
+
const next = nodes.get(key);
|
|
666
|
+
const claims = previous?.claims ?? {};
|
|
667
|
+
if (!next) delete claims[request.key];
|
|
668
|
+
else claims[request.key] = {
|
|
669
|
+
expiresAt: fetchedAt + maxAge,
|
|
670
|
+
fetchedAt,
|
|
671
|
+
paths: [...next.paths]
|
|
672
|
+
};
|
|
673
|
+
let value = next?.value;
|
|
674
|
+
if (previous && key.startsWith("r:")) {
|
|
675
|
+
const before = decodeHydrationValue(previous.value);
|
|
676
|
+
const after = next?.value;
|
|
677
|
+
value = project({
|
|
678
|
+
paths: [.../* @__PURE__ */ new Set([...before.paths, ...after?.paths ?? []])],
|
|
679
|
+
record: {
|
|
680
|
+
...before.record,
|
|
681
|
+
...after?.record
|
|
682
|
+
}
|
|
683
|
+
}, Object.values(claims).flatMap((claim) => claim.paths));
|
|
684
|
+
}
|
|
685
|
+
writes.set(this.nodeKey(key), Object.keys(claims).length ? {
|
|
686
|
+
claims,
|
|
687
|
+
value: value !== void 0 ? encodeHydrationValue(value) : previous.value
|
|
688
|
+
} : void 0);
|
|
689
|
+
await this.step();
|
|
690
|
+
}
|
|
691
|
+
return writes;
|
|
692
|
+
};
|
|
693
|
+
let writes = await prepare();
|
|
694
|
+
for (;;) {
|
|
695
|
+
let added = 0;
|
|
696
|
+
for (const [key, value] of writes) {
|
|
697
|
+
const before = await this.storage.read(key);
|
|
698
|
+
added += Math.max(0, (value === void 0 ? 0 : sized(key, value).bytes) - (before?.bytes ?? 0));
|
|
699
|
+
await this.step();
|
|
700
|
+
}
|
|
701
|
+
if ((await this.storage.read(`${this.prefix}bytes`) ?? 0) + added + this.journalBytes + this.overhead <= this.maxBytes) break;
|
|
702
|
+
const candidate = (await this.storage.scan(`${this.prefix}lru:`, void 0, 2)).find((entry) => entry.value.value !== request.key);
|
|
703
|
+
if (!candidate) {
|
|
704
|
+
await this.remove(request.key);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const id = candidate.value.value;
|
|
708
|
+
if (await this.read(this.rootKey(id))) await this.remove(id);
|
|
709
|
+
else await this.put([[candidate.key, void 0]]);
|
|
710
|
+
writes = await prepare();
|
|
711
|
+
}
|
|
712
|
+
const entries = [...writes];
|
|
713
|
+
for (let index = 0; index < entries.length; index += 64) {
|
|
714
|
+
await this.put(entries.slice(index, index + 64));
|
|
715
|
+
await yieldTask$1();
|
|
716
|
+
}
|
|
717
|
+
await this.put([[this.rootKey(request.key), root]]);
|
|
718
|
+
}
|
|
719
|
+
/** Called under the shared write lock. Expiry/LRU indexes avoid whole-cache scans. */
|
|
720
|
+
async prune(journalBytes) {
|
|
721
|
+
this.journalBytes = journalBytes;
|
|
722
|
+
while (true) {
|
|
723
|
+
const [entry] = await this.storage.scan(`${this.prefix}expiry:`, void 0, 1);
|
|
724
|
+
if (!entry) break;
|
|
725
|
+
const id = entry.value.value;
|
|
726
|
+
const root = await this.read(this.rootKey(id));
|
|
727
|
+
if (root && root.fetchedAt + root.maxAge > Date.now()) break;
|
|
728
|
+
if (root) await this.remove(id);
|
|
729
|
+
else await this.put([[entry.key, void 0]]);
|
|
730
|
+
await this.step();
|
|
731
|
+
}
|
|
732
|
+
while ((await this.storage.read(`${this.prefix}bytes`) ?? 0) + journalBytes + this.overhead > this.maxBytes) {
|
|
733
|
+
const [entry] = await this.storage.scan(`${this.prefix}lru:`, void 0, 1);
|
|
734
|
+
if (!entry) break;
|
|
735
|
+
const id = entry.value.value;
|
|
736
|
+
if (await this.read(this.rootKey(id))) await this.remove(id);
|
|
737
|
+
else await this.put([[entry.key, void 0]]);
|
|
738
|
+
await this.step();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async fits(journalBytes) {
|
|
742
|
+
return (await this.storage.read(`${this.prefix}bytes`) ?? 0) + journalBytes + this.overhead <= this.maxBytes;
|
|
743
|
+
}
|
|
744
|
+
async flush(journalBytes) {
|
|
745
|
+
if (!await this.active()) {
|
|
746
|
+
this.reset();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
await this.prune(journalBytes);
|
|
750
|
+
const pending = this.pending;
|
|
751
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
752
|
+
const changes = this.changed;
|
|
753
|
+
const fields = this.fields;
|
|
754
|
+
const listBases = this.listBases;
|
|
755
|
+
this.listBases = /* @__PURE__ */ new Map();
|
|
756
|
+
const recovery = this.recoveryChanges;
|
|
757
|
+
this.recoveryChanges = /* @__PURE__ */ new Map();
|
|
758
|
+
const updates = new Map(recovery);
|
|
759
|
+
for (const [key, value] of changes) updates.set(key, mergeCacheUpdate(key, updates.get(key), {
|
|
760
|
+
fields: fields.get(key) ? [...fields.get(key)] : void 0,
|
|
761
|
+
previousList: listBases.get(key),
|
|
762
|
+
value
|
|
763
|
+
}));
|
|
764
|
+
this.updates = new Map([...updates].map(([key, update]) => [key, update.value]));
|
|
765
|
+
this.changed = /* @__PURE__ */ new Map();
|
|
766
|
+
this.updateListBases = new Map([...updates].map(([key, update]) => [key, update.previousList]));
|
|
767
|
+
this.updateFields = new Map([...updates].map(([key, update]) => [key, update.fields ? new Set(update.fields) : void 0]));
|
|
768
|
+
this.fields = /* @__PURE__ */ new Map();
|
|
769
|
+
try {
|
|
770
|
+
const affected = /* @__PURE__ */ new Map();
|
|
771
|
+
for (const key of this.updates.keys()) {
|
|
772
|
+
const node = await this.node(key);
|
|
773
|
+
const incoming = this.updates.get(key);
|
|
774
|
+
if (node && incoming && key.startsWith("r:")) {
|
|
775
|
+
const before = decodeHydrationValue(node.value);
|
|
776
|
+
const after = applyRecordUpdate(before, incoming, this.updateFields.get(key));
|
|
777
|
+
if (!Object.keys({
|
|
778
|
+
...before.record,
|
|
779
|
+
...after.record
|
|
780
|
+
}).some((field) => references(before.record[field]) !== references(after.record[field]))) {
|
|
781
|
+
const value = encodeHydrationValue(project({
|
|
782
|
+
paths: [.../* @__PURE__ */ new Set([...before.paths, ...after.paths])],
|
|
783
|
+
record: {
|
|
784
|
+
...before.record,
|
|
785
|
+
...after.record
|
|
786
|
+
}
|
|
787
|
+
}, Object.values(node.claims).flatMap((claim) => claim.paths)));
|
|
788
|
+
const next = {
|
|
789
|
+
...node,
|
|
790
|
+
value
|
|
791
|
+
};
|
|
792
|
+
const beforeSize = (await this.storage.read(this.nodeKey(key))).bytes;
|
|
793
|
+
const growth = Math.max(0, sized(this.nodeKey(key), next).bytes - beforeSize);
|
|
794
|
+
await this.prune(journalBytes + growth);
|
|
795
|
+
const retained = await this.node(key);
|
|
796
|
+
if (retained) {
|
|
797
|
+
next.claims = retained.claims;
|
|
798
|
+
next.value = encodeHydrationValue(project(decodeHydrationValue(value), Object.values(retained.claims).flatMap((claim) => claim.paths)));
|
|
799
|
+
await this.put([[this.nodeKey(key), next]]);
|
|
800
|
+
}
|
|
801
|
+
this.journalBytes = journalBytes;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (const rootId of Object.keys(node?.claims ?? {})) {
|
|
806
|
+
if (pending.has(rootId) || affected.has(rootId)) continue;
|
|
807
|
+
const root = await this.read(this.rootKey(rootId));
|
|
808
|
+
if (root) affected.set(rootId, root);
|
|
809
|
+
}
|
|
810
|
+
await this.step();
|
|
811
|
+
}
|
|
812
|
+
for (const root of affected.values()) await this.save(decodeRequests(root.request)[0], root.fetchedAt, root.maxAge, root.usedAt);
|
|
813
|
+
for (const { fetchedAt, maxAge, request } of pending.values()) if (fetchedAt !== void 0) await this.save(request, fetchedAt, maxAge, Date.now(), true);
|
|
814
|
+
for (const { fetchedAt, maxAge, request } of pending.values()) {
|
|
815
|
+
if (fetchedAt !== void 0) continue;
|
|
816
|
+
if (!maxAge) {
|
|
817
|
+
await this.remove(request.key);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const cached = await this.collect(request, false);
|
|
821
|
+
if (cached.complete && Number.isFinite(cached.fetchedAt)) {
|
|
822
|
+
const root = await this.read(this.rootKey(request.key));
|
|
823
|
+
await this.save(request, root?.fetchedAt ?? cached.fetchedAt, maxAge);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
this.updates.clear();
|
|
827
|
+
await this.prune(journalBytes);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (!this.disposed) {
|
|
830
|
+
for (const [key, update] of recovery) this.recoveryChanges.set(key, this.recoveryChanges.has(key) ? mergeCacheUpdate(key, update, this.recoveryChanges.get(key)) : update);
|
|
831
|
+
for (const [key, value] of changes) {
|
|
832
|
+
if (key.startsWith("l:")) this.listBases.set(key, listBases.get(key));
|
|
833
|
+
if (!this.changed.has(key)) {
|
|
834
|
+
this.changed.set(key, value);
|
|
835
|
+
this.fields.set(key, fields.get(key));
|
|
836
|
+
} else if (key.startsWith("r:")) {
|
|
837
|
+
const before = fields.get(key);
|
|
838
|
+
const after = this.fields.get(key);
|
|
839
|
+
this.fields.set(key, before && after ? /* @__PURE__ */ new Set([...before, ...after]) : void 0);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
for (const [key, entry] of pending) if (!this.pending.has(key)) {
|
|
843
|
+
this.pending.set(key, entry);
|
|
844
|
+
pending.delete(key);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
throw error;
|
|
848
|
+
} finally {
|
|
849
|
+
this.updates.clear();
|
|
850
|
+
this.updateFields.clear();
|
|
851
|
+
this.updateListBases.clear();
|
|
852
|
+
for (const entry of pending.values()) entry.release();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async clear() {
|
|
856
|
+
this.reset();
|
|
857
|
+
while (true) {
|
|
858
|
+
const entries = await this.storage.scan(this.prefix, void 0, 64);
|
|
859
|
+
if (!entries.length) return;
|
|
860
|
+
await this.storage.writeBatch(entries.map(({ key }) => [key, void 0]));
|
|
861
|
+
await yieldTask$1();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
dispose() {
|
|
865
|
+
this.disposed = true;
|
|
866
|
+
this.reset();
|
|
867
|
+
}
|
|
868
|
+
reset() {
|
|
869
|
+
this.recoveryChanges.clear();
|
|
870
|
+
this.policies = /* @__PURE__ */ new WeakMap();
|
|
871
|
+
for (const entry of this.pending.values()) entry.release();
|
|
872
|
+
this.pending.clear();
|
|
873
|
+
this.changed.clear();
|
|
874
|
+
this.updates.clear();
|
|
875
|
+
this.fields.clear();
|
|
876
|
+
this.updateFields.clear();
|
|
877
|
+
this.listBases.clear();
|
|
878
|
+
this.updateListBases.clear();
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/persistence-journal.ts
|
|
883
|
+
const invalid = () => /* @__PURE__ */ new Error("fate: Unsupported or corrupt persistence data. It has not been overwritten.");
|
|
884
|
+
const valid = (entry) => entry && typeof entry.id === "string" && typeof entry.scope === "string" && [
|
|
885
|
+
"queued",
|
|
886
|
+
"sending",
|
|
887
|
+
"failed",
|
|
888
|
+
"confirmed"
|
|
889
|
+
].includes(entry.status) && Number.isInteger(entry.attempts) && entry.attempts >= 0;
|
|
890
|
+
const same = (left, right) => {
|
|
891
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => left[key] === right[key]);
|
|
892
|
+
};
|
|
893
|
+
const size = (key, value) => new TextEncoder().encode(JSON.stringify([key, value])).byteLength;
|
|
894
|
+
const yieldTask = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
895
|
+
/** All methods run under the account's write lock, including reads across scan pages. */
|
|
896
|
+
var PersistenceJournal = class {
|
|
897
|
+
constructor(storage, key, validate) {
|
|
898
|
+
this.storage = storage;
|
|
899
|
+
this.key = key;
|
|
900
|
+
this.validate = validate;
|
|
901
|
+
this.disposed = false;
|
|
902
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
903
|
+
this.prefix = `${JSON.stringify([key, "journal"])}:`;
|
|
904
|
+
}
|
|
905
|
+
dispose() {
|
|
906
|
+
this.disposed = true;
|
|
907
|
+
this.entries.clear();
|
|
908
|
+
this.prepared = void 0;
|
|
909
|
+
}
|
|
910
|
+
assertActive() {
|
|
911
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
912
|
+
}
|
|
913
|
+
async load() {
|
|
914
|
+
this.assertActive();
|
|
915
|
+
const value = await this.storage.read(this.key);
|
|
916
|
+
this.assertActive();
|
|
917
|
+
if (value == null) {
|
|
918
|
+
this.header = void 0;
|
|
919
|
+
this.entries.clear();
|
|
920
|
+
} else if (value.version === 1) {
|
|
921
|
+
if (!Array.isArray(value.mutations) || value.mutations.some((entry) => !valid(entry)) || new Set(value.mutations.map(({ id }) => id)).size !== value.mutations.length) throw invalid();
|
|
922
|
+
let checked = 0;
|
|
923
|
+
for (const entry of value.mutations) {
|
|
924
|
+
this.validate?.(entry);
|
|
925
|
+
if (++checked % 64 === 0) {
|
|
926
|
+
await yieldTask();
|
|
927
|
+
this.assertActive();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
this.header = void 0;
|
|
931
|
+
this.entries.clear();
|
|
932
|
+
await this.save({ mutations: value.mutations });
|
|
933
|
+
} else if (value.version !== 2 || typeof value.revision !== "string" || !Number.isSafeInteger(value.nextSequence) || value.nextSequence < 0) throw invalid();
|
|
934
|
+
else if (this.header?.revision !== value.revision) {
|
|
935
|
+
const entries = /* @__PURE__ */ new Map();
|
|
936
|
+
let after;
|
|
937
|
+
while (true) {
|
|
938
|
+
const page = await this.storage.scan(this.prefix, after, 64);
|
|
939
|
+
this.assertActive();
|
|
940
|
+
for (const { key, value } of page) {
|
|
941
|
+
const stored = value;
|
|
942
|
+
if (!stored || !valid(stored.entry) || !Number.isSafeInteger(stored.bytes) || stored.bytes <= 0 || entries.has(stored.entry.id)) throw invalid();
|
|
943
|
+
entries.set(stored.entry.id, {
|
|
944
|
+
...stored,
|
|
945
|
+
key
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (page.length < 64) break;
|
|
949
|
+
after = page.at(-1).key;
|
|
950
|
+
await yieldTask();
|
|
951
|
+
}
|
|
952
|
+
this.assertActive();
|
|
953
|
+
this.header = value;
|
|
954
|
+
this.entries = entries;
|
|
955
|
+
}
|
|
956
|
+
this.prepared = void 0;
|
|
957
|
+
return { mutations: [...this.entries.values()].map(({ entry }) => ({ ...entry })) };
|
|
958
|
+
}
|
|
959
|
+
async measure(data) {
|
|
960
|
+
this.assertActive();
|
|
961
|
+
const header = {
|
|
962
|
+
nextSequence: this.header?.nextSequence ?? 0,
|
|
963
|
+
revision: crypto.randomUUID(),
|
|
964
|
+
version: 2
|
|
965
|
+
};
|
|
966
|
+
const entries = /* @__PURE__ */ new Map();
|
|
967
|
+
const writes = [];
|
|
968
|
+
let bytes = 0;
|
|
969
|
+
let count = 0;
|
|
970
|
+
for (const entry of data.mutations) {
|
|
971
|
+
const previous = this.entries.get(entry.id);
|
|
972
|
+
let stored = previous;
|
|
973
|
+
if (!previous || !same(previous.entry, entry)) {
|
|
974
|
+
const key = previous?.key ?? `${this.prefix}${String(header.nextSequence++).padStart(16, "0")}:${entry.id}`;
|
|
975
|
+
const value = {
|
|
976
|
+
bytes: 0,
|
|
977
|
+
entry
|
|
978
|
+
};
|
|
979
|
+
let measured = size(key, value);
|
|
980
|
+
while (measured !== value.bytes) {
|
|
981
|
+
value.bytes = measured;
|
|
982
|
+
measured = size(key, value);
|
|
983
|
+
}
|
|
984
|
+
stored = {
|
|
985
|
+
...value,
|
|
986
|
+
entry: { ...entry },
|
|
987
|
+
key
|
|
988
|
+
};
|
|
989
|
+
writes.push([key, value]);
|
|
990
|
+
}
|
|
991
|
+
entries.set(entry.id, stored);
|
|
992
|
+
bytes += stored.bytes;
|
|
993
|
+
if (++count % 64 === 0) {
|
|
994
|
+
await yieldTask();
|
|
995
|
+
this.assertActive();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
for (const [id, { key }] of this.entries) if (!entries.has(id)) writes.push([key, void 0]);
|
|
999
|
+
bytes += size(this.key, header);
|
|
1000
|
+
this.prepared = {
|
|
1001
|
+
bytes,
|
|
1002
|
+
data,
|
|
1003
|
+
entries,
|
|
1004
|
+
header,
|
|
1005
|
+
writes
|
|
1006
|
+
};
|
|
1007
|
+
return bytes;
|
|
1008
|
+
}
|
|
1009
|
+
async save(data) {
|
|
1010
|
+
this.assertActive();
|
|
1011
|
+
if (this.prepared?.data !== data) await this.measure(data);
|
|
1012
|
+
this.assertActive();
|
|
1013
|
+
const prepared = this.prepared;
|
|
1014
|
+
if (prepared.writes.length || !this.header) {
|
|
1015
|
+
await this.storage.writeBatch([...prepared.writes, [this.key, prepared.header]]);
|
|
1016
|
+
this.assertActive();
|
|
1017
|
+
this.header = prepared.header;
|
|
1018
|
+
this.entries = prepared.entries;
|
|
1019
|
+
}
|
|
1020
|
+
this.prepared = void 0;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
//#endregion
|
|
1024
|
+
//#region src/persistence.ts
|
|
1025
|
+
const encodeCommand = (command) => encodeHydrationValue({
|
|
1026
|
+
...command,
|
|
1027
|
+
plan: command.plan ? serializePlan(command.plan) : void 0
|
|
1028
|
+
});
|
|
1029
|
+
const decodeCommand = (value) => {
|
|
1030
|
+
const command = decodeHydrationValue(value);
|
|
1031
|
+
if (!command || typeof command.key !== "string" || typeof command.entity !== "string" || ![
|
|
1032
|
+
"after",
|
|
1033
|
+
"before",
|
|
1034
|
+
"none"
|
|
1035
|
+
].includes(command.insert)) throw new Error("fate: Invalid persisted mutation.");
|
|
1036
|
+
return {
|
|
1037
|
+
...command,
|
|
1038
|
+
plan: command.plan ? deserializePlan(command.plan) : void 0
|
|
1039
|
+
};
|
|
1040
|
+
};
|
|
1041
|
+
/**
|
|
1042
|
+
* Enables cache persistence and durable delivery through the existing mutation API.
|
|
1043
|
+
* Import this entry point only in applications using persistence. Storage keys must
|
|
1044
|
+
* isolate authenticated accounts/workspaces and match the server's identity scope.
|
|
1045
|
+
*/
|
|
1046
|
+
function createPersistence({ key, maxAge = 864e5, maxBytes = 26214400, online = () => typeof navigator === "undefined" || navigator.onLine, retryDelay = 1e3, storage }) {
|
|
1047
|
+
if (!key || key.length > 1024) throw new Error("fate: Persistence requires an account-scoped key of 1–1024 characters.");
|
|
1048
|
+
if (!Number.isFinite(retryDelay) || retryDelay <= 0) throw new Error("fate: retryDelay must be a positive finite number.");
|
|
1049
|
+
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.");
|
|
1050
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error("fate: Persistence maxBytes must be a positive safe integer.");
|
|
1051
|
+
return { attach: (client) => new Session(client, storage, key, online, retryDelay, maxAge, maxBytes) };
|
|
1052
|
+
}
|
|
1053
|
+
var Session = class {
|
|
1054
|
+
constructor(client, storage, key, online, retryDelay, maxAge, maxBytes) {
|
|
1055
|
+
this.client = client;
|
|
1056
|
+
this.storage = storage;
|
|
1057
|
+
this.key = key;
|
|
1058
|
+
this.online = online;
|
|
1059
|
+
this.retryDelay = retryDelay;
|
|
1060
|
+
this.maxBytes = maxBytes;
|
|
1061
|
+
this.snapshot = {
|
|
1062
|
+
mutations: [],
|
|
1063
|
+
status: "restoring"
|
|
1064
|
+
};
|
|
1065
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1066
|
+
this.commands = /* @__PURE__ */ new Map();
|
|
1067
|
+
this.operations = /* @__PURE__ */ new Map();
|
|
1068
|
+
this.mutationAdmission = Promise.resolve();
|
|
1069
|
+
this.needsCacheRecovery = false;
|
|
1070
|
+
this.needsCheckpoint = false;
|
|
1071
|
+
this.waiters = /* @__PURE__ */ new Map();
|
|
1072
|
+
this.disposed = false;
|
|
1073
|
+
this.draining = false;
|
|
1074
|
+
this.onOnline = () => this.retry();
|
|
1075
|
+
this.getSnapshot = () => this.snapshot;
|
|
1076
|
+
this.subscribe = (listener) => {
|
|
1077
|
+
this.listeners.add(listener);
|
|
1078
|
+
return () => {
|
|
1079
|
+
this.listeners.delete(listener);
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
this.load = () => this.storage.exclusive(`${this.key}:write`, () => this.journal.load());
|
|
1083
|
+
this.update = (apply, admit = false) => this.storage.exclusive(`${this.key}:write`, async () => {
|
|
1084
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1085
|
+
const data = await this.journal.load();
|
|
1086
|
+
const result = await apply(data);
|
|
1087
|
+
const bytes = await this.journal.measure(data);
|
|
1088
|
+
if (admit && bytes > this.maxBytes) throw new Error("fate: Persistence size limit exceeded; unconfirmed mutations have been preserved.");
|
|
1089
|
+
try {
|
|
1090
|
+
await this.cache.prune(bytes);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (admit) throw error;
|
|
1093
|
+
this.report(error);
|
|
1094
|
+
}
|
|
1095
|
+
if (admit && !await this.cache.fits(bytes)) throw new Error("fate: Persistence size limit exceeded; unconfirmed mutations have been preserved.");
|
|
1096
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1097
|
+
await this.journal.save(data);
|
|
1098
|
+
return result;
|
|
1099
|
+
});
|
|
1100
|
+
this.journal = new PersistenceJournal(storage, key, (entry) => {
|
|
1101
|
+
decodeCommand(entry.command);
|
|
1102
|
+
});
|
|
1103
|
+
this.cache = new PersistenceCache(client, storage, key, maxAge, maxBytes);
|
|
1104
|
+
try {
|
|
1105
|
+
client.validatePersistence();
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
this.mutationError = error instanceof Error ? error : new Error(String(error));
|
|
1108
|
+
this.snapshot = {
|
|
1109
|
+
...this.snapshot,
|
|
1110
|
+
error: this.mutationError
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
this.ready = this.restore().catch((error) => {
|
|
1114
|
+
if (!this.disposed) this.snapshot = {
|
|
1115
|
+
...this.snapshot,
|
|
1116
|
+
status: "error"
|
|
1117
|
+
};
|
|
1118
|
+
throw error;
|
|
1119
|
+
});
|
|
1120
|
+
this.ready.catch((error) => this.report(error));
|
|
1121
|
+
}
|
|
1122
|
+
command(entry) {
|
|
1123
|
+
let cached = this.commands.get(entry.id);
|
|
1124
|
+
if (cached?.value !== entry.command) {
|
|
1125
|
+
cached = {
|
|
1126
|
+
command: decodeCommand(entry.command),
|
|
1127
|
+
input: decodeHydrationValue(entry.command).input,
|
|
1128
|
+
value: entry.command
|
|
1129
|
+
};
|
|
1130
|
+
this.commands.set(entry.id, cached);
|
|
1131
|
+
}
|
|
1132
|
+
return cached.command;
|
|
1133
|
+
}
|
|
1134
|
+
publish(data) {
|
|
1135
|
+
if (data) this.snapshot = {
|
|
1136
|
+
...this.snapshot,
|
|
1137
|
+
mutations: data.mutations.filter((entry) => entry.status !== "confirmed").map((entry) => ({
|
|
1138
|
+
error: entry.error?.message,
|
|
1139
|
+
id: entry.id,
|
|
1140
|
+
input: this.commands.get(entry.id).input,
|
|
1141
|
+
name: this.command(entry).key,
|
|
1142
|
+
status: entry.status
|
|
1143
|
+
}))
|
|
1144
|
+
};
|
|
1145
|
+
for (const listener of this.listeners) try {
|
|
1146
|
+
listener();
|
|
1147
|
+
} catch {}
|
|
1148
|
+
}
|
|
1149
|
+
report(error) {
|
|
1150
|
+
if (this.disposed) return;
|
|
1151
|
+
this.snapshot = {
|
|
1152
|
+
...this.snapshot,
|
|
1153
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1154
|
+
status: this.snapshot.status === "restoring" ? "error" : this.snapshot.status
|
|
1155
|
+
};
|
|
1156
|
+
this.publish();
|
|
1157
|
+
}
|
|
1158
|
+
async restore() {
|
|
1159
|
+
const data = await this.load();
|
|
1160
|
+
await this.cache.initialize();
|
|
1161
|
+
if (this.disposed) return;
|
|
1162
|
+
for (const entry of data.mutations) if (entry.status === "queued" || entry.status === "sending") {
|
|
1163
|
+
if (entry.base && entry.base.scope === this.client.getPersistenceScope()) this.cache.restoreBase(decodeClientHydrationState(entry.base.data));
|
|
1164
|
+
}
|
|
1165
|
+
this.snapshot = {
|
|
1166
|
+
error: this.mutationError,
|
|
1167
|
+
mutations: [],
|
|
1168
|
+
status: "ready"
|
|
1169
|
+
};
|
|
1170
|
+
await this.reconcile(data);
|
|
1171
|
+
if (this.needsCacheRecovery) try {
|
|
1172
|
+
await this.flushCache();
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
this.report(error);
|
|
1175
|
+
}
|
|
1176
|
+
if (this.disposed) return;
|
|
1177
|
+
this.unsubscribe = this.storage.subscribe?.(this.key, () => {
|
|
1178
|
+
this.refresh().catch((error) => this.report(error));
|
|
1179
|
+
});
|
|
1180
|
+
if (typeof globalThis.addEventListener === "function") globalThis.addEventListener("online", this.onOnline);
|
|
1181
|
+
this.retry();
|
|
1182
|
+
}
|
|
1183
|
+
async reconcile(data) {
|
|
1184
|
+
if (this.disposed) return;
|
|
1185
|
+
let processed = 0;
|
|
1186
|
+
for (const entry of data.mutations) {
|
|
1187
|
+
if (++processed % 64 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1188
|
+
if (this.disposed) return;
|
|
1189
|
+
this.command(entry);
|
|
1190
|
+
if (entry.status === "confirmed" || entry.status === "failed") {
|
|
1191
|
+
const operation = this.operations.get(entry.id);
|
|
1192
|
+
if (entry.status === "confirmed") {
|
|
1193
|
+
this.needsCheckpoint = true;
|
|
1194
|
+
const result = operation || this.waiters.has(entry.id) ? decodeHydrationValue(entry.result) : void 0;
|
|
1195
|
+
operation?.commit(result);
|
|
1196
|
+
if (entry.cacheUpdates && entry.scope === this.client.getPersistenceScope()) this.needsCacheRecovery = true;
|
|
1197
|
+
this.waiters.get(entry.id)?.resolve(result);
|
|
1198
|
+
} else {
|
|
1199
|
+
operation?.rollback();
|
|
1200
|
+
this.waiters.get(entry.id)?.reject(new FateRequestError("BAD_REQUEST", entry.error?.message ?? "Mutation failed.", { status: entry.error?.status ?? 400 }));
|
|
1201
|
+
}
|
|
1202
|
+
this.operations.delete(entry.id);
|
|
1203
|
+
this.waiters.delete(entry.id);
|
|
1204
|
+
} else if (!this.operations.has(entry.id) && entry.scope === this.client.getPersistenceScope()) {
|
|
1205
|
+
if (entry.base) this.cache.restoreBase(decodeClientHydrationState(entry.base.data));
|
|
1206
|
+
const command = this.command(entry);
|
|
1207
|
+
if (this.client.hasMutation(command.key, command.entity)) this.operations.set(entry.id, this.prepareOperation(command));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
const retained = new Set(data.mutations.map(({ id }) => id));
|
|
1211
|
+
for (const id of this.commands.keys()) if (!retained.has(id)) this.commands.delete(id);
|
|
1212
|
+
this.publish(data);
|
|
1213
|
+
}
|
|
1214
|
+
prepareOperation(command) {
|
|
1215
|
+
const operation = prepareMutation(this.client, command, this.client.getTypeConfig(command.entity), true);
|
|
1216
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
1217
|
+
let cacheUpdates;
|
|
1218
|
+
if (operation.entityId) this.client.registerPendingOptimisticMutation(operation.entityId, promise);
|
|
1219
|
+
return {
|
|
1220
|
+
...operation,
|
|
1221
|
+
commit: (result, persist = true) => {
|
|
1222
|
+
if (persist) cacheUpdates ??= this.cache.captureChanges(() => operation.commit(result));
|
|
1223
|
+
else this.cache.withoutChanges(() => operation.commit(result));
|
|
1224
|
+
resolve();
|
|
1225
|
+
},
|
|
1226
|
+
getCacheUpdates: () => cacheUpdates,
|
|
1227
|
+
rollback: (persist = true) => {
|
|
1228
|
+
try {
|
|
1229
|
+
if (persist) operation.rollback();
|
|
1230
|
+
else this.cache.withoutChanges(() => operation.rollback());
|
|
1231
|
+
} finally {
|
|
1232
|
+
resolve();
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
refresh() {
|
|
1238
|
+
return this.refreshing ??= this.load().then(async (data) => {
|
|
1239
|
+
await this.reconcile(data);
|
|
1240
|
+
this.retry();
|
|
1241
|
+
}).finally(() => {
|
|
1242
|
+
this.refreshing = void 0;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async mutate(command) {
|
|
1246
|
+
if (this.mutationError) throw this.mutationError;
|
|
1247
|
+
const encoded = encodeCommand(command);
|
|
1248
|
+
await this.ready;
|
|
1249
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1250
|
+
const id = crypto.randomUUID();
|
|
1251
|
+
const admission = this.mutationAdmission.then(async () => {
|
|
1252
|
+
if (this.needsCheckpoint) await this.flushCache().catch((error) => this.report(error));
|
|
1253
|
+
return this.update(async (data) => {
|
|
1254
|
+
await this.reconcile(data);
|
|
1255
|
+
const base = await this.cache.mutationBase(decodeCommand(encoded));
|
|
1256
|
+
data.mutations.push({
|
|
1257
|
+
attempts: 0,
|
|
1258
|
+
base,
|
|
1259
|
+
command: encoded,
|
|
1260
|
+
id,
|
|
1261
|
+
scope: base.scope,
|
|
1262
|
+
status: "queued"
|
|
1263
|
+
});
|
|
1264
|
+
return base;
|
|
1265
|
+
}, true);
|
|
1266
|
+
});
|
|
1267
|
+
this.mutationAdmission = admission.catch(() => {});
|
|
1268
|
+
const base = await admission;
|
|
1269
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1270
|
+
this.cache.restoreBase(decodeClientHydrationState(base.data));
|
|
1271
|
+
const result = new Promise((resolve, reject) => this.waiters.set(id, {
|
|
1272
|
+
reject,
|
|
1273
|
+
resolve
|
|
1274
|
+
}));
|
|
1275
|
+
try {
|
|
1276
|
+
if (!this.operations.has(id)) this.operations.set(id, this.prepareOperation(decodeCommand(encoded)));
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
await this.fail(id, error);
|
|
1279
|
+
}
|
|
1280
|
+
this.refresh().catch((error) => this.report(error));
|
|
1281
|
+
this.retry();
|
|
1282
|
+
return result;
|
|
1283
|
+
}
|
|
1284
|
+
async restoreRequest(request) {
|
|
1285
|
+
await this.ready;
|
|
1286
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1287
|
+
try {
|
|
1288
|
+
if (this.needsCacheRecovery) await this.flush();
|
|
1289
|
+
await this.cache.restore(request);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
if (this.disposed) throw error;
|
|
1292
|
+
this.report(error);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
fetched(request, options) {
|
|
1296
|
+
if (this.disposed) return;
|
|
1297
|
+
this.cache.fetched(request, options?.maxAge);
|
|
1298
|
+
this.changed();
|
|
1299
|
+
}
|
|
1300
|
+
used(request, options) {
|
|
1301
|
+
if (this.disposed) return;
|
|
1302
|
+
this.cache.fetched(request, options?.maxAge, false);
|
|
1303
|
+
this.changed();
|
|
1304
|
+
}
|
|
1305
|
+
changed(kind, key, paths, previousList) {
|
|
1306
|
+
if (this.disposed) return;
|
|
1307
|
+
if (this.cache.changedNode(kind, key, paths, previousList)) this.needsCacheRecovery = true;
|
|
1308
|
+
if (this.disposed || this.snapshot.status !== "ready") return;
|
|
1309
|
+
if (!this.saveTimer) this.saveTimer = setTimeout(() => {
|
|
1310
|
+
this.saveTimer = void 0;
|
|
1311
|
+
this.flush().catch((error) => this.report(error));
|
|
1312
|
+
}, 25);
|
|
1313
|
+
}
|
|
1314
|
+
async flush() {
|
|
1315
|
+
await this.ready;
|
|
1316
|
+
await this.flushCache();
|
|
1317
|
+
}
|
|
1318
|
+
async flushCache() {
|
|
1319
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1320
|
+
await this.storage.exclusive(`${this.key}:write`, async () => {
|
|
1321
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1322
|
+
const data = await this.journal.load();
|
|
1323
|
+
await this.reconcile(data);
|
|
1324
|
+
this.cache.replayChanges(data.mutations.flatMap((entry) => entry.cacheUpdates && entry.scope === this.client.getPersistenceScope() ? [entry.cacheUpdates] : []));
|
|
1325
|
+
if (data.mutations.some((entry) => entry.status === "queued" || entry.status === "sending")) {
|
|
1326
|
+
for (const entry of data.mutations) if ((entry.status === "queued" || entry.status === "sending") && entry.scope === this.client.getPersistenceScope()) entry.base = await this.cache.mutationBase(this.command(entry), entry.base);
|
|
1327
|
+
await this.cache.prune(await this.journal.measure(data));
|
|
1328
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1329
|
+
await this.journal.save(data);
|
|
1330
|
+
}
|
|
1331
|
+
await this.cache.flush(await this.journal.measure(data));
|
|
1332
|
+
if (this.disposed) throw new Error("fate: Persistence was disposed.");
|
|
1333
|
+
const confirmed = data.mutations.filter((entry) => entry.status === "confirmed");
|
|
1334
|
+
if (confirmed.length) {
|
|
1335
|
+
const ids = new Set(confirmed.map(({ id }) => id));
|
|
1336
|
+
data.mutations = data.mutations.filter(({ id }) => !ids.has(id));
|
|
1337
|
+
await this.journal.measure(data);
|
|
1338
|
+
await this.journal.save(data);
|
|
1339
|
+
}
|
|
1340
|
+
this.needsCacheRecovery = this.cache.hasChanges;
|
|
1341
|
+
this.needsCheckpoint = false;
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
retry() {
|
|
1345
|
+
if (this.mutationError || this.disposed || this.snapshot.status !== "ready" || this.draining) return;
|
|
1346
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1347
|
+
this.timer = setTimeout(() => {
|
|
1348
|
+
this.timer = void 0;
|
|
1349
|
+
this.drain().catch((error) => {
|
|
1350
|
+
this.report(error);
|
|
1351
|
+
this.scheduleRetry();
|
|
1352
|
+
});
|
|
1353
|
+
}, 0);
|
|
1354
|
+
}
|
|
1355
|
+
scheduleRetry(delay = this.retryDelay) {
|
|
1356
|
+
if (!this.disposed && !this.timer) this.timer = setTimeout(() => {
|
|
1357
|
+
this.timer = void 0;
|
|
1358
|
+
this.retry();
|
|
1359
|
+
}, delay);
|
|
1360
|
+
}
|
|
1361
|
+
async fail(id, error) {
|
|
1362
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
1363
|
+
await this.update((data) => {
|
|
1364
|
+
const entry = data.mutations.find((item) => item.id === id);
|
|
1365
|
+
if (entry) {
|
|
1366
|
+
entry.status = "failed";
|
|
1367
|
+
delete entry.base;
|
|
1368
|
+
entry.error = {
|
|
1369
|
+
message: failure.message,
|
|
1370
|
+
status: getErrorStatusCode(failure)
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
await this.reconcile(await this.load());
|
|
1375
|
+
}
|
|
1376
|
+
async drain() {
|
|
1377
|
+
if (this.draining || this.disposed || !this.online()) return;
|
|
1378
|
+
this.draining = true;
|
|
1379
|
+
try {
|
|
1380
|
+
await this.storage.exclusive(`${this.key}:delivery`, async () => {
|
|
1381
|
+
while (!this.disposed && this.online()) {
|
|
1382
|
+
const data = await this.load();
|
|
1383
|
+
await this.reconcile(data);
|
|
1384
|
+
const retained = new Set(data.mutations.map(({ id }) => id));
|
|
1385
|
+
for (const [id, operation] of this.operations) {
|
|
1386
|
+
if (retained.has(id)) continue;
|
|
1387
|
+
try {
|
|
1388
|
+
const result = await operation.execute({
|
|
1389
|
+
id,
|
|
1390
|
+
replayOnly: true,
|
|
1391
|
+
scope: this.key
|
|
1392
|
+
});
|
|
1393
|
+
if (this.disposed) return;
|
|
1394
|
+
operation.commit(result, false);
|
|
1395
|
+
this.waiters.get(id)?.resolve(result);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (this.disposed) return;
|
|
1398
|
+
if (!(error instanceof Error) || getErrorStatusCode(error) !== 404) {
|
|
1399
|
+
this.report(error);
|
|
1400
|
+
this.scheduleRetry();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
operation.rollback(false);
|
|
1404
|
+
this.waiters.get(id)?.reject(/* @__PURE__ */ new Error("fate: Mutation was discarded."));
|
|
1405
|
+
}
|
|
1406
|
+
this.operations.delete(id);
|
|
1407
|
+
this.waiters.delete(id);
|
|
1408
|
+
}
|
|
1409
|
+
const entry = data.mutations.find((item) => item.status !== "confirmed" && item.status !== "failed");
|
|
1410
|
+
if (!entry) return;
|
|
1411
|
+
if (entry.nextAttemptAt && entry.nextAttemptAt > Date.now()) {
|
|
1412
|
+
this.scheduleRetry(entry.nextAttemptAt - Date.now());
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
const command = this.command(entry);
|
|
1416
|
+
if (entry.scope !== this.client.getPersistenceScope() || !this.client.hasMutation(command.key, command.entity)) {
|
|
1417
|
+
await this.fail(entry.id, new FateRequestError("BAD_REQUEST", "Persisted mutation is incompatible with this client. Its input has been preserved."));
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
const operation = this.operations.get(entry.id);
|
|
1421
|
+
const claimed = await this.update((latest) => {
|
|
1422
|
+
const item = latest.mutations.find((item) => item.id === entry.id);
|
|
1423
|
+
if (!item) return false;
|
|
1424
|
+
item.status = "sending";
|
|
1425
|
+
item.attempts += 1;
|
|
1426
|
+
return true;
|
|
1427
|
+
});
|
|
1428
|
+
if (this.disposed) return;
|
|
1429
|
+
if (!claimed) continue;
|
|
1430
|
+
try {
|
|
1431
|
+
const result = await operation.execute({
|
|
1432
|
+
id: entry.id,
|
|
1433
|
+
scope: this.key
|
|
1434
|
+
});
|
|
1435
|
+
if (this.disposed) return;
|
|
1436
|
+
const encoded = encodeHydrationValue(result);
|
|
1437
|
+
operation.commit(result);
|
|
1438
|
+
await this.update(async (latest) => {
|
|
1439
|
+
this.cache.replayChanges([operation.getCacheUpdates()]);
|
|
1440
|
+
const item = latest.mutations.find((item) => item.id === entry.id);
|
|
1441
|
+
if (item) {
|
|
1442
|
+
item.status = "confirmed";
|
|
1443
|
+
delete item.base;
|
|
1444
|
+
item.result = encoded;
|
|
1445
|
+
item.cacheUpdates = operation.getCacheUpdates();
|
|
1446
|
+
item.error = void 0;
|
|
1447
|
+
}
|
|
1448
|
+
for (const pending of latest.mutations) if ((pending.status === "queued" || pending.status === "sending") && pending.scope === this.client.getPersistenceScope()) pending.base = await this.cache.mutationBase(decodeCommand(pending.command), pending.base);
|
|
1449
|
+
});
|
|
1450
|
+
await this.reconcile(await this.load());
|
|
1451
|
+
await this.flush().catch((error) => this.report(error));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
if (this.disposed) return;
|
|
1454
|
+
const status = error instanceof Error ? getErrorStatusCode(error) : void 0;
|
|
1455
|
+
if (status && status >= 400 && status < 500 && ![
|
|
1456
|
+
401,
|
|
1457
|
+
403,
|
|
1458
|
+
408,
|
|
1459
|
+
429
|
|
1460
|
+
].includes(status)) {
|
|
1461
|
+
await this.fail(entry.id, error);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
await this.update((latest) => {
|
|
1465
|
+
const item = latest.mutations.find((item) => item.id === entry.id);
|
|
1466
|
+
if (item && item.status !== "confirmed") {
|
|
1467
|
+
item.status = "queued";
|
|
1468
|
+
item.nextAttemptAt = Date.now() + Math.min(3e4, this.retryDelay * 2 ** Math.min(item.attempts - 1, 10));
|
|
1469
|
+
item.error = {
|
|
1470
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1471
|
+
status
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
await this.reconcile(await this.load());
|
|
1476
|
+
this.scheduleRetry();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
} finally {
|
|
1482
|
+
this.draining = false;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async discard(id) {
|
|
1486
|
+
await this.ready;
|
|
1487
|
+
if (await this.update((data) => {
|
|
1488
|
+
const entry = data.mutations.find((entry) => entry.id === id);
|
|
1489
|
+
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;
|
|
1491
|
+
data.mutations = data.mutations.filter((entry) => entry.id !== id);
|
|
1492
|
+
return true;
|
|
1493
|
+
})) {
|
|
1494
|
+
this.operations.get(id)?.rollback();
|
|
1495
|
+
this.operations.delete(id);
|
|
1496
|
+
this.waiters.get(id)?.reject(/* @__PURE__ */ new Error("fate: Mutation was discarded."));
|
|
1497
|
+
this.waiters.delete(id);
|
|
1498
|
+
}
|
|
1499
|
+
await this.refresh();
|
|
1500
|
+
}
|
|
1501
|
+
async clearCache() {
|
|
1502
|
+
await this.ready;
|
|
1503
|
+
await this.storage.exclusive(`${this.key}:write`, () => this.cache.clear());
|
|
1504
|
+
await this.cache.initialize();
|
|
1505
|
+
}
|
|
1506
|
+
dispose() {
|
|
1507
|
+
if (this.disposed) return;
|
|
1508
|
+
this.disposed = true;
|
|
1509
|
+
this.client.disposePersistence();
|
|
1510
|
+
this.cache.dispose();
|
|
1511
|
+
this.journal.dispose();
|
|
1512
|
+
clearTimeout(this.timer);
|
|
1513
|
+
clearTimeout(this.saveTimer);
|
|
1514
|
+
this.unsubscribe?.();
|
|
1515
|
+
if (typeof globalThis.removeEventListener === "function") globalThis.removeEventListener("online", this.onOnline);
|
|
1516
|
+
for (const operation of this.operations.values()) operation.rollback();
|
|
1517
|
+
this.operations.clear();
|
|
1518
|
+
this.commands.clear();
|
|
1519
|
+
for (const waiter of this.waiters.values()) waiter.reject(/* @__PURE__ */ new Error("fate: Persistence was disposed; pending mutations remain stored."));
|
|
1520
|
+
this.waiters.clear();
|
|
1521
|
+
this.snapshot = {
|
|
1522
|
+
...this.snapshot,
|
|
1523
|
+
status: "disposed"
|
|
1524
|
+
};
|
|
1525
|
+
this.publish();
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
//#endregion
|
|
1529
|
+
export { createPersistence };
|