@volter/world-core 2.0.37 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +4 -5
  2. package/app-route.cjs +12 -6
  3. package/app-route.d.cts +1 -1
  4. package/dist/app-route.cjs +12 -6
  5. package/dist/app-route.d.cts +1 -1
  6. package/dist/generated/pack-facts.json +1410 -3069
  7. package/dist/inject.cjs +64 -9
  8. package/dist/pack-facts.cjs +44 -0
  9. package/dist/src/actions.d.ts +3 -3
  10. package/dist/src/actions.js +22 -16
  11. package/dist/src/ancestry.d.ts +14 -2
  12. package/dist/src/ancestry.js +92 -2
  13. package/dist/src/anthropic-wire.d.ts +39 -0
  14. package/dist/src/anthropic-wire.js +136 -0
  15. package/dist/src/bytes.d.ts +7 -0
  16. package/dist/src/bytes.js +35 -0
  17. package/dist/src/changeset.d.ts +1 -1
  18. package/dist/src/changeset.js +0 -0
  19. package/dist/src/clickhouse/index.d.ts +3 -0
  20. package/dist/src/clickhouse/index.js +6 -0
  21. package/dist/src/clickhouse/sql.d.ts +233 -0
  22. package/dist/src/clickhouse/sql.js +4329 -0
  23. package/dist/src/clickhouse/types.d.ts +18 -0
  24. package/dist/src/clickhouse/types.js +47 -0
  25. package/dist/src/clickhouse/values.d.ts +146 -0
  26. package/dist/src/clickhouse/values.js +858 -0
  27. package/dist/src/client-bundle.js +2 -3
  28. package/dist/src/cors.d.ts +15 -0
  29. package/dist/src/cors.js +31 -0
  30. package/dist/src/derived-core.d.ts +487 -24
  31. package/dist/src/derived-core.js +788 -144
  32. package/dist/src/derived-real.d.ts +13 -0
  33. package/dist/src/derived-real.js +518 -0
  34. package/dist/src/derived.d.ts +35 -1
  35. package/dist/src/derived.js +61 -9
  36. package/dist/src/emit.js +1 -2
  37. package/dist/src/events.d.ts +206 -0
  38. package/dist/src/events.js +341 -0
  39. package/dist/src/executor.d.ts +3 -0
  40. package/dist/src/executor.js +19 -2
  41. package/dist/src/file-response.d.ts +6 -0
  42. package/dist/src/file-response.js +30 -0
  43. package/dist/src/fork.js +3 -2
  44. package/dist/src/git/history.d.ts +7 -0
  45. package/dist/src/git/history.js +24 -0
  46. package/dist/src/git/index.d.ts +1 -0
  47. package/dist/src/git/index.js +1 -0
  48. package/dist/src/git/lfs.d.ts +28 -0
  49. package/dist/src/git/lfs.js +66 -0
  50. package/dist/src/git/objects.js +3 -8
  51. package/dist/src/git/smart-http.d.ts +3 -1
  52. package/dist/src/git/smart-http.js +67 -6
  53. package/dist/src/graphql-wire.d.ts +29 -0
  54. package/dist/src/graphql-wire.js +101 -0
  55. package/dist/src/grpc-wire.d.ts +67 -0
  56. package/dist/src/grpc-wire.js +170 -0
  57. package/dist/src/h2.d.ts +40 -0
  58. package/dist/src/h2.js +656 -0
  59. package/dist/src/head.d.ts +32 -3
  60. package/dist/src/head.js +161 -40
  61. package/dist/src/history.d.ts +1 -1
  62. package/dist/src/history.js +6 -6
  63. package/dist/src/hpack.json +1 -0
  64. package/dist/src/index.d.ts +64 -75
  65. package/dist/src/index.js +58 -101
  66. package/dist/src/log.js +28 -19
  67. package/dist/src/machines.d.ts +50 -0
  68. package/dist/src/machines.js +151 -0
  69. package/dist/src/managed-database.d.ts +86 -0
  70. package/dist/src/managed-database.js +283 -0
  71. package/dist/src/multipart.d.ts +11 -0
  72. package/dist/src/multipart.js +51 -0
  73. package/dist/src/observe.d.ts +15 -5
  74. package/dist/src/observe.js +23 -9
  75. package/dist/src/openai-wire.d.ts +108 -0
  76. package/dist/src/openai-wire.js +337 -0
  77. package/dist/src/pack-assets.d.ts +3 -4
  78. package/dist/src/pack-assets.js +15 -10
  79. package/dist/src/pack-fetch.d.ts +77 -0
  80. package/dist/src/pack-fetch.js +449 -0
  81. package/dist/src/pack-paths.d.ts +12 -0
  82. package/dist/src/pack-paths.js +86 -0
  83. package/dist/src/packRegistry.d.ts +69 -162
  84. package/dist/src/packRegistry.js +55 -20
  85. package/dist/src/people.d.ts +13 -0
  86. package/dist/src/people.js +18 -0
  87. package/dist/src/placeholder-image.d.ts +5 -0
  88. package/dist/src/placeholder-image.js +114 -0
  89. package/dist/src/protobuf.d.ts +28 -0
  90. package/dist/src/protobuf.js +332 -0
  91. package/dist/src/redis/engine.js +1 -1
  92. package/dist/src/request-scope.d.ts +1 -1
  93. package/dist/src/request-scope.js +6 -4
  94. package/dist/src/resource-blob.d.ts +5 -0
  95. package/dist/src/resource-blob.js +11 -0
  96. package/dist/src/runtime.d.ts +85 -0
  97. package/dist/src/runtime.js +104 -0
  98. package/dist/src/s3/wire.d.ts +60 -0
  99. package/dist/src/s3/wire.js +157 -0
  100. package/dist/src/scenario.d.ts +3 -0
  101. package/dist/src/scenario.js +2 -0
  102. package/dist/src/schema-sample.d.ts +1 -0
  103. package/dist/src/schema-sample.js +21 -0
  104. package/dist/src/sealed-box.d.ts +14 -0
  105. package/dist/src/sealed-box.js +225 -0
  106. package/dist/src/serve-http.d.ts +14 -0
  107. package/dist/src/serve-http.js +27 -3
  108. package/dist/src/serve.d.ts +6 -0
  109. package/dist/src/serve.js +69 -14
  110. package/dist/src/signing.d.ts +135 -0
  111. package/dist/src/signing.js +222 -0
  112. package/dist/src/sigv4.d.ts +48 -0
  113. package/dist/src/sigv4.js +167 -0
  114. package/dist/src/smtp.d.ts +16 -0
  115. package/dist/src/smtp.js +72 -0
  116. package/dist/src/sockets.d.ts +51 -0
  117. package/dist/src/sockets.js +90 -0
  118. package/dist/src/state-system.d.ts +1 -0
  119. package/dist/src/state-system.js +1 -1
  120. package/dist/src/storage.d.ts +1 -1
  121. package/dist/src/storage.js +3 -3
  122. package/dist/src/trace-context.js +1 -1
  123. package/dist/src/twin-fetch.d.ts +0 -7
  124. package/dist/src/twin-fetch.js +0 -14
  125. package/dist/src/vendor-call.d.ts +6 -0
  126. package/dist/src/vendor-call.js +41 -0
  127. package/dist/src/world-store.js +1 -1
  128. package/dist/vendor-hosts.cjs +36 -125
  129. package/dist/vendor-hosts.d.cts +8 -0
  130. package/generated/pack-facts.json +1410 -3069
  131. package/inject.cjs +64 -9
  132. package/pack-facts.cjs +44 -0
  133. package/package.json +17 -3
  134. package/src/actions.ts +23 -16
  135. package/src/ancestry.ts +74 -2
  136. package/src/anthropic-wire.ts +137 -0
  137. package/src/bytes.ts +42 -0
  138. package/src/changeset.ts +5 -5
  139. package/src/clickhouse/index.ts +6 -0
  140. package/src/clickhouse/sql.ts +3059 -0
  141. package/src/clickhouse/types.ts +44 -0
  142. package/src/clickhouse/values.ts +697 -0
  143. package/src/client-bundle.ts +2 -3
  144. package/src/cors.ts +34 -0
  145. package/src/derived-core.ts +1013 -146
  146. package/src/derived-real.ts +434 -0
  147. package/src/derived.ts +73 -3
  148. package/src/emit.ts +1 -2
  149. package/src/events.ts +449 -0
  150. package/src/executor.ts +24 -2
  151. package/src/file-response.ts +27 -0
  152. package/src/fork.ts +3 -2
  153. package/src/git/history.ts +19 -0
  154. package/src/git/index.ts +1 -0
  155. package/src/git/lfs.ts +67 -0
  156. package/src/git/objects.ts +3 -5
  157. package/src/git/smart-http.ts +56 -6
  158. package/src/graphql-wire.ts +106 -0
  159. package/src/grpc-wire.ts +159 -0
  160. package/src/h2.ts +627 -0
  161. package/src/head.ts +132 -41
  162. package/src/history.ts +6 -6
  163. package/src/hpack.json +1 -0
  164. package/src/index.ts +82 -329
  165. package/src/log.ts +27 -18
  166. package/src/machines.ts +151 -0
  167. package/src/managed-database.ts +299 -0
  168. package/src/multipart.ts +51 -0
  169. package/src/observe.ts +31 -15
  170. package/src/openai-wire.ts +371 -0
  171. package/src/pack-assets.ts +15 -11
  172. package/src/pack-fetch.ts +458 -0
  173. package/src/pack-paths.ts +72 -0
  174. package/src/packRegistry.ts +79 -167
  175. package/src/people.ts +31 -0
  176. package/src/placeholder-image.ts +88 -0
  177. package/src/protobuf.ts +251 -0
  178. package/src/redis/engine.ts +1 -1
  179. package/src/request-scope.ts +8 -4
  180. package/src/resource-blob.ts +13 -0
  181. package/src/runtime.ts +344 -0
  182. package/src/s3/wire.ts +172 -0
  183. package/src/scenario.ts +4 -0
  184. package/src/schema-sample.ts +24 -0
  185. package/src/sealed-box.ts +182 -0
  186. package/src/serve-http.ts +31 -3
  187. package/src/serve.ts +58 -14
  188. package/src/signing.ts +231 -0
  189. package/src/sigv4.ts +158 -0
  190. package/src/smtp.ts +76 -0
  191. package/src/sockets.ts +140 -0
  192. package/src/state-system.ts +2 -2
  193. package/src/storage.ts +3 -3
  194. package/src/trace-context.ts +1 -1
  195. package/src/twin-fetch.ts +0 -20
  196. package/src/vendor-call.ts +41 -0
  197. package/src/world-store.ts +1 -1
  198. package/vendor-hosts.cjs +36 -125
  199. package/vendor-hosts.d.cts +8 -0
  200. package/dist/src/mirror-shell.d.ts +0 -2
  201. package/dist/src/mirror-shell.js +0 -13
  202. package/dist/src/v1-removed.d.ts +0 -159
  203. package/dist/src/v1-removed.js +0 -124
  204. package/src/mirror-shell.ts +0 -15
  205. package/src/v1-removed.ts +0 -172
