@volter/world-core 2.0.12 → 2.0.14

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.
@@ -163,8 +163,7 @@ export type DerivedManifest = {
163
163
  template: string;
164
164
  acceptProvided?: boolean;
165
165
  };
166
- /** operations the derived core must not claim though their resource is declared: served by
167
- * the pack's existing code, or a gap, until someone models them */
166
+ /** operations the derived core must not claim though their resource is declared: a gap until someone models them */
168
167
  unmodeled?: string[];
169
168
  /** operations that only read though nothing in the spec says so (a POST that returns data):
170
169
  * a read-only twin answers them */
@@ -297,6 +296,9 @@ export type DerivedManifest = {
297
296
  root?: string;
298
297
  occurredAt: string;
299
298
  request: Request;
299
+ /** the writing call's context, narrow as a handler's, at the write's moment: what the hook reads to render the event
300
+ * it sends (until `declared-semantics` makes events data the kernel renders) */
301
+ context(): Promise<WriteHookContext>;
300
302
  }) => Promise<void>;
301
303
  };
302
304
  /** A form body's bracket notation (`a[b][0][c]=v`, `expand[]=x`) as nested objects and arrays. A form carries only
@@ -429,12 +431,31 @@ export type SemanticsContext = {
429
431
  expand(resource: string, body: Record<string, unknown>): Record<string, unknown>;
430
432
  /** This context at another moment of the World clock (ISO): a move time makes is written at the moment it fell due. */
431
433
  at(when: string): Promise<SemanticsContext>;
434
+ /** This call's context over another manifest of the same service, at the same moment: a lane over its vendor's
435
+ * state reads and writes the vendor's resources through the vendor's manifest ("Pack layout"). */
436
+ over(other: DerivedManifest): Promise<SemanticsContext>;
432
437
  /** The generic core's answer to this call, or undefined where the core does not serve it. */
433
438
  core(): Promise<Response | undefined>;
434
439
  /** A stored row's own fields, as the vendor's object carries them (an `id`, `type` or `updatedAt` of its own restored). */
435
440
  own(row: Record<string, unknown>): Record<string, unknown>;
436
441
  };
437
442
  export type Semantics = (ctx: SemanticsContext) => Promise<Response>;
443
+ /** The context a derived pack's handler is given ("What an author writes, and how"): the kernel's own members
444
+ * (`now`, `core`, `atomically`, `tree`, `writeDetailed`) and the tree's location (`root`) are not on it, so a handler
445
+ * typed `Handler` cannot reach them. Its reads are those the contract lists (`row`, `rowsRaw`, `rows`, `get`,
446
+ * `history`, `resolve`, `expand`); `at` and `over` give the same narrow context at another moment or over the
447
+ * vendor's manifest. */
448
+ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically' | 'tree' | 'writeDetailed' | 'root' | 'at' | 'over'> & {
449
+ /** This context at another moment of the World clock (ISO): a move time makes is written at the moment it fell due. */
450
+ at(when: string): Promise<HandlerContext>;
451
+ /** This call's context over the manifest of the vendor whose state a lane shares, at the same moment. */
452
+ over(other: DerivedManifest): Promise<HandlerContext>;
453
+ };
454
+ /** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
455
+ * cannot write again (and run itself again). */
456
+ export type WriteHookContext = Omit<HandlerContext, 'write' | 'record' | 'at' | 'over'>;
457
+ /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
458
+ export type Handler = (ctx: HandlerContext) => Promise<Response>;
438
459
  /** A pack's semantics handlers as the dispatch's handlers. */
439
460
  export declare function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope?: CoreScope): Record<string, DerivedHandler>;
440
461
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
@@ -311,7 +311,7 @@ async function writeDetailed(m, call, resource, id, fields, operation, params, r
311
311
  { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { operationId: call.operation.id, params: call.params, body: params }, occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) } }, root);
312
312
  const body = view(m, resource, row);
313
313
  if (m.onWrite)
314
- await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
314
+ await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request, context: () => contextFor(m, { ...call, request: call.request.clone() }, { ...(root !== undefined ? { root } : {}), clock: () => occurredAt }) });
315
315
  // the subject's id as it stands after the write: the vendor's, when a live head minted one
316
316
  return { body, id: result.externalId ?? row.id, ...(result.vendorData !== undefined ? { vendorData: result.vendorData } : {}) };
317
317
  }
