@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
@@ -6,8 +6,9 @@ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue
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
8
 
9
- import { applyTwinWrite, applyTwinWriteAtomic, twinResources, type TwinResource } from './serve.ts';
10
- import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from './log.ts';
9
+ import { applyTwinWrite, applyTwinWriteAtomic, applyTwinWrites, twinResources, type TwinResource } from './serve.ts';
10
+ import { copyResource, foldLocalWrite, heldSubject, ownFields, parentStamp, subjectHistory, treeChangesSince, treeResources, treeStamp, type Subject } from './log.ts';
11
+ import { performsAtHeadNow } from './head.ts';
11
12
  import { getActiveWorldStore } from './world-store.ts';
12
13
  import { getActiveBlobStore } from './blob-store.ts';
13
14
  import { worldPaths } from './storage.ts';
@@ -24,8 +25,9 @@ import { askApplication, deliverEvents, type ApplicationAnswer, type Application
24
25
  import type { RateBudgetDeclaration } from './rateBudget.ts';
25
26
  import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom, type HandlerCrypto } from './signing.ts';
26
27
  import { hashFieldValue } from './hash.ts';
27
- import { readResourceBlob, removeResourceBlob, writeResourceBlob } from './resource-blob.ts';
28
+ import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, writeResourceBlob, writeResourceBlobStream } from './resource-blob.ts';
28
29
  import { managedDatabase, unboundDatabase, type ManagedDatabase } from './managed-database.ts';
30
+ import { redisServiceTimes } from './redis/service-times.ts';
29
31
  import { execRedisRun, type RedisDialect, type RunItem } from './redis/engine.ts';
30
32
  import { sendMail, type Mail, type SmtpRoute } from './smtp.ts';
31
33
  import { ownField, parseBracketForm } from './form.ts';