package/src/head.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
11
  import { homedir } from 'node:os';
12
12
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
13
- import { confirmAction, isTwinBookkeeping, resolveSubjectId, revertAction, type TwinAction } from './actions.ts';
13
+ import { confirmAction, currentCorrelationId, isTwinBookkeeping, resolveSubjectId, revertAction, type TwinAction } from './actions.ts';
14
14
  import { keySealer, type CredentialPayload, type SealedCredential, type Sealer } from './credential.ts';
15
15
  import { buildRemoteExecute, validateRemoteOrigin, type CredentialCustody } from './executor.ts';
16
16
  import { getPack } from './packRegistry.ts';
@@ -26,7 +26,13 @@ import { getActiveWorldStore } from './world-store.ts';
26
26
 
27
27
  /** What a perform answers: the id the vendor minted (the local id when it kept ours), its URL, and
28
28
  * the vendor's answer as fields for the landed copy. */
29
- export type PushOutcome = { externalId: string; url?: string; data?: Record<string, unknown> };
29
+ export type PushOutcome = {
30
+ externalId: string; url?: string; data?: Record<string, unknown>;
31
+ /** the group's entry the id is for, when it is not the group's first (a cascade writes children first) */
32
+ actionId?: string;
33
+ /** the group's other entries the answer named an id for (a resource's `companions`) */
34
+ also?: Array<{ actionId: string; externalId: string }>;
35
+ };
30
36
  /** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
31
37
  * entry authored against `twin-1` crosses against `REAL-42` once that subject's landing adopted it). */
