@volter/world-core 3.0.155 → 3.0.157

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.
Files changed (150) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +204 -4
  6. package/dist/inject.cjs +150 -86
  7. package/dist/network-policy.cjs +11 -1
  8. package/dist/network-policy.d.cts +1 -0
  9. package/dist/pack-facts.d.cts +3 -0
  10. package/dist/required-credential-transport.cjs +29 -0
  11. package/dist/required-credential-transport.d.cts +3 -0
  12. package/dist/src/actions.d.ts +15 -0
  13. package/dist/src/actions.js +82 -21
  14. package/dist/src/ancestry.d.ts +5 -4
  15. package/dist/src/ancestry.js +53 -28
  16. package/dist/src/blob-store.d.ts +16 -0
  17. package/dist/src/blob-store.js +76 -1
  18. package/dist/src/changeset.js +1 -1
  19. package/dist/src/clickhouse/compression.d.ts +28 -0
  20. package/dist/src/clickhouse/compression.js +281 -0
  21. package/dist/src/clickhouse/index.d.ts +1 -0
  22. package/dist/src/clickhouse/index.js +1 -0
  23. package/dist/src/derived-core.d.ts +84 -7
  24. package/dist/src/derived-core.js +336 -61
  25. package/dist/src/derived-real.js +97 -15
  26. package/dist/src/derived.d.ts +5 -0
  27. package/dist/src/derived.js +3 -0
  28. package/dist/src/executor.d.ts +7 -0
  29. package/dist/src/executor.js +15 -1
  30. package/dist/src/fork.js +1 -1
  31. package/dist/src/git/lfs.d.ts +13 -3
  32. package/dist/src/git/lfs.js +41 -9
  33. package/dist/src/grpc-wire.js +2 -1
  34. package/dist/src/h2.js +63 -10
  35. package/dist/src/head.d.ts +5 -0
  36. package/dist/src/head.js +15 -0
  37. package/dist/src/history.d.ts +1 -1
  38. package/dist/src/history.js +7 -7
  39. package/dist/src/index.d.ts +5 -1
  40. package/dist/src/index.js +6 -1
  41. package/dist/src/live/ingest.js +9 -13
  42. package/dist/src/live/rtmp.js +7 -5
  43. package/dist/src/live/segmenter.js +7 -1
  44. package/dist/src/log.d.ts +23 -3
  45. package/dist/src/log.js +160 -37
  46. package/dist/src/machines.js +5 -1
  47. package/dist/src/managed-go-programs.d.ts +55 -0
  48. package/dist/src/managed-go-programs.js +133 -0
  49. package/dist/src/managed-original-inputs.d.ts +101 -0
  50. package/dist/src/managed-original-inputs.js +125 -0
  51. package/dist/src/mongodb/client.d.ts +8 -0
  52. package/dist/src/mongodb/client.js +27 -0
  53. package/dist/src/mongodb/index.d.ts +2 -0
  54. package/dist/src/mongodb/index.js +5 -0
  55. package/dist/src/mongodb/stream.d.ts +97 -0
  56. package/dist/src/mongodb/stream.js +206 -0
  57. package/dist/src/observe.js +1 -1
  58. package/dist/src/pack-fetch.d.ts +6 -1
  59. package/dist/src/pack-fetch.js +69 -14
  60. package/dist/src/pack-paths.d.ts +2 -1
  61. package/dist/src/pack-paths.js +6 -1
  62. package/dist/src/packRegistry.d.ts +10 -14
  63. package/dist/src/packRegistry.js +2 -13
  64. package/dist/src/protocol.d.ts +13 -0
  65. package/dist/src/protocol.js +13 -0
  66. package/dist/src/redis/engine.d.ts +10 -4
  67. package/dist/src/redis/engine.js +103 -32
  68. package/dist/src/redis/member-storage.js +20 -8
  69. package/dist/src/redis/service-times.d.ts +25 -0
  70. package/dist/src/redis/service-times.js +87 -0
  71. package/dist/src/redis/stream.d.ts +2 -1
  72. package/dist/src/redis/stream.js +109 -20
  73. package/dist/src/resource-blob.d.ts +4 -0
  74. package/dist/src/resource-blob.js +8 -1
  75. package/dist/src/runtime-state.d.ts +11 -0
  76. package/dist/src/runtime-state.js +13 -0
  77. package/dist/src/runtime.d.ts +7 -5
  78. package/dist/src/runtime.js +7 -4
  79. package/dist/src/serve-http.d.ts +2 -0
  80. package/dist/src/serve-http.js +73 -6
  81. package/dist/src/serve.d.ts +10 -0
  82. package/dist/src/serve.js +43 -3
  83. package/dist/src/sockets.d.ts +4 -1
  84. package/dist/src/sockets.js +59 -6
  85. package/dist/src/storage.d.ts +2 -1
  86. package/dist/src/storage.js +6 -3
  87. package/dist/src/vendor-call.js +58 -0
  88. package/dist/src/world-boot.d.ts +1 -0
  89. package/dist/src/world-boot.js +6 -0
  90. package/dist/src/world-clock.js +6 -2
  91. package/dist/src/world-store.d.ts +122 -7
  92. package/dist/src/world-store.js +959 -103
  93. package/dist/src/xml.d.ts +30 -0
  94. package/dist/src/xml.js +74 -0
  95. package/generated/pack-facts.json +204 -4
  96. package/inject.cjs +150 -86
  97. package/network-policy.cjs +11 -1
  98. package/network-policy.d.cts +1 -0
  99. package/pack-facts.d.cts +3 -0
  100. package/package.json +27 -1
  101. package/required-credential-transport.cjs +29 -0
  102. package/required-credential-transport.d.cts +3 -0
  103. package/src/actions.ts +78 -21
  104. package/src/ancestry.ts +46 -24
  105. package/src/blob-store.ts +59 -1
  106. package/src/changeset.ts +1 -1
  107. package/src/clickhouse/compression.ts +172 -0
  108. package/src/clickhouse/index.ts +2 -0
  109. package/src/derived-core.ts +356 -61
  110. package/src/derived-real.ts +80 -14
  111. package/src/derived.ts +9 -1
  112. package/src/executor.ts +17 -3
  113. package/src/fork.ts +1 -1
  114. package/src/git/lfs.ts +40 -9
  115. package/src/grpc-wire.ts +2 -1
  116. package/src/h2.ts +46 -5
  117. package/src/head.ts +15 -0
  118. package/src/history.ts +7 -7
  119. package/src/index.ts +7 -1
  120. package/src/live/ingest.ts +7 -3
  121. package/src/live/rtmp.ts +7 -5
  122. package/src/live/segmenter.ts +7 -1
  123. package/src/log.ts +147 -32
  124. package/src/machines.ts +5 -1
  125. package/src/managed-go-programs.ts +146 -0
  126. package/src/managed-original-inputs.ts +163 -0
  127. package/src/mongodb/client.ts +27 -0
  128. package/src/mongodb/index.ts +5 -0
  129. package/src/mongodb/stream.ts +223 -0
  130. package/src/observe.ts +1 -1
  131. package/src/pack-fetch.ts +66 -16
  132. package/src/pack-paths.ts +5 -1
  133. package/src/packRegistry.ts +3 -15
  134. package/src/protocol.ts +13 -0
  135. package/src/redis/engine.ts +100 -57
  136. package/src/redis/member-storage.ts +17 -9
  137. package/src/redis/service-times.ts +81 -0
  138. package/src/redis/stream.ts +66 -14
  139. package/src/resource-blob.ts +9 -1
  140. package/src/runtime-state.ts +14 -0
  141. package/src/runtime.ts +10 -3
  142. package/src/serve-http.ts +65 -6
  143. package/src/serve.ts +45 -3
  144. package/src/sockets.ts +39 -8
  145. package/src/storage.ts +5 -3
  146. package/src/vendor-call.ts +54 -0
  147. package/src/world-boot.ts +6 -0
  148. package/src/world-clock.ts +6 -2
  149. package/src/world-store.ts +768 -91
  150. package/src/xml.ts +71 -0
