@camstack/types 1.2.393 → 1.2.395

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.
@@ -166,7 +166,7 @@ function omitTransientConfig(schema, config) {
166
166
  return next;
167
167
  }
168
168
  function collectOmitted(field, out) {
169
- if (!isRecord$6(field)) return;
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$6(field)) return;
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$6(value) {
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$6(schema)) return [];
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$6(node)) return [];
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$6(field)) return;
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$5(value) {
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$5(value)) return false;
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$5(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
285
+ return isRecord$6(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
286
286
  }
287
287
  function isSecretClear(value) {
288
- return isRecord$5(value) && value["$secretClear"] === true;
288
+ return isRecord$6(value) && value["$secretClear"] === true;
289
289
  }
290
290
  function isSecretFieldView(value) {
291
- return isRecord$5(value) && value["set"] === true && isSecretMarkerLike(value);
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$4(value) {
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$4(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
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$4) : [];
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$4(row)) {
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$4(incoming)) {
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$4(stored) ? stored : {};
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$4(v)) {
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$3(value) {
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$3(item) && isRecord$3(twin)) return inArrayRow(pass, item, twin);
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$3(value) && isRecord$3(twin)) out[key] = inArrayRow(pass, value, twin);
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$3(incoming) || !isRecord$3(stored)) return incoming;
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$3(a) && isRecord$3(b)) {
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$3(value)) return null;
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$2(value) {
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$2(element) ? element[idKey] : void 0;
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$2(node)) return node;
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$1(value) {
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$1(parsed.data)) throw new InvalidRecordError(`${this.name}/${id}: the body must be an object`);
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$1(value)) throw new InvalidRecordError("the record body must be an object");
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
- * In-memory `RecordsAccess` for specs. Kinds are declared by the caller.
4150
- * Lives in `@camstack/types` so a spec does not import another package's source.
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
- /** RFC 7396: objects merge key by key, `null` removes, anything else replaces. */
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) && isRecord(current) ? mergePatch(current, value) : value;
4384
+ out[key] = isRecord(value) ? mergePatch(isRecord(current) ? current : {}, value) : value;
4166
4385
  }
4167
4386
  return out;
4168
4387
  }