@@ -82,6 +84,8 @@ export type Transition = {
82
84
  export type StateField = {
83
85
  initial: string | boolean;
84
86
  transitions: Transition[];
87
+ /** Plain updates may repeat the current representation without another state move. */
88
+ unchanged?: 'allow';
85
89
  derive?: Array<{ state: string; when: { equals?: unknown; gt?: number; truthy?: boolean; absent?: boolean } }>;
86
90
  vendorInitial?: string;
87
91
  };
@@ -147,6 +151,8 @@ export type ResourceDecl = {
147
151
  * fields when its stored id is not what the path names (a repository stored as `repo:7` with `owner`
148
152
  * and `name`, addressed as `{owner}/{repo}`): each stored field against a template over the path. */
149
153
  parent?: { param?: string; params?: string[]; value?: string; field: string; resource: string; allowDeleted?: boolean; where?: Record<string, string>;
154
+ /** Global subject paths omit the collection's parent labels; tenancy still applies. */
155
+ whenNamed?: true;
150
156
  /** private fields a child observed under its parent keeps from the parent's parameters (`_jurisdiction:
151
157
  * '{jurisdiction}'`), where the child's own read names none and a later read under it needs one ("Refresh") */
152
158
  keep?: Record<string, string> };
@@ -199,7 +205,9 @@ export type ResourceDecl = {
199
205
  * holds this one's id (or vendor `parentKey`), only those with `present` set or `absent` unset when named and without
200
206
  * any matching `unless` field, plus `plus` (a customer's
201
207
  * `next_invoice_sequence` is one past its numbered invoices). `by` may be a dot path; exact field names take
202
- * precedence. The kernel keeps each whenever a child is written. */
208
+ * precedence. The kernel keeps each whenever a child is written, the parent it left too when the write moves it; `of`
209
+ * may name a bookkeeping type no resource declares, written with `ctx.record`. A count is kept by the lane that
210
+ * declares it: a child another lane writes is not counted there. */
203
211
  counts?: Record<string, { of: string; by: string; parentKey?: string; present?: string; absent?: string; unless?: Record<string, unknown>; plus?: number }>;
204
212
  /** how the vendor's state of this type is read back into a root (the real-system adapters, "Refresh"): the list that
205
213
  * enumerates it (once per parent, under `parent`; `complete: false` when it is not the whole type; `items`, the answer's
@@ -215,7 +223,7 @@ export type ResourceDecl = {
215
223
  * jurisdictions). A blob read by an `operation` (a GET of the unit's surface, filled from the subject's fields and
216
224
  * its parent's parameters) keeps the bytes in the World's blobs, their key in `field`, and in `headersField` the
217
225
  * answer's headers `keepHeaders` names (a name ending `*` a prefix: `x-amz-meta-*`), both fields private. */
218
- refresh?: ({ list: string; unpaginated?: true; keyed?: true; detail?: string; detailMerge?: string; detailWith?: Record<string, string>; complete?: false; items?: string; shape?: RefreshShape; next?: string | Record<string, string>; nextLink?: { header: string; rel: string; query: string; when?: Record<string, string> }; cursor?: string; more?: string } | { get: string; known?: true }) & { params?: Record<string, string>; headers?: Record<string, string>; variants?: ReadonlyArray<{ headers: Record<string, string> }>; blobs?: ReadonlyArray<{ path: string; key: string; resource?: string; fields?: Record<string, string> } | { operation: string; field: string; headers?: Record<string, string>; headersField?: string; keepHeaders?: string[] }>; query?: Record<string, string>; idAs?: string; graphql?: { query: string; variables?: Record<string, unknown>; items: string; pageInfo?: string; cursor?: string } } | { none: string };
226
+ refresh?: ({ list: string; unpaginated?: true; keyed?: true; detail?: string; detailMerge?: string; detailWith?: Record<string, string>; complete?: false; items?: string; shape?: RefreshShape; next?: string | Record<string, string>; nextLink?: { header: string; rel: string; query: string; when?: Record<string, string> }; cursor?: string; more?: string } | { get: string; known?: true; gone?: { status: number; says: string } }) & { params?: Record<string, string>; headers?: Record<string, string>; variants?: ReadonlyArray<{ headers: Record<string, string> }>; blobs?: ReadonlyArray<{ path: string; key: string; resource?: string; fields?: Record<string, string> } | { operation: string; field: string; headers?: Record<string, string>; headersField?: string; keepHeaders?: string[] }>; query?: Record<string, string>; idAs?: string; graphql?: { query: string; variables?: Record<string, unknown>; items: string; pageInfo?: string; cursor?: string } } | { none: string };
219
227
  /** the other resources one create of this one makes, and where its answer names each's id (a JSON path) */
220
228
  companions?: Record<string, string>;
221
229
  /** the field telling a newer copy of a subject from an older (an `updated_at`): an ingested event older than the
@@ -272,6 +280,11 @@ export type VendorManifest = { vendor: string; discovery?: DerivedManifest['disc
272
280
 
273
281
  /** `code` as the vendor writes it: a string, or a number (Discord's JSON error codes are integers) */
274
282
  export type ErrorSpec = { status: number; message: string; code?: string | number; param?: string; kind?: string };
283
+ /** How a form value outside its spec's list is refused (DerivedOperation.enums): the error, its message filled with
284
+ * `{param}` (the bracket name) and `{values}` (the values it takes); `documented`, values the vendor documents outside
285
+ * its spec, by operation and path; `version`, the header naming the API version a request is made at and the version
286
+ * the spec is, so a request pinned to another version, whose lists the pack does not hold, is not checked. */
287
+ export type EnumRefusal = ErrorSpec & { documented?: Record<string, Record<string, string[]>>; version?: { header: string; served: string } };
275
288
 
276
289
  /** A vendor screen a World serves (docs/contributing/architecture.md, "Screens"). A `flow` is a hosted
277
290
  * page an application sends its user through, held to its documented round trip; a `workspace` is a
@@ -298,6 +311,11 @@ export type ScreenDecl = {
298
311
  * takes, with its demand (an upload bucket a page posts a lease's form to: Reddit's S3 hosts) (architecture, "A page
299
312
  * at a World's place") */
300
313
  takes?: ReadonlyArray<'POST'>;
314
+ /** a workspace page that shows one record: the stored type and the path parameter its id fills (a pull request's
315
+ * page, `{number}`); `?show=<type>:<id>` asked of the pack's workspaces opens it (architecture, "Workspace") */
316
+ shows?: { type: string; param: string };
317
+ /** the stored types a workspace page lists, where `?show=` lands for a record no page shows by itself */
318
+ lists?: ReadonlyArray<string>;
301
319
  /** the vendor's documentation of the round trip or the screen */
302
320
  source: string;
303
321
  };
@@ -333,13 +351,20 @@ export type DerivedManifest = {
333
351
  /** `strict`: a vendor that refuses a body field its operation does not declare, a value of another type, or a number
334
352
  * outside the spec's bounds (a NestJS validation pipe: Supabase's "property x should not exist"), with its messages;
335
353
  * `{name}`, `{type}` (`a string`, `an integer`), `{min}` and `{max}` are filled. `ctx.fields` checks a body so. */
336
- body: { form?: { coerce: boolean }; json?: 'always'; validation?: { operations?: ReadonlyArray<string>; status: number; missing: unknown; type: unknown; range: unknown }; strict?: { status: number; code?: string; unknown: string; type: string; range: string } };
354
+ body: { form?: { coerce: boolean; enumRefusal?: EnumRefusal }; json?: 'always'; validation?: { operations?: ReadonlyArray<string>; status: number; missing: unknown; type: unknown; range: unknown }; strict?: { status: number; code?: string; unknown: string; type: string; range: string; arrays?: Record<string, 'string'> } };
337
355
  /** `acceptProvided`: a create that names its own `id` keeps it (a vendor that seeds by id). The template counts
338
356
  * (`{prefix}_{n}`), or is `{uuid}` for a vendor whose ids are UUIDs: the next is derived from the resource and its
339
357
  * count, so a World mints the same ids every run */
340
358
  ids: { template: string; acceptProvided?: boolean };
341
359
  /** operations the derived core must not claim though their resource is declared: a gap until someone models them */
342
360
  unmodeled?: string[];
361
+ /** documented variants of a served operation that no demand asks for, by operation: each a parameter present
362
+ * (`pause_collection`), present on any item of a list (`items[].price_data`), or with a value
363
+ * (`payment_method_data.type=sepa_debit`, `payment_method_types[]=sepa_debit`), or holding one entry of a comma-separated
364
+ * value (`tweet.fields~=geo`; a parameter whose name holds a dot is read by that name). A request asking one is answered the
365
+ * manifest's gap before its handler runs, as an operation no handler serves is (architecture, "Creating a pack",
366
+ * step 7), so no pack line answers it */
367
+ variants?: Record<string, string[]>;
343
368
  /** operations that only read though nothing in the spec says so (a POST that returns data):
344
369
  * a read-only twin answers them */
345
370
  reads?: string[];
@@ -368,6 +393,9 @@ export type DerivedManifest = {
368
393
  readOnly: ErrorSpec;
369
394
  /** what a body labelled JSON that does not parse answers; absent, a 400 in the vendor's error body */
370
395
  malformedBody?: ErrorSpec;
396
+ /** the vendor's front merges repeated slashes in a path before it routes (nginx's default `merge_slashes`): a request
397
+ * to `//r/x//about` is `/r/x/about`, merged on arrival before a door, a screen or an operation is matched */
398
+ mergeSlashes?: boolean;
371
399
  /** a request header that selects the vendor's API version, and the answer to a malformed one
372
400
  * (`{value}` in its message is the header's value) */
373
401
  version?: { header: string; pattern: string; error: ErrorSpec };
@@ -473,7 +501,9 @@ export type DerivedManifest = {
473
501
  };
474
502
  /** The World's doors (architecture, "Doors, screens and the gap"): each a method and a path under `/_twin/` (with
475
503
  * `{param}` segments), answered by the export of `semantics/doors.ts` its id names, over the contract's context. */
476
- doors?: Array<{ id: string; method: string; path: string; note?: string }>;
504
+ doors?: Array<{ id: string; method: string; path: string; note?: string; redactBody?: string[] }>;
505
+ /** REST refresh binds observed tenant metadata through the root account identity endpoint. */
506
+ refreshIdentity?: { operation: string; field: string };
477
507
  /** The vendor's GraphQL API beside its REST one (`semantics/graphql.ts` over `generated/graphql-sdl.gen.json`): the
478
508
  * paths it is POSTed to, and the answer to a query selecting a field nothing models (`{field}`, `{type}`). */
479
509
  graphql?: { paths: ReadonlyArray<string>; unmodeled: { message: string; type?: string; status?: number } };
@@ -566,12 +596,13 @@ export function strictFields(m: DerivedManifest, operation: DerivedOperation, bo
566
596
  const field = declared.get(name);
567
597
  if (!field) return refuse(strict.unknown, { name });
568
598
  const { type } = field;
569
- const fits = value === null
599
+ const fits = (value === null && !(type === 'array' && strict.arrays?.[name]))
570
600
  || (type === 'boolean' && typeof value === 'boolean')
571
601
  || (type === 'integer' && isExactIntegerValue(value))
572
602
  || (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
573
603
  || (type === 'string' && typeof value === 'string')
574
- || !['boolean', 'integer', 'number', 'string'].includes(type);
604
+ || (type === 'array' && Array.isArray(value) && (!strict.arrays?.[name] || value.every(item => typeof item === strict.arrays![name])))
605
+ || !['boolean', 'integer', 'number', 'string', 'array'].includes(type);
575
606
  if (!fits) return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
576
607
  if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
577
608
  return refuse(strict.range, { name, min: field.minimum ?? null, max: field.maximum ?? null });
@@ -738,13 +769,15 @@ export function resourcesOfType(service: string, type: string, root?: string): T
738
769
  for (const [k, r] of byKey) if (!delta.appended.includes(k)) (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r);
739
770
  for (const k of delta.appended) { const r = byKey.get(k)!; (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r); }
740
771
  held.stamp = delta.stamp;
741
- } else if (!held || held.stamp !== treeStamp(service, root)) {
742
- // the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
772
+ } else {
773
+ // the stamp is read once, before the fold, so a write between them can only leave a newer fold under an older stamp
743
774
  const stamp = treeStamp(service, root);
744
- const byType = new Map<string, Map<string, TwinResource>>();
745
- for (const r of twinResources(service, root)) (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)!).set(`${r.type}:${r.id}`, r);
746
- held = { stamp, byType };
747
- indexes.set(key, held);
775
+ if (!held || held.stamp !== stamp) {
776
+ const byType = new Map<string, Map<string, TwinResource>>();
777
+ for (const r of twinResources(service, root)) (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)!).set(`${r.type}:${r.id}`, r);
778
+ held = { stamp, byType };
779
+ indexes.set(key, held);
780
+ }
748
781
  }
749
782
  return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
750
783
  }
@@ -775,12 +808,20 @@ function parentField(row: unknown, path: string): unknown {
775
808
  return value;
776
809
  }
777
810
 
778
- function missingParent(m: DerivedManifest, resource: string, call: DerivedCall, root?: string): Response | undefined {
779
- const parent = m.resources[resource]?.parent;
811
+ function parentOfCall(decl: ResourceDecl | undefined, call: DerivedCall): ResourceDecl['parent'] {
812
+ const parent = decl?.parent;
813
+ const names = parent?.params ?? (parent?.param === undefined ? [] : [parent.param]);
814
+ return parent?.whenNamed && names.length && !names.some((name) => Object.hasOwn(call.params, name))
815
+ ? undefined : parent;
816
+ }
817
+
818
+ function missingParent(m: DerivedManifest, resource: string, call: DerivedCall, root?: string, tenant?: string): Response | undefined {
819
+ const parent = parentOfCall(m.resources[resource], call);
780
820
  if (!parent) return undefined;
781
821
  const id = parentValue(parent, call.params);
782
822
  const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))] as const);