32
38
  /** `credential`: the sealed credential's keyed fingerprint (credential.ts — an HMAC under the sealing key, or the
@@ -34,7 +40,8 @@ export type PushOutcome = { externalId: string; url?: string; data?: Record<stri
34
40
  * never the secret and no offline oracle for it), so a pack keys its budget ledger per real credential:
35
41
  * every World and branch performing with one vendor key then shares that key's one allowance. */
36
42
  export type PerformContext = { resolve: (type: string, localId: string) => string; service?: string; root?: string; credential?: string };
37
- export type PerformAction = (execute: RemoteExecute, action: TwinAction, ctx: PerformContext) => Promise<PushOutcome>;
43
+ /** One request's entries (a group: they share its `correlationId`), performed as the one call that made them. */
44
+ export type PerformAction = (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext) => Promise<PushOutcome>;
38
45
  export function performContext(service: string, root?: string, credential?: string): PerformContext {
39
46
  return { resolve: (type, localId) => resolveSubjectId(service, type, localId, root), service, ...(root !== undefined ? { root } : {}), ...(credential !== undefined ? { credential } : {}) };
40
47
  }
@@ -255,6 +262,8 @@ export type DeployReport = {
255
262
  skipped: string[];
256
263
  /** entries whose landed copy already said `deployed` — not performed again */
257
264
  replayed: string[];
265
+ /** entries the World's own doors made (a door, a screen, the gap, the clock, a seed): never offered to the vendor */
266
+ worldOnly: string[];
258
267
  };
259
268
 
260
269
  /** The receipt a branch entry's latest landed copy carries, if any. */
@@ -264,18 +273,45 @@ function receiptsByEntry(parent: Entry[]): Map<string, Receipt> {
264
273
  return out;
265
274
  }
266
275
 
