@dee-wan/cache 1.0.1 → 2.0.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 +208 -3
- package/dist/chunk-A4WV6RY4.js +311 -0
- package/dist/chunk-A4WV6RY4.js.map +1 -0
- package/dist/chunk-AENVFT2S.js +6 -0
- package/dist/chunk-AENVFT2S.js.map +1 -0
- package/dist/chunk-E6OZZ5UX.js +133 -0
- package/dist/chunk-E6OZZ5UX.js.map +1 -0
- package/dist/chunk-JPK2AF7X.js +18 -0
- package/dist/chunk-JPK2AF7X.js.map +1 -0
- package/dist/chunk-LAXBP5DQ.js +330 -0
- package/dist/chunk-LAXBP5DQ.js.map +1 -0
- package/dist/chunk-LHOK33GI.js +12 -0
- package/dist/chunk-LHOK33GI.js.map +1 -0
- package/dist/chunk-M4UOCIDQ.js +557 -0
- package/dist/chunk-M4UOCIDQ.js.map +1 -0
- package/dist/chunk-NFBMWKY5.js +54 -0
- package/dist/chunk-NFBMWKY5.js.map +1 -0
- package/dist/chunk-O3KHMFHX.js +425 -0
- package/dist/chunk-O3KHMFHX.js.map +1 -0
- package/dist/chunk-WJNCFFF3.js +637 -0
- package/dist/chunk-WJNCFFF3.js.map +1 -0
- package/dist/chunk-WKZ7TKGM.js +82 -0
- package/dist/chunk-WKZ7TKGM.js.map +1 -0
- package/dist/chunk-ZBYAKDNZ.js +45 -0
- package/dist/chunk-ZBYAKDNZ.js.map +1 -0
- package/dist/chunk-ZCMZJKGN.js +78 -0
- package/dist/chunk-ZCMZJKGN.js.map +1 -0
- package/dist/client.d.ts +63 -0
- package/dist/client.js +260 -0
- package/dist/client.js.map +1 -0
- package/dist/cloudflare/index.d.ts +15 -2
- package/dist/cloudflare/index.js +413 -45
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/d1/config.d.ts +41 -0
- package/dist/d1/config.js +102 -0
- package/dist/d1/config.js.map +1 -0
- package/dist/d1/index.d.ts +80 -0
- package/dist/d1/index.js +218 -0
- package/dist/d1/index.js.map +1 -0
- package/dist/d1/response-lease.d.ts +9 -0
- package/dist/d1/response-lease.js +44 -0
- package/dist/d1/response-lease.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -616
- package/dist/index.js.map +1 -1
- package/dist/profile--LBtbQI_.d.ts +30 -0
- package/dist/response-lease-DeeM-ofO.d.ts +19 -0
- package/dist/response-lease.d.ts +7 -0
- package/dist/response-lease.js +14 -0
- package/dist/response-lease.js.map +1 -0
- package/dist/response-warmup-BWQvnT1G.d.ts +38 -0
- package/dist/scoped-read.d.ts +12 -0
- package/dist/scoped-read.js +30 -0
- package/dist/scoped-read.js.map +1 -0
- package/dist/{types-DwtU0KpX.d.ts → types-CtErcz2a.d.ts} +25 -1
- package/docs/cache-contract.md +49 -1
- package/package.json +31 -2
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { PLANNER_VERSION, CONFIG_FORMAT, MAX_LOCK_MS, MAX_LEASE_MS, MAX_VALUE_BYTES, MAX_GENERATION_LENGTH, GLOBAL_FENCE, writeFence, modelFence, entityFence, TTL_JITTER_RATIO, MAX_SHAPE_TTL_SECONDS } from './chunk-OQRNVGQE.js';
|
|
2
|
+
import { sha256Hex, stableStringify, decode, encode } from './chunk-ZCMZJKGN.js';
|
|
3
|
+
|
|
4
|
+
// src/core/pk.ts
|
|
5
|
+
function isPlainRecord(value) {
|
|
6
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function encodePk(parts) {
|
|
9
|
+
return parts.map((part) => encodeURIComponent(String(part))).join(":");
|
|
10
|
+
}
|
|
11
|
+
function unwrapEquals(value) {
|
|
12
|
+
return isPlainRecord(value) && "equals" in value ? value.equals : value;
|
|
13
|
+
}
|
|
14
|
+
function isScalarPk(value) {
|
|
15
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
16
|
+
}
|
|
17
|
+
function pkFromWhere(where, pkFields, compoundName) {
|
|
18
|
+
if (!isPlainRecord(where)) return null;
|
|
19
|
+
const compound = where[compoundName ?? pkFields.join("_")];
|
|
20
|
+
const source = pkFields.length > 1 && isPlainRecord(compound) ? compound : where;
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const field of pkFields) {
|
|
23
|
+
const value = source === where ? unwrapEquals(where[field]) : source[field];
|
|
24
|
+
if (!isScalarPk(value)) return null;
|
|
25
|
+
parts.push(value);
|
|
26
|
+
}
|
|
27
|
+
return encodePk(parts);
|
|
28
|
+
}
|
|
29
|
+
function pkFromRow(row, pkFields) {
|
|
30
|
+
if (!isPlainRecord(row)) return null;
|
|
31
|
+
const parts = [];
|
|
32
|
+
for (const field of pkFields) {
|
|
33
|
+
const value = row[field];
|
|
34
|
+
if (!isScalarPk(value)) return null;
|
|
35
|
+
parts.push(value);
|
|
36
|
+
}
|
|
37
|
+
return encodePk(parts);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/core/plan.ts
|
|
41
|
+
var MAX_NESTED_WRITE_DEPTH = 5;
|
|
42
|
+
var MAX_ENTITY_FENCES_PER_MODEL = 100;
|
|
43
|
+
var READ_OPERATIONS = /* @__PURE__ */ new Set(["aggregate", "count", "findFirst", "findFirstOrThrow", "findMany", "findUnique", "findUniqueOrThrow", "groupBy"]);
|
|
44
|
+
var WRITE_OPERATIONS = /* @__PURE__ */ new Set([
|
|
45
|
+
"create",
|
|
46
|
+
"createMany",
|
|
47
|
+
"createManyAndReturn",
|
|
48
|
+
"delete",
|
|
49
|
+
"deleteMany",
|
|
50
|
+
"update",
|
|
51
|
+
"updateMany",
|
|
52
|
+
"updateManyAndReturn",
|
|
53
|
+
"upsert"
|
|
54
|
+
]);
|
|
55
|
+
var pkFieldsOf = (config, model) => config.primaryKeyFieldsByModel[model] ?? ["id"];
|
|
56
|
+
function touch(plan, model) {
|
|
57
|
+
const existing = plan.touched.get(model);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
const created = { pks: /* @__PURE__ */ new Set(), wide: false };
|
|
60
|
+
plan.touched.set(model, created);
|
|
61
|
+
return created;
|
|
62
|
+
}
|
|
63
|
+
var touchWrite = (ctx, model) => void touch(ctx.plan, model);
|
|
64
|
+
var touchWide = (ctx, model) => void (touch(ctx.plan, model).wide = true);
|
|
65
|
+
function touchEntity(ctx, model, where) {
|
|
66
|
+
const pk = pkFromWhere(where, pkFieldsOf(ctx.config, model), ctx.config.primaryKeyNameByModel[model]);
|
|
67
|
+
if (pk === null) touchWide(ctx, model);
|
|
68
|
+
else touch(ctx.plan, model).pks.add(pk);
|
|
69
|
+
}
|
|
70
|
+
function referencedFields(config, model) {
|
|
71
|
+
const fields = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const relations of Object.values(config.relationGraph)) {
|
|
73
|
+
for (const relation of relations) {
|
|
74
|
+
if (relation.targetModel === model && relation.localFields.length > 0) {
|
|
75
|
+
for (const field of relation.foreignFields) fields.add(field);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return fields;
|
|
80
|
+
}
|
|
81
|
+
function touchReferencing(ctx, model, seen = /* @__PURE__ */ new Set()) {
|
|
82
|
+
if (seen.has(model)) return;
|
|
83
|
+
seen.add(model);
|
|
84
|
+
for (const [owner, relations] of Object.entries(ctx.config.relationGraph)) {
|
|
85
|
+
const references = relations.some((relation) => relation.targetModel === model && relation.localFields.length > 0);
|
|
86
|
+
if (!references) continue;
|
|
87
|
+
touchWide(ctx, owner);
|
|
88
|
+
touchReferencing(ctx, owner, seen);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function touchReferencingIfKeyChanges(ctx, model, data) {
|
|
92
|
+
if (!isPlainRecord(data)) return;
|
|
93
|
+
const referenced = referencedFields(ctx.config, model);
|
|
94
|
+
if (Object.keys(data).some((field) => referenced.has(field))) touchReferencing(ctx, model);
|
|
95
|
+
}
|
|
96
|
+
var asItems = (value) => Array.isArray(value) ? value : [value];
|
|
97
|
+
var ONE_TO_ONE_REPARENTING_OPS = /* @__PURE__ */ new Set(["connect", "connectOrCreate", "create", "upsert"]);
|
|
98
|
+
var FOREIGN_KEY_CHANGING_OPS = /* @__PURE__ */ new Set(["connect", "connectOrCreate", "create", "delete", "disconnect", "set", "upsert"]);
|
|
99
|
+
var overlaps = (fields, referenced) => fields.some((field) => referenced.has(field));
|
|
100
|
+
function widenIfForeignKeyReferenced(ctx, model, relation) {
|
|
101
|
+
if (overlaps(relation.localFields, referencedFields(ctx.config, model))) touchReferencing(ctx, model);
|
|
102
|
+
const opposite = (ctx.config.relationGraph[relation.targetModel] ?? []).find(
|
|
103
|
+
(candidate) => candidate !== relation && candidate.relationName !== void 0 && candidate.relationName === relation.relationName
|
|
104
|
+
);
|
|
105
|
+
if (opposite && overlaps(opposite.localFields, referencedFields(ctx.config, relation.targetModel))) {
|
|
106
|
+
touchReferencing(ctx, relation.targetModel);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function nestedWrites(ctx, model, data, depth) {
|
|
110
|
+
if (!isPlainRecord(data)) return;
|
|
111
|
+
for (const relation of ctx.config.relationGraph[model] ?? []) {
|
|
112
|
+
const ops = data[relation.fieldName];
|
|
113
|
+
if (!isPlainRecord(ops)) continue;
|
|
114
|
+
const target = relation.targetModel;
|
|
115
|
+
if (depth >= MAX_NESTED_WRITE_DEPTH) {
|
|
116
|
+
ctx.plan.all = true;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
touchWrite(ctx, target);
|
|
120
|
+
const recurse = (value) => nestedWrites(ctx, target, value, depth + 1);
|
|
121
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
122
|
+
const items = asItems(value);
|
|
123
|
+
if (relation.oneToOne === true && ONE_TO_ONE_REPARENTING_OPS.has(op)) {
|
|
124
|
+
touchWide(ctx, target);
|
|
125
|
+
touchWide(ctx, model);
|
|
126
|
+
}
|
|
127
|
+
if (FOREIGN_KEY_CHANGING_OPS.has(op)) widenIfForeignKeyReferenced(ctx, model, relation);
|
|
128
|
+
switch (op) {
|
|
129
|
+
case "create":
|
|
130
|
+
items.forEach(recurse);
|
|
131
|
+
break;
|
|
132
|
+
case "createMany":
|
|
133
|
+
break;
|
|
134
|
+
case "connect":
|
|
135
|
+
items.forEach((item) => touchEntity(ctx, target, item));
|
|
136
|
+
break;
|
|
137
|
+
case "connectOrCreate":
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
touchEntity(ctx, target, isPlainRecord(item) ? item.where : void 0);
|
|
140
|
+
if (isPlainRecord(item)) recurse(item.create);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
case "disconnect":
|
|
144
|
+
items.forEach((item) => isPlainRecord(item) ? touchEntity(ctx, target, item) : touchWide(ctx, target));
|
|
145
|
+
break;
|
|
146
|
+
case "delete":
|
|
147
|
+
items.forEach((item) => isPlainRecord(item) ? touchEntity(ctx, target, item) : touchWide(ctx, target));
|
|
148
|
+
touchReferencing(ctx, target);
|
|
149
|
+
break;
|
|
150
|
+
case "deleteMany":
|
|
151
|
+
touchWide(ctx, target);
|
|
152
|
+
touchReferencing(ctx, target);
|
|
153
|
+
break;
|
|
154
|
+
case "update":
|
|
155
|
+
case "upsert":
|
|
156
|
+
for (const item of items) {
|
|
157
|
+
const hasWhere = isPlainRecord(item) && "where" in item;
|
|
158
|
+
if (hasWhere) touchEntity(ctx, target, item.where);
|
|
159
|
+
else touchWide(ctx, target);
|
|
160
|
+
const payloads = isPlainRecord(item) ? [item.data, item.create, item.update, hasWhere ? void 0 : item] : [];
|
|
161
|
+
for (const payload of payloads) {
|
|
162
|
+
touchReferencingIfKeyChanges(ctx, target, payload);
|
|
163
|
+
recurse(payload);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
touchWide(ctx, target);
|
|
169
|
+
for (const item of items) if (isPlainRecord(item)) touchReferencingIfKeyChanges(ctx, target, item.data);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function planWrite(config, model, operation, args) {
|
|
175
|
+
const ctx = { config, plan: { all: false, touched: /* @__PURE__ */ new Map() } };
|
|
176
|
+
const input = isPlainRecord(args) ? args : {};
|
|
177
|
+
touchWrite(ctx, model);
|
|
178
|
+
switch (operation) {
|
|
179
|
+
case "create":
|
|
180
|
+
nestedWrites(ctx, model, input.data, 0);
|
|
181
|
+
break;
|
|
182
|
+
case "createMany":
|
|
183
|
+
case "createManyAndReturn":
|
|
184
|
+
break;
|
|
185
|
+
case "update":
|
|
186
|
+
touchEntity(ctx, model, input.where);
|
|
187
|
+
touchReferencingIfKeyChanges(ctx, model, input.data);
|
|
188
|
+
nestedWrites(ctx, model, input.data, 0);
|
|
189
|
+
break;
|
|
190
|
+
case "upsert":
|
|
191
|
+
touchEntity(ctx, model, input.where);
|
|
192
|
+
touchReferencingIfKeyChanges(ctx, model, input.update);
|
|
193
|
+
nestedWrites(ctx, model, input.create, 0);
|
|
194
|
+
nestedWrites(ctx, model, input.update, 0);
|
|
195
|
+
break;
|
|
196
|
+
case "delete":
|
|
197
|
+
touchEntity(ctx, model, input.where);
|
|
198
|
+
touchReferencing(ctx, model);
|
|
199
|
+
break;
|
|
200
|
+
case "deleteMany":
|
|
201
|
+
touchWide(ctx, model);
|
|
202
|
+
touchReferencing(ctx, model);
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
touchWide(ctx, model);
|
|
206
|
+
touchReferencingIfKeyChanges(ctx, model, input.data);
|
|
207
|
+
}
|
|
208
|
+
return ctx.plan;
|
|
209
|
+
}
|
|
210
|
+
function planFences(plan) {
|
|
211
|
+
if (plan.all) return [GLOBAL_FENCE];
|
|
212
|
+
const fences = [];
|
|
213
|
+
for (const [model, { pks, wide }] of plan.touched) {
|
|
214
|
+
fences.push(writeFence(model));
|
|
215
|
+
if (wide || pks.size > MAX_ENTITY_FENCES_PER_MODEL) fences.push(modelFence(model));
|
|
216
|
+
else for (const pk of pks) fences.push(entityFence(model, pk));
|
|
217
|
+
}
|
|
218
|
+
return fences;
|
|
219
|
+
}
|
|
220
|
+
function rawWritePlanFences(config, target) {
|
|
221
|
+
if ("all" in target) return [GLOBAL_FENCE];
|
|
222
|
+
const ctx = { config, plan: { all: false, touched: /* @__PURE__ */ new Map() } };
|
|
223
|
+
const known = new Set(config.modelNames);
|
|
224
|
+
const models = "models" in target ? target.models : target.entities.map(({ model }) => model);
|
|
225
|
+
const unknown = models.filter((model) => !known.has(model));
|
|
226
|
+
if (unknown.length > 0) throw new TypeError(`cache invalidation: unknown models ${unknown.join(", ")}`);
|
|
227
|
+
if ("models" in target) target.models.forEach((model) => touchWide(ctx, model));
|
|
228
|
+
else {
|
|
229
|
+
for (const { model, pk } of target.entities) {
|
|
230
|
+
const fields = pkFieldsOf(config, model);
|
|
231
|
+
const encoded = pk.length === fields.length ? pkFromRow(Object.fromEntries(fields.map((field, i) => [field, pk[i]])), fields) : null;
|
|
232
|
+
if (encoded === null) touchWide(ctx, model);
|
|
233
|
+
else touch(ctx.plan, model).pks.add(encoded);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
models.forEach((model) => touchReferencing(ctx, model));
|
|
237
|
+
return planFences(ctx.plan);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/core/shape.ts
|
|
241
|
+
var DEFAULT_SHAPE = { freshTtlSeconds: 3600, negativeTtlSeconds: 600, softTtlRatio: 0.8 };
|
|
242
|
+
function validate(label, shape) {
|
|
243
|
+
if (!shape) return;
|
|
244
|
+
for (const field of ["freshTtlSeconds", "negativeTtlSeconds"]) {
|
|
245
|
+
const value = shape[field];
|
|
246
|
+
if (value !== void 0 && !(Number.isFinite(value) && value > 0 && value <= MAX_SHAPE_TTL_SECONDS)) {
|
|
247
|
+
throw new TypeError(`cache shape ${label}: ${field} must be in (0, ${MAX_SHAPE_TTL_SECONDS}]`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const ratio = shape.softTtlRatio;
|
|
251
|
+
if (ratio !== void 0 && !(Number.isFinite(ratio) && ratio > 0 && ratio <= 1)) {
|
|
252
|
+
throw new TypeError(`cache shape ${label}: softTtlRatio must be in (0, 1]`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function validateShapes(shape) {
|
|
256
|
+
validate("default", shape?.default);
|
|
257
|
+
for (const [model, value] of Object.entries(shape?.byModel ?? {})) validate(`byModel.${model}`, value);
|
|
258
|
+
}
|
|
259
|
+
function resolveShape(shape, model) {
|
|
260
|
+
const pick = (field) => shape?.byModel?.[model]?.[field] ?? shape?.default?.[field] ?? DEFAULT_SHAPE[field];
|
|
261
|
+
return { freshTtlSeconds: pick("freshTtlSeconds"), negativeTtlSeconds: pick("negativeTtlSeconds"), softTtlRatio: pick("softTtlRatio") };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/core/targets.ts
|
|
265
|
+
var MAX_RELATION_DEPTH = 10;
|
|
266
|
+
var RELATION_FILTER_KEYS = ["some", "every", "none", "is", "isNot"];
|
|
267
|
+
var LOGICAL_KEYS = /* @__PURE__ */ new Set(["AND", "OR", "NOT"]);
|
|
268
|
+
function relationsOf(graph, model) {
|
|
269
|
+
return new Map((graph[model] ?? []).map((relation) => [relation.fieldName, relation]));
|
|
270
|
+
}
|
|
271
|
+
function reach(walk, relation) {
|
|
272
|
+
walk.out.models.add(relation.targetModel);
|
|
273
|
+
walk.out.relational = true;
|
|
274
|
+
}
|
|
275
|
+
function walkFilter(walk, model, value, depth) {
|
|
276
|
+
if (depth > MAX_RELATION_DEPTH) return false;
|
|
277
|
+
if (Array.isArray(value)) return value.every((item) => walkFilter(walk, model, item, depth + 1));
|
|
278
|
+
if (!isPlainRecord(value)) return true;
|
|
279
|
+
const relations = relationsOf(walk.graph, model);
|
|
280
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
281
|
+
if (LOGICAL_KEYS.has(key)) {
|
|
282
|
+
if (!walkFilter(walk, model, nested, depth + 1)) return false;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const relation = relations.get(key);
|
|
286
|
+
if (!relation) continue;
|
|
287
|
+
reach(walk, relation);
|
|
288
|
+
if (!isPlainRecord(nested)) continue;
|
|
289
|
+
const wrapped = RELATION_FILTER_KEYS.filter((wrapper) => wrapper in nested);
|
|
290
|
+
const inner = wrapped.length > 0 ? wrapped.map((wrapper) => nested[wrapper]) : [nested];
|
|
291
|
+
if (!inner.every((item) => walkFilter(walk, relation.targetModel, item, depth + 1))) return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
function walkCount(walk, model, count, depth) {
|
|
296
|
+
if (count === void 0) return true;
|
|
297
|
+
const select = isPlainRecord(count) && isPlainRecord(count.select) ? count.select : null;
|
|
298
|
+
for (const relation of walk.graph[model] ?? []) {
|
|
299
|
+
const selected = select ? select[relation.fieldName] : count;
|
|
300
|
+
if (!selected) continue;
|
|
301
|
+
reach(walk, relation);
|
|
302
|
+
if (isPlainRecord(selected) && !walkFilter(walk, relation.targetModel, selected.where, depth + 1)) return false;
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
function walkArgs(walk, model, args, depth) {
|
|
307
|
+
walk.out.models.add(model);
|
|
308
|
+
if (!isPlainRecord(args)) return true;
|
|
309
|
+
for (const filter of [args.where, args.orderBy, args.cursor]) {
|
|
310
|
+
if (!walkFilter(walk, model, filter, depth)) return false;
|
|
311
|
+
}
|
|
312
|
+
for (const projection of [args.include, args.select]) {
|
|
313
|
+
if (!isPlainRecord(projection)) continue;
|
|
314
|
+
if (!walkCount(walk, model, projection._count, depth)) return false;
|
|
315
|
+
for (const relation of walk.graph[model] ?? []) {
|
|
316
|
+
const nested = projection[relation.fieldName];
|
|
317
|
+
if (!nested) continue;
|
|
318
|
+
reach(walk, relation);
|
|
319
|
+
if (!walkArgs(walk, relation.targetModel, nested, depth + 1)) return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
function readTargets(graph, model, args) {
|
|
325
|
+
const walk = { graph, out: { models: /* @__PURE__ */ new Set(), relational: false } };
|
|
326
|
+
return walkArgs(walk, model, args, 0) ? walk.out : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/core/extension.ts
|
|
330
|
+
var CacheUnavailableError = class extends Error {
|
|
331
|
+
constructor(cause) {
|
|
332
|
+
super("cache coordinator unavailable; the write was not executed", { cause });
|
|
333
|
+
this.name = "CacheUnavailableError";
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var DEFAULT_FILL_POLL_DELAYS_MS = [20, 40, 80, 160, 320, 640];
|
|
337
|
+
var DEFAULT_LOCK_MS = 3e4;
|
|
338
|
+
var DEFAULT_LEASE_MS = 6e4;
|
|
339
|
+
var DEFAULT_MAX_VALUE_BYTES = 15e5;
|
|
340
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
341
|
+
var pickEpochs = (epochs, fences) => Object.fromEntries(fences.map((fence) => [fence, epochs[fence] ?? 0]));
|
|
342
|
+
var isNegative = (value) => value === null || Array.isArray(value) && value.length === 0;
|
|
343
|
+
var sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
344
|
+
function internalParams(params) {
|
|
345
|
+
const internal = isPlainRecord(params.__internalParams) ? params.__internalParams : {};
|
|
346
|
+
const transaction = internal.transaction;
|
|
347
|
+
return {
|
|
348
|
+
dataPath: Array.isArray(internal.dataPath) ? internal.dataPath : [],
|
|
349
|
+
inBatch: isPlainRecord(transaction) && transaction.kind === "batch"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function validateOptions(options) {
|
|
353
|
+
if (options.config.configFormat !== CONFIG_FORMAT) {
|
|
354
|
+
throw new TypeError(
|
|
355
|
+
`cache extension: generated config format ${String(options.config.configFormat)} does not match ${CONFIG_FORMAT}; run prisma generate`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const inRange = (value, max) => value === void 0 || Number.isFinite(value) && value > 0 && value <= max;
|
|
359
|
+
const problems = [
|
|
360
|
+
!inRange(options.lockMs, MAX_LOCK_MS) && `lockMs must be in (0, ${MAX_LOCK_MS}]`,
|
|
361
|
+
!inRange(options.leaseMs, MAX_LEASE_MS) && `leaseMs must be in (0, ${MAX_LEASE_MS}]`,
|
|
362
|
+
!inRange(options.maxValueBytes, MAX_VALUE_BYTES) && `maxValueBytes must be in (0, ${MAX_VALUE_BYTES}]`,
|
|
363
|
+
options.fillPollDelaysMs?.some((delay) => !(Number.isFinite(delay) && delay >= 0)) && "fillPollDelaysMs must be non-negative",
|
|
364
|
+
options.excludeModels?.some((model) => !options.config.modelNames.includes(model)) && "excludeModels names an unknown model",
|
|
365
|
+
options.config.hasDecimalFields && !options.decimal && "decimal is required because the schema has Decimal fields (pass Prisma.Decimal)",
|
|
366
|
+
options.generation !== void 0 && !(options.generation.length > 0 && options.generation.length <= MAX_GENERATION_LENGTH) && `generation must be 1..${MAX_GENERATION_LENGTH} characters`,
|
|
367
|
+
options.coordinationGeneration !== void 0 && !(options.coordinationGeneration.length > 0 && options.coordinationGeneration.length <= MAX_GENERATION_LENGTH) && `coordinationGeneration must be 1..${MAX_GENERATION_LENGTH} characters`
|
|
368
|
+
].filter((problem) => typeof problem === "string");
|
|
369
|
+
if (problems.length > 0) throw new TypeError(`cache extension: ${problems.join("; ")}`);
|
|
370
|
+
validateShapes(options.shape);
|
|
371
|
+
}
|
|
372
|
+
function createCacheExtension(options) {
|
|
373
|
+
validateOptions(options);
|
|
374
|
+
const { config, store, unsafeTestHooks: hooks } = options;
|
|
375
|
+
const enabled = options.enabled ?? true;
|
|
376
|
+
const excluded = new Set(options.excludeModels ?? []);
|
|
377
|
+
const scope = options.scope ?? "";
|
|
378
|
+
const generation = options.coordinationGeneration ?? `${PLANNER_VERSION}/${config.cacheVersion}/${options.generation ?? ""}`;
|
|
379
|
+
const pollDelays = options.fillPollDelaysMs ?? DEFAULT_FILL_POLL_DELAYS_MS;
|
|
380
|
+
const lockMs = options.lockMs ?? DEFAULT_LOCK_MS;
|
|
381
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
382
|
+
const maxValueBytes = options.maxValueBytes ?? DEFAULT_MAX_VALUE_BYTES;
|
|
383
|
+
const report = (error, context) => options.onError?.(error, context);
|
|
384
|
+
const isCacheable = (model) => enabled && config.cacheableByModel[model] === true && !excluded.has(model);
|
|
385
|
+
function readContext(model, operation, args, query, key) {
|
|
386
|
+
const targets = readTargets(config.relationGraph, model, args);
|
|
387
|
+
if (!targets || [...targets.models].some((target) => !isCacheable(target))) return null;
|
|
388
|
+
const rootPk = operation.startsWith("findUnique") ? pkFromWhere(
|
|
389
|
+
isPlainRecord(args) ? args.where : void 0,
|
|
390
|
+
config.primaryKeyFieldsByModel[model] ?? ["id"],
|
|
391
|
+
config.primaryKeyNameByModel[model]
|
|
392
|
+
) : null;
|
|
393
|
+
const base = [GLOBAL_FENCE, modelFence(model)];
|
|
394
|
+
const entity = rootPk === null ? null : entityFence(model, rootPk);
|
|
395
|
+
const fences = [...base, ...[...targets.models].map(writeFence), ...entity ? [entity] : []];
|
|
396
|
+
const entityFences = entity && !targets.relational ? [...base, entity] : null;
|
|
397
|
+
return { args, entityFences, fences, key, model, operation, query };
|
|
398
|
+
}
|
|
399
|
+
const release = (key, lockToken) => store.release({ key, lockToken }).catch((error) => report(error, "release"));
|
|
400
|
+
function serialize(value) {
|
|
401
|
+
try {
|
|
402
|
+
const serialized = JSON.stringify(encode(value));
|
|
403
|
+
if (new TextEncoder().encode(serialized).byteLength <= maxValueBytes) return serialized;
|
|
404
|
+
report(new Error(`cache value exceeds ${maxValueBytes} bytes`), "serialize");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
report(error, "serialize");
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
async function fill(ctx, lockToken, epochs) {
|
|
411
|
+
let value;
|
|
412
|
+
let cached;
|
|
413
|
+
try {
|
|
414
|
+
const loaded = options.sessionCache ? await options.sessionCache.fill(() => ctx.query(ctx.args)) : { value: await ctx.query(ctx.args), cached: void 0 };
|
|
415
|
+
value = loaded.value;
|
|
416
|
+
cached = options.sessionCache ? loaded.cached : value;
|
|
417
|
+
await hooks?.afterFillQuery?.();
|
|
418
|
+
} catch (error) {
|
|
419
|
+
await release(ctx.key, lockToken);
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
const serialized = serialize(cached);
|
|
423
|
+
if (serialized === null) {
|
|
424
|
+
await release(ctx.key, lockToken);
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
const shape = resolveShape(options.shape, ctx.model);
|
|
428
|
+
const baseTtl = ctx.operation !== "count" && isNegative(value) ? shape.negativeTtlSeconds : shape.freshTtlSeconds;
|
|
429
|
+
const ttlMs = Math.round(baseTtl * 1e3 * (1 + Math.random() * TTL_JITTER_RATIO));
|
|
430
|
+
const snapshotFences = ctx.entityFences && isPlainRecord(value) ? ctx.entityFences : ctx.fences;
|
|
431
|
+
try {
|
|
432
|
+
const result = await store.write({
|
|
433
|
+
key: ctx.key,
|
|
434
|
+
lockToken,
|
|
435
|
+
snapshot: pickEpochs(epochs, snapshotFences),
|
|
436
|
+
softTtlMs: Math.round(ttlMs * shape.softTtlRatio),
|
|
437
|
+
ttlMs,
|
|
438
|
+
value: serialized
|
|
439
|
+
});
|
|
440
|
+
if (!result.ok) report(new Error(`cache write rejected: ${result.reason}`), "write");
|
|
441
|
+
} catch (error) {
|
|
442
|
+
report(error, "write");
|
|
443
|
+
await release(ctx.key, lockToken);
|
|
444
|
+
}
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
async function scheduleRefresh(ctx, response) {
|
|
448
|
+
const token = response.refreshToken;
|
|
449
|
+
if (!token) return;
|
|
450
|
+
if (!options.waitUntil) return release(ctx.key, token);
|
|
451
|
+
try {
|
|
452
|
+
const refresh = () => fill(ctx, token, response.epochs).then(() => void 0);
|
|
453
|
+
options.waitUntil((options.sessionCache?.background ? options.sessionCache.background(refresh) : refresh()).catch((error) => report(error, "refresh")));
|
|
454
|
+
} catch (error) {
|
|
455
|
+
report(error, "waitUntil");
|
|
456
|
+
await release(ctx.key, token);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function handleRead(model, operation, args, query, dataPath) {
|
|
460
|
+
let key;
|
|
461
|
+
try {
|
|
462
|
+
const identity = [PLANNER_VERSION, config.cacheVersion, scope, ...options.sessionCache ? [options.sessionCache.key()] : [], model, operation, dataPath, (options.keyArgs ? options.keyArgs(args) : args) ?? null];
|
|
463
|
+
key = `${model}:${await sha256Hex(stableStringify(identity))}`;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
report(error, "key");
|
|
466
|
+
return query(args);
|
|
467
|
+
}
|
|
468
|
+
const ctx = readContext(model, operation, args, query, key);
|
|
469
|
+
if (!ctx) return query(args);
|
|
470
|
+
for (let attempt = 0; ; attempt++) {
|
|
471
|
+
let response;
|
|
472
|
+
try {
|
|
473
|
+
response = await store.read({ fences: ctx.fences, generation, key, lockMs, refresh: !!options.waitUntil });
|
|
474
|
+
} catch (error) {
|
|
475
|
+
report(error, "read");
|
|
476
|
+
return query(args);
|
|
477
|
+
}
|
|
478
|
+
if (response.kind === "hit") {
|
|
479
|
+
let value;
|
|
480
|
+
try {
|
|
481
|
+
value = decode(JSON.parse(response.value), options.decimal);
|
|
482
|
+
if (options.sessionCache) {
|
|
483
|
+
const cached = options.sessionCache.read(value);
|
|
484
|
+
if (!cached) {
|
|
485
|
+
if (response.refreshToken) await release(key, response.refreshToken);
|
|
486
|
+
return (await options.sessionCache.fill(() => query(args))).value;
|
|
487
|
+
}
|
|
488
|
+
value = cached.value;
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
report(error, "decode");
|
|
492
|
+
if (response.refreshToken) await release(key, response.refreshToken);
|
|
493
|
+
return query(args);
|
|
494
|
+
}
|
|
495
|
+
await scheduleRefresh(ctx, response);
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
if (response.lockToken) return fill(ctx, response.lockToken, response.epochs);
|
|
499
|
+
const delay = pollDelays[attempt];
|
|
500
|
+
const holderTooSlow = response.lockHeldForMs !== null && response.lockHeldForMs > sum(pollDelays);
|
|
501
|
+
if (response.pending || delay === void 0 || holderTooSlow) return query(args);
|
|
502
|
+
await sleep(delay);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function withLease(fences, run) {
|
|
506
|
+
const token = crypto.randomUUID();
|
|
507
|
+
try {
|
|
508
|
+
await store.beginWrite({ fences, generation, leaseMs, token });
|
|
509
|
+
} catch (error) {
|
|
510
|
+
report(error, "beginWrite");
|
|
511
|
+
await endWrite(token, fences);
|
|
512
|
+
throw new CacheUnavailableError(error);
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await hooks?.afterBeginWrite?.();
|
|
516
|
+
return await run();
|
|
517
|
+
} finally {
|
|
518
|
+
if (!hooks?.skipEndWrite?.()) await endWrite(token, fences);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function endWrite(token, fences) {
|
|
522
|
+
await store.endWrite({ fences, token }).catch((error) => report(error, "endWrite"));
|
|
523
|
+
}
|
|
524
|
+
const targetFences = (target) => rawWritePlanFences(config, target);
|
|
525
|
+
return {
|
|
526
|
+
client: {
|
|
527
|
+
async $cacheInvalidate(target) {
|
|
528
|
+
await store.invalidate(targetFences(target));
|
|
529
|
+
},
|
|
530
|
+
async $cacheRawWrite(target, run) {
|
|
531
|
+
const fences = targetFences(target);
|
|
532
|
+
return enabled ? withLease(fences, run) : run();
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
name: "dee-wan-cache",
|
|
536
|
+
query: {
|
|
537
|
+
$allModels: {
|
|
538
|
+
$allOperations(params) {
|
|
539
|
+
const { args, model, operation, query } = params;
|
|
540
|
+
if (!model) return query(args);
|
|
541
|
+
const { dataPath, inBatch } = internalParams(params);
|
|
542
|
+
if (READ_OPERATIONS.has(operation) && isCacheable(model) && !inBatch) {
|
|
543
|
+
return handleRead(model, operation, args, query, dataPath);
|
|
544
|
+
}
|
|
545
|
+
if (WRITE_OPERATIONS.has(operation) && enabled) {
|
|
546
|
+
return withLease(planFences(planWrite(config, model, operation, args)), () => query(args));
|
|
547
|
+
}
|
|
548
|
+
return query(args);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export { CacheUnavailableError, createCacheExtension, readTargets };
|
|
556
|
+
//# sourceMappingURL=chunk-M4UOCIDQ.js.map
|
|
557
|
+
//# sourceMappingURL=chunk-M4UOCIDQ.js.map
|