783
- const found = (r: TwinResource): boolean => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id);
823
+ const found = (r: TwinResource): boolean => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
824
+ && (!m.tenant || m.resources[parent.resource]?.shared || (r as Record<string, unknown>)[m.tenant.field] === tenant);
784
825
  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));
785
826
  }
786
827
 
@@ -1164,7 +1205,11 @@ function recordedInput(m: DerivedManifest, call: DerivedCall, merged: Record<str
1164
1205
  const id = call.operation.id;
1165
1206
  const sent = sentOf.get(call);
1166
1207
  const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
1167
- 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 };
1208
+ const door = m.doors?.find(d => d.id === id);
1209
+ const body = sent?.body ?? merged;
1210
+ const safeBody = door?.redactBody?.length && body && typeof body === 'object' && !Array.isArray(body)
1211
+ ? Object.fromEntries(Object.entries(body).filter(([key]) => !door.redactBody!.includes(key))) : body;
1212
+ 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 };
1168
1213
  if (id === 'graphql') {
1169
1214
  const b = (sent?.body ?? {}) as { query?: unknown; variables?: unknown; operationName?: unknown };
1170
1215
  return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
@@ -1182,34 +1227,33 @@ const requestOfCall = new WeakMap<DerivedCall, Request>();
1182
1227
 
1183
1228
  async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: string, id: string, fields: Record<string, unknown>, operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, checks?: StateChecks): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
1184
1229
  const before = stateBeforeWrite(m, resource, id, root);
1230
+ const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) as Record<string, unknown> | undefined : undefined;
1185
1231
  const withFiles = async (input: Record<string, unknown>): Promise<Record<string, unknown>> => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
1186
1232
  const { resource: row, result } = await applyTwinWrite(
1187
1233
  m.service,
1188
- // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
1189
- // history can say who did each thing
1190
1234
  // 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
1191
1235
  inVendorMove()
1192
1236
  ? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
1193
- : { 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) } : {}) } },
1237
+ : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } },
1194
1238
  root,
1195
1239
  );
1196
1240
  observeStateWrite(m, result, before, root, checks);
1197
- return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt);
1241
+ return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
1198
1242
  }
1199
1243
 
1200
1244
  async function atomicWrite(m: DerivedManifest, call: DerivedCall, resource: string,
1201
1245
  decide: (snapshot: readonly TwinResource[]) => { id: string; fields: Record<string, unknown> } | undefined,
1202
- operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, unchangedId?: string, checks?: StateChecks,
1246
+ operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, unchangedId?: string, checks?: StateChecks, omitInput = false,
1203
1247
  ): Promise<Record<string, unknown> | undefined> {
1204
- const input = recordedInput(m, call, params);
1205
- const files = input.operationId ? await keptFiles(m, call, root) : undefined;
1248
+ const input = omitInput ? undefined : recordedInput(m, call, params);
1249
+ const files = input?.operationId ? await keptFiles(m, call, root) : undefined;
1206
1250
  let before: TwinResource | undefined;
1207
1251
  const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
1208
1252
  const selected = decide(snapshot);
1209
- if (selected && transitionObserver) before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
1253
+ if (selected && (transitionObserver || counted(m, resource))) before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
1210
1254
  return selected ? { kind: 'write', value: selected.id, write: {
1211
1255
  operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
1212
- input: files ? { ...input, files } : input, occurredAt, actor: { kind: 'agent' as const, ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
1256
+ ...(input ? { input: files ? { ...input, files } : input } : {}), occurredAt, actor: { kind: 'agent' as const, ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
1213
1257
  } } : { kind: 'skip', value: undefined };
1214
1258
  }, root);
1215
1259
  if (!committed.result || !committed.value) {
@@ -1218,14 +1262,14 @@ async function atomicWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1218
1262
  }
1219
1263
  observeStateWrite(m, committed.result, before, root, checks);
1220
1264
  const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value!, root))!;