267
- /** The branch entries a root twin still has to perform: `set` rows, not reverted, whose landed copy
268
- * is absent or says `failed`/`skipped` (a `deployed` or `refused` copy settles the entry). */
269
- export function deployableEntries(service: string, root?: string): TwinAction[] {
276
+ /** Whether an entry records a call of the vendor's surface (an operation or a GraphQL mutation): what a perform can send
277
+ * again (the real-system adapters, "What the derived core records with a write"). */
278
+ function surfaceWrite(e: { input?: Record<string, unknown> }): boolean {
279
+ return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null);
280
+ }
281
+
282
+ /** The branch entries a root twin still has to settle: `set` rows, not reverted, whose landed copy is absent or says
283
+ * `failed`/`skipped` (a `deployed` or `refused` copy settles the entry), split into what is offered to the vendor (a
284
+ * surface write on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
285
+ * clock or a seed made, and any write on a subject only the World's own writes made. */
286
+ export function settlingEntries(service: string, root?: string): { offered: TwinAction[]; worldOnly: TwinAction[] } {
270
287
  const parent = parentEntries(service, root);
271
288
  const receipts = receiptsByEntry(parent);
272
289
  const branch = branchEntries(service, root);
273
290
  const reverted = new Set(branch.filter((e) => e.op === 'revert' && e.revertsActionId).map((e) => e.revertsActionId!));
274
- return branch.filter((e) => {
275
- if (e.op !== 'set' || reverted.has(e.id) || isTwinBookkeeping(e as TwinAction)) return false;
276
- const r = receipts.get(e.id);
277
- return r === undefined || r.status === 'failed' || r.status === 'skipped';
278
- }) as TwinAction[];
291
+ const live = branch.filter((e) => e.op === 'set' && !reverted.has(e.id) && !isTwinBookkeeping(e as TwinAction)) as TwinAction[];
292
+ // a subject some surface write made (or the vendor holds: the parent log knows it) is the vendor's; one only the
293
+ // World's own writes made is not, and nothing written to it is sent
294
+ const key = (e: TwinAction): string => `${e.subject.type}\u0000${e.subject.id}`;
295
+ const first = new Map<string, TwinAction>();
296
+ for (const e of live) if (!first.has(key(e))) first.set(key(e), e);
297
+ const vendors = new Set([...[...first].filter(([, e]) => surfaceWrite(e)).map(([k]) => k), ...(parent as Array<TwinAction & { landsId?: string }>).filter((e) => !e.landsId).map(key)]);
298
+ const pending = live.filter((e) => { const r = receipts.get(e.id); return r === undefined || r.status === 'failed' || r.status === 'skipped'; });
299
+ return {
300
+ offered: pending.filter((e) => surfaceWrite(e) && vendors.has(key(e))),
301
+ worldOnly: pending.filter((e) => !surfaceWrite(e) || !vendors.has(key(e))),
302
+ };
303
+ }
304
+
305
+ /** The entries a root twin offers the vendor (settlingEntries' `offered`). */
306
+ export function deployableEntries(service: string, root?: string): TwinAction[] {
307
+ return settlingEntries(service, root).offered;
308
+ }
309
+
310
+ /** Entries in groups of the request that made them (their `correlationId`), each group in log order. */
311
+ function groupsOf(entries: TwinAction[]): TwinAction[][] {
312
+ const groups = new Map<string, TwinAction[]>();
313
+ for (const e of entries) { const k = e.correlationId ?? e.id; groups.set(k, [...(groups.get(k) ?? []), e]); }
314
+ return [...groups.values()];
279
315
  }
280
316
 
281
317
  const queues = new Map<string, Promise<unknown>>(); // one performer per twin: wire writes perform in append order
@@ -316,57 +352,75 @@ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<D
316
352
  const bound = performRoot(service, root, opts.execute !== undefined);
317
353
  const vendor = bound.vendor;
318
354
  const adapters = opts.adapters ?? stateSystemFor(vendor) ?? stateSystemFor(service);
319
- if (!adapters?.perform) throw new Error(`the ${vendor} twin has no perform adapter — a protocol 2 pack names one in its descriptor and registers itself (registerPack)`);
355
+ if (!adapters?.perform) throw new Error(`the ${vendor} twin has no perform adapter — a pack names one in its descriptor and registers itself (registerPack)`);
320
356
  // the caller's executor, or the one built over the sealed credential; an unsealed root has no other
321
- const execute = opts.execute ?? buildRemoteExecute(validateRemoteOrigin(bound.sealed!.url, { allowInsecureLoopback: true }), await openRootCredential(bound.sealed!, vendor), authStrategyFor(vendor), getPack(vendor)?.hosts, { custody: rootCustody(bound.sealed!, vendor) });
357
+ const execute = opts.execute ?? buildRemoteExecute(validateRemoteOrigin(bound.sealed!.url, { allowInsecureLoopback: true }), await openRootCredential(bound.sealed!, vendor), authStrategyFor(vendor), getPack(vendor)?.hosts, { custody: rootCustody(bound.sealed!, vendor), vendor });
322
358
  // an unsealed root's World is where its state resolves (the root given, PROJECT_ROOT, or the working directory)
323
359
  const checks = opts.checks ?? (await loadChecks(bound.sealed ? worldRootOf(bound.sealed) : worldPaths(service, root).root));