@@ -5,8 +5,9 @@ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue
5
5
  // how lists page, how errors read, which fields are state and how they move). From those two this
6
6
  // serves CRUD generically and applies declared transitions, over the kernel's tree and write path.
7
7
  // Nothing here knows a vendor: every vendor difference is a manifest value.
8
- import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
9
- import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from "./log.js";
8
+ import { applyTwinWrite, applyTwinWriteAtomic, applyTwinWrites, twinResources } from "./serve.js";
9
+ import { copyResource, foldLocalWrite, heldSubject, ownFields, parentStamp, subjectHistory, treeChangesSince, treeResources, treeStamp } from "./log.js";
10
+ import { performsAtHeadNow } from "./head.js";
10
11
  import { getActiveWorldStore } from "./world-store.js";
11
12
  import { getActiveBlobStore } from "./blob-store.js";
12
13
  import { worldPaths } from "./storage.js";
@@ -21,8 +22,9 @@ import { MACHINE_POOL, poolFor, guardedMachinePool } from "./machines.js";
21
22
  import { askApplication, deliverEvents } from "./events.js";
22
23
  import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom } from "./signing.js";
23
24
  import { hashFieldValue } from "./hash.js";
24
- import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
25
+ import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, writeResourceBlob, writeResourceBlobStream } from "./resource-blob.js";
25
26
  import { managedDatabase, unboundDatabase } from "./managed-database.js";
27
+ import { redisServiceTimes } from "./redis/service-times.js";
26
28
  import { execRedisRun } from "./redis/engine.js";
27
29
  import { sendMail } from "./smtp.js";
28
30
  import { ownField, parseBracketForm } from "./form.js";