1221
- return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt)).body;
1265
+ return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt, before as unknown as Record<string, unknown> | undefined)).body;
1222
1266
  }
1223
1267
 
1224
1268
  async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: string, row: TwinResource,
1225
1269
  result: { externalId?: string; vendorData?: unknown }, operation: string, params: Record<string, unknown>,
1226
- root: string | undefined, occurredAt: string,
1270
+ root: string | undefined, occurredAt: string, prior?: Record<string, unknown>,
1227
1271
  ): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
1228
- await keepCounts(m, call, resource, row as unknown as Record<string, unknown>, params, root, occurredAt);
1272
+ await keepCounts(m, call, resource, row as unknown as Record<string, unknown>, params, root, occurredAt, prior);
1229
1273
  const body = view(m, resource, row);
1230
1274
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
1231
1275
  // the managed database), at the write's moment
@@ -1246,24 +1290,30 @@ async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1246
1290
 
1247
1291
  /** The declared counts a written subject is a child in (a resource's `counts`), each kept on its parent: written when
1248
1292
  * the count moved, through the write path, so the parent's history says when. */
1249
- async function keepCounts(m: DerivedManifest, call: DerivedCall, resource: string, row: Record<string, unknown>, params: Record<string, unknown>, root: string | undefined, occurredAt: string): Promise<void> {
1293
+ async function keepCounts(m: DerivedManifest, call: DerivedCall, resource: string, row: Record<string, unknown>, params: Record<string, unknown>, root: string | undefined, occurredAt: string, prior?: Record<string, unknown>): Promise<void> {
1250
1294
  for (const [parent, decl] of Object.entries(m.resources)) {
1251
1295
  for (const [field, c] of Object.entries(decl.counts ?? {})) {
1252
1296
  if (storedType(m, c.of) !== storedType(m, resource)) continue;
1253
- const parentId = parentField(row, c.by);
1254
- if (parentId === undefined || parentId === null) continue;
1255
- const pid = String(parentId);
1256
- const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid) as Record<string, unknown> | undefined;
1257
- if (!held) continue;
1258
- const n = stored(m, c.of, root).filter((r) => {
1259
- const x = r as Record<string, unknown>;
1260
- 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);
1261
- }).length + (c.plus ?? 0);
1262
- if (held[field] !== n) await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
1297
+ // the parent the subject is under now, and the one it left when the write moved it: both counts changed
1298
+ const pids = new Set([parentField(row, c.by), prior === undefined ? undefined : parentField(prior, c.by)].filter((v) => v !== undefined && v !== null).map(String));
1299
+ for (const pid of pids) {
1300
+ const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid) as Record<string, unknown> | undefined;
1301
+ if (!held) continue;
1302
+ const n = stored(m, c.of, root).filter((r) => {
1303
+ const x = r as Record<string, unknown>;
1304
+ 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);
1305
+ }).length + (c.plus ?? 0);
1306
+ if (held[field] !== n) await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
1307
+ }
1263
1308
  }
1264
1309
  }
1265
1310
  }
1266
1311
 
1312
+ /** Whether any resource counts this one (`counts`): only then does a write read the subject as it stood, for the parent
1313
+ * it may leave. */
1314
+ const counted = (m: DerivedManifest, resource: string): boolean =>
1315
+ Object.values(m.resources).some((d) => Object.values(d.counts ?? {}).some((c) => storedType(m, c.of) === storedType(m, resource)));
1316
+
1267
1317
  /** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
1268
1318
  function envelope(m: DerivedManifest, op: DerivedOperation, body: Record<string, unknown>): Record<string, unknown> {
1269
1319
  return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
@@ -1596,14 +1646,14 @@ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: Co
1596
1646
  // the body is read from a copy, so the call's own request is still unread for the write hook's context
1597
1647
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
1598
1648
  const idParam = subjectOf(m, resource, call);
1599
- const orphan = missingParent(m, resource, call, root);
1600
- if (orphan) return { served: orphan };
1601
- const parent = decl.parent;
1649
+ const parent = parentOfCall(decl, call);
1602
1650
  const parentId = parent ? parentValue(parent, call.params) : undefined;
1603
1651
  // the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
1604
1652
  const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
1605
1653
  const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
1606
1654
  if (tenantField && tenant === undefined) return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
1655
+ const orphan = missingParent(m, resource, call, root, tenant);
1656
+ if (orphan) return { served: orphan };
1607
1657
  const underParent = (r: TwinResource): boolean => (!parent || (r as Record<string, unknown>)[parent.field] === parentId)
1608
1658
  && (!tenantField || (r as Record<string, unknown>)[tenantField] === tenant);
1609
1659
  const paths = expandPaths(m, params);
@@ -1677,6 +1727,7 @@ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: Co
1677
1727
  for (const [field, sdecl] of Object.entries(decl.state ?? {})) {
1678
1728
  const requested = op.class === 'update' ? data[field] : undefined;
1679
1729
  if (op.class === 'update' && requested === undefined) continue;
1730
+ if (op.class === 'update' && sdecl.unchanged === 'allow' && stateOf(sdecl, current[field]) === stateOf(sdecl, requested)) { changes[field] = requested; continue; }
1680
1731
  const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
1681
1732
  if (refusal) return { served: vendorError(m, refusal) };
1682
1733
  if (!move) {
@@ -1764,8 +1815,6 @@ export type SemanticsContext = {
1764
1815
  /** A mail the vendor sends a person, over SMTP to the route its settings name (smtp.ts), under the World's egress rule;
1765
1816
  * throws when it cannot be delivered. */
1766
1817
  mail(route: SmtpRoute, mail: Mail, headers?: Record<string, string>): Promise<void>;
1767
- /** who is calling, when the manifest says how to tell */
1768
- actor: string | undefined;
1769
1818
  now(): unknown;
1770
1819
  /** Render a stored row or vendor view using its resource declaration, retaining the stored subject id fallback. */