4169
- function markersOf(decl, body) {
4170
- const out = /* @__PURE__ */ new Map();
4171
- mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
4172
- if (isSecretMarker(leaf.value)) out.set(leaf.concrete, leaf.value.$secret);
4173
- return leaf.value;
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 makeInMemoryRecords(owner = "spec") {
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
- let clock = 1;
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 Error(`kind "${kind}" is not declared`);
4533
+ if (decl === void 0) throw new UnknownRecordKindError(`"${owner}" declared no kind "${kind}"`);
4229
4534
  return decl;
4230
4535
  };
4231
- const redact = (decl, body) => {
4232
- if (decl.secretPaths.length === 0) return body;
4233
- const out = mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => leaf.value === null ? null : REDACTED_SECRET);
4234
- return isRecord(out) ? out : body;
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 = (kind, row) => ({
4542
+ const view = (decl, row) => ({
4237
4543
  id: row.id,
4238
- deviceId: null,
4544
+ deviceId: row.deviceId,
4239
4545
  at: row.at,
4240
- expiresAt: null,
4546
+ expiresAt: row.expiresAt,
4241
4547
  rev: row.rev,
4242
4548
  updatedAt: row.updatedAt,
4243
- data: redact(declOf(kind), row.data)
4549
+ data: redactBody(decl, jsonCopy(row.data))
4244
4550
  });
4245
- const enforce = (kind, id, body, stored) => {
4246
- const decl = declOf(kind);
4247
- if (decl.secretPaths.length === 0) return {
4248
- body,
4249
- released: []
4250
- };
4251
- const before = stored === null ? /* @__PURE__ */ new Map() : markersOf(decl, stored);
4252
- const next = mapSecretLeaves(body, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
4253
- const expected = recordSecretRef(owner, kind, id, leaf.concrete);
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
- if (leaf.value === "__camstack_redacted__") {
4259
- const keep = before.get(leaf.concrete);
4260
- return keep === void 0 ? REMOVE_LEAF : {
4261
- $secret: keep,
4262
- v: 1
4263
- };
4264
- }
4265
- if (leaf.value === null || leaf.value === void 0) return REMOVE_LEAF;
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
- const write = (kind, input) => {
4276
- if (writesFail) throw new Error("records engine unreachable");
4277
- if (declOf(kind).at === "required" && input.at === void 0) throw new Error(`${kind}/${input.id}: the kind needs "at" on every write`);
4278
- const prev = table(kind).get(input.id);
4279
- const { body, released } = enforce(kind, input.id, input.data, prev?.data ?? null);
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
- clock += 1;
4282
- table(kind).set(input.id, {
4283
- id: input.id,
4284
- at: input.at ?? null,
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: clock,
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 unimplemented = (op) => {
4295
- throw new Error(`the fake records transport does not implement ${op}`);
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
- let n = 0;
4304
- for (const [name, decl] of Object.entries(kinds)) {
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: n,
4309
- state: "created",
4310
- createdIndexes: decl.indexes.map((i) => i.name),
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
- if (readsFail) throw new Error("records engine unreachable");
4663
+ reading();
4664
+ const decl = declOf(kind);
4318
4665
  const row = table(kind).get(id);
4319
- return row === void 0 ? null : view(kind, row);
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 !== void 0) out[id] = Object.fromEntries(markersOf(decl, row.data));
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
- if (ifRev !== void 0 && (table(kind).get(record.id)?.rev ?? 0) !== ifRev) throw new Error("stale ifRev");
4336
- const { rev, released } = write(kind, record);
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
- for (const record of records) released.push(...write(kind, record).released);
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 ({ kind, id, data, at, ifRev }) => {
4351
- const prev = table(kind).get(id);
4352
- if (prev === void 0) throw new Error(`${kind}/${id}: no such record`);
4353
- if (ifRev !== void 0 && prev.rev !== ifRev) throw new Error("stale ifRev");
4354
- const { rev, released } = write(kind, {
4355
- id,
4356
- ...at !== void 0 && at !== null ? { at } : prev.at !== null ? { at: prev.at } : {},
4357
- data: mergePatch(prev.data, data)
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: released
4750
+ releasedRefs: [...markersOf(decl, before.data).values()].filter((r) => !kept.has(r))
4362
4751
  };
4363
4752
  },
4364
4753
  delete: async ({ kind, id }) => {
4365
- if (writesFail) throw new Error("records engine unreachable");
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: [...markersOf(declOf(kind), row.data).values()]
4764
+ secretRefs: refsOf(decl, [row])
4375
4765
  };
4376
4766
  },
4377
4767
  deleteMany: async ({ kind, ids }) => {
4378
- let deleted = 0;
4379
- const refs = [];
4380
- for (const id of ids) {
4381
- const row = table(kind).get(id);
4382
- if (row === void 0) continue;
4383
- table(kind).delete(id);
4384
- deleted += 1;
4385
- refs.push(...markersOf(declOf(kind), row.data).values());
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
- deleted,
4389
- secretRefs: refs
4838
+ records: page.map((r) => view(decl, r)),
4839
+ nextCursor
4390
4840
  };
4391
4841
  },
4392
- query: async (q) => {
4393
- if (readsFail) throw new Error("records engine unreachable");
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
- const asked = Object.keys(q.where ?? {});
4396
- if (asked.length > 0) {
4397
- if (!decl.indexes.some((index) => {
4398
- const leading = new Set(index.fields.slice(0, asked.length));
4399
- return asked.length <= index.fields.length && asked.every((f) => leading.has(f));
4400
- })) throw new Error(`${q.kind}: no declared index serves [${asked.join(", ")}]`);
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
- records: page.map((r) => view(q.kind, r)),
4412
- nextCursor: more ? String(start + q.limit) : null
4930
+ deleted,
4931
+ secretRefs
4413
4932
  };
4414
4933
  },
4415
- count: async ({ kind }) => ({ count: table(kind).size }),
4416
- histogram: async () => unimplemented("histogram"),
4417
- aggregate: async () => unimplemented("aggregate"),
4418
- deleteWhere: async () => unimplemented("deleteWhere"),
4419
- deleteDevice: async () => unimplemented("deleteDevice"),
4420
- allocateId: async () => unimplemented("allocateId")
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).set(row.id, {
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
  };