@@ -71,12 +73,13 @@ export function strictFields(m, operation, body) {
71
73
  if (!field)
72
74
  return refuse(strict.unknown, { name });
73
75
  const { type } = field;
74
- const fits = value === null
76
+ const fits = (value === null && !(type === 'array' && strict.arrays?.[name]))
75
77
  || (type === 'boolean' && typeof value === 'boolean')
76
78
  || (type === 'integer' && isExactIntegerValue(value))
77
79
  || (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
78
80
  || (type === 'string' && typeof value === 'string')
79
- || !['boolean', 'integer', 'number', 'string'].includes(type);
81
+ || (type === 'array' && Array.isArray(value) && (!strict.arrays?.[name] || value.every(item => typeof item === strict.arrays[name])))
82
+ || !['boolean', 'integer', 'number', 'string', 'array'].includes(type);
80
83
  if (!fits)
81
84
  return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
82
85
  if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
@@ -263,14 +266,16 @@ export function resourcesOfType(service, type, root) {
263
266
  }
264
267
  held.stamp = delta.stamp;
265
268
  }
266
- else if (!held || held.stamp !== treeStamp(service, root)) {
267
- // the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
269
+ else {
270
+ // the stamp is read once, before the fold, so a write between them can only leave a newer fold under an older stamp
268
271
  const stamp = treeStamp(service, root);
269
- const byType = new Map();
270
- for (const r of twinResources(service, root))
271
- (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
272
- held = { stamp, byType };
273
- indexes.set(key, held);
272
+ if (!held || held.stamp !== stamp) {
273
+ const byType = new Map();
274
+ for (const r of twinResources(service, root))
275
+ (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
276
+ held = { stamp, byType };
277
+ indexes.set(key, held);
278
+ }
274
279
  }
275
280
  return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
276
281
  }
@@ -299,13 +304,20 @@ function parentField(row, path) {
299
304
  value = value && typeof value === 'object' ? value[key] : undefined;
300
305
  return value;
301
306
  }
302
- function missingParent(m, resource, call, root) {
303
- const parent = m.resources[resource]?.parent;
307
+ function parentOfCall(decl, call) {
308
+ const parent = decl?.parent;
309
+ const names = parent?.params ?? (parent?.param === undefined ? [] : [parent.param]);
310
+ return parent?.whenNamed && names.length && !names.some((name) => Object.hasOwn(call.params, name))
311
+ ? undefined : parent;
312
+ }
313
+ function missingParent(m, resource, call, root, tenant) {
314
+ const parent = parentOfCall(m.resources[resource], call);
304
315
  if (!parent)
305
316
  return undefined;
306
317
  const id = parentValue(parent, call.params);
307
318
  const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
308
- const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id);
319
+ const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
320
+ && (!m.tenant || m.resources[parent.resource]?.shared || r[m.tenant.field] === tenant);
309
321
  return id !== undefined && stored(m, parent.resource, root, { withDeleted: parent.allowDeleted === true }).some(found) ? undefined : notFound(m, parent.resource, String(id), parent.param ?? parent.params?.at(-1));
310
322
  }
311
323
  /** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
@@ -688,7 +700,11 @@ function recordedInput(m, call, merged) {
688
700
  const id = call.operation.id;
689
701
  const sent = sentOf.get(call);
690
702
  const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
691
- const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: sent.body, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: merged };
703
+ const door = m.doors?.find(d => d.id === id);
704
+ const body = sent?.body ?? merged;
705
+ const safeBody = door?.redactBody?.length && body && typeof body === 'object' && !Array.isArray(body)
706
+ ? Object.fromEntries(Object.entries(body).filter(([key]) => !door.redactBody.includes(key))) : body;
707
+ const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: safeBody, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: safeBody };
692
708
  if (id === 'graphql') {
693
709
  const b = (sent?.body ?? {});
694
710
  return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
@@ -707,28 +723,27 @@ const scopeOfCall = new WeakMap();
707
723
  const requestOfCall = new WeakMap();
708
724
  async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
709
725
  const before = stateBeforeWrite(m, resource, id, root);
726
+ const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) : undefined;
710
727
  const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
711
728
  const { resource: row, result } = await applyTwinWrite(m.service,
712
- // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
713
- // history can say who did each thing
714
729
  // a vendor's own move (ctx.asVendor, the clock) is the World's, as the clock's writes are: no API call a head performs
715
730
  inVendorMove()
716
731
  ? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
717
- : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) } }, root);
732
+ : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } }, root);
718
733
  observeStateWrite(m, result, before, root, checks);
719
- return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt);
734
+ return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
720
735
  }
721
- async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks) {
722
- const input = recordedInput(m, call, params);
723
- const files = input.operationId ? await keptFiles(m, call, root) : undefined;
736
+ async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks, omitInput = false) {
737
+ const input = omitInput ? undefined : recordedInput(m, call, params);
738
+ const files = input?.operationId ? await keptFiles(m, call, root) : undefined;
724
739
  let before;
725
740
  const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
726
741
  const selected = decide(snapshot);
727
- if (selected && transitionObserver)
742
+ if (selected && (transitionObserver || counted(m, resource)))
728
743
  before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
729
744
  return selected ? { kind: 'write', value: selected.id, write: {
730
745
  operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
731
- input: files ? { ...input, files } : input, occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
746
+ ...(input ? { input: files ? { ...input, files } : input } : {}), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
732
747
  } } : { kind: 'skip', value: undefined };
733
748
  }, root);
734
749
  if (!committed.result || !committed.value) {
@@ -737,10 +752,10 @@ async function atomicWrite(m, call, resource, decide, operation, params, root, o
737
752
  }
738
753
  observeStateWrite(m, committed.result, before, root, checks);
739
754
  const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value, root));
740
- return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt)).body;
755
+ return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt, before)).body;
741
756
  }
742
- async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt) {
743
- await keepCounts(m, call, resource, row, params, root, occurredAt);
757
+ async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior) {
758
+ await keepCounts(m, call, resource, row, params, root, occurredAt, prior);
744
759
  const body = view(m, resource, row);
745
760
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
746
761
  // the managed database), at the write's moment
@@ -762,27 +777,30 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
762
777
  }
763
778
  /** The declared counts a written subject is a child in (a resource's `counts`), each kept on its parent: written when
764
779
  * the count moved, through the write path, so the parent's history says when. */
765
- async function keepCounts(m, call, resource, row, params, root, occurredAt) {
780
+ async function keepCounts(m, call, resource, row, params, root, occurredAt, prior) {
766
781
  for (const [parent, decl] of Object.entries(m.resources)) {
767
782
  for (const [field, c] of Object.entries(decl.counts ?? {})) {
768
783
  if (storedType(m, c.of) !== storedType(m, resource))
769
784
  continue;
770
- const parentId = parentField(row, c.by);
771
- if (parentId === undefined || parentId === null)
772
- continue;
773
- const pid = String(parentId);
774
- const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid);
775
- if (!held)
776
- continue;
777
- const n = stored(m, c.of, root).filter((r) => {
778
- const x = r;
779
- return String(parentField(x, c.by)) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null)) && (c.absent === undefined || parentField(ownFields(r), c.absent) === undefined || parentField(ownFields(r), c.absent) === null) && Object.entries(c.unless ?? {}).every(([key, value]) => ownFields(r)[key] !== value);
780
- }).length + (c.plus ?? 0);
781
- if (held[field] !== n)
782
- await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
785
+ // the parent the subject is under now, and the one it left when the write moved it: both counts changed
786
+ const pids = new Set([parentField(row, c.by), prior === undefined ? undefined : parentField(prior, c.by)].filter((v) => v !== undefined && v !== null).map(String));
787
+ for (const pid of pids) {
788
+ const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid);
789
+ if (!held)
790
+ continue;
791
+ const n = stored(m, c.of, root).filter((r) => {
792
+ const x = r;
793
+ return String(parentField(x, c.by)) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null)) && (c.absent === undefined || parentField(ownFields(r), c.absent) === undefined || parentField(ownFields(r), c.absent) === null) && Object.entries(c.unless ?? {}).every(([key, value]) => ownFields(r)[key] !== value);
794
+ }).length + (c.plus ?? 0);
795
+ if (held[field] !== n)
796
+ await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
797
+ }
783
798
  }