1771
1820
  render(row: Record<string, unknown>, resource?: string): Record<string, unknown>;
@@ -1804,10 +1853,21 @@ export type SemanticsContext = {
1804
1853
  /** Allocate a stored occurrence and append its fields under one action lock. The optional field factory is pure. */
1805
1854
  create(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<Record<string, unknown>>;
1806
1855
  /** Create under declared unique keys and the action lock; a collision returns the existing own-field view without a write. */
1807
- createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<{ created: boolean; row: Record<string, unknown> }>;
1856
+ createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string, options?: { input: 'omit' }): Promise<{ created: boolean; row: Record<string, unknown> }>;
1808
1857
  /** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
1809
- change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string): Promise<Record<string, unknown> | undefined>;
1858
+ change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string, options?: { input: 'omit' }): Promise<Record<string, unknown> | undefined>;
1810
1859
  write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
1860
+ /**
1861
+ * One command's writes as one group (S2 R17; docs/contributing/architecture.md, "A command's writes as one group").
1862
+ * Inside `fn`, `write` stages its write and answers the row as it will stand; `row` and `rowsRaw` read the tree with
1863
+ * the staged writes folded over it, as it will be once they land. When `fn` returns or throws, every staged write
1864
+ * lands, in order, as one append; then its value is answered or its error thrown. Every other way to write, or to
1865
+ * wait on a write, lands the staged writes first; any other read of the tree (`get`, `rows`, `tree`, `history`, ...)
1866
+ * while writes are staged is refused, never answered without them. Each write keeps its own action, id, ordinal,
1867
+ * moment and recorded request. Where the pack has write hooks, events, sockets or counts, a state observer is on,
1868
+ * a head performs, or ids are aliased, `fn` simply runs, each write landing as it is made.
1869
+ */
1870
+ batch<T>(fn: () => Promise<T>): Promise<T>;
1811
1871
  /** Add numeric deltas to an existing bookkeeping row under the kernel's atomic write seam. */
1812
1872
  accumulate(resource: string, id: string, deltas: Record<string, number>, operation: string): Promise<void>;
1813
1873
  /** `write`, with what a live vendor answered when the head performed it */
@@ -1894,7 +1954,16 @@ export type SemanticsContext = {
1894
1954
  parts(): Promise<MultipartPart[]>;
1895
1955
  /** The pack's resource blobs (an object's bytes), by key, on the World's branch: they branch with the World, and a
1896
1956
  * read falls back to an ancestor's. The row that names a blob is the pack's; the bytes are here. */
1897
- blobs: { put(key: string, bytes: Uint8Array): Promise<void>; get(key: string): Promise<Uint8Array | null>; remove(key: string): Promise<void> };
1957
+ blobs: {
1958
+ put(key: string, bytes: Uint8Array): Promise<void>; get(key: string): Promise<Uint8Array | null>; remove(key: string): Promise<void>;
1959
+ /** a body written as it arrives (a request's own stream), kept only when `accept` holds for its size and SHA-256:
1960
+ * an upload of a large object never held whole. True when kept */
1961
+ putStream(key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: { size: number; sha256: string }) => boolean): Promise<boolean>;
1962
+ /** a blob's length, or null when there is none: a large object is answered without loading it */
1963
+ size(key: string): Promise<number | null>;
1964
+ /** the bytes `start`..`endInclusive` of a blob (a ranged read where the store has one), or null when there is none */
1965
+ range(key: string, start: number, endInclusive: number): Promise<Uint8Array | null>;
1966
+ };
1898
1967
  /** A body's fields checked against the operation's declared ones under the manifest's `body.strict`: the fields, or
1899
1968
  * the vendor's refusal of the first that is unknown, of another type or out of its bounds. */
1900
1969
  fields(body: Record<string, unknown>): { fields: Record<string, unknown> } | { refused: Response };
@@ -1917,11 +1986,17 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
1917
1986
  };
1918
1987
  /** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
1919
1988
  * cannot write again (and run itself again). */
1920
- export type WriteHookContext = Omit<HandlerContext, 'write' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
1989
+ export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
1921
1990
  /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
1922
1991
  export type Handler = (ctx: HandlerContext) => Promise<Response>;
1923
1992
 
1924
1993
  /** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
1994
+ /** A media type whose body the core reads for a context (and records with a write): JSON, a form, multipart, text,
1995
+ * or none named. */
1996
+ const coreReadsBody = (type: string | null): boolean => {
1997
+ const media = (type ?? '').split(';')[0]!.trim().toLowerCase();
1998
+ return media === '' || media === 'application/json' || media.endsWith('+json') || media === 'application/x-www-form-urlencoded' || media.startsWith('multipart/') || media.startsWith('text/');
1999
+ };
1925
2000
  export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext> {
1926
2001
  const root = scope.root;
1927
2002
  const asAsked = call;
@@ -1931,8 +2006,10 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
1931
2006
  // the guard's parse when it made one (taken once), else this context's own
1932
2007
  // a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
1933
2008
  const payload = bytesPayload(call.operation);
1934
- const parsed = payload ? undefined : takeParsedBody(call.request);
1935
- const text = await bodyTextOf(call.request.clone()).catch(() => '');
2009
+ // a screen's body the core does not read (an upload's bytes) is left to its handler, unread (DerivedCall.streamed)
2010
+ const opaque = call.streamed === true && !coreReadsBody(call.request.headers.get('content-type'));
2011
+ const parsed = payload || opaque ? undefined : takeParsedBody(call.request);
2012
+ const text = opaque ? '' : await bodyTextOf(call.request.clone()).catch(() => '');
1936
2013
  let body: unknown = parsed?.value;
1937
2014
  if (!parsed) { if (payload) body = text || undefined; else { try { body = text ? parseExactJson(text) : undefined; } catch { body = text; } } }
1938
2015
  // the request as it came, before the core's parameter merge and alias adoption: what a write records, so a perform
@@ -1963,7 +2040,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
1963
2040
  rawResources.set(row as Record<string, unknown>, name ?? resource);
1964
2041
  return row as Record<string, unknown>;
1965
2042
  };