@@ -428,7 +428,8 @@ export async function serveCore(m, call, scope = {}) {
428
428
  if (!resource || !decl)
429
429
  return { unmodeled: `no manifest entry for resource ${resource ?? '(none)'}` };
430
430
  call = adoptedPath(m, call, root);
431
- const params = await boundaryParams(m, call, root);
431
+ // the body is read from a copy, so the call's own request is still unread for the write hook's context
432
+ const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
432
433
  const idParam = subjectOf(m, resource, call);
433
434
  const orphan = missingParent(m, resource, call, root);
434
435
  if (orphan)
@@ -601,6 +602,7 @@ export function sse(m, events, operationId) {
601
602
  }
602
603
  async function contextFor(m, call, scope) {
603
604
  const root = scope.root;
605
+ const asAsked = call;
604
606
  call = adoptedPath(m, call, root);
605
607
  const at = (scope.clock ?? worldNow)();
606
608
  const text = await call.request.clone().text().catch(() => '');
@@ -680,6 +682,7 @@ async function contextFor(m, call, scope) {
680
682
  sse: (events) => sse(m, events, call.operation.id),
681
683
  expand: (resource, body) => (paths.length ? expand(m, resource, body, paths, root) : body),
682
684
  at: (when) => contextFor(m, { ...call, request: new Request(call.request.url) }, { ...scope, clock: () => when }),
685
+ over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
683
686
  core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
684
687
  own: (row) => ownFields(row),
685
688
  };
@@ -54,10 +54,8 @@ export type DerivedFetchOptions = {
54
54
  };
55
55
  /** An RPC wire: every method is reached by any HTTP method (Slack's clients POST reads too). */
56
56
  anyMethod?: boolean;
57
- /** The pack's existing fetch while it moves: everything no handler or core owns, and what the core cannot model. */
58
- legacy?: (request: Request) => Promise<Response>;
59
57
  /** What every operation a handler or the core serves passes through (read-only refusal, version
60
- * checks, idempotent replay); the pack's existing fetch keeps its own while it moves. */
58
+ * checks, idempotent replay). */
61
59
  around?: (call: DerivedCall, next: () => Promise<Response>) => Promise<Response>;
62
60
  /** The vendor's answer for an operation nothing models: the pack's gap. */
63
61
  gap: (request: Request, operation: DerivedOperation | undefined, reason: string) => Response;
@@ -74,7 +72,7 @@ export declare function matchOperation(routes: Map<string, Route[]>, method: str
74
72
  operation: DerivedOperation;
75
73
  params: Record<string, string>;
76
74
  } | undefined;
77
- export type DerivedOwner = 'handler' | 'core' | 'legacy' | 'gap';
75
+ export type DerivedOwner = 'handler' | 'core' | 'gap';
78
76
  export type DerivedFetch = ((request: Request) => Promise<Response>) & {
79
77
  /** Per operationId, who serves it: a semantics handler, the derived core, the pack's existing
80
78
  * fetch while it moves, or nothing (the vendor's gap). */
@@ -85,7 +85,7 @@ export function createDerivedFetch(options) {
85
85
  if (unknown.length)
86
86
  throw new Error(`derived dispatch: handlers name operations the surface does not have: ${unknown.sort().join(', ')}`);
87
87
  const routes = compileSurface(options.surface);
88
- const fallback = (request, operation, reason) => options.legacy ? options.legacy(request) : Promise.resolve(options.gap(request, operation, reason));
88
+ const fallback = (request, operation, reason) => Promise.resolve(options.gap(request, operation, reason));
89
89
  // a valid incoming W3C traceparent scopes the dispatch, so every entry it appends records it (trace-context.ts)
90
90
  const fetch = ((request) => runWithRequestTrace(request, () => dispatch(request)));
91
91
  const dispatch = async (request) => {
@@ -120,6 +120,6 @@ export function createDerivedFetch(options) {
120
120
  }
121
121
  return fallback(request, matched.operation, 'no handler');
122
122
  };
123
- fetch.owners = () => Object.fromEntries(options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : options.legacy ? 'legacy' : 'gap']));
123
+ fetch.owners = () => Object.fromEntries(options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : 'gap']));
124
124
  return fetch;
125
125
  }
@@ -10,7 +10,7 @@ export { currentTraceparent, deliveryTraceHeaders, parseTraceparent, runWithRequ
10
10
  export type { Traceparent } from './trace-context.js';
11
11
  export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface } from './derived.js';
12
12
  export { bindSemantics, coreFor, crossCutting, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.js';
13
- export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, ResourceDecl, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver } from './derived-core.js';
13
+ export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, Handler, HandlerContext, ResourceDecl, WriteHookContext, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver } from './derived-core.js';
14
14
  export type { RemoteExecute, RemoteExecuteRequest, RemoteExecuteResponse } from './remote-execute.js';