324
- const deployable = deployableEntries(service, root);
325
- const entries = opts.actionIds === undefined ? deployable : opts.actionIds.map((id) => deployable.find((a) => a.id === id)).filter((a): a is TwinAction => a !== undefined);
326
- const report: DeployReport = { pushed: 0, deployed: [], skipped: [], replayed: [] };
360
+ const settling = settlingEntries(service, root);
361
+ const pick = (list: TwinAction[]): TwinAction[] => (opts.actionIds === undefined ? list : opts.actionIds.map((id) => list.find((a) => a.id === id)).filter((a): a is TwinAction => a !== undefined));
362
+ const report: DeployReport = { pushed: 0, deployed: [], skipped: [], replayed: [], worldOnly: pick(settling.worldOnly).map((e) => e.id) };
327
363
  if (opts.actionIds) {
328
364
  const settled = receiptsByEntry(parentEntries(service, root));
329
365
  for (const id of opts.actionIds) if (settled.get(id)?.status === 'deployed') report.replayed.push(id);
330
366
  }
367
+ const settle = (action: TwinAction, at: string, receipt: Partial<Receipt>, fields: Record<string, unknown> = action.fields ?? {}, vendorSubjectId?: string): void => {
368
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields, occurredAt: at, ...(vendorSubjectId ? { vendorSubjectId } : {}), receipt, ...(root !== undefined ? { root } : {}) });
369
+ };
370
+ // the subjects the vendor refused and never took: a request naming one is the World's alone, never sent (the
371
+ // real-system adapters, "Perform" 3), settled as it landed here
372
+ const parent = parentEntries(service, root);
373
+ const taken = new Set<string>(); const refusedIds = new Set<string>();
374
+ for (const e of parent) { if (e.receipt?.status === 'deployed') taken.add(e.aliasOf ?? e.subject.id); else if (e.receipt?.status === 'refused') refusedIds.add(e.subject.id); }
375
+ for (const id of taken) refusedIds.delete(id);
376
+ const named = (action: TwinAction): string[] => { const out: string[] = []; const walk = (v: unknown): void => { if (typeof v === 'string') out.push(v); else if (v && typeof v === 'object') for (const x of Object.values(v)) walk(x); }; const i = (action.input ?? {}) as Record<string, unknown>; walk(i.path); walk(i.query); walk(i.body); return out; };
331
377
  let stopped = false;
332
- for (const action of entries) {
333
- if (stopped) { report.skipped.push(action.id); continue; }
378
+ for (const group of groupsOf(pick(settling.offered))) {
379
+ if (stopped) { report.skipped.push(...group.map((a) => a.id)); continue; }
380
+ if (named(group[0]!).some((v) => refusedIds.has(v))) {
381
+ const at = opts.at ?? worldNow();
382
+ for (const action of group) { settle(action, at, { status: 'landed', reason: 'names a subject the vendor refused: the World\'s alone' }); refusedIds.add(action.subject.id); }
383
+ report.worldOnly.push(...group.map((a) => a.id));
384
+ continue;
385
+ }
334
386
  const at = opts.at ?? worldNow();
335
- const verdict = await runChecks(checks, action, readTree(service, root));
387
+ // checks in front of every entry: one refused refuses the group, which is one call
388
+ let verdict: Awaited<ReturnType<typeof runChecks>> = null;
389
+ for (const action of group) { verdict = await runChecks(checks, action, readTree(service, root)); if (verdict) break; }
336
390
  if (verdict) {
337
- confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'refused', reason: `${verdict.name}: ${verdict.reason}` }, ...(root !== undefined ? { root } : {}) });
338
- report.refused = { actionId: action.id, check: verdict.name, reason: verdict.reason };
391
+ for (const action of group) settle(action, at, { status: 'refused', reason: `${verdict.name}: ${verdict.reason}` });
392
+ report.refused = { actionId: group[0]!.id, check: verdict.name, reason: verdict.reason };
339
393
  stopped = true;
340
394
  continue;
341
395
  }