1966
- return {
2043
+ const context: SemanticsContext = {
1967
2044
  call,
1968
2045
  params,
1969
2046
  id: Object.values(call.params).at(-1),
@@ -1981,7 +2058,6 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
1981
2058
  return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1982
2059
  },
1983
2060
  mail: (route, mail, headers) => sendMail(route, mail, headers),
1984
- actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
1985
2061
  now: () => now(m, at),
1986
2062
  render: (row, resource) => {
1987
2063
  const raw = rawResources.has(row);
@@ -2018,10 +2094,15 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2018
2094
  secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
2019
2095
  record: async (type, fields, id, operation) => {
2020
2096
  if (!type.startsWith('_')) throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
2097
+ // a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
2098
+ // child does, the parent it left recounted too
2099
+ const keeps = counted(m, type);
2021
2100
  if (id !== undefined) {
2022
2101
  const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
2023
- 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);
2102
+ const prior = keeps ? resourcesOfType(m.service, type, root).find((r) => r.id === id) : undefined;
2103
+ 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);
2024
2104
  if (operation) observeStateWrite(m, result, before, root, stateChecks);
2105
+ if (keeps) await keepCounts(m, call, type, row as unknown as Record<string, unknown>, params, root, at, prior && { ...prior } as Record<string, unknown>);
2025
2106
  return id;
2026
2107
  }
2027
2108
  const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
@@ -2039,6 +2120,8 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2039
2120
  return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
2040
2121
  }, root, () => resourcesOfType(m.service, type, root));
2041
2122
  if (operation && result) observeStateWrite(m, result, undefined, root, stateChecks);
2123
+ const made = keeps && value !== undefined ? resourcesOfType(m.service, type, root).find((r) => r.id === value) : undefined;
2124
+ if (made) await keepCounts(m, call, type, made as unknown as Record<string, unknown>, params, root, at);
2042
2125
  return value;
2043
2126
  },
2044
2127
  get machines() {
@@ -2051,7 +2134,8 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2051
2134
  const id = mintId(m, resource, root, at, snapshot);
2052
2135
  return { id, fields: typeof fields === 'function' ? fields(id) : fields };
2053
2136
  }, operation, params, root, at, undefined, stateChecks).then((row) => row!),
2054
- createUnique: async (resource, fields, operation) => {
2137
+ createUnique: async (resource, fields, operation, options) => {
2138
+ if (options?.input === 'omit' && !resource.startsWith('_')) throw new Error('only bookkeeping writes may omit request input');
2055
2139
  const rules = m.resources[resource]?.unique;
2056
2140
  if (!rules?.length) throw new Error('createUnique requires declared unique keys');
2057
2141
  let existing: Record<string, unknown> | undefined;
@@ -2061,15 +2145,18 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2061
2145
  const hit = snapshot.find((r) => r.type === storedType(m, resource) && r.deleted !== true && rules.some((rule) => rule.fields.every((f) => selected[f] !== undefined && view(m, resource, r)[f] === selected[f])));
2062
2146
  if (hit) { existing = view(m, resource, hit); return undefined; }
2063
2147
  return { id, fields: selected };
2064
- }, operation, params, root, at, undefined, stateChecks);
2148
+ }, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
2065
2149
  return existing ? { created: false, row: existing } : { created: true, row: row! };
2066
2150
  },
2067
- change: (resource, id, decide, operation) => atomicWrite(m, call, resource, (snapshot) => {
2151
+ change: (resource, id, decide, operation, options) => {
2152
+ if (options?.input === 'omit' && !resource.startsWith('_')) throw new Error('only bookkeeping writes may omit request input');
2153
+ return atomicWrite(m, call, resource, (snapshot) => {
2068
2154
  const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
2069
2155
  if (!row) return undefined;
2070
2156
  const fields = decide(row as Record<string, unknown>);
2071
2157
  return fields === undefined ? undefined : { id, fields };
2072
- }, operation, params, root, at, id, stateChecks),
2158
+ }, operation, params, root, at, id, stateChecks, options?.input === 'omit');
2159
+ },
2073
2160
  write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
2074
2161
  accumulate: async (resource, id, deltas, operation) => {
2075
2162
  if (!resource.startsWith('_') || !m.resources[resource]) throw new Error('accumulate requires a declared bookkeeping resource');
@@ -2151,8 +2238,11 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2151
2238
  live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root) as unknown as Array<Record<string, unknown>>, blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
2152
2239
  blobs: {
2153
2240
  put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
2241
+ putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
2154
2242
  get: (key) => readResourceBlob(m.service, key, root),
2155
2243
  remove: (key) => removeResourceBlob(m.service, key, root),
2244
+ size: (key) => resourceBlobSize(m.service, key, root),
2245
+ range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
2156
2246
  },
2157
2247
  merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