15
15
  export type { TwinFetchAdapterConfig, TwinFetchHandlerRequest, TwinFetchHandlerResult, TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.js';
16
16
  export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -147,8 +147,7 @@ export type DerivedManifest = {
147
147
  * (`{prefix}_{n}`), or is `{uuid}` for a vendor whose ids are UUIDs: the next is derived from the resource and its
148
148
  * count, so a World mints the same ids every run */
149
149
  ids: { template: string; acceptProvided?: boolean };
150
- /** operations the derived core must not claim though their resource is declared: served by
151
- * the pack's existing code, or a gap, until someone models them */
150
+ /** operations the derived core must not claim though their resource is declared: a gap until someone models them */
152
151
  unmodeled?: string[];
153
152
  /** operations that only read though nothing in the spec says so (a POST that returns data):
154
153
  * a read-only twin answers them */
@@ -227,7 +226,12 @@ export type DerivedManifest = {
227
226
  /** the vendor's screens this pack serves or owes (demand decides which exist) */
228
227
  screens?: ScreenDecl[];
229
228
  /** called after every stored write with the rendered resource, its stored type and the kernel operation name */
230
- onWrite?: (write: { operation: string; storedType: string; body: Record<string, unknown>; root?: string; occurredAt: string; request: Request }) => Promise<void>;
229
+ onWrite?: (write: {
230
+ operation: string; storedType: string; body: Record<string, unknown>; root?: string; occurredAt: string; request: Request;
231
+ /** the writing call's context, narrow as a handler's, at the write's moment: what the hook reads to render the event
232
+ * it sends (until `declared-semantics` makes events data the kernel renders) */
233
+ context(): Promise<WriteHookContext>;
234
+ }) => Promise<void>;
231
235
  };
232
236
 
233
237
  // ── request parsing ──────────────────────────────────────────────────────────────────────────
@@ -505,7 +509,7 @@ async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: st
505
509
  root,
506
510
  );
507
511
  const body = view(m, resource, row);
508
- if (m.onWrite) await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
512
+ if (m.onWrite) await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request, context: () => contextFor(m, { ...call, request: call.request.clone() }, { ...(root !== undefined ? { root } : {}), clock: () => occurredAt }) });
509
513
  // the subject's id as it stands after the write: the vendor's, when a live head minted one
510
514
  return { body, id: result.externalId ?? row.id, ...(result.vendorData !== undefined ? { vendorData: result.vendorData } : {}) };
511
515
  }
@@ -621,7 +625,8 @@ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: Co
621
625
  const decl = resource ? m.resources[resource] : undefined;
622
626
  if (!resource || !decl) return { unmodeled: `no manifest entry for resource ${resource ?? '(none)'}` };
623
627
  call = adoptedPath(m, call, root);
624
- const params = await boundaryParams(m, call, root);
628
+ // the body is read from a copy, so the call's own request is still unread for the write hook's context
629
+ const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
625
630
  const idParam = subjectOf(m, resource, call);
626
631
  const orphan = missingParent(m, resource, call, root);
627
632
  if (orphan) return { served: orphan };
@@ -835,6 +840,9 @@ export type SemanticsContext = {
835
840
  expand(resource: string, body: Record<string, unknown>): Record<string, unknown>;
836
841
  /** This context at another moment of the World clock (ISO): a move time makes is written at the moment it fell due. */
837
842
  at(when: string): Promise<SemanticsContext>;
843
+ /** This call's context over another manifest of the same service, at the same moment: a lane over its vendor's
844
+ * state reads and writes the vendor's resources through the vendor's manifest ("Pack layout"). */
845
+ over(other: DerivedManifest): Promise<SemanticsContext>;
838
846
  /** The generic core's answer to this call, or undefined where the core does not serve it. */
839
847
  core(): Promise<Response | undefined>;
840
848
  /** A stored row's own fields, as the vendor's object carries them (an `id`, `type` or `updatedAt` of its own restored). */
@@ -842,8 +850,26 @@ export type SemanticsContext = {
842
850
  };
843
851
  export type Semantics = (ctx: SemanticsContext) => Promise<Response>;
844
852
 
853
+ /** The context a derived pack's handler is given ("What an author writes, and how"): the kernel's own members
854
+ * (`now`, `core`, `atomically`, `tree`, `writeDetailed`) and the tree's location (`root`) are not on it, so a handler
855
+ * typed `Handler` cannot reach them. Its reads are those the contract lists (`row`, `rowsRaw`, `rows`, `get`,
856
+ * `history`, `resolve`, `expand`); `at` and `over` give the same narrow context at another moment or over the
857
+ * vendor's manifest. */
858
+ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically' | 'tree' | 'writeDetailed' | 'root' | 'at' | 'over'> & {
859
+ /** This context at another moment of the World clock (ISO): a move time makes is written at the moment it fell due. */
860
+ at(when: string): Promise<HandlerContext>;
861
+ /** This call's context over the manifest of the vendor whose state a lane shares, at the same moment. */
862
+ over(other: DerivedManifest): Promise<HandlerContext>;
863
+ };
864
+ /** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
865
+ * cannot write again (and run itself again). */
866
+ export type WriteHookContext = Omit<HandlerContext, 'write' | 'record' | 'at' | 'over'>;
867
+ /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
868
+ export type Handler = (ctx: HandlerContext) => Promise<Response>;
869
+
845
870
  async function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext> {
846
871
  const root = scope.root;
872
+ const asAsked = call;
847
873
  call = adoptedPath(m, call, root);
848
874
  const at = (scope.clock ?? worldNow)();
849
875
  const text = await call.request.clone().text().catch(() => '');
@@ -916,6 +942,7 @@ async function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScop
916
942
  sse: (events) => sse(m, events, call.operation.id),
917
943
  expand: (resource, body) => (paths.length ? expand(m, resource, body, paths, root) : body),
918
944
  at: (when) => contextFor(m, { ...call, request: new Request(call.request.url) }, { ...scope, clock: () => when }),
945
+ over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
919
946
  core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
920
947
  own: (row) => ownFields(row as unknown as TwinResource),
921
948
  };