396
+ // a declared reference to an adopted id is resolved before the pack sees the entry: the vendor is
397
+ // asked about #7, never the local #3 (docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary)
398
+ const aliases = aliasesFrom(parentEntries(service, root));
399
+ const resolved = group.map((action) => { const fields = resolveReferences(service, action.subject.type, action.fields ?? {}, aliases); return fields === action.fields ? action : { ...action, fields }; });
342
400
  try {
343
- // a declared reference to an adopted id is resolved before the pack sees the entry: the vendor is
344
- // asked about #7, never the local #3 (docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary)
345
- const fields = resolveReferences(service, action.subject.type, action.fields ?? {}, aliasesFrom(parentEntries(service, root)));
346
- const outcome = await adapters.perform(execute, fields === action.fields ? action : { ...action, fields }, performContext(service, root, bound.sealed ? rootCredentialFingerprint(bound.sealed) : undefined));
347
- // the vendor's answer, when the adapter returned one as fields, is what the landed copy holds
401
+ const outcome = await adapters.perform(execute, resolved, performContext(service, root, bound.sealed ? rootCredentialFingerprint(bound.sealed) : undefined));
402
+ // the vendor's answer, when the adapter returned one as fields, is what the primary entry's landed copy holds
348
403
  const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? (outcome.data as Record<string, unknown>) : {};
349
- confirmAction({
350
- service, actionId: action.id, subject: action.subject, fields: { ...fields, ...answered }, occurredAt: at,
351
- ...(outcome.externalId ? { vendorSubjectId: outcome.externalId } : {}),
352
- receipt: { status: 'deployed', ...(outcome.url ? { url: outcome.url } : {}) },
353
- ...(root !== undefined ? { root } : {}),
354
- });
355
- report.pushed += 1;
356
- report.deployed.push({ actionId: action.id, at, ...(outcome.externalId ? { externalId: outcome.externalId } : {}), ...(outcome.data !== undefined ? { data: outcome.data } : {}) });
404
+ const primary = resolved.find((a) => a.id === outcome.actionId) ?? resolved[0]!;
405
+ const also = new Map((outcome.also ?? []).map((a) => [a.actionId, a.externalId]));
406
+ for (const action of resolved) {
407
+ const externalId = action === primary ? outcome.externalId : also.get(action.id);
408
+ settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, action === primary ? { ...action.fields, ...answered } : action.fields ?? {}, externalId || undefined);
409
+ report.pushed += 1;
410
+ report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(action === primary && outcome.data !== undefined ? { data: outcome.data } : {}) });
411
+ }
357
412
  } catch (error) {
358
- // a write the pack itself refuses (it can never be sent: its subject never reached the vendor) is
359
- // settled as refused, as a check's refusal is, so the next deploy moves on to the entry after it;
360
- // anything else failed and stays deployable for a retry
413
+ // a write the vendor refuses (a 4xx: it can never be sent as it is) settles the group refused, as a check's
414
+ // refusal does, so the next deploy moves on to the group after it; anything else failed and stays offered
361
415
  if (error instanceof RefusedWriteError) {
362
- confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'refused', reason: error.message }, ...(root !== undefined ? { root } : {}) });
363
- report.refused = { actionId: action.id, check: error.check, reason: error.reason };
416
+ for (const action of group) { settle(action, at, { status: 'refused', reason: error.message }); refusedIds.add(action.subject.id); }
417
+ report.refused = { actionId: group[0]!.id, check: error.check, reason: error.reason };
364
418
  stopped = true;
365
419
  continue;
366
420
  }
367
421
  const message = error instanceof Error ? error.message : String(error);
368
- confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'failed', reason: message }, ...(root !== undefined ? { root } : {}) });
369
- report.failed = { actionId: action.id, error: message };
422
+ for (const action of group) settle(action, at, { status: 'failed', reason: message });
423
+ report.failed = { actionId: group[0]!.id, error: message };
370
424
  stopped = true;
371
425
  }
372
426
  }
@@ -379,6 +433,10 @@ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<D
379
433
  * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
380
434
  */
381
435
  export async function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{ performed: boolean; externalId?: string; data?: unknown }> {
436
+ // a pack fetch performs its request's entries as one group after the handler (performRequestGroup)
437
+ const request = currentCorrelationId();
438
+ const mark = request !== undefined ? deferred.get(request) : undefined;
439
+ if (mark) { mark.appended = true; return { performed: false }; }
382
440
  const head = headOf(service, root);
383
441
  if (head.kind !== 'real' || head.deploy !== 'auto') return { performed: false };
384
442
  let report: DeployReport;
@@ -394,6 +452,39 @@ export async function performAtHead(service: string, action: Pick<TwinAction, 'i
394
452
  return { performed: done !== undefined, ...(done?.externalId ? { externalId: done.externalId } : {}), ...(done?.data !== undefined ? { data: done.data } : {}) };
395
453
  }
396
454
 
455
+ /** The requests a pack fetch is answering, whose entries it performs as one group when the handler returns, and
456
+ * whether each has appended any. */
457
+ const deferred = new Map<string, { appended: boolean }>();
458
+ /** Run a pack fetch's handling of one request with its writes' perform deferred to the group (`performRequestGroup`):
459
+ * its answer, and whether it appended (a request that wrote nothing has no group, and consults no head). */
460
+ export async function deferPerform<T>(request: string, fn: () => Promise<T>): Promise<{ value: T; appended: boolean }> {
461
+ const mark = { appended: false };
462
+ deferred.set(request, mark);
463
+ try { return { value: await fn(), appended: mark.appended }; } finally { deferred.delete(request); }
464
+ }
465
+ /** Under `auto`, perform the entries one request made as one call (the real-system adapters, "Perform"): the ids the
466
+ * vendor gave them by local id, or the vendor's refusal or failure, the request's entries reverted as performAtHead
467
+ * reverts one. Nothing when the head is not real and auto, or the request wrote nothing the vendor is offered. */
468
+ export async function performRequestGroup(service: string, request: string, root?: string): Promise<{ ids: Map<string, string> } | RefusedWriteError | VendorWriteError> {
469
+ const head = headOf(service, root);
470
+ const ids = new Map<string, string>();
471
+ if (head.kind !== 'real' || head.deploy !== 'auto') return { ids };
472
+ const group = deployableEntries(service, root).filter((e) => e.correlationId === request);
473
+ if (!group.length) return { ids };
474
+ let report: DeployReport;
475
+ try { report = await performEntries({ service, actionIds: group.map((e) => e.id), ...(root !== undefined ? { root } : {}) }); }
476
+ catch (error) { throw error instanceof HeadError ? error : new HeadError(error instanceof Error ? error.message : String(error)); }
477
+ if (report.refused || report.failed) {
478
+ for (const e of [...group].reverse()) revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
479
+ return report.refused ? new RefusedWriteError(report.refused.check, report.refused.reason, report.refused.actionId) : new VendorWriteError(report.failed!.error, report.failed!.actionId);
480
+ }
481
+ for (const d of report.deployed) {
482
+ const e = group.find((g) => g.id === d.actionId);
483
+ if (e && d.externalId && d.externalId !== e.subject.id) ids.set(e.subject.id, d.externalId);
484
+ }
485
+ return { ids };
486
+ }
487
+
397
488
  /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
398
489
  * vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
399
490
  * would have said. Anything else propagates. */
package/src/history.ts CHANGED
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
4
  import { resolveReferences } from './references.ts';
5
5
  import { canonicalJson } from './hash.ts';
6
- import { canonicalStatePath, checkParent, commitParentPin, pinParent, stateGeneration, withAncestryLock } from './ancestry.ts';
6
+ import { canonicalStatePath, checkParent, commitParentPin, pinParent, stateGeneration, withAncestryLock, withHistoryLock } from './ancestry.ts';
7
7
  import { getActiveWorldStore } from './world-store.ts';
8
8
  import { worldPaths } from './storage.ts';
9
9
  import { aliasesFrom, foldHistory, branchEntries, isUrlParent, readBranchMeta, toEntry, type Entry } from './log.ts';
@@ -114,9 +114,9 @@ export function inheritedHistory(service: string, root?: string): HistoryView |
114
114
  if (!isUrlParent(parent.at)) checkParent(worldPaths(service, parent.at).dir, parent.generation);
115
115
  return historyPrefix(readHistoryView(directory, parent.view), parent.position);
116
116
  }