2158
2248
  conflict: (resource, fields, except) => {
@@ -2172,13 +2262,218 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2172
2262
  over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
2173
2263
  core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
2174
2264
  own: (row) => ownFields(row as unknown as TwinResource),
2265
+ batch: (fn) => fn(),
2266
+ };
2267
+ if (batchable(m, root)) bindBatch(m, call, context, params, root, at, rawRow);
2268
+ return context;
2269
+ }
2270
+
2271
+ /**
2272
+ * Whether a call's writes may be grouped (`ctx.batch`) with nothing about them changing: every effect a write has
2273
+ * besides its action and the tree (write hooks, events, sockets, a parent's counts, a state observer, a head that
2274
+ * performs it, an alias a read resolves through) is one the grouped path would move, so where any is present the
2275
+ * writes land one at a time, as they always have.
2276
+ */
2277
+ function batchable(m: DerivedManifest, root: string | undefined): boolean {
2278
+ return !m.onWrite && !m.events && !m.sockets?.length && !transitionObserver
2279
+ && !Object.values(m.resources).some((decl) => decl.counts && Object.keys(decl.counts).length > 0)
2280
+ && !performsAtHeadNow(m.service, root) && subjectAliases(m.service, root).size === 0;
2281
+ }
2282
+
2283
+ /** A staged write of `ctx.batch`, as `write` was asked it. */
2284
+ type StagedWrite = { resource: string; id: string; fields: Record<string, unknown>; operation: string };
2285
+
2286
+ /** The members of a context that read the tree other than `row`/`rowsRaw`: refused while writes are staged. */
2287
+ const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', 'expand', 'mint', 'conflict', 'ownerRow', 'render'] as const;
2288
+ /** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
2289
+ const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
2290
+ // a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
2291
+ 'secret', 'signingKey'] as const;
2292
+
2293
+ /** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
2294
+ function bindBatch(m: DerivedManifest, call: DerivedCall, context: SemanticsContext, params: Record<string, unknown>, root: string | undefined, at: string,
2295
+ rawRow: (resource: string, row: TwinResource) => Record<string, unknown>): void {
2296
+ let staged: StagedWrite[] | undefined;
2297
+ // each staged subject as it will stand, by `type:id`, in the order first written
2298
+ const subjects = new Map<string, Subject>();
2299
+ const land = async (): Promise<void> => {
2300
+ if (!staged?.length) return;
2301
+ const writes = staged;
2302
+ staged = [];
2303
+ subjects.clear();
2304
+ await writeGroup(m, call, writes, params, root, at);
2305
+ };
2306
+ const ctx = context as unknown as Record<string, unknown>;
2307
+ for (const name of BATCH_LANDING_MEMBERS) {
2308
+ const plain = ctx[name];
2309
+ if (typeof plain !== 'function') continue;
2310
+ ctx[name] = async (...args: unknown[]) => { await land(); return (plain as (...a: unknown[]) => unknown)(...args); };
2311
+ }
2312
+ for (const name of BATCH_REFUSED_READS) {
2313
+ const plain = ctx[name];
2314
+ if (typeof plain !== 'function') continue;
2315
+ ctx[name] = (...args: unknown[]) => {
2316
+ if (staged?.length) throw new Error(`ctx.${name} reads the tree while ctx.batch holds staged writes: only row and rowsRaw see them`);
2317
+ return (plain as (...a: unknown[]) => unknown)(...args);
2318
+ };
2319
+ }
2320
+ const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
2321
+ context.write = async (resource, id, fields, operation) => {
2322
+ // a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
2323
+ if (!staged || inVendorMove()) { await land(); return plainWrite(resource, id, fields, operation); }
2324
+ const type = storedType(m, resource), key = `${type}:${id}`;
2325
+ // the one subject, as the memoized tree holds it (readTreeMap would copy the whole tree on every staged write)
2326
+ const subject = foldLocalWrite(subjects.get(key) ?? heldSubject(m.service, type, id, root), type, id, fields as SubjectFields, at);
2327
+ subjects.set(key, subject);
2328
+ staged.push({ resource, id, fields, operation });
2329
+ return view(m, resource, treeResources(new Map([[key, subject]]))[0]!);
2330
+ };
2331
+ // the stored rows of a resource's type with the staged subjects folded over them, filtered as `stored` filters
2332
+ const overlaid = (resource: string, opts?: { withDeleted?: boolean }): TwinResource[] | undefined => {
2333
+ if (!staged?.length) return undefined;
2334
+ const type = storedType(m, resource), prefix = `${type}:`;
2335
+ const mine = [...subjects].filter(([key]) => key.startsWith(prefix));
2336
+ if (!mine.length) return undefined;
2337
+ const replaced = new Map(mine.map(([key, subject]) => [key, treeResources(new Map([[key, subject]]))[0]!]));
2338
+ const base = resourcesOfType(m.service, type, root);
2339
+ // a subject the tree holds stays where it stands; one new to it goes to the end, in the order first written
2340
+ const rows = base.map((row) => replaced.get(`${prefix}${row.id}`) ?? row);
2341
+ const held = new Set(base.map((row) => `${prefix}${row.id}`));
2342
+ for (const [key, row] of replaced) if (!held.has(key)) rows.push(row);
2343
+ const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
2344
+ return rows.filter((r) => (opts?.withDeleted || r.deleted !== true) && when.every(([k, v]) => (r as Record<string, unknown>)[k] === v));
2345
+ };
2346
+ context.row = (resource, id, opts) => {
2347
+ const rows = overlaid(resource, opts);
2348
+ if (!rows) return plainRow(resource, id, opts);
2349
+ const row = rows.find((r) => r.id === id);
2350
+ return row ? rawRow(resource, row) : undefined;
2351
+ };
2352
+ context.rowsRaw = (resource, opts) => {
2353
+ const rows = overlaid(resource, opts);
2354
+ return rows ? rows.map((row) => rawRow(resource, row)) : plainRowsRaw(resource, opts);
2355
+ };
2356
+ context.batch = async (fn) => {
2357
+ // a batch inside a batch is the outer one's
2358
+ if (staged !== undefined) return fn();
2359
+ staged = [];
2360
+ try { return await fn(); }
2361
+ finally {
2362
+ // every write `write` answered lands, whether fn returned or threw: an ordered insert's prefix stays written
2363
+ try { await land(); } finally { staged = undefined; subjects.clear(); }
2364
+ }
2175
2365
  };
2176
2366
  }
2177
2367
 
2368
+ /**
2369
+ * A command's staged writes landed as one group: each recorded with the request as `writeDetailed` records it (one
2370
+ * recordedInput, the same for every write of the call), the call's moment and actor, in order, under one lock and one
2371
+ * append (serve.ts applyTwinWrites); then a held read on each written type is told, as each write tells it.
2372
+ */
2373
+ async function writeGroup(m: DerivedManifest, call: DerivedCall, writes: readonly StagedWrite[], params: Record<string, unknown>,
2374
+ root: string | undefined, occurredAt: string): Promise<void> {
2375
+ const input = recordedInput(m, call, params);
2376
+ const files = input.operationId ? await keptFiles(m, call, root) : undefined;
2377
+ const recorded = files ? { ...input, files } : input;
2378
+ const actor = { kind: 'agent' as const, ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) };
2379
+ await applyTwinWrites(m.service, writes.map((write) => ({ operation: write.operation, subjectType: storedType(m, write.resource),
2380
+ subjectId: write.id, fields: write.fields, input: recorded, occurredAt, actor })), root);
2381
+ for (const type of new Set(writes.map((write) => storedType(m, write.resource)))) heldReadsWrite(m.service, root, type);
2382
+ }
2383
+
2178
2384
  /** A pack's semantics handlers as the dispatch's handlers. */