784
799
  }
785
800
  }
801
+ /** Whether any resource counts this one (`counts`): only then does a write read the subject as it stood, for the parent
802
+ * it may leave. */
803
+ const counted = (m, resource) => Object.values(m.resources).some((d) => Object.values(d.counts ?? {}).some((c) => storedType(m, c.of) === storedType(m, resource)));
786
804
  /** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
787
805
  function envelope(m, op, body) {
788
806
  return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
@@ -1129,16 +1147,16 @@ export async function serveCore(m, call, scope = {}) {
1129
1147
  // the body is read from a copy, so the call's own request is still unread for the write hook's context
1130
1148
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
1131
1149
  const idParam = subjectOf(m, resource, call);
1132
- const orphan = missingParent(m, resource, call, root);
1133
- if (orphan)
1134
- return { served: orphan };
1135
- const parent = decl.parent;
1150
+ const parent = parentOfCall(decl, call);
1136
1151
  const parentId = parent ? parentValue(parent, call.params) : undefined;
1137
1152
  // the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
1138
1153
  const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
1139
1154
  const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
1140
1155
  if (tenantField && tenant === undefined)
1141
1156
  return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
1157
+ const orphan = missingParent(m, resource, call, root, tenant);
1158
+ if (orphan)
1159
+ return { served: orphan };
1142
1160
  const underParent = (r) => (!parent || r[parent.field] === parentId)
1143
1161
  && (!tenantField || r[tenantField] === tenant);
1144
1162
  const paths = expandPaths(m, params);
@@ -1222,6 +1240,10 @@ export async function serveCore(m, call, scope = {}) {
1222
1240
  const requested = op.class === 'update' ? data[field] : undefined;
1223
1241
  if (op.class === 'update' && requested === undefined)
1224
1242
  continue;
1243
+ if (op.class === 'update' && sdecl.unchanged === 'allow' && stateOf(sdecl, current[field]) === stateOf(sdecl, requested)) {
1244
+ changes[field] = requested;
1245
+ continue;
1246
+ }
1225
1247
  const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
1226
1248
  if (refusal)
1227
1249
  return { served: vendorError(m, refusal) };
@@ -1282,6 +1304,12 @@ export function sse(m, events, operationId) {
1282
1304
  return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
1283
1305
  }
1284
1306
  /** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
1307
+ /** A media type whose body the core reads for a context (and records with a write): JSON, a form, multipart, text,
1308
+ * or none named. */
1309
+ const coreReadsBody = (type) => {
1310
+ const media = (type ?? '').split(';')[0].trim().toLowerCase();
1311
+ return media === '' || media === 'application/json' || media.endsWith('+json') || media === 'application/x-www-form-urlencoded' || media.startsWith('multipart/') || media.startsWith('text/');
1312
+ };
1285
1313
  export async function contextFor(m, call, scope) {
1286
1314
  const root = scope.root;
1287
1315
  const asAsked = call;
@@ -1291,8 +1319,10 @@ export async function contextFor(m, call, scope) {
1291
1319
  // the guard's parse when it made one (taken once), else this context's own
1292
1320
  // a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
1293
1321
  const payload = bytesPayload(call.operation);
1294
- const parsed = payload ? undefined : takeParsedBody(call.request);
1295
- const text = await bodyTextOf(call.request.clone()).catch(() => '');
1322
+ // a screen's body the core does not read (an upload's bytes) is left to its handler, unread (DerivedCall.streamed)
1323
+ const opaque = call.streamed === true && !coreReadsBody(call.request.headers.get('content-type'));
1324
+ const parsed = payload || opaque ? undefined : takeParsedBody(call.request);
1325
+ const text = opaque ? '' : await bodyTextOf(call.request.clone()).catch(() => '');
1296
1326
  let body = parsed?.value;
1297
1327
  if (!parsed) {
1298
1328
  if (payload)
@@ -1336,7 +1366,7 @@ export async function contextFor(m, call, scope) {
1336
1366
  rawResources.set(row, name ?? resource);
1337
1367
  return row;
1338
1368
  };
1339
- return {
1369
+ const context = {
1340
1370
  call,
1341
1371
  params,
1342
1372
  id: Object.values(call.params).at(-1),
@@ -1355,7 +1385,6 @@ export async function contextFor(m, call, scope) {
1355
1385
  return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1356
1386
  },
1357
1387
  mail: (route, mail, headers) => sendMail(route, mail, headers),
1358
- actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
1359
1388
  now: () => now(m, at),
1360
1389
  render: (row, resource) => {
1361
1390
  const raw = rawResources.has(row);
@@ -1396,11 +1425,17 @@ export async function contextFor(m, call, scope) {
1396
1425
  record: async (type, fields, id, operation) => {
1397
1426
  if (!type.startsWith('_'))
1398
1427
  throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
1428
+ // a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
1429
+ // child does, the parent it left recounted too
1430
+ const keeps = counted(m, type);
1399
1431
  if (id !== undefined) {
1400
1432
  const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
1401
- const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
1433
+ const prior = keeps ? resourcesOfType(m.service, type, root).find((r) => r.id === id) : undefined;
1434
+ const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
1402
1435
  if (operation)
1403
1436
  observeStateWrite(m, result, before, root, stateChecks);
1437
+ if (keeps)
1438
+ await keepCounts(m, call, type, row, params, root, at, prior && { ...prior });
1404
1439
  return id;
1405
1440
  }
1406
1441
  const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
@@ -1421,6 +1456,9 @@ export async function contextFor(m, call, scope) {
1421
1456
  }, root, () => resourcesOfType(m.service, type, root));
1422
1457
  if (operation && result)
1423
1458
  observeStateWrite(m, result, undefined, root, stateChecks);
1459
+ const made = keeps && value !== undefined ? resourcesOfType(m.service, type, root).find((r) => r.id === value) : undefined;
1460
+ if (made)
1461
+ await keepCounts(m, call, type, made, params, root, at);
1424
1462
  return value;
1425
1463
  },
1426
1464
  get machines() {
@@ -1433,7 +1471,9 @@ export async function contextFor(m, call, scope) {
1433
1471
  const id = mintId(m, resource, root, at, snapshot);
1434
1472
  return { id, fields: typeof fields === 'function' ? fields(id) : fields };
1435
1473
  }, operation, params, root, at, undefined, stateChecks).then((row) => row),
1436
- createUnique: async (resource, fields, operation) => {
1474
+ createUnique: async (resource, fields, operation, options) => {
1475
+ if (options?.input === 'omit' && !resource.startsWith('_'))
1476
+ throw new Error('only bookkeeping writes may omit request input');
1437
1477
  const rules = m.resources[resource]?.unique;
1438
1478
  if (!rules?.length)
1439
1479
  throw new Error('createUnique requires declared unique keys');
@@ -1447,16 +1487,20 @@ export async function contextFor(m, call, scope) {
1447
1487
  return undefined;
1448
1488
  }
1449
1489
  return { id, fields: selected };
1450
- }, operation, params, root, at, undefined, stateChecks);
1490
+ }, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
1451
1491
  return existing ? { created: false, row: existing } : { created: true, row: row };
1452
1492
  },
1453
- change: (resource, id, decide, operation) => atomicWrite(m, call, resource, (snapshot) => {
1454
- const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
1455
- if (!row)
1456
- return undefined;
1457
- const fields = decide(row);
1458
- return fields === undefined ? undefined : { id, fields };
1459
- }, operation, params, root, at, id, stateChecks),
1493
+ change: (resource, id, decide, operation, options) => {
1494
+ if (options?.input === 'omit' && !resource.startsWith('_'))
1495
+ throw new Error('only bookkeeping writes may omit request input');
1496
+ return atomicWrite(m, call, resource, (snapshot) => {
1497
+ const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
1498
+ if (!row)
1499
+ return undefined;
1500
+ const fields = decide(row);
1501
+ return fields === undefined ? undefined : { id, fields };
1502
+ }, operation, params, root, at, id, stateChecks, options?.input === 'omit');
1503
+ },
1460
1504
  write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
1461
1505
  accumulate: async (resource, id, deltas, operation) => {
1462
1506
  if (!resource.startsWith('_') || !m.resources[resource])
@@ -1542,8 +1586,11 @@ export async function contextFor(m, call, scope) {
1542
1586
  live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root), blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
1543
1587
  blobs: {
1544
1588
  put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
1589
+ putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
1545
1590
  get: (key) => readResourceBlob(m.service, key, root),
1546
1591
  remove: (key) => removeResourceBlob(m.service, key, root),
1592
+ size: (key) => resourceBlobSize(m.service, key, root),
1593
+ range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
1547
1594
  },
1548
1595
  merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
1549
1596
  conflict: (resource, fields, except) => {
@@ -1563,12 +1610,240 @@ export async function contextFor(m, call, scope) {
1563
1610
  over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
1564
1611
  core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
1565
1612
  own: (row) => ownFields(row),
1613
+ batch: (fn) => fn(),
1614
+ };
1615
+ if (batchable(m, root))
1616
+ bindBatch(m, call, context, params, root, at, rawRow);
1617
+ return context;
1618
+ }
1619
+ /**
1620
+ * Whether a call's writes may be grouped (`ctx.batch`) with nothing about them changing: every effect a write has
1621
+ * besides its action and the tree (write hooks, events, sockets, a parent's counts, a state observer, a head that
1622
+ * performs it, an alias a read resolves through) is one the grouped path would move, so where any is present the
1623
+ * writes land one at a time, as they always have.
1624
+ */
1625
+ function batchable(m, root) {
1626
+ return !m.onWrite && !m.events && !m.sockets?.length && !transitionObserver
1627
+ && !Object.values(m.resources).some((decl) => decl.counts && Object.keys(decl.counts).length > 0)
1628
+ && !performsAtHeadNow(m.service, root) && subjectAliases(m.service, root).size === 0;
1629
+ }
1630
+ /** The members of a context that read the tree other than `row`/`rowsRaw`: refused while writes are staged. */
1631
+ const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', 'expand', 'mint', 'conflict', 'ownerRow', 'render'];
1632
+ /** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
1633
+ const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
1634
+ // a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
1635
+ 'secret', 'signingKey'];
1636
+ /** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
1637
+ function bindBatch(m, call, context, params, root, at, rawRow) {
1638
+ let staged;
1639
+ // each staged subject as it will stand, by `type:id`, in the order first written
1640
+ const subjects = new Map();
1641
+ const land = async () => {
1642
+ if (!staged?.length)
1643
+ return;
1644
+ const writes = staged;
1645
+ staged = [];
1646
+ subjects.clear();
1647
+ await writeGroup(m, call, writes, params, root, at);
1566
1648
  };
1649
+ const ctx = context;
1650
+ for (const name of BATCH_LANDING_MEMBERS) {
1651
+ const plain = ctx[name];
1652
+ if (typeof plain !== 'function')
1653
+ continue;
1654
+ ctx[name] = async (...args) => { await land(); return plain(...args); };
1655
+ }
1656
+ for (const name of BATCH_REFUSED_READS) {
1657
+ const plain = ctx[name];
1658
+ if (typeof plain !== 'function')
1659
+ continue;
1660
+ ctx[name] = (...args) => {
1661
+ if (staged?.length)
1662
+ throw new Error(`ctx.${name} reads the tree while ctx.batch holds staged writes: only row and rowsRaw see them`);
1663
+ return plain(...args);
1664
+ };
1665
+ }
1666
+ const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
1667
+ context.write = async (resource, id, fields, operation) => {
1668
+ // a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
1669
+ if (!staged || inVendorMove()) {
1670
+ await land();
1671
+ return plainWrite(resource, id, fields, operation);
1672
+ }
1673
+ const type = storedType(m, resource), key = `${type}:${id}`;
1674
+ // the one subject, as the memoized tree holds it (readTreeMap would copy the whole tree on every staged write)
1675
+ const subject = foldLocalWrite(subjects.get(key) ?? heldSubject(m.service, type, id, root), type, id, fields, at);
1676
+ subjects.set(key, subject);
1677
+ staged.push({ resource, id, fields, operation });
1678
+ return view(m, resource, treeResources(new Map([[key, subject]]))[0]);
1679
+ };
1680
+ // the stored rows of a resource's type with the staged subjects folded over them, filtered as `stored` filters
1681
+ const overlaid = (resource, opts) => {
1682
+ if (!staged?.length)
1683
+ return undefined;
1684
+ const type = storedType(m, resource), prefix = `${type}:`;
1685
+ const mine = [...subjects].filter(([key]) => key.startsWith(prefix));
1686
+ if (!mine.length)
1687
+ return undefined;
1688
+ const replaced = new Map(mine.map(([key, subject]) => [key, treeResources(new Map([[key, subject]]))[0]]));
1689
+ const base = resourcesOfType(m.service, type, root);
1690
+ // a subject the tree holds stays where it stands; one new to it goes to the end, in the order first written
1691
+ const rows = base.map((row) => replaced.get(`${prefix}${row.id}`) ?? row);
1692
+ const held = new Set(base.map((row) => `${prefix}${row.id}`));
1693
+ for (const [key, row] of replaced)
1694
+ if (!held.has(key))
1695
+ rows.push(row);
1696
+ const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
1697
+ return rows.filter((r) => (opts?.withDeleted || r.deleted !== true) && when.every(([k, v]) => r[k] === v));
1698
+ };
1699
+ context.row = (resource, id, opts) => {
1700
+ const rows = overlaid(resource, opts);
1701
+ if (!rows)
1702
+ return plainRow(resource, id, opts);
1703
+ const row = rows.find((r) => r.id === id);
1704
+ return row ? rawRow(resource, row) : undefined;
1705
+ };
1706
+ context.rowsRaw = (resource, opts) => {
1707
+ const rows = overlaid(resource, opts);
1708
+ return rows ? rows.map((row) => rawRow(resource, row)) : plainRowsRaw(resource, opts);
1709
+ };
1710
+ context.batch = async (fn) => {
1711
+ // a batch inside a batch is the outer one's
1712
+ if (staged !== undefined)
1713
+ return fn();
1714
+ staged = [];
1715
+ try {
1716
+ return await fn();
1717
+ }
1718
+ finally {
1719
+ // every write `write` answered lands, whether fn returned or threw: an ordered insert's prefix stays written
1720
+ try {
1721
+ await land();
1722
+ }
1723
+ finally {
1724
+ staged = undefined;
1725
+ subjects.clear();
1726
+ }
1727
+ }
1728
+ };
1729
+ }
1730
+ /**
1731
+ * A command's staged writes landed as one group: each recorded with the request as `writeDetailed` records it (one
1732
+ * recordedInput, the same for every write of the call), the call's moment and actor, in order, under one lock and one
1733
+ * append (serve.ts applyTwinWrites); then a held read on each written type is told, as each write tells it.
1734
+ */
1735
+ async function writeGroup(m, call, writes, params, root, occurredAt) {
1736
+ const input = recordedInput(m, call, params);
1737
+ const files = input.operationId ? await keptFiles(m, call, root) : undefined;
1738
+ const recorded = files ? { ...input, files } : input;
1739
+ const actor = { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) };
1740
+ await applyTwinWrites(m.service, writes.map((write) => ({ operation: write.operation, subjectType: storedType(m, write.resource),
1741
+ subjectId: write.id, fields: write.fields, input: recorded, occurredAt, actor })), root);
1742
+ for (const type of new Set(writes.map((write) => storedType(m, write.resource))))
1743
+ heldReadsWrite(m.service, root, type);
1567
1744
  }
1568
1745
  /** A pack's semantics handlers as the dispatch's handlers. */
1569
1746
  export function bindSemantics(m, handlers, scope = {}) {
1570
1747
  prepareViewOmissions(m);
1571
- return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => h(await contextFor(m, call, scope))]));
1748
+ return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => {
1749
+ const ctx = await contextFor(m, call, scope);
1750
+ redisServiceTimes.lap('6c the handler\'s own context opened');
1751
+ // an operation's own variants, else the ones every operation shares (`'*'`)
1752
+ const variants = m.variants?.[id] ?? m.variants?.['*'];
1753
+ if (variants && variantAsked(ctx.params, variants, call.request.headers))
1754
+ return variantGap(m, call.request);
1755
+ const outside = enumRefused(m, call, ctx.params);
1756
+ return outside ?? h(ctx);
1757
+ }]));
1758
+ }
1759
+ /** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
1760
+ * bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
1761
+ export function valueOutside(params, enums) {
1762
+ const at = (value, path, name, values) => {
1763
+ if (value === undefined || value === null)
1764
+ return undefined;
1765
+ if (!path.length)
1766
+ return typeof value === 'object' || values.includes(String(value)) ? undefined : { param: name, values };
1767
+ const [head, ...rest] = path;
1768
+ if (head === '[]') {
1769
+ const items = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : value && typeof value === 'object' ? Object.entries(value) : [];
1770
+ for (const [i, v] of items) {
1771
+ const found = at(v, rest, `${name}[${i}]`, values);
1772
+ if (found)
1773
+ return found;
1774
+ }
1775
+ return undefined;
1776
+ }
1777
+ return value && typeof value === 'object' && !Array.isArray(value) ? at(value[head], rest, `${name}[${head}]`, values) : undefined;
1778
+ };
1779
+ for (const [path, values] of Object.entries(enums)) {
1780
+ const [first, ...rest] = path.split('.');
1781
+ const found = at(params[first], rest, first, values);
1782
+ if (found)
1783
+ return found;
1784
+ }
1785
+ return undefined;
1786
+ }
1787
+ /** The manifest's refusal of a form value outside the operation's spec lists (and the values documented beside
1788
+ * them), unless the request names an API version other than the one the spec is. */
1789
+ function enumRefused(m, call, params) {
1790
+ const refusal = m.body.form?.enumRefusal;
1791
+ if (!refusal || !call.operation.enums)
1792
+ return undefined;
1793
+ const pinned = refusal.version ? call.request.headers.get(refusal.version.header) : null;
1794
+ if (pinned && pinned !== refusal.version.served)
1795
+ return undefined;
1796
+ const documented = refusal.documented?.[call.operation.id] ?? {};
1797
+ const enums = Object.fromEntries(Object.entries(call.operation.enums).map(([path, values]) => [path, [...values, ...(documented[path] ?? [])]]));
1798
+ const outside = valueOutside(params, enums);
1799
+ if (!outside)
1800
+ return undefined;
1801
+ const { documented: _d, version: _v, ...error } = refusal;
1802
+ return vendorError(m, { ...error, param: outside.param, message: error.message.replace(/\{param\}/g, outside.param).replace(/\{values\}/g, oneOfList(outside.values)) });
1803
+ }
1804
+ /** A list of values as a sentence names them: `a, b, or c`. */
1805
+ const oneOfList = (values) => (values.length < 2 ? values.join('') : `${values.slice(0, -1).join(', ')}, or ${values[values.length - 1]}`);
1806
+ /** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
1807
+ export function variantAsked(params, variants, headers) {
1808
+ const valuesAt = (value, path) => {
1809
+ if (!path.length)
1810
+ return value === undefined ? [] : [value];
1811
+ const [head, ...rest] = path;
1812
+ // `!name` is the field left out: the value itself where it has none (`items[].!id`, an item naming no id)
1813
+ if (head.startsWith('!'))
1814
+ return value && typeof value === 'object' && value[head.slice(1)] === undefined ? [value] : [];
1815
+ const list = head.endsWith('[]');
1816
+ const key = list ? head.slice(0, -2) : head;
1817
+ const next = value && typeof value === 'object' ? value[key] : undefined;
1818
+ if (!list)
1819
+ return valuesAt(next, rest);
1820
+ const items = Array.isArray(next) ? next : next && typeof next === 'object' ? Object.values(next) : [];
1821
+ return items.flatMap((item) => valuesAt(item, rest));
1822
+ };
1823
+ // a variant joined with `&` is its parts asked together (`trial_end=now&items`): the combination, not either alone;
1824
+ // a part led by `!` is a parameter left out (`items[].price&!items[].id`, an item added rather than changed)
1825
+ return variants.some((variant) => variant.split('&').every((part) => {
1826
+ const absent = part.startsWith('!');
1827
+ // `name~=value` is one entry of a comma-separated value (`tweet.fields~=geo`, X's fields lists); `name=value` the whole
1828
+ const body = absent ? part.slice(1) : part;
1829
+ const listed = body.includes('~=');
1830
+ const [path, wanted] = listed ? body.split('~=') : body.split('=');
1831
+ // a part led by `@` names a request header (`@stripe-account&application_fee_amount`, a charge made on a connected
1832
+ // account with the platform's fee)
1833
+ const header = path.startsWith('@') ? headers?.get(path.slice(1)) : undefined;
1834
+ // a parameter whose own name holds a dot (X's `tweet.fields`) is read by that name before the path is walked
1835
+ const found = path.startsWith('@') ? (header === null || header === undefined ? [] : [header])
1836
+ : Object.hasOwn(params, path) ? (params[path] === undefined ? [] : [params[path]]) : valuesAt(params, path.split('.'));
1837
+ const asked = wanted === undefined ? found.length > 0
1838
+ : found.some((value) => (listed ? String(value).split(',').map((entry) => entry.trim()).includes(wanted) : String(value) === wanted));
1839
+ return absent ? !asked : asked;
1840
+ }));
1841
+ }
1842
+ /** The manifest's gap, as a variant is answered: the unknown-request refusal (else its not-found). */
1843
+ function variantGap(m, request) {
1844
+ const nf = m.notFound;
1845
+ const gap = m.gap ?? { status: nf.status, message: nf.message, ...(nf.code !== undefined ? { code: nf.code } : {}), ...(nf.kind !== undefined ? { kind: nf.kind } : {}) };
1846
+ return vendorError(m, { ...gap, message: gap.message.replace(/\{method\}/g, request.method).replace(/\{path\}/g, new URL(request.url).pathname) });
1572
1847
  }
1573
1848
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
1574
1849
  export function coreFor(m, scope = {}) {