117
- /** Capture under the same coordinator as append and complete observation batches. */
117
+ /** Capture under the directory's history lock, as appends and complete observation batches are made. */
118
118
  export function captureHistory(service: string, root?: string): { view: string; position: number; descriptor: HistoryView } {
119
- return withAncestryLock(() => {
119
+ return withHistoryLock(worldPaths(service, root).dir, () => {
120
120
  const directory = canonicalStatePath(worldPaths(service, root).dir);
121
121
  getActiveWorldStore().mkdir(directory);
122
122
  if (!getActiveWorldStore().exists(join(directory, 'history-format.json'))) getActiveWorldStore().writeAtomic(join(directory, 'history-format.json'), '{"version":1}');
@@ -137,7 +137,7 @@ export function originHead(service: string, root?: string): { view: string; remo
137
137
  }
138
138
  /** A complete remote view shares cached rows by content, never by mutable offsets or entry ID. */
139
139
  export function publishOriginHistory(service: string, root: string, remoteView: string, layout: HistoryLayout, entries: Entry[]): { view: string; appended: number } {
140
- return withAncestryLock(() => {
140
+ return withHistoryLock(worldPaths(service, root).dir, () => {
141
141
  if (entries.length !== historyLength(layout)) throw new Error('Incomplete origin history view');
142
142
  const directory = canonicalStatePath(worldPaths(service, root).dir); const store = getActiveWorldStore(); store.mkdir(directory);
143
143
  if (!store.exists(join(directory, 'history-format.json'))) store.writeAtomic(join(directory, 'history-format.json'), '{"version":1}');
@@ -160,7 +160,7 @@ export function publishOriginHistory(service: string, root: string, remoteView:
160
160
 
161
161
  /** A changeset binds the complete inherited view, including this root's observations. */
162
162
  export function captureParentHistory(service: string, root?: string): HistoryReference {
163
- return withAncestryLock(() => {
163
+ return withHistoryLock(worldPaths(service, root).dir, () => {
164
164
  const head = captureHistory(service, root);
165
165
  const position = head.position - head.descriptor.layout.branch;
166
166
  return { view: saveHistoryView(worldPaths(service, root).dir, historyPrefix(head.descriptor, position)), position };
@@ -186,7 +186,7 @@ export function historyChanges(service: string, before: HistoryView, after: Hist
186
186
 
187
187
  /** A timestamp can select across layers; represent it as a view, never a misleading offset. */
188
188
  export function historyAtInstant(service: string, instant: string, root?: string, selectedView?: string): HistoryReference {
189
- return withAncestryLock(() => {
189
+ return withHistoryLock(worldPaths(service, root).dir, () => {
190
190
  const stamp = Date.parse(instant); if (!Number.isFinite(stamp)) throw new Error('Invalid history instant');
191
191
  const directory = worldPaths(service, root).dir;
192
192
  const source = selectedView ? readHistoryView(directory, selectedView) : captureHistory(service, root).descriptor;
package/src/hpack.json ADDED
@@ -0,0 +1 @@
1
+ {"source":"RFC 7541 Appendix A and B","staticTable":[[":authority",""],[":method","GET"],[":method","POST"],[":path","/"],[":path","/index.html"],[":scheme","http"],[":scheme","https"],[":status","200"],[":status","204"],[":status","206"],[":status","304"],[":status","400"],[":status","404"],[":status","500"],["accept-charset",""],["accept-encoding","gzip, deflate"],["accept-language",""],["accept-ranges",""],["accept",""],["access-control-allow-origin",""],["age",""],["allow",""],["authorization",""],["cache-control",""],["content-disposition",""],["content-encoding",""],["content-language",""],["content-length",""],["content-location",""],["content-range",""],["content-type",""],["cookie",""],["date",""],["etag",""],["expect",""],["expires",""],["from",""],["host",""],["if-match",""],["if-modified-since",""],["if-none-match",""],["if-range",""],["if-unmodified-since",""],["last-modified",""],["link",""],["location",""],["max-forwards",""],["proxy-authenticate",""],["proxy-authorization",""],["range",""],["referer",""],["refresh",""],["retry-after",""],["server",""],["set-cookie",""],["strict-transport-security",""],["transfer-encoding",""],["user-agent",""],["vary",""],["via",""],["www-authenticate",""]],"huffman":[[8184,13],[8388568,23],[268435426,28],[268435427,28],[268435428,28],[268435429,28],[268435430,28],[268435431,28],[268435432,28],[16777194,24],[1073741820,30],[268435433,28],[268435434,28],[1073741821,30],[268435435,28],[268435436,28],[268435437,28],[268435438,28],[268435439,28],[268435440,28],[268435441,28],[268435442,28],[1073741822,30],[268435443,28],[268435444,28],[268435445,28],[268435446,28],[268435447,28],[268435448,28],[268435449,28],[268435450,28],[268435451,28],[20,6],[1016,10],[1017,10],[4090,12],[8185,13],[21,6],[248,8],[2042,11],[1018,10],[1019,10],[249,8],[2043,11],[250,8],[22,6],[23,6],[24,6],[0,5],[1,5],[2,5],[25,6],[26,6],[27,6],[28,6],[29,6],[30,6],[31,6],[92,7],[251,8],[32764,15],[32,6],[4091,12],[1020,10],[8186,13],[33,6],[93,7],[94,7],[95,7],[96,7],[97,7],[98,7],[99,7],[100,7],[101,7],[102,7],[103,7],[104,7],[105,7],[106,7],[107,7],[108,7],[109,7],[110,7],[111,7],[112,7],[113,7],[114,7],[252,8],[115,7],[253,8],[8187,13],[524272,19],[8188,13],[16380,14],[34,6],[32765,15],[3,5],[35,6],[4,5],[36,6],[5,5],[37,6],[38,6],[39,6],[6,5],[116,7],[117,7],[40,6],[41,6],[42,6],[7,5],[43,6],[118,7],[44,6],[8,5],[9,5],[45,6],[119,7],[120,7],[121,7],[122,7],[123,7],[32766,15],[2044,11],[16381,14],[8189,13],[268435452,28],[1048550,20],[4194258,22],[1048551,20],[1048552,20],[4194259,22],[4194260,22],[4194261,22],[8388569,23],[4194262,22],[8388570,23],[8388571,23],[8388572,23],[8388573,23],[8388574,23],[16777195,24],[8388575,23],[16777196,24],[16777197,24],[4194263,22],[8388576,23],[16777198,24],[8388577,23],[8388578,23],[8388579,23],[8388580,23],[2097116,21],[4194264,22],[8388581,23],[4194265,22],[8388582,23],[8388583,23],[16777199,24],[4194266,22],[2097117,21],[1048553,20],[4194267,22],[4194268,22],[8388584,23],[8388585,23],[2097118,21],[8388586,23],[4194269,22],[4194270,22],[16777200,24],[2097119,21],[4194271,22],[8388587,23],[8388588,23],[2097120,21],[2097121,21],[4194272,22],[2097122,21],[8388589,23],[4194273,22],[8388590,23],[8388591,23],[1048554,20],[4194274,22],[4194275,22],[4194276,22],[8388592,23],[4194277,22],[4194278,22],[8388593,23],[67108832,26],[67108833,26],[1048555,20],[524273,19],[4194279,22],[8388594,23],[4194280,22],[33554412,25],[67108834,26],[67108835,26],[67108836,26],[134217694,27],[134217695,27],[67108837,26],[16777201,24],[33554413,25],[524274,19],[2097123,21],[67108838,26],[134217696,27],[134217697,27],[67108839,26],[134217698,27],[16777202,24],[2097124,21],[2097125,21],[67108840,26],[67108841,26],[268435453,28],[134217699,27],[134217700,27],[134217701,27],[1048556,20],[16777203,24],[1048557,20],[2097126,21],[4194281,22],[2097127,21],[2097128,21],[8388595,23],[4194282,22],[4194283,22],[33554414,25],[33554415,25],[16777204,24],[16777205,24],[67108842,26],[8388596,23],[67108843,26],[134217702,27],[67108844,26],[67108845,26],[134217703,27],[134217704,27],[134217705,27],[134217706,27],[134217707,27],[268435454,28],[134217708,27],[134217709,27],[134217710,27],[134217711,27],[134217712,27],[67108846,26],[1073741823,30]]}