2179
2385
  export function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope: CoreScope = {}): Record<string, DerivedHandler> {
2180
2386
  prepareViewOmissions(m);
2181
- return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) => h(await contextFor(m, call, scope))]));
2387
+ return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) => {
2388
+ const ctx = await contextFor(m, call, scope);
2389
+ redisServiceTimes.lap('6c the handler\'s own context opened');
2390
+ // an operation's own variants, else the ones every operation shares (`'*'`)
2391
+ const variants = m.variants?.[id] ?? m.variants?.['*'];
2392
+ if (variants && variantAsked(ctx.params as Record<string, unknown>, variants, call.request.headers)) return variantGap(m, call.request);
2393
+ const outside = enumRefused(m, call, ctx.params as Record<string, unknown>);
2394
+ return outside ?? h(ctx);
2395
+ }]));
2396
+ }
2397
+
2398
+ /** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
2399
+ * bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
2400
+ export function valueOutside(params: Record<string, unknown>, enums: Readonly<Record<string, ReadonlyArray<string>>>): { param: string; values: ReadonlyArray<string> } | undefined {
2401
+ const at = (value: unknown, path: string[], name: string, values: ReadonlyArray<string>): { param: string; values: ReadonlyArray<string> } | undefined => {
2402
+ if (value === undefined || value === null) return undefined;
2403
+ if (!path.length) return typeof value === 'object' || values.includes(String(value)) ? undefined : { param: name, values };
2404
+ const [head, ...rest] = path;
2405
+ if (head === '[]') {
2406
+ const items = Array.isArray(value) ? value.map((v, i) => [String(i), v] as const) : value && typeof value === 'object' ? Object.entries(value) : [];
2407
+ for (const [i, v] of items) { const found = at(v, rest, `${name}[${i}]`, values); if (found) return found; }
2408
+ return undefined;
2409
+ }
2410
+ return value && typeof value === 'object' && !Array.isArray(value) ? at((value as Record<string, unknown>)[head!], rest, `${name}[${head}]`, values) : undefined;
2411
+ };
2412
+ for (const [path, values] of Object.entries(enums)) {
2413
+ const [first, ...rest] = path.split('.');
2414
+ const found = at(params[first!], rest, first!, values);
2415
+ if (found) return found;
2416
+ }
2417
+ return undefined;
2418
+ }
2419
+
2420
+ /** The manifest's refusal of a form value outside the operation's spec lists (and the values documented beside
2421
+ * them), unless the request names an API version other than the one the spec is. */
2422
+ function enumRefused(m: DerivedManifest, call: DerivedCall, params: Record<string, unknown>): Response | undefined {
2423
+ const refusal = m.body.form?.enumRefusal;
2424
+ if (!refusal || !call.operation.enums) return undefined;
2425
+ const pinned = refusal.version ? call.request.headers.get(refusal.version.header) : null;
2426
+ if (pinned && pinned !== refusal.version!.served) return undefined;
2427
+ const documented = refusal.documented?.[call.operation.id] ?? {};
2428
+ const enums = Object.fromEntries(Object.entries(call.operation.enums).map(([path, values]) => [path, [...values, ...(documented[path] ?? [])]]));
2429
+ const outside = valueOutside(params, enums);
2430
+ if (!outside) return undefined;
2431
+ const { documented: _d, version: _v, ...error } = refusal;
2432
+ return vendorError(m, { ...error, param: outside.param, message: error.message.replace(/\{param\}/g, outside.param).replace(/\{values\}/g, oneOfList(outside.values)) });
2433
+ }
2434
+
2435
+ /** A list of values as a sentence names them: `a, b, or c`. */
2436
+ const oneOfList = (values: ReadonlyArray<string>): string => (values.length < 2 ? values.join('') : `${values.slice(0, -1).join(', ')}, or ${values[values.length - 1]}`);
2437
+
2438
+ /** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
2439
+ export function variantAsked(params: Record<string, unknown>, variants: readonly string[], headers?: Headers): boolean {
2440
+ const valuesAt = (value: unknown, path: string[]): unknown[] => {
2441
+ if (!path.length) return value === undefined ? [] : [value];
2442
+ const [head, ...rest] = path;
2443
+ // `!name` is the field left out: the value itself where it has none (`items[].!id`, an item naming no id)
2444
+ if (head!.startsWith('!')) return value && typeof value === 'object' && (value as Record<string, unknown>)[head!.slice(1)] === undefined ? [value] : [];
2445
+ const list = head!.endsWith('[]');
2446
+ const key = list ? head!.slice(0, -2) : head!;
2447
+ const next = value && typeof value === 'object' ? (value as Record<string, unknown>)[key] : undefined;
2448
+ if (!list) return valuesAt(next, rest);
2449
+ const items = Array.isArray(next) ? next : next && typeof next === 'object' ? Object.values(next) : [];
2450
+ return items.flatMap((item) => valuesAt(item, rest));
2451
+ };
2452
+ // a variant joined with `&` is its parts asked together (`trial_end=now&items`): the combination, not either alone;
2453
+ // a part led by `!` is a parameter left out (`items[].price&!items[].id`, an item added rather than changed)
2454
+ return variants.some((variant) => variant.split('&').every((part) => {
2455
+ const absent = part.startsWith('!');
2456
+ // `name~=value` is one entry of a comma-separated value (`tweet.fields~=geo`, X's fields lists); `name=value` the whole
2457
+ const body = absent ? part.slice(1) : part;
2458
+ const listed = body.includes('~=');
2459
+ const [path, wanted] = listed ? body.split('~=') : body.split('=');
2460
+ // a part led by `@` names a request header (`@stripe-account&application_fee_amount`, a charge made on a connected
2461
+ // account with the platform's fee)
2462
+ const header = path!.startsWith('@') ? headers?.get(path!.slice(1)) : undefined;
2463
+ // a parameter whose own name holds a dot (X's `tweet.fields`) is read by that name before the path is walked
2464
+ const found = path!.startsWith('@') ? (header === null || header === undefined ? [] : [header])
2465
+ : Object.hasOwn(params, path!) ? (params[path!] === undefined ? [] : [params[path!]]) : valuesAt(params, path!.split('.'));
2466
+ const asked = wanted === undefined ? found.length > 0
2467
+ : found.some((value) => (listed ? String(value).split(',').map((entry) => entry.trim()).includes(wanted) : String(value) === wanted));
2468
+ return absent ? !asked : asked;
2469
+ }));
2470
+ }
2471
+
2472
+ /** The manifest's gap, as a variant is answered: the unknown-request refusal (else its not-found). */
2473
+ function variantGap(m: DerivedManifest, request: Request): Response {
2474
+ const nf = m.notFound;
2475
+ const gap: ErrorSpec = m.gap ?? { status: nf.status, message: nf.message, ...(nf.code !== undefined ? { code: nf.code } : {}), ...(nf.kind !== undefined ? { kind: nf.kind } : {}) };
2476
+ return vendorError(m, { ...gap, message: gap.message.replace(/\{method\}/g, request.method).replace(/\{path\}/g, new URL(request.url).pathname) });
2182
2477
  }
2183
2478
 
2184
2479
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */