@camstack/types 1.2.394 → 1.2.396
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/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/records/in-memory-records-plan.d.ts +63 -0
- package/dist/records/in-memory-records-rows.d.ts +31 -0
- package/dist/records/in-memory-records-support.d.ts +29 -0
- package/dist/records/in-memory-records.d.ts +28 -9
- package/dist/records/index.d.ts +1 -1
- package/dist/{sleep-gRQc2IoX.js → sleep-Re8daN0Y.js} +683 -159
- package/dist/{sleep-CSuN0thG.mjs → sleep-_sV1BE2f.mjs} +683 -159
- package/package.json +1 -1
|
@@ -166,7 +166,7 @@ function omitTransientConfig(schema, config) {
|
|
|
166
166
|
return next;
|
|
167
167
|
}
|
|
168
168
|
function collectOmitted(field, out) {
|
|
169
|
-
if (!isRecord$
|
|
169
|
+
if (!isRecord$7(field)) return;
|
|
170
170
|
const type = field["type"];
|
|
171
171
|
const key = field["key"];
|
|
172
172
|
if (field["omitFromSettings"] === true && typeof key === "string" && key.length > 0) out.add(key);
|
|
@@ -186,7 +186,7 @@ function collectKeychainStoredKeys(schema) {
|
|
|
186
186
|
return keys;
|
|
187
187
|
}
|
|
188
188
|
function walkKeychainField(field, out) {
|
|
189
|
-
if (!isRecord$
|
|
189
|
+
if (!isRecord$7(field)) return;
|
|
190
190
|
const type = field["type"];
|
|
191
191
|
const key = field["key"];
|
|
192
192
|
const isSecret = type === "password" || field["secret"] === true;
|
|
@@ -211,21 +211,21 @@ function schemaDeclaresAnyField(schema) {
|
|
|
211
211
|
for (const section of sectionsOf(schema)) if (fieldsOf(section).length > 0) return true;
|
|
212
212
|
return false;
|
|
213
213
|
}
|
|
214
|
-
function isRecord$
|
|
214
|
+
function isRecord$7(value) {
|
|
215
215
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
216
216
|
}
|
|
217
217
|
function sectionsOf(schema) {
|
|
218
|
-
if (!isRecord$
|
|
218
|
+
if (!isRecord$7(schema)) return [];
|
|
219
219
|
const sections = schema["sections"];
|
|
220
220
|
return Array.isArray(sections) ? sections : [];
|
|
221
221
|
}
|
|
222
222
|
function fieldsOf(node) {
|
|
223
|
-
if (!isRecord$
|
|
223
|
+
if (!isRecord$7(node)) return [];
|
|
224
224
|
const fields = node["fields"];
|
|
225
225
|
return Array.isArray(fields) ? fields : [];
|
|
226
226
|
}
|
|
227
227
|
function walkField(field, out) {
|
|
228
|
-
if (!isRecord$
|
|
228
|
+
if (!isRecord$7(field)) return;
|
|
229
229
|
const type = field["type"];
|
|
230
230
|
const key = field["key"];
|
|
231
231
|
if ((type === "password" || field["secret"] === true) && typeof key === "string" && key.length > 0) out.add(key);
|
|
@@ -265,7 +265,7 @@ function walkField(field, out) {
|
|
|
265
265
|
var SECRET_CLEAR = { $secretClear: true };
|
|
266
266
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
267
267
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
268
|
-
function isRecord$
|
|
268
|
+
function isRecord$6(value) {
|
|
269
269
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
270
270
|
}
|
|
271
271
|
function makeSecretMarker(ref) {
|
|
@@ -276,19 +276,19 @@ function makeSecretMarker(ref) {
|
|
|
276
276
|
}
|
|
277
277
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
278
278
|
function isSecretMarker(value) {
|
|
279
|
-
if (!isRecord$
|
|
279
|
+
if (!isRecord$6(value)) return false;
|
|
280
280
|
const ref = value["$secret"];
|
|
281
281
|
return typeof ref === "string" && ref.length > 0;
|
|
282
282
|
}
|
|
283
283
|
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
284
284
|
function isSecretMarkerLike(value) {
|
|
285
|
-
return isRecord$
|
|
285
|
+
return isRecord$6(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
286
286
|
}
|
|
287
287
|
function isSecretClear(value) {
|
|
288
|
-
return isRecord$
|
|
288
|
+
return isRecord$6(value) && value["$secretClear"] === true;
|
|
289
289
|
}
|
|
290
290
|
function isSecretFieldView(value) {
|
|
291
|
-
return isRecord$
|
|
291
|
+
return isRecord$6(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
292
292
|
}
|
|
293
293
|
/**
|
|
294
294
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -1069,7 +1069,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
1069
1069
|
}
|
|
1070
1070
|
};
|
|
1071
1071
|
var KEEP = { kind: "keep" };
|
|
1072
|
-
function isRecord$
|
|
1072
|
+
function isRecord$5(value) {
|
|
1073
1073
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1074
1074
|
}
|
|
1075
1075
|
function defaultMintId() {
|
|
@@ -1082,7 +1082,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
1082
1082
|
return out;
|
|
1083
1083
|
}
|
|
1084
1084
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
1085
|
-
else if (isRecord$
|
|
1085
|
+
else if (isRecord$5(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
1086
1086
|
return out;
|
|
1087
1087
|
}
|
|
1088
1088
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -1137,7 +1137,7 @@ function rowId(row, idKey) {
|
|
|
1137
1137
|
return null;
|
|
1138
1138
|
}
|
|
1139
1139
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
1140
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
1140
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$5) : [];
|
|
1141
1141
|
if (!Array.isArray(incoming)) {
|
|
1142
1142
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
1143
1143
|
return {
|
|
@@ -1153,7 +1153,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
1153
1153
|
const kept = /* @__PURE__ */ new Set();
|
|
1154
1154
|
const rows = [];
|
|
1155
1155
|
for (const row of incoming) {
|
|
1156
|
-
if (!isRecord$
|
|
1156
|
+
if (!isRecord$5(row)) {
|
|
1157
1157
|
rows.push(row);
|
|
1158
1158
|
continue;
|
|
1159
1159
|
}
|
|
@@ -1178,7 +1178,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
1178
1178
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
1179
1179
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
1180
1180
|
case "object": {
|
|
1181
|
-
if (!isRecord$
|
|
1181
|
+
if (!isRecord$5(incoming)) {
|
|
1182
1182
|
if (incoming === void 0) return KEEP;
|
|
1183
1183
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
1184
1184
|
return {
|
|
@@ -1186,7 +1186,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
1186
1186
|
value: incoming
|
|
1187
1187
|
};
|
|
1188
1188
|
}
|
|
1189
|
-
const storedObj = isRecord$
|
|
1189
|
+
const storedObj = isRecord$5(stored) ? stored : {};
|
|
1190
1190
|
return {
|
|
1191
1191
|
kind: "value",
|
|
1192
1192
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -1309,7 +1309,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
1309
1309
|
value: items
|
|
1310
1310
|
};
|
|
1311
1311
|
}
|
|
1312
|
-
if (isRecord$
|
|
1312
|
+
if (isRecord$5(v)) {
|
|
1313
1313
|
const out = {};
|
|
1314
1314
|
for (const [k, child] of Object.entries(v)) {
|
|
1315
1315
|
const s = substitute(child);
|
|
@@ -1343,7 +1343,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
1343
1343
|
this.name = "SecretOverwriteRefusedError";
|
|
1344
1344
|
}
|
|
1345
1345
|
};
|
|
1346
|
-
function isRecord$
|
|
1346
|
+
function isRecord$4(value) {
|
|
1347
1347
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1348
1348
|
}
|
|
1349
1349
|
function resolvedValue(pass, marker) {
|
|
@@ -1372,7 +1372,7 @@ function inArray(pass, incoming, stored) {
|
|
|
1372
1372
|
return incoming.map((item, index) => {
|
|
1373
1373
|
const twin = stored[index];
|
|
1374
1374
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
1375
|
-
if (isRecord$
|
|
1375
|
+
if (isRecord$4(item) && isRecord$4(twin)) return inArrayRow(pass, item, twin);
|
|
1376
1376
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
1377
1377
|
return item;
|
|
1378
1378
|
});
|
|
@@ -1383,7 +1383,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
1383
1383
|
const twin = stored[key];
|
|
1384
1384
|
if (isSecretMarker(twin)) {
|
|
1385
1385
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
1386
|
-
} else if (isRecord$
|
|
1386
|
+
} else if (isRecord$4(value) && isRecord$4(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
1387
1387
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
1388
1388
|
}
|
|
1389
1389
|
return out;
|
|
@@ -1391,7 +1391,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
1391
1391
|
async function walk(pass, incoming, stored) {
|
|
1392
1392
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
1393
1393
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
1394
|
-
if (!isRecord$
|
|
1394
|
+
if (!isRecord$4(incoming) || !isRecord$4(stored)) return incoming;
|
|
1395
1395
|
const out = { ...incoming };
|
|
1396
1396
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
1397
1397
|
return out;
|
|
@@ -1422,7 +1422,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
1422
1422
|
function isJsonEqual(a, b) {
|
|
1423
1423
|
if (Object.is(a, b)) return true;
|
|
1424
1424
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
1425
|
-
if (isRecord$
|
|
1425
|
+
if (isRecord$4(a) && isRecord$4(b)) {
|
|
1426
1426
|
const keys = Object.keys(a);
|
|
1427
1427
|
if (keys.length !== Object.keys(b).length) return false;
|
|
1428
1428
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -1446,7 +1446,7 @@ function markerKeysOf(blob, owner) {
|
|
|
1446
1446
|
}
|
|
1447
1447
|
function nodeFromStored(value, owner) {
|
|
1448
1448
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
1449
|
-
if (!isRecord$
|
|
1449
|
+
if (!isRecord$4(value)) return null;
|
|
1450
1450
|
const children = /* @__PURE__ */ new Map();
|
|
1451
1451
|
for (const [key, child] of Object.entries(value)) {
|
|
1452
1452
|
const node = nodeFromStored(child, owner);
|
|
@@ -3505,11 +3505,11 @@ function secretArrayPrefixes(path) {
|
|
|
3505
3505
|
}
|
|
3506
3506
|
return prefixes;
|
|
3507
3507
|
}
|
|
3508
|
-
function isRecord$
|
|
3508
|
+
function isRecord$3(value) {
|
|
3509
3509
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3510
3510
|
}
|
|
3511
3511
|
function elementId(element, idKey, declared, prefix) {
|
|
3512
|
-
const id = isRecord$
|
|
3512
|
+
const id = isRecord$3(element) ? element[idKey] : void 0;
|
|
3513
3513
|
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
3514
3514
|
return id;
|
|
3515
3515
|
}
|
|
@@ -3520,7 +3520,7 @@ function elementId(element, idKey, declared, prefix) {
|
|
|
3520
3520
|
*/
|
|
3521
3521
|
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
3522
3522
|
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
3523
|
-
if (!isRecord$
|
|
3523
|
+
if (!isRecord$3(node)) return node;
|
|
3524
3524
|
const segment = segments[index];
|
|
3525
3525
|
if (segment === void 0) return node;
|
|
3526
3526
|
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
@@ -3762,7 +3762,7 @@ function declarationsOf(kinds) {
|
|
|
3762
3762
|
}
|
|
3763
3763
|
//#endregion
|
|
3764
3764
|
//#region src/records/records-access.ts
|
|
3765
|
-
function isRecord$
|
|
3765
|
+
function isRecord$2(value) {
|
|
3766
3766
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3767
3767
|
}
|
|
3768
3768
|
/** Run a transport call and give a wire failure its typed class back. */
|
|
@@ -3887,7 +3887,7 @@ var KindHandle = class {
|
|
|
3887
3887
|
}) : data;
|
|
3888
3888
|
const parsed = this.def.schema.safeParse(shaped);
|
|
3889
3889
|
if (!parsed.success) throw new InvalidRecordError(`${this.name}/${id}: ${zodSummary(parsed.error)}`);
|
|
3890
|
-
if (!isRecord$
|
|
3890
|
+
if (!isRecord$2(parsed.data)) throw new InvalidRecordError(`${this.name}/${id}: the body must be an object`);
|
|
3891
3891
|
return parsed.data;
|
|
3892
3892
|
}
|
|
3893
3893
|
async get(id) {
|
|
@@ -4056,7 +4056,7 @@ var KindHandle = class {
|
|
|
4056
4056
|
}
|
|
4057
4057
|
};
|
|
4058
4058
|
function toBody(value) {
|
|
4059
|
-
if (!isRecord$
|
|
4059
|
+
if (!isRecord$2(value)) throw new InvalidRecordError("the record body must be an object");
|
|
4060
4060
|
return value;
|
|
4061
4061
|
}
|
|
4062
4062
|
/** Build the owner's records handle. */
|
|
@@ -4144,17 +4144,236 @@ function defineEntryKind(name, spec) {
|
|
|
4144
4144
|
return defined;
|
|
4145
4145
|
}
|
|
4146
4146
|
//#endregion
|
|
4147
|
-
//#region src/records/in-memory-records.ts
|
|
4147
|
+
//#region src/records/in-memory-records-plan.ts
|
|
4148
|
+
function isRecord$1(value) {
|
|
4149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4150
|
+
}
|
|
4151
|
+
/** `json_extract(data, '$.a.b')`. */
|
|
4152
|
+
function extractField(data, path) {
|
|
4153
|
+
let current = data;
|
|
4154
|
+
for (const segment of path.split(".")) {
|
|
4155
|
+
if (!isRecord$1(current)) return null;
|
|
4156
|
+
current = current[segment];
|
|
4157
|
+
}
|
|
4158
|
+
if (current === void 0 || current === null) return null;
|
|
4159
|
+
if (typeof current === "boolean") return current ? 1 : 0;
|
|
4160
|
+
if (typeof current === "number" || typeof current === "string") return current;
|
|
4161
|
+
return JSON.stringify(current);
|
|
4162
|
+
}
|
|
4163
|
+
/** A bound parameter. */
|
|
4164
|
+
function bindable(value) {
|
|
4165
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
4166
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
4167
|
+
throw new InvalidRecordError("a where value must be a string, number or boolean");
|
|
4168
|
+
}
|
|
4169
|
+
function compareText(a, b) {
|
|
4170
|
+
const ai = a[Symbol.iterator]();
|
|
4171
|
+
const bi = b[Symbol.iterator]();
|
|
4172
|
+
for (;;) {
|
|
4173
|
+
const x = ai.next();
|
|
4174
|
+
const y = bi.next();
|
|
4175
|
+
if (x.done === true) return y.done === true ? 0 : -1;
|
|
4176
|
+
if (y.done === true) return 1;
|
|
4177
|
+
const cx = x.value.codePointAt(0) ?? 0;
|
|
4178
|
+
const cy = y.value.codePointAt(0) ?? 0;
|
|
4179
|
+
if (cx !== cy) return cx < cy ? -1 : 1;
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
/** ORDER BY order: NULL < numbers < text. */
|
|
4183
|
+
function compareSql(a, b) {
|
|
4184
|
+
if (a === null || b === null) return a === b ? 0 : a === null ? -1 : 1;
|
|
4185
|
+
if (typeof a === "number" && typeof b === "number") return a === b ? 0 : a < b ? -1 : 1;
|
|
4186
|
+
if (typeof a === "number") return -1;
|
|
4187
|
+
if (typeof b === "number") return 1;
|
|
4188
|
+
return compareText(a, b);
|
|
4189
|
+
}
|
|
4148
4190
|
/**
|
|
4149
|
-
*
|
|
4150
|
-
*
|
|
4191
|
+
* A row-value comparison `(a, b, …) > (x, y, …)` with SQL's three values:
|
|
4192
|
+
* the first unequal pair decides, and a NULL met before it makes the whole
|
|
4193
|
+
* comparison NULL (the row is not returned).
|
|
4194
|
+
*/
|
|
4195
|
+
function rowValueAfter(row, cursor, desc) {
|
|
4196
|
+
for (let i = 0; i < row.length; i += 1) {
|
|
4197
|
+
const a = row[i] ?? null;
|
|
4198
|
+
const b = cursor[i] ?? null;
|
|
4199
|
+
if (a === null || b === null) return false;
|
|
4200
|
+
const c = compareSql(a, b);
|
|
4201
|
+
if (c !== 0) return desc ? c < 0 : c > 0;
|
|
4202
|
+
}
|
|
4203
|
+
return false;
|
|
4204
|
+
}
|
|
4205
|
+
function predicateOf(field, value) {
|
|
4206
|
+
if (typeof value === "object") return {
|
|
4207
|
+
field,
|
|
4208
|
+
shape: "in" in value ? "in" : "range",
|
|
4209
|
+
value
|
|
4210
|
+
};
|
|
4211
|
+
return {
|
|
4212
|
+
field,
|
|
4213
|
+
shape: "eq",
|
|
4214
|
+
value
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
function predicateTest(p) {
|
|
4218
|
+
const value = p.value;
|
|
4219
|
+
if (p.shape === "eq") {
|
|
4220
|
+
const wanted = bindable(value);
|
|
4221
|
+
return (data) => {
|
|
4222
|
+
const got = extractField(data, p.field);
|
|
4223
|
+
return got !== null && compareSql(got, wanted) === 0;
|
|
4224
|
+
};
|
|
4225
|
+
}
|
|
4226
|
+
if (typeof value === "object" && "in" in value) {
|
|
4227
|
+
const set = value.in.map(bindable);
|
|
4228
|
+
return (data) => {
|
|
4229
|
+
const got = extractField(data, p.field);
|
|
4230
|
+
return got !== null && set.some((w) => w !== null && compareSql(got, w) === 0);
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
if (typeof value !== "object") return () => false;
|
|
4234
|
+
const active = [
|
|
4235
|
+
["gt" in value ? value.gt : void 0, (c) => c > 0],
|
|
4236
|
+
["gte" in value ? value.gte : void 0, (c) => c >= 0],
|
|
4237
|
+
["lt" in value ? value.lt : void 0, (c) => c < 0],
|
|
4238
|
+
["lte" in value ? value.lte : void 0, (c) => c <= 0]
|
|
4239
|
+
].flatMap(([b, ok]) => b === void 0 ? [] : [{
|
|
4240
|
+
bound: bindable(b),
|
|
4241
|
+
ok
|
|
4242
|
+
}]);
|
|
4243
|
+
return (data) => {
|
|
4244
|
+
const got = extractField(data, p.field);
|
|
4245
|
+
if (got === null) return false;
|
|
4246
|
+
return active.every(({ bound, ok }) => ok(compareSql(got, bound)));
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
/** `planWhere` of the engine: find the declared index, or refuse with the engine's error. */
|
|
4250
|
+
function planWhere(kind, decl, where, opts) {
|
|
4251
|
+
const predicates = Object.entries(where ?? {}).map(([f, v]) => predicateOf(f, v));
|
|
4252
|
+
const extra = opts.extraFields ?? [];
|
|
4253
|
+
if (predicates.length === 0 && extra.length === 0) return null;
|
|
4254
|
+
const equalities = predicates.filter((p) => p.shape !== "range");
|
|
4255
|
+
const ranges = predicates.filter((p) => p.shape === "range");
|
|
4256
|
+
if (ranges.length > 1) throw new UnindexedFilterError(`${kind}: at most one range predicate (${ranges.map((r) => r.field).join(", ")}) - it must be the field right after the equality fields of one index`);
|
|
4257
|
+
if (!opts.allowIn && equalities.some((p) => p.shape === "in")) throw new UnindexedFilterError(`${kind}: an "in" set cannot be paged in index order - ask once per value`);
|
|
4258
|
+
const eqCount = equalities.length;
|
|
4259
|
+
const eqSet = new Set(equalities.map((p) => p.field));
|
|
4260
|
+
const range = ranges[0];
|
|
4261
|
+
const candidates = decl.indexes.filter((index) => {
|
|
4262
|
+
const leading = index.fields.slice(0, eqCount);
|
|
4263
|
+
if (leading.length !== eqCount || !leading.every((f) => eqSet.has(f))) return false;
|
|
4264
|
+
if (range !== void 0 && index.fields[eqCount] !== range.field) return false;
|
|
4265
|
+
return extra.every((f) => index.fields.includes(f));
|
|
4266
|
+
});
|
|
4267
|
+
const groupBy = opts.groupBy;
|
|
4268
|
+
const candidate = (groupBy === void 0 ? void 0 : candidates.find((index) => index.fields[eqCount] === groupBy)) ?? candidates[0];
|
|
4269
|
+
if (candidate === void 0) {
|
|
4270
|
+
const declared = decl.indexes.map((i) => `${i.name}(${i.fields.join(", ")})`).join("; ");
|
|
4271
|
+
throw new UnindexedFilterError(`${kind}: no declared index serves [${[...predicates.map((p) => p.field), ...extra].join(", ")}]. Declared: ${declared.length > 0 ? declared : "none"}. A filter on an unindexed field does not compile - declare an index`);
|
|
4272
|
+
}
|
|
4273
|
+
const tests = predicates.map(predicateTest);
|
|
4274
|
+
return {
|
|
4275
|
+
indexName: candidate.name,
|
|
4276
|
+
orderFields: candidate.fields.slice(eqCount),
|
|
4277
|
+
matches: (data) => tests.every((t) => t(data))
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
function assertWindowAllowed(kind, decl, w) {
|
|
4281
|
+
if (w.deviceId !== void 0 && decl.device === "none") throw new InvalidRecordError(`${kind}: declared device: "none", it cannot be filtered by deviceId`);
|
|
4282
|
+
if ((w.atFrom !== void 0 || w.atTo !== void 0) && decl.at === "none") throw new InvalidRecordError(`${kind}: declared at: "none", it cannot be filtered by time`);
|
|
4283
|
+
}
|
|
4284
|
+
function inWindow(row, w) {
|
|
4285
|
+
if (w.deviceId !== void 0 && row.deviceId !== w.deviceId) return false;
|
|
4286
|
+
if (w.atFrom !== void 0 && (row.at === null || row.at < w.atFrom)) return false;
|
|
4287
|
+
if (w.atTo !== void 0 && (row.at === null || row.at >= w.atTo)) return false;
|
|
4288
|
+
return true;
|
|
4289
|
+
}
|
|
4290
|
+
//#endregion
|
|
4291
|
+
//#region src/records/in-memory-records-rows.ts
|
|
4292
|
+
/**
|
|
4293
|
+
* The row rules of the records engine for the in-memory world. MIRRORS
|
|
4294
|
+
* `packages/system/src/builtins/sqlite-storage/records/records-rows.ts`
|
|
4295
|
+
* (`validateHeader`, `enforceSecrets`, the `maxDataBytes` refusal); the
|
|
4296
|
+
* contract spec in `@camstack/system` holds the two together.
|
|
4151
4297
|
*/
|
|
4152
4298
|
function isRecord(value) {
|
|
4153
4299
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4154
4300
|
}
|
|
4155
|
-
|
|
4301
|
+
function validateHeader(kind, decl, rec, nowMs) {
|
|
4302
|
+
const fail = (why) => {
|
|
4303
|
+
throw new InvalidRecordError(`${kind}/${rec.id.slice(0, 64)}: ${why}`);
|
|
4304
|
+
};
|
|
4305
|
+
if (rec.id.length === 0 || rec.id.length > 256) fail(`the id must be 1..256 characters`);
|
|
4306
|
+
if (decl.device === "required" && rec.deviceId === void 0) fail("deviceId is required");
|
|
4307
|
+
if (decl.device === "none" && rec.deviceId !== void 0) fail("this kind is not device-scoped");
|
|
4308
|
+
if (decl.at === "required" && rec.at === void 0) fail("at is required");
|
|
4309
|
+
if (decl.at === "none" && rec.at !== void 0) fail("this kind has no time");
|
|
4310
|
+
const { retention } = decl;
|
|
4311
|
+
let expiresAt = rec.expiresAt ?? null;
|
|
4312
|
+
if ("ttlMs" in retention) {
|
|
4313
|
+
if (rec.expiresAt !== void 0) fail("expiresAt is set by the engine on a kind with a ttl retention");
|
|
4314
|
+
expiresAt = (retention.from === "at" ? rec.at ?? nowMs : nowMs) + retention.ttlMs;
|
|
4315
|
+
} else if (rec.expiresAt !== void 0 && decl.secretPaths.length > 0) fail("a kind with secret fields cannot expire by the engine (its keychain values would be orphaned)");
|
|
4316
|
+
return {
|
|
4317
|
+
id: rec.id,
|
|
4318
|
+
deviceId: rec.deviceId ?? null,
|
|
4319
|
+
at: rec.at ?? null,
|
|
4320
|
+
expiresAt
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
/** The stored text of a body; refuses one over `maxDataBytes`. */
|
|
4324
|
+
function serializeBody(kind, decl, id, body) {
|
|
4325
|
+
const json = JSON.stringify(body);
|
|
4326
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
4327
|
+
if (bytes > decl.maxDataBytes) throw new RecordTooLargeError(`${kind}/${id.slice(0, 64)}: ${bytes} bytes exceeds maxDataBytes ${decl.maxDataBytes}. Bytes are never record fields - use files and ctx.peerBytes`);
|
|
4328
|
+
return json;
|
|
4329
|
+
}
|
|
4330
|
+
/** Markers found in a stored body, by concrete path. */
|
|
4331
|
+
function markersOf(decl, body) {
|
|
4332
|
+
const out = /* @__PURE__ */ new Map();
|
|
4333
|
+
if (decl.secretPaths.length === 0) return out;
|
|
4334
|
+
for (const leaf of collectSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys)) if (isSecretMarker(leaf.value)) out.set(leaf.concrete, leaf.value.$secret);
|
|
4335
|
+
return out;
|
|
4336
|
+
}
|
|
4337
|
+
/** Every set secret field becomes the redaction token. */
|
|
4338
|
+
function redactBody(decl, body) {
|
|
4339
|
+
if (decl.secretPaths.length === 0) return body;
|
|
4340
|
+
const redacted = mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => leaf.value === null ? null : REDACTED_SECRET);
|
|
4341
|
+
return isRecord(redacted) ? redacted : body;
|
|
4342
|
+
}
|
|
4343
|
+
/** The engine's secret rule of a write (D878): see `records-rows.ts` `enforceSecrets`. */
|
|
4344
|
+
function enforceSecrets(owner, kind, decl, id, body, stored, mode) {
|
|
4345
|
+
if (decl.secretPaths.length === 0) return {
|
|
4346
|
+
body,
|
|
4347
|
+
releasedRefs: []
|
|
4348
|
+
};
|
|
4349
|
+
const storedMarkers = stored === null ? /* @__PURE__ */ new Map() : markersOf(decl, stored);
|
|
4350
|
+
const next = mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
4351
|
+
const expected = recordSecretRef(owner, kind, id, leaf.concrete);
|
|
4352
|
+
if (isSecretMarker(leaf.value)) {
|
|
4353
|
+
if (leaf.value.$secret !== expected) throw new ForeignSecretMarkerError(`${kind}/${id.slice(0, 64)} field "${leaf.concrete}": the marker is not this row's own ref`);
|
|
4354
|
+
return leaf.value;
|
|
4355
|
+
}
|
|
4356
|
+
if (leaf.value === "__camstack_redacted__") {
|
|
4357
|
+
const keep = storedMarkers.get(leaf.concrete);
|
|
4358
|
+
if (keep === void 0) return REMOVE_LEAF;
|
|
4359
|
+
if (mode === "patch" && !leaf.concrete.includes("[")) return REMOVE_LEAF;
|
|
4360
|
+
return {
|
|
4361
|
+
$secret: keep,
|
|
4362
|
+
v: 1
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4365
|
+
if (leaf.value === null || leaf.value === void 0) return mode === "patch" ? null : REMOVE_LEAF;
|
|
4366
|
+
throw new PlaintextSecretError(`${kind}/${id.slice(0, 64)} field "${leaf.concrete}": a secret field takes a keychain marker, never a value`);
|
|
4367
|
+
});
|
|
4368
|
+
if (!isRecord(next)) throw new InvalidRecordError(`${kind}/${id}: the body must be an object`);
|
|
4369
|
+
const nextMarkers = markersOf(decl, next);
|
|
4370
|
+
return {
|
|
4371
|
+
body: next,
|
|
4372
|
+
releasedRefs: [...storedMarkers.entries()].filter(([path, ref]) => mode === "put" && nextMarkers.get(path) !== ref).map(([, ref]) => ref)
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
/** RFC 7396, as `jsonb_patch`: objects merge key by key, `null` removes, anything else replaces. */
|
|
4156
4376
|
function mergePatch(target, patch) {
|
|
4157
|
-
if (!isRecord(patch)) return target;
|
|
4158
4377
|
const out = { ...target };
|
|
4159
4378
|
for (const [key, value] of Object.entries(patch)) {
|
|
4160
4379
|
if (value === null) {
|
|
@@ -4162,18 +4381,24 @@ function mergePatch(target, patch) {
|
|
|
4162
4381
|
continue;
|
|
4163
4382
|
}
|
|
4164
4383
|
const current = out[key];
|
|
4165
|
-
out[key] = isRecord(value)
|
|
4384
|
+
out[key] = isRecord(value) ? mergePatch(isRecord(current) ? current : {}, value) : value;
|
|
4166
4385
|
}
|
|
4167
4386
|
return out;
|
|
4168
4387
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
});
|
|
4175
|
-
return out;
|
|
4388
|
+
/** A JSON round trip: what the engine stores and hands back is never the caller's object. */
|
|
4389
|
+
function jsonCopy(body) {
|
|
4390
|
+
const parsed = JSON.parse(JSON.stringify(body));
|
|
4391
|
+
if (!isRecord(parsed)) throw new InvalidRecordError("a stored body is not an object");
|
|
4392
|
+
return parsed;
|
|
4176
4393
|
}
|
|
4394
|
+
//#endregion
|
|
4395
|
+
//#region src/records/in-memory-records-support.ts
|
|
4396
|
+
/**
|
|
4397
|
+
* Helpers of the in-memory records world (`in-memory-records.ts`): the spec
|
|
4398
|
+
* keychain, the keyset cursor, the declaration and ring rules, and the
|
|
4399
|
+
* aggregate fold. Each mirrors its engine counterpart under
|
|
4400
|
+
* `packages/system/src/builtins/sqlite-storage/records/`.
|
|
4401
|
+
*/
|
|
4177
4402
|
function makeSpecKeychain(owner) {
|
|
4178
4403
|
const values = /* @__PURE__ */ new Map();
|
|
4179
4404
|
return { access: {
|
|
@@ -4209,10 +4434,90 @@ function makeSpecKeychain(owner) {
|
|
|
4209
4434
|
forOwner: null
|
|
4210
4435
|
} };
|
|
4211
4436
|
}
|
|
4212
|
-
function
|
|
4437
|
+
function encodeCursor(tag, values) {
|
|
4438
|
+
return Buffer.from(JSON.stringify({
|
|
4439
|
+
t: tag,
|
|
4440
|
+
v: values
|
|
4441
|
+
}), "utf8").toString("base64url");
|
|
4442
|
+
}
|
|
4443
|
+
function decodeCursor(cursor) {
|
|
4444
|
+
try {
|
|
4445
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
4446
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
4447
|
+
const t = Reflect.get(parsed, "t");
|
|
4448
|
+
const v = Reflect.get(parsed, "v");
|
|
4449
|
+
if (typeof t === "string" && Array.isArray(v)) {
|
|
4450
|
+
const values = [];
|
|
4451
|
+
for (const item of v) if (typeof item === "string" || typeof item === "number" || item === null) values.push(item);
|
|
4452
|
+
else throw new Error("bad cursor value");
|
|
4453
|
+
return {
|
|
4454
|
+
tag: t,
|
|
4455
|
+
values
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
} catch {}
|
|
4460
|
+
throw new InvalidRecordError("the cursor is not valid");
|
|
4461
|
+
}
|
|
4462
|
+
function assertCursor(cursor, tag, length) {
|
|
4463
|
+
if (cursor.tag !== tag || cursor.values.length !== length) throw new InvalidRecordError("the cursor does not belong to this query");
|
|
4464
|
+
}
|
|
4465
|
+
function compareKeys(a, b) {
|
|
4466
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
4467
|
+
const c = compareSql(a[i] ?? null, b[i] ?? null);
|
|
4468
|
+
if (c !== 0) return c;
|
|
4469
|
+
}
|
|
4470
|
+
return 0;
|
|
4471
|
+
}
|
|
4472
|
+
/** What a re-declaration may not change (the engine's `assertAdditive`). */
|
|
4473
|
+
function assertAdditive(kind, before, next) {
|
|
4474
|
+
const refuse = (why) => {
|
|
4475
|
+
throw new DeclarationRefusedError(`kind "${kind}": ${why} - the engine never migrates data; a changed index is a new name`);
|
|
4476
|
+
};
|
|
4477
|
+
if (before.database !== next.database) refuse("the database cannot change");
|
|
4478
|
+
if (before.device !== next.device) refuse("\"device\" cannot change");
|
|
4479
|
+
if (before.at !== next.at) refuse("\"at\" cannot change");
|
|
4480
|
+
for (const old of before.indexes) {
|
|
4481
|
+
const same = next.indexes.find((i) => i.name === old.name);
|
|
4482
|
+
if (same === void 0) continue;
|
|
4483
|
+
if (same.unique !== old.unique || same.fields.join(",") !== old.fields.join(",")) refuse(`index "${old.name}" was declared differently before`);
|
|
4484
|
+
}
|
|
4485
|
+
for (const path of before.secretPaths) if (!next.secretPaths.includes(path)) refuse(`secret path "${path}" cannot stop being secret`);
|
|
4486
|
+
}
|
|
4487
|
+
/** The row bounds of a kind whose retention is a ring, else null. */
|
|
4488
|
+
function ringBounds(decl) {
|
|
4489
|
+
const { retention } = decl;
|
|
4490
|
+
if ("maxRows" in retention || "maxRowsPerDevice" in retention) return {
|
|
4491
|
+
...retention.maxRows === void 0 ? {} : { maxRows: retention.maxRows },
|
|
4492
|
+
...retention.maxRowsPerDevice === void 0 ? {} : { maxRowsPerDevice: retention.maxRowsPerDevice }
|
|
4493
|
+
};
|
|
4494
|
+
return null;
|
|
4495
|
+
}
|
|
4496
|
+
/** Buckets one histogram answer may carry (the engine's `HISTOGRAM_MAX_BUCKETS`). */
|
|
4497
|
+
var HISTOGRAM_MAX_BUCKETS = 1e4;
|
|
4498
|
+
/** SUM / MIN / MAX / COUNT over `json_extract` values, as SQLite folds them. */
|
|
4499
|
+
function aggregateOf(op, field, rows) {
|
|
4500
|
+
if (field === void 0) return rows.length;
|
|
4501
|
+
const values = rows.map((r) => extractField(r.data, field)).filter((v) => v !== null);
|
|
4502
|
+
if (op === "count") return values.length;
|
|
4503
|
+
if (values.length === 0) return null;
|
|
4504
|
+
if (op === "sum") return values.reduce((s, v) => s + (typeof v === "number" ? v : Number(v) || 0), 0);
|
|
4505
|
+
const sorted = [...values].sort(compareSql);
|
|
4506
|
+
const picked = op === "min" ? sorted[0] : sorted.at(-1);
|
|
4507
|
+
return typeof picked === "number" ? picked : null;
|
|
4508
|
+
}
|
|
4509
|
+
//#endregion
|
|
4510
|
+
//#region src/records/in-memory-records.ts
|
|
4511
|
+
function makeInMemoryRecords(owner = "spec", options = {}) {
|
|
4512
|
+
const now = options.now ?? Date.now;
|
|
4213
4513
|
const decls = /* @__PURE__ */ new Map();
|
|
4514
|
+
const kindIds = /* @__PURE__ */ new Map();
|
|
4214
4515
|
const tables = /* @__PURE__ */ new Map();
|
|
4215
|
-
|
|
4516
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
4517
|
+
const pendingAllocated = /* @__PURE__ */ new Set();
|
|
4518
|
+
const writesSinceTrim = /* @__PURE__ */ new Map();
|
|
4519
|
+
const queries = [];
|
|
4520
|
+
let nextRid = 1;
|
|
4216
4521
|
let writesFail = false;
|
|
4217
4522
|
let readsFail = false;
|
|
4218
4523
|
const table = (kind) => {
|
|
@@ -4225,74 +4530,109 @@ function makeInMemoryRecords(owner = "spec") {
|
|
|
4225
4530
|
};
|
|
4226
4531
|
const declOf = (kind) => {
|
|
4227
4532
|
const decl = decls.get(kind);
|
|
4228
|
-
if (decl === void 0) throw new
|
|
4533
|
+
if (decl === void 0) throw new UnknownRecordKindError(`"${owner}" declared no kind "${kind}"`);
|
|
4229
4534
|
return decl;
|
|
4230
4535
|
};
|
|
4231
|
-
const
|
|
4232
|
-
if (
|
|
4233
|
-
|
|
4234
|
-
|
|
4536
|
+
const reading = () => {
|
|
4537
|
+
if (readsFail) throw new Error("records engine unreachable");
|
|
4538
|
+
};
|
|
4539
|
+
const writing = () => {
|
|
4540
|
+
if (writesFail) throw new Error("records engine unreachable");
|
|
4235
4541
|
};
|
|
4236
|
-
const view = (
|
|
4542
|
+
const view = (decl, row) => ({
|
|
4237
4543
|
id: row.id,
|
|
4238
|
-
deviceId:
|
|
4544
|
+
deviceId: row.deviceId,
|
|
4239
4545
|
at: row.at,
|
|
4240
|
-
expiresAt:
|
|
4546
|
+
expiresAt: row.expiresAt,
|
|
4241
4547
|
rev: row.rev,
|
|
4242
4548
|
updatedAt: row.updatedAt,
|
|
4243
|
-
data:
|
|
4549
|
+
data: redactBody(decl, jsonCopy(row.data))
|
|
4244
4550
|
});
|
|
4245
|
-
const
|
|
4246
|
-
const
|
|
4247
|
-
|
|
4248
|
-
body,
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
if (isSecretMarker(leaf.value)) {
|
|
4255
|
-
if (leaf.value.$secret !== expected) throw new Error("foreign secret marker");
|
|
4256
|
-
return leaf.value;
|
|
4551
|
+
const assertUnique = (kind, decl, id, body) => {
|
|
4552
|
+
for (const index of decl.indexes) {
|
|
4553
|
+
if (index.unique !== true) continue;
|
|
4554
|
+
const mine = index.fields.map((f) => extractField(body, f));
|
|
4555
|
+
if (mine.some((v) => v === null)) continue;
|
|
4556
|
+
for (const other of table(kind).values()) {
|
|
4557
|
+
if (other.id === id) continue;
|
|
4558
|
+
const theirs = index.fields.map((f) => extractField(other.data, f));
|
|
4559
|
+
if (compareKeys(mine, theirs) === 0 && theirs.every((v) => v !== null)) throw new RecordUniqueViolationError(`${kind}: the unique index "${index.name}" refused the write`);
|
|
4257
4560
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
throw new Error(`${kind}/${id}: a secret field takes a keychain marker, never a value`);
|
|
4561
|
+
}
|
|
4562
|
+
};
|
|
4563
|
+
const store = (kind, prev, next) => {
|
|
4564
|
+
const rid = prev?.rid ?? nextRid;
|
|
4565
|
+
if (prev === void 0) nextRid += 1;
|
|
4566
|
+
table(kind).set(next.id, {
|
|
4567
|
+
...next,
|
|
4568
|
+
rid
|
|
4267
4569
|
});
|
|
4268
|
-
if (!isRecord(next)) throw new Error("the body must be an object");
|
|
4269
|
-
const after = markersOf(decl, next);
|
|
4270
|
-
return {
|
|
4271
|
-
body: next,
|
|
4272
|
-
released: [...before.entries()].filter(([p, ref]) => after.get(p) !== ref).map(([, r]) => r)
|
|
4273
|
-
};
|
|
4274
4570
|
};
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
const
|
|
4279
|
-
const
|
|
4571
|
+
/** One row of a put / putMany: the engine's `writeRecord`. */
|
|
4572
|
+
const writeRecord = (kind, rec, ifRev, nowMs) => {
|
|
4573
|
+
const decl = declOf(kind);
|
|
4574
|
+
const header = validateHeader(kind, decl, rec, nowMs);
|
|
4575
|
+
const allocatedKey = `${kind}:${rec.id}`;
|
|
4576
|
+
const prev = table(kind).get(rec.id);
|
|
4577
|
+
if (pendingAllocated.has(allocatedKey) && prev !== void 0) throw new RevConflictError(`${kind}/${rec.id}: an allocated id is insert-only and a row already holds it`);
|
|
4578
|
+
if (ifRev !== void 0) {
|
|
4579
|
+
if (ifRev === 0 && prev !== void 0) throw new RevConflictError(`${kind}/${rec.id}: the row exists (ifRev 0 means "must not exist")`);
|
|
4580
|
+
if (ifRev > 0 && prev?.rev !== ifRev) throw new RevConflictError(`${kind}/${rec.id}: expected rev ${ifRev}, found ${prev === void 0 ? "no row" : `rev ${prev.rev}`}`);
|
|
4581
|
+
}
|
|
4582
|
+
const checked = enforceSecrets(owner, kind, decl, rec.id, rec.data, prev?.data ?? null, "put");
|
|
4583
|
+
serializeBody(kind, decl, rec.id, checked.body);
|
|
4584
|
+
const body = jsonCopy(checked.body);
|
|
4585
|
+
assertUnique(kind, decl, rec.id, body);
|
|
4280
4586
|
const rev = (prev?.rev ?? 0) + 1;
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
at:
|
|
4587
|
+
store(kind, prev, {
|
|
4588
|
+
id: header.id,
|
|
4589
|
+
deviceId: header.deviceId,
|
|
4590
|
+
at: header.at,
|
|
4591
|
+
expiresAt: header.expiresAt,
|
|
4285
4592
|
rev,
|
|
4286
|
-
updatedAt:
|
|
4593
|
+
updatedAt: nowMs,
|
|
4287
4594
|
data: body
|
|
4288
4595
|
});
|
|
4596
|
+
pendingAllocated.delete(allocatedKey);
|
|
4289
4597
|
return {
|
|
4290
4598
|
rev,
|
|
4291
|
-
released
|
|
4599
|
+
released: checked.releasedRefs
|
|
4292
4600
|
};
|
|
4293
4601
|
};
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4602
|
+
const trimScope = (kind, max, deviceId) => {
|
|
4603
|
+
const scope = [...table(kind).values()].filter((r) => deviceId === void 0 || r.deviceId === deviceId);
|
|
4604
|
+
const excess = Math.min(scope.length - max, RECORD_DELETE_BATCH);
|
|
4605
|
+
if (excess <= 0) return;
|
|
4606
|
+
scope.sort((a, b) => compareKeys([a.at, a.rid], [b.at, b.rid]));
|
|
4607
|
+
for (const row of scope.slice(0, excess)) table(kind).delete(row.id);
|
|
4608
|
+
};
|
|
4609
|
+
/** The engine's ring trim, at the same cadence (once per bound / TRIM_SLACK writes). */
|
|
4610
|
+
const afterWrite = (kind, devices) => {
|
|
4611
|
+
const bounds = ringBounds(declOf(kind));
|
|
4612
|
+
if (bounds === null) return;
|
|
4613
|
+
const bound = Math.max(bounds.maxRows ?? 0, bounds.maxRowsPerDevice ?? 0);
|
|
4614
|
+
const interval = bound <= 100 ? 1 : Math.floor(bound / 100);
|
|
4615
|
+
const count = (writesSinceTrim.get(kind) ?? 0) + devices.length;
|
|
4616
|
+
if (count < interval) {
|
|
4617
|
+
writesSinceTrim.set(kind, count);
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
writesSinceTrim.set(kind, 0);
|
|
4621
|
+
if (bounds.maxRows !== void 0) trimScope(kind, bounds.maxRows, void 0);
|
|
4622
|
+
if (bounds.maxRowsPerDevice !== void 0) {
|
|
4623
|
+
const seen = new Set(devices.filter((d) => d !== void 0));
|
|
4624
|
+
for (const device of seen) trimScope(kind, bounds.maxRowsPerDevice, device);
|
|
4625
|
+
}
|
|
4626
|
+
};
|
|
4627
|
+
const refsOf = (decl, rows) => rows.flatMap((row) => [...markersOf(decl, row.data).values()]);
|
|
4628
|
+
const removeWhere = (kind, test) => {
|
|
4629
|
+
const decl = declOf(kind);
|
|
4630
|
+
const gone = [...table(kind).values()].filter(test);
|
|
4631
|
+
for (const row of gone) table(kind).delete(row.id);
|
|
4632
|
+
return {
|
|
4633
|
+
deleted: gone.length,
|
|
4634
|
+
secretRefs: refsOf(decl, gone)
|
|
4635
|
+
};
|
|
4296
4636
|
};
|
|
4297
4637
|
return {
|
|
4298
4638
|
records: createRecordsAccess({
|
|
@@ -4300,69 +4640,119 @@ function makeInMemoryRecords(owner = "spec") {
|
|
|
4300
4640
|
transport: {
|
|
4301
4641
|
declareKinds: async ({ kinds }) => {
|
|
4302
4642
|
const out = {};
|
|
4303
|
-
|
|
4304
|
-
|
|
4643
|
+
for (const [name, raw] of Object.entries(kinds)) {
|
|
4644
|
+
const decl = RecordKindDeclarationSchema.parse(raw);
|
|
4645
|
+
assertValidDeclaration(name, decl);
|
|
4646
|
+
const prior = decls.get(name);
|
|
4647
|
+
if (prior !== void 0) assertAdditive(name, prior, decl);
|
|
4648
|
+
const kindId = kindIds.get(name) ?? kindIds.size + 1;
|
|
4649
|
+
kindIds.set(name, kindId);
|
|
4650
|
+
const created = decl.indexes.filter((i) => prior?.indexes.every((p) => p.name !== i.name) ?? true).map((i) => i.name);
|
|
4651
|
+
const state = prior === void 0 ? "created" : JSON.stringify(prior) === JSON.stringify(decl) ? "unchanged" : "updated";
|
|
4305
4652
|
decls.set(name, decl);
|
|
4306
|
-
n += 1;
|
|
4307
4653
|
out[name] = {
|
|
4308
|
-
kindId
|
|
4309
|
-
state
|
|
4310
|
-
createdIndexes:
|
|
4654
|
+
kindId,
|
|
4655
|
+
state,
|
|
4656
|
+
createdIndexes: created,
|
|
4311
4657
|
adoptionPending: null
|
|
4312
4658
|
};
|
|
4313
4659
|
}
|
|
4314
4660
|
return { kinds: out };
|
|
4315
4661
|
},
|
|
4316
4662
|
get: async ({ kind, id }) => {
|
|
4317
|
-
|
|
4663
|
+
reading();
|
|
4664
|
+
const decl = declOf(kind);
|
|
4318
4665
|
const row = table(kind).get(id);
|
|
4319
|
-
return row === void 0 ? null : view(
|
|
4666
|
+
return row === void 0 ? null : view(decl, row);
|
|
4667
|
+
},
|
|
4668
|
+
getMany: async ({ kind, ids }) => {
|
|
4669
|
+
reading();
|
|
4670
|
+
const decl = declOf(kind);
|
|
4671
|
+
return [...new Set(ids)].sort((a, b) => compareSql(a, b)).flatMap((id) => {
|
|
4672
|
+
const row = table(kind).get(id);
|
|
4673
|
+
return row === void 0 ? [] : [view(decl, row)];
|
|
4674
|
+
});
|
|
4320
4675
|
},
|
|
4321
|
-
getMany: async ({ kind, ids }) => ids.flatMap((id) => {
|
|
4322
|
-
const row = table(kind).get(id);
|
|
4323
|
-
return row === void 0 ? [] : [view(kind, row)];
|
|
4324
|
-
}),
|
|
4325
4676
|
secretRefs: async ({ kind, ids }) => {
|
|
4677
|
+
reading();
|
|
4326
4678
|
const decl = declOf(kind);
|
|
4327
4679
|
const out = {};
|
|
4328
4680
|
for (const id of ids) {
|
|
4329
4681
|
const row = table(kind).get(id);
|
|
4330
|
-
if (row
|
|
4682
|
+
if (row === void 0) continue;
|
|
4683
|
+
const refs = Object.fromEntries(markersOf(decl, row.data));
|
|
4684
|
+
if (Object.keys(refs).length > 0) out[id] = refs;
|
|
4331
4685
|
}
|
|
4332
4686
|
return out;
|
|
4333
4687
|
},
|
|
4334
4688
|
put: async ({ kind, record, ifRev }) => {
|
|
4335
|
-
|
|
4336
|
-
const
|
|
4689
|
+
writing();
|
|
4690
|
+
const written = writeRecord(kind, record, ifRev, now());
|
|
4691
|
+
afterWrite(kind, [record.deviceId]);
|
|
4337
4692
|
return {
|
|
4338
|
-
rev,
|
|
4339
|
-
releasedRefs: released
|
|
4693
|
+
rev: written.rev,
|
|
4694
|
+
releasedRefs: [...written.released]
|
|
4340
4695
|
};
|
|
4341
4696
|
},
|
|
4342
4697
|
putMany: async ({ kind, records }) => {
|
|
4698
|
+
writing();
|
|
4699
|
+
declOf(kind);
|
|
4700
|
+
if (records.length > 1e3) throw new BatchTooLargeError(`${records.length} records exceed ${RECORD_BATCH_MAX}`);
|
|
4701
|
+
const bytes = records.reduce((sum, r) => sum + Buffer.byteLength(JSON.stringify(r.data), "utf8"), 0);
|
|
4702
|
+
if (bytes > 4194304) throw new BatchTooLargeError(`${bytes} bytes exceed ${RECORD_BATCH_MAX_BYTES}`);
|
|
4703
|
+
const nowMs = now();
|
|
4704
|
+
const before = new Map(table(kind));
|
|
4705
|
+
const ridBefore = nextRid;
|
|
4343
4706
|
const released = [];
|
|
4344
|
-
|
|
4707
|
+
try {
|
|
4708
|
+
for (const rec of records) released.push(...writeRecord(kind, rec, void 0, nowMs).released);
|
|
4709
|
+
} catch (err) {
|
|
4710
|
+
tables.set(kind, before);
|
|
4711
|
+
nextRid = ridBefore;
|
|
4712
|
+
throw err;
|
|
4713
|
+
}
|
|
4714
|
+
let top = -1;
|
|
4715
|
+
for (const r of records) if (/^\d{1,15}$/.test(r.id)) top = Math.max(top, Number(r.id));
|
|
4716
|
+
if (top >= 0) sequences.set(kind, Math.max(sequences.get(kind) ?? 0, top + 1));
|
|
4717
|
+
afterWrite(kind, records.map((r) => r.deviceId));
|
|
4345
4718
|
return {
|
|
4346
4719
|
count: records.length,
|
|
4347
4720
|
releasedRefs: released
|
|
4348
4721
|
};
|
|
4349
4722
|
},
|
|
4350
|
-
patch: async (
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
if (
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4723
|
+
patch: async (input) => {
|
|
4724
|
+
writing();
|
|
4725
|
+
const decl = declOf(input.kind);
|
|
4726
|
+
if (input.at !== void 0 && decl.at !== "required") throw new InvalidRecordError(`${input.kind}: declared at: "none"`);
|
|
4727
|
+
if (input.at === null) throw new InvalidRecordError(`${input.kind}: "at" cannot be removed`);
|
|
4728
|
+
if (input.expiresAt !== void 0 && decl.secretPaths.length > 0) throw new InvalidRecordError(`${input.kind}: a kind with secret fields cannot expire by the engine (its keychain values would be orphaned)`);
|
|
4729
|
+
if (input.expiresAt !== void 0 && "ttlMs" in decl.retention) throw new InvalidRecordError(`${input.kind}: expiresAt is set by the engine`);
|
|
4730
|
+
const before = table(input.kind).get(input.id);
|
|
4731
|
+
const checked = enforceSecrets(owner, input.kind, decl, input.id, input.data, before?.data ?? null, "patch");
|
|
4732
|
+
if (before === void 0) throw new RecordNotFoundError(`${input.kind}/${input.id}: no such row`);
|
|
4733
|
+
if (input.ifRev !== void 0 && before.rev !== input.ifRev) throw new RevConflictError(`${input.kind}/${input.id}: expected rev ${String(input.ifRev)}, found rev ${before.rev}`);
|
|
4734
|
+
const merged = jsonCopy(mergePatch(before.data, jsonCopy(checked.body)));
|
|
4735
|
+
const size = [...JSON.stringify(merged)].length;
|
|
4736
|
+
if (size > decl.maxDataBytes) throw new RecordTooLargeError(`${input.kind}/${input.id}: the patched body is ${size} bytes, over maxDataBytes ${decl.maxDataBytes}`);
|
|
4737
|
+
assertUnique(input.kind, decl, input.id, merged);
|
|
4738
|
+
const rev = before.rev + 1;
|
|
4739
|
+
store(input.kind, before, {
|
|
4740
|
+
...before,
|
|
4741
|
+
at: input.at ?? before.at,
|
|
4742
|
+
expiresAt: input.expiresAt === void 0 ? before.expiresAt : input.expiresAt,
|
|
4743
|
+
rev,
|
|
4744
|
+
updatedAt: now(),
|
|
4745
|
+
data: merged
|
|
4358
4746
|
});
|
|
4747
|
+
const kept = new Set(markersOf(decl, merged).values());
|
|
4359
4748
|
return {
|
|
4360
4749
|
rev,
|
|
4361
|
-
releasedRefs:
|
|
4750
|
+
releasedRefs: [...markersOf(decl, before.data).values()].filter((r) => !kept.has(r))
|
|
4362
4751
|
};
|
|
4363
4752
|
},
|
|
4364
4753
|
delete: async ({ kind, id }) => {
|
|
4365
|
-
|
|
4754
|
+
writing();
|
|
4755
|
+
const decl = declOf(kind);
|
|
4366
4756
|
const row = table(kind).get(id);
|
|
4367
4757
|
if (row === void 0) return {
|
|
4368
4758
|
deleted: false,
|
|
@@ -4371,57 +4761,189 @@ function makeInMemoryRecords(owner = "spec") {
|
|
|
4371
4761
|
table(kind).delete(id);
|
|
4372
4762
|
return {
|
|
4373
4763
|
deleted: true,
|
|
4374
|
-
secretRefs:
|
|
4764
|
+
secretRefs: refsOf(decl, [row])
|
|
4375
4765
|
};
|
|
4376
4766
|
},
|
|
4377
4767
|
deleteMany: async ({ kind, ids }) => {
|
|
4378
|
-
|
|
4379
|
-
const
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4768
|
+
writing();
|
|
4769
|
+
const wanted = new Set(ids);
|
|
4770
|
+
return removeWhere(kind, (row) => wanted.has(row.id));
|
|
4771
|
+
},
|
|
4772
|
+
query: async (q) => {
|
|
4773
|
+
reading();
|
|
4774
|
+
const decl = declOf(q.kind);
|
|
4775
|
+
queries.push({
|
|
4776
|
+
kind: q.kind,
|
|
4777
|
+
limit: q.limit
|
|
4778
|
+
});
|
|
4779
|
+
assertWindowAllowed(q.kind, decl, q);
|
|
4780
|
+
const desc = q.desc === true;
|
|
4781
|
+
const dir = desc ? "DESC" : "ASC";
|
|
4782
|
+
const where = planWhere(q.kind, decl, q.where, { allowIn: false });
|
|
4783
|
+
const cursor = q.cursor === void 0 ? void 0 : decodeCursor(q.cursor);
|
|
4784
|
+
let rows = [...table(q.kind).values()].filter((r) => inWindow(r, q));
|
|
4785
|
+
let keysOf;
|
|
4786
|
+
let keyCount;
|
|
4787
|
+
let tag;
|
|
4788
|
+
if (where !== null) {
|
|
4789
|
+
if (q.order !== void 0) throw new InvalidRecordError(`${q.kind}: with a where the order is the index's; drop "order"`);
|
|
4790
|
+
const fields = where.orderFields;
|
|
4791
|
+
keysOf = (row) => [...fields.map((f) => extractField(row.data, f)), row.rid];
|
|
4792
|
+
keyCount = fields.length;
|
|
4793
|
+
tag = `rx:${where.indexName}:${dir}`;
|
|
4794
|
+
rows = rows.filter((r) => where.matches(r.data));
|
|
4795
|
+
if (cursor !== void 0) {
|
|
4796
|
+
assertCursor(cursor, tag, fields.length + 1);
|
|
4797
|
+
rows = rows.filter((r) => rowValueAfter(keysOf(r), cursor.values, desc));
|
|
4798
|
+
}
|
|
4799
|
+
} else {
|
|
4800
|
+
const order = q.order ?? (decl.at === "required" ? "at" : "id");
|
|
4801
|
+
keyCount = 1;
|
|
4802
|
+
if (order === "at") {
|
|
4803
|
+
if (decl.at !== "required") throw new InvalidRecordError(`${q.kind}: declared at: "none", it has no time order - use order: "id"`);
|
|
4804
|
+
keysOf = (row) => [row.at, row.rid];
|
|
4805
|
+
tag = `at:${dir}`;
|
|
4806
|
+
if (cursor !== void 0) {
|
|
4807
|
+
assertCursor(cursor, tag, 2);
|
|
4808
|
+
rows = rows.filter((r) => rowValueAfter(keysOf(r), cursor.values, desc));
|
|
4809
|
+
}
|
|
4810
|
+
} else {
|
|
4811
|
+
if (q.deviceId !== void 0 || q.atFrom !== void 0 || q.atTo !== void 0) throw new UnindexedFilterError(`${q.kind}: order "id" cannot be combined with a device or time window (use order "at")`);
|
|
4812
|
+
keysOf = (row) => [row.id, row.rid];
|
|
4813
|
+
tag = `id:${dir}`;
|
|
4814
|
+
if (cursor !== void 0) {
|
|
4815
|
+
assertCursor(cursor, tag, 2);
|
|
4816
|
+
const last = cursor.values[0] ?? null;
|
|
4817
|
+
rows = rows.filter((r) => rowValueAfter([r.id], [last], desc));
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
rows.sort((a, b) => (desc ? -1 : 1) * compareKeys(keysOf(a), keysOf(b)));
|
|
4822
|
+
const page = rows.slice(0, q.limit);
|
|
4823
|
+
const more = rows.length > q.limit;
|
|
4824
|
+
const last = page.at(-1);
|
|
4825
|
+
let nextCursor = null;
|
|
4826
|
+
if (more && last !== void 0) {
|
|
4827
|
+
const all = keysOf(last);
|
|
4828
|
+
const keys = [];
|
|
4829
|
+
for (let i = 0; i < keyCount; i += 1) {
|
|
4830
|
+
const value = all[i];
|
|
4831
|
+
if (typeof value !== "string" && typeof value !== "number") throw new InvalidRecordError(`${q.kind}: a row has no value for an ordering field, so this query cannot be paged`);
|
|
4832
|
+
keys.push(value);
|
|
4833
|
+
}
|
|
4834
|
+
keys.push(last.rid);
|
|
4835
|
+
nextCursor = encodeCursor(tag, keys);
|
|
4386
4836
|
}
|
|
4387
4837
|
return {
|
|
4388
|
-
|
|
4389
|
-
|
|
4838
|
+
records: page.map((r) => view(decl, r)),
|
|
4839
|
+
nextCursor
|
|
4390
4840
|
};
|
|
4391
4841
|
},
|
|
4392
|
-
|
|
4393
|
-
|
|
4842
|
+
count: async (q) => {
|
|
4843
|
+
reading();
|
|
4844
|
+
const decl = declOf(q.kind);
|
|
4845
|
+
assertWindowAllowed(q.kind, decl, q);
|
|
4846
|
+
const where = planWhere(q.kind, decl, q.where, { allowIn: true });
|
|
4847
|
+
let n = 0;
|
|
4848
|
+
for (const row of table(q.kind).values()) if (inWindow(row, q) && (where === null || where.matches(row.data))) n += 1;
|
|
4849
|
+
return { count: n };
|
|
4850
|
+
},
|
|
4851
|
+
histogram: async (q) => {
|
|
4852
|
+
reading();
|
|
4853
|
+
const decl = declOf(q.kind);
|
|
4854
|
+
assertWindowAllowed(q.kind, decl, {
|
|
4855
|
+
deviceId: q.deviceId,
|
|
4856
|
+
atFrom: q.from
|
|
4857
|
+
});
|
|
4858
|
+
if (!(q.to > q.from)) throw new InvalidRecordError(`${q.kind}: histogram needs from < to`);
|
|
4859
|
+
if (Math.ceil((q.to - q.from) / q.bucketMs) > 1e4) throw new InvalidRecordError(`${q.kind}: histogram would exceed ${HISTOGRAM_MAX_BUCKETS} buckets`);
|
|
4860
|
+
const from = Math.floor(q.from);
|
|
4861
|
+
const to = Math.ceil(q.to);
|
|
4862
|
+
const step = Math.trunc(q.bucketMs);
|
|
4863
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4864
|
+
for (const row of table(q.kind).values()) {
|
|
4865
|
+
if (q.deviceId !== void 0 && row.deviceId !== q.deviceId) continue;
|
|
4866
|
+
if (row.at === null || row.at < from || row.at >= to) continue;
|
|
4867
|
+
const b = Math.trunc((row.at - from) / step);
|
|
4868
|
+
counts.set(b, (counts.get(b) ?? 0) + 1);
|
|
4869
|
+
}
|
|
4870
|
+
return { buckets: [...counts.entries()].sort(([a], [b]) => a - b).map(([b, count]) => ({
|
|
4871
|
+
bucketStart: from + b * q.bucketMs,
|
|
4872
|
+
count
|
|
4873
|
+
})) };
|
|
4874
|
+
},
|
|
4875
|
+
aggregate: async (q) => {
|
|
4876
|
+
reading();
|
|
4394
4877
|
const decl = declOf(q.kind);
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4878
|
+
if (q.op !== "count" && q.field === void 0) throw new InvalidRecordError(`${q.kind}: ${q.op} needs a field`);
|
|
4879
|
+
const extra = [q.field, q.groupBy].filter((f) => f !== void 0);
|
|
4880
|
+
if (extra.length === 0 && q.where === void 0) throw new InvalidRecordError(`${q.kind}: a count with no field and no groupBy is "count"`);
|
|
4881
|
+
const where = planWhere(q.kind, decl, q.where, {
|
|
4882
|
+
allowIn: true,
|
|
4883
|
+
extraFields: extra,
|
|
4884
|
+
groupBy: q.groupBy
|
|
4885
|
+
});
|
|
4886
|
+
if (where === null) throw new InvalidRecordError(`${q.kind}: aggregate has nothing to read`);
|
|
4887
|
+
const rows = [...table(q.kind).values()].filter((r) => where.matches(r.data));
|
|
4888
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4889
|
+
if (q.groupBy === void 0) groups.set(null, rows);
|
|
4890
|
+
else {
|
|
4891
|
+
const by = q.groupBy;
|
|
4892
|
+
for (const row of rows) {
|
|
4893
|
+
const g = extractField(row.data, by);
|
|
4894
|
+
groups.set(g, [...groups.get(g) ?? [], row]);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
return { rows: [...groups.entries()].sort(([a], [b]) => compareSql(a, b)).map(([group, members]) => ({
|
|
4898
|
+
group,
|
|
4899
|
+
value: aggregateOf(q.op, q.field, members)
|
|
4900
|
+
})) };
|
|
4901
|
+
},
|
|
4902
|
+
deleteWhere: async (q) => {
|
|
4903
|
+
writing();
|
|
4904
|
+
const decl = declOf(q.kind);
|
|
4905
|
+
if (q.deviceId === void 0 && q.atBefore === void 0 && (q.where === void 0 || Object.keys(q.where).length === 0)) throw new FilterRequiredError(`${q.kind}: deleteWhere needs deviceId, atBefore or where - there is no "delete all"`);
|
|
4906
|
+
assertWindowAllowed(q.kind, decl, {
|
|
4907
|
+
deviceId: q.deviceId,
|
|
4908
|
+
atFrom: q.atBefore
|
|
4909
|
+
});
|
|
4910
|
+
const where = planWhere(q.kind, decl, q.where, { allowIn: true });
|
|
4911
|
+
return removeWhere(q.kind, (row) => {
|
|
4912
|
+
if (where !== null && !where.matches(row.data)) return false;
|
|
4913
|
+
if (q.deviceId !== void 0 && row.deviceId !== q.deviceId) return false;
|
|
4914
|
+
if (q.atBefore !== void 0 && (row.at === null || row.at >= q.atBefore)) return false;
|
|
4915
|
+
return true;
|
|
4916
|
+
});
|
|
4917
|
+
},
|
|
4918
|
+
deleteDevice: async ({ deviceId, kinds }) => {
|
|
4919
|
+
writing();
|
|
4920
|
+
const deleted = {};
|
|
4921
|
+
const secretRefs = [];
|
|
4922
|
+
for (const [kind, decl] of decls) {
|
|
4923
|
+
if (decl.deviceRemoval !== "delete") continue;
|
|
4924
|
+
if (kinds !== void 0 && !kinds.includes(kind)) continue;
|
|
4925
|
+
const result = removeWhere(kind, (row) => row.deviceId === deviceId);
|
|
4926
|
+
deleted[kind] = result.deleted;
|
|
4927
|
+
secretRefs.push(...result.secretRefs);
|
|
4401
4928
|
}
|
|
4402
|
-
let rows = [...table(q.kind).values()];
|
|
4403
|
-
for (const [field, wanted] of Object.entries(q.where ?? {})) rows = rows.filter((r) => r.data[field] === wanted);
|
|
4404
|
-
const byAt = q.order === "at" || q.order === void 0 && decl.at === "required";
|
|
4405
|
-
rows.sort((a, b) => byAt ? (a.at ?? 0) - (b.at ?? 0) : a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
4406
|
-
if (q.desc === true) rows.reverse();
|
|
4407
|
-
const start = q.cursor === void 0 ? 0 : Number(q.cursor);
|
|
4408
|
-
const page = rows.slice(start, start + q.limit);
|
|
4409
|
-
const more = start + q.limit < rows.length;
|
|
4410
4929
|
return {
|
|
4411
|
-
|
|
4412
|
-
|
|
4930
|
+
deleted,
|
|
4931
|
+
secretRefs
|
|
4413
4932
|
};
|
|
4414
4933
|
},
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4934
|
+
allocateId: async ({ kind }) => {
|
|
4935
|
+
writing();
|
|
4936
|
+
declOf(kind);
|
|
4937
|
+
const id = sequences.get(kind) ?? 1;
|
|
4938
|
+
sequences.set(kind, id + 1);
|
|
4939
|
+
pendingAllocated.add(`${kind}:${id}`);
|
|
4940
|
+
return { id };
|
|
4941
|
+
}
|
|
4421
4942
|
},
|
|
4422
4943
|
secrets: makeSpecKeychain(owner).access
|
|
4423
4944
|
}),
|
|
4424
4945
|
tables,
|
|
4946
|
+
queries,
|
|
4425
4947
|
failWrites: (on) => {
|
|
4426
4948
|
writesFail = on;
|
|
4427
4949
|
},
|
|
@@ -4429,12 +4951,14 @@ function makeInMemoryRecords(owner = "spec") {
|
|
|
4429
4951
|
readsFail = on;
|
|
4430
4952
|
},
|
|
4431
4953
|
seed: (kind, row) => {
|
|
4432
|
-
table(kind).
|
|
4954
|
+
store(kind, table(kind).get(row.id), {
|
|
4433
4955
|
id: row.id,
|
|
4956
|
+
deviceId: row.deviceId ?? null,
|
|
4434
4957
|
at: row.at ?? null,
|
|
4958
|
+
expiresAt: row.expiresAt ?? null,
|
|
4435
4959
|
rev: 1,
|
|
4436
4960
|
updatedAt: 1,
|
|
4437
|
-
data: row.data
|
|
4961
|
+
data: jsonCopy(row.data)
|
|
4438
4962
|
});
|
|
4439
4963
|
}
|
|
4440
4964
|
};
|