package/src/derived.ts CHANGED
@@ -46,10 +46,8 @@ export type DerivedFetchOptions = {
46
46
  core?: { owns: (operation: DerivedOperation) => boolean; serve: (call: DerivedCall) => Promise<DerivedCoreOutcome> };
47
47
  /** An RPC wire: every method is reached by any HTTP method (Slack's clients POST reads too). */
48
48
  anyMethod?: boolean;
49
- /** The pack's existing fetch while it moves: everything no handler or core owns, and what the core cannot model. */
50
- legacy?: (request: Request) => Promise<Response>;
51
49
  /** What every operation a handler or the core serves passes through (read-only refusal, version
52
- * checks, idempotent replay); the pack's existing fetch keeps its own while it moves. */
50
+ * checks, idempotent replay). */
53
51
  around?: (call: DerivedCall, next: () => Promise<Response>) => Promise<Response>;
54
52
  /** The vendor's answer for an operation nothing models: the pack's gap. */
55
53
  gap: (request: Request, operation: DerivedOperation | undefined, reason: string) => Response;
@@ -126,7 +124,7 @@ export function matchOperation(routes: Map<string, Route[]>, method: string, pat
126
124
  return undefined;
127
125
  }
128
126
 
129
- export type DerivedOwner = 'handler' | 'core' | 'legacy' | 'gap';
127
+ export type DerivedOwner = 'handler' | 'core' | 'gap';
130
128
 
131
129
  export type DerivedFetch = ((request: Request) => Promise<Response>) & {
132
130
  /** Per operationId, who serves it: a semantics handler, the derived core, the pack's existing
@@ -141,7 +139,7 @@ export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
141
139
  if (unknown.length) throw new Error(`derived dispatch: handlers name operations the surface does not have: ${unknown.sort().join(', ')}`);
142
140
  const routes = compileSurface(options.surface);
143
141
  const fallback = (request: Request, operation: DerivedOperation | undefined, reason: string): Promise<Response> =>
144
- options.legacy ? options.legacy(request) : Promise.resolve(options.gap(request, operation, reason));
142
+ Promise.resolve(options.gap(request, operation, reason));
145
143
  // a valid incoming W3C traceparent scopes the dispatch, so every entry it appends records it (trace-context.ts)
146
144
  const fetch = ((request: Request): Promise<Response> => runWithRequestTrace(request, () => dispatch(request))) as DerivedFetch;
147
145
  const dispatch = async (request: Request): Promise<Response> => {
@@ -173,7 +171,7 @@ export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
173
171
  };
174
172
  fetch.owners = () =>
175
173
  Object.fromEntries(
176
- options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : options.legacy ? 'legacy' : 'gap'] as const),
174
+ options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : 'gap'] as const),
177
175
  );
178
176
  return fetch;
179
177
  }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ export { currentTraceparent, deliveryTraceHeaders, parseTraceparent, runWithRequ
22
22
  export type { Traceparent } from './trace-context.ts';
23
23
  export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface } from './derived.ts';
24
24
  export { bindSemantics, coreFor, crossCutting, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.ts';
25
- export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, ResourceDecl, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver } from './derived-core.ts';
25
+ export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, Handler, HandlerContext, ResourceDecl, WriteHookContext, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver } from './derived-core.ts';
26
26
  export type { RemoteExecute, RemoteExecuteRequest, RemoteExecuteResponse } from './remote-execute.ts';
27
27
  export type { TwinFetchAdapterConfig, TwinFetchHandlerRequest, TwinFetchHandlerResult, TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.ts';
28
28
  export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.ts';