@fuzdev/fuz_app 0.48.0 → 0.50.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 (38) hide show
  1. package/dist/actions/CLAUDE.md +128 -50
  2. package/dist/actions/action_codegen.d.ts +196 -37
  3. package/dist/actions/action_codegen.d.ts.map +1 -1
  4. package/dist/actions/action_codegen.js +297 -59
  5. package/dist/actions/action_registry.d.ts +41 -9
  6. package/dist/actions/action_registry.d.ts.map +1 -1
  7. package/dist/actions/action_registry.js +109 -32
  8. package/dist/actions/action_types.d.ts +2 -2
  9. package/dist/actions/action_types.js +2 -2
  10. package/dist/actions/cancel.d.ts +13 -11
  11. package/dist/actions/cancel.d.ts.map +1 -1
  12. package/dist/actions/cancel.js +13 -11
  13. package/dist/actions/frontend_rpc_client.d.ts +9 -0
  14. package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
  15. package/dist/actions/heartbeat.d.ts +11 -8
  16. package/dist/actions/heartbeat.d.ts.map +1 -1
  17. package/dist/actions/heartbeat.js +11 -8
  18. package/dist/actions/protocol.d.ts +47 -0
  19. package/dist/actions/protocol.d.ts.map +1 -0
  20. package/dist/actions/protocol.js +46 -0
  21. package/dist/actions/register_action_ws.d.ts +4 -3
  22. package/dist/actions/register_action_ws.d.ts.map +1 -1
  23. package/dist/actions/register_action_ws.js +1 -1
  24. package/dist/auth/account_action_specs.d.ts +1 -1
  25. package/dist/auth/account_action_specs.js +1 -1
  26. package/dist/auth/account_actions.d.ts +2 -2
  27. package/dist/auth/account_actions.js +2 -2
  28. package/dist/auth/admin_action_specs.d.ts +1 -1
  29. package/dist/auth/admin_action_specs.js +1 -1
  30. package/dist/auth/admin_actions.d.ts +2 -2
  31. package/dist/auth/admin_actions.js +2 -2
  32. package/dist/auth/permit_offer_action_specs.d.ts +1 -1
  33. package/dist/auth/permit_offer_action_specs.js +1 -1
  34. package/dist/auth/permit_offer_actions.d.ts +1 -1
  35. package/dist/auth/permit_offer_actions.js +1 -1
  36. package/dist/auth/standard_action_specs.d.ts +1 -1
  37. package/dist/auth/standard_action_specs.js +1 -1
  38. package/package.json +1 -1
@@ -2,24 +2,27 @@ import { UnreachableError } from '@fuzdev/fuz_util/error.js';
2
2
  import { zod_get_base_type } from '@fuzdev/fuz_util/zod.js';
3
3
  import { ActionRegistry } from './action_registry.js';
4
4
  /**
5
- * Method names of composable actions exported from fuz_app — `heartbeat` (auth-aware
6
- * client liveness probe) and `cancel` (request-scoped abort signal). Consumers spread
7
- * this list when filtering backend request_response methods so the dispatcher-owned
8
- * composables don't show up in `BackendRequestResponseMethod` / handler maps.
5
+ * Method names of fuz_app's protocol actions — `heartbeat` (auth-aware client
6
+ * liveness probe) and `cancel` (request-scoped abort signal). Consumers spread
7
+ * this list when filtering backend request_response methods so the
8
+ * dispatcher-owned protocol actions don't show up in
9
+ * `BackendRequestResponseMethod` / handler maps. Pairs with `protocol_actions`
10
+ * / `protocol_action_specs` from `actions/protocol.ts` (the runtime bundles).
9
11
  */
10
- export const COMPOSABLE_ACTION_METHODS = ['heartbeat', 'cancel'];
11
- const COMPOSABLE_METHOD_SET = new Set(COMPOSABLE_ACTION_METHODS);
12
+ export const PROTOCOL_ACTION_METHODS = ['heartbeat', 'cancel'];
13
+ const PROTOCOL_METHOD_SET = new Set(PROTOCOL_ACTION_METHODS);
12
14
  /**
13
- * Type predicate for filtering composable methods out of a typed `ActionsApi`
14
- * `method_filter`. Avoids the `(... as never)` cast required to call
15
- * `Array.prototype.includes` on the readonly tuple at narrow string types.
15
+ * Type predicate for filtering protocol-action methods out of a typed
16
+ * `FrontendActionsApi` `method_filter`. Avoids the `(... as never)` cast
17
+ * required to call `Array.prototype.includes` on the readonly tuple at narrow
18
+ * string types.
16
19
  *
17
20
  * @example
18
- * generate_actions_api(specs, imports, {
19
- * method_filter: (s) => !is_composable_action_method(s.method),
21
+ * generate_frontend_actions_api(specs, imports, {
22
+ * method_filter: (s) => !is_protocol_action_method(s.method),
20
23
  * });
21
24
  */
22
- export const is_composable_action_method = (method) => COMPOSABLE_METHOD_SET.has(method);
25
+ export const is_protocol_action_method = (method) => PROTOCOL_METHOD_SET.has(method);
23
26
  /**
24
27
  * Manages imports for generated code, building them on demand.
25
28
  * Automatically optimizes type-only imports to use `import type` syntax.
@@ -312,10 +315,10 @@ export const to_action_spec_identifier = (method) => `${method}_action_spec`;
312
315
  export const to_action_spec_input_identifier = (method) => `${to_action_spec_identifier(method)}.input`;
313
316
  export const to_action_spec_output_identifier = (method) => `${to_action_spec_identifier(method)}.output`;
314
317
  /**
315
- * Generates one method line of the typed `ActionsApi` interface for a single
316
- * spec. Encapsulates the input/options/return-type signature shape so the
317
- * surface evolves in one place when fields like `signal` or `transport_name`
318
- * are added to per-call options.
318
+ * Generates one method line of the typed `FrontendActionsApi` interface for a
319
+ * single spec. Encapsulates the input/options/return-type signature shape so
320
+ * the surface evolves in one place when fields like `signal` or
321
+ * `transport_name` are added to per-call options.
319
322
  *
320
323
  * Async methods (`request_response`, `remote_notification`, async
321
324
  * `local_call`) get an optional second `options?: RpcClientCallOptions` arg
@@ -336,7 +339,7 @@ export const to_action_spec_output_identifier = (method) => `${to_action_spec_id
336
339
  * @param options.sync_returns_value - when true (default), sync local_call
337
340
  * methods return the output value directly; when false they're wrapped in
338
341
  * `Result<{value, error}>` like async methods. Set to `false` if your
339
- * ActionsApi treats every method uniformly.
342
+ * FrontendActionsApi treats every method uniformly.
340
343
  * @returns one line like `foo: (input: ActionInputs['foo'], options?: RpcClientCallOptions) => Promise<Result<...>>;`
341
344
  */
342
345
  export const generate_actions_api_method_signature = (spec, options) => {
@@ -356,6 +359,40 @@ export const generate_actions_api_method_signature = (spec, options) => {
356
359
  : result_return;
357
360
  return `${spec.method}: (${input_param}${options_param}) => ${return_type};`;
358
361
  };
362
+ // --------------------------------------------------------------------------
363
+ // High-level codegen helpers — compose the lower-level primitives above into
364
+ // the literal blocks consumer `*.gen.ts` producers emit. Tier 1 consumers
365
+ // (HTTP-only, e.g. tx) call the value-side helpers; Tier 2 (`TypedActionEvent`-
366
+ // aware, e.g. zzz) also call the typed-event + frontend-handlers helpers.
367
+ //
368
+ // **Multi-source consumers.** Helpers that reference specs at runtime
369
+ // (`generate_action_specs_record`, `generate_action_inputs_outputs`,
370
+ // `generate_backend_actions_api`) default to a single `* as specs from
371
+ // specs_module` namespace import and emit `specs.{method}_action_spec`. Pass
372
+ // `qualify_spec?: (spec) => string` to emit a per-spec qualified identifier
373
+ // (e.g. `admin_specs.account_list_action_spec`) for consumers stitching local
374
+ // specs together with multiple upstream sources (`all_admin_action_specs` /
375
+ // `all_permit_offer_action_specs` / `all_account_action_specs` /
376
+ // `all_self_service_role_action_specs` from fuz_app). When `qualify_spec` is
377
+ // set, the helper does NOT add a `* as specs` import — the consumer manages
378
+ // the multiple `* as ns` imports itself — and `specs_module` is ignored.
379
+ // `create_namespace_qualifier` automates the source-table → qualifier wiring.
380
+ // --------------------------------------------------------------------------
381
+ /**
382
+ * Format a `z.enum([...])` runtime const + matching `z.infer` type alias.
383
+ * Caller is responsible for ensuring `methods` is non-empty (`z.enum([])` is
384
+ * invalid) and registering the `zod` import on the `ImportBuilder`.
385
+ */
386
+ const format_method_enum_block = (name, jsdoc, methods) => {
387
+ const lines = methods.map((m) => `\t'${m}',`).join('\n');
388
+ return `/**
389
+ * ${jsdoc}
390
+ */
391
+ export const ${name} = z.enum([
392
+ ${lines}
393
+ ]);
394
+ export type ${name} = z.infer<typeof ${name}>;`;
395
+ };
359
396
  /** Default emit set — every enum kind. */
360
397
  export const ACTION_METHOD_ENUM_KINDS_ALL = new Set([
361
398
  'all',
@@ -364,14 +401,19 @@ export const ACTION_METHOD_ENUM_KINDS_ALL = new Set([
364
401
  'local_call',
365
402
  'frontend',
366
403
  'backend',
404
+ 'frontend_handled',
405
+ 'backend_handled',
406
+ 'broadcast',
367
407
  ]);
368
408
  /**
369
409
  * Filter `heartbeat` / `cancel` out of `specs` unless the consumer opts back in.
370
- * Composables ship from fuz_app and are spread into every consumer's `actions`
371
- * array at registration time they should not appear in consumer-owned typed
372
- * surfaces (`ActionMethod`, `ActionsApi`, `ActionInputs`, etc.) by default.
410
+ * Protocol actions ship from fuz_app and are spread into every consumer's
411
+ * `actions` array at registration time (via `protocol_actions` from
412
+ * `actions/protocol.ts`) they should not appear in consumer-owned typed
413
+ * surfaces (`ActionMethod`, `FrontendActionsApi`, `ActionInputs`, etc.) by
414
+ * default.
373
415
  */
374
- const filter_composables = (specs, include_composables) => include_composables ? specs : specs.filter((s) => !is_composable_action_method(s.method));
416
+ const filter_protocol_actions = (specs, include_protocol_actions) => include_protocol_actions ? specs : specs.filter((s) => !is_protocol_action_method(s.method));
375
417
  /**
376
418
  * Resolve the per-spec identifier qualifier used by the multi-source helpers
377
419
  * (`generate_action_specs_record`, `generate_action_inputs_outputs`,
@@ -391,24 +433,30 @@ const resolve_spec_qualifier = (imports, options) => {
391
433
  /**
392
434
  * Emit one or more `z.enum([...])` declarations for action method names —
393
435
  * `ActionMethod`, `RequestResponseActionMethod`, `RemoteNotificationActionMethod`,
394
- * `LocalCallActionMethod`, `FrontendActionMethod`, `BackendActionMethod`. Pairs
395
- * each runtime const with a `z.infer` type alias under the same identifier.
436
+ * `LocalCallActionMethod`, `FrontendActionMethod`, `BackendActionMethod`,
437
+ * `FrontendRequestResponseMethod`, `BackendRequestResponseMethod`,
438
+ * `BroadcastActionMethod`. Pairs each runtime const with a `z.infer` type
439
+ * alias under the same identifier.
396
440
  *
397
- * Composable methods (`heartbeat`, `cancel`) are filtered out by default —
398
- * pass `include_composables: true` if a consumer genuinely wants them on
399
- * their typed surface. Empty kinds are skipped so the helper never emits
400
- * `z.enum([])` (zod runtime-throws on that).
441
+ * Protocol-action methods (`heartbeat`, `cancel`) are filtered out by
442
+ * default — pass `include_protocol_actions: true` if a consumer genuinely
443
+ * wants them on their typed surface. Empty kinds are skipped so the helper
444
+ * never emits `z.enum([])` (zod runtime-throws on that).
401
445
  *
402
446
  * Adds `import {z} from 'zod';` to `imports` only when at least one block
403
447
  * is emitted (idempotent).
404
448
  *
405
- * @param options.emit - subset of enums to emit; defaults to all six.
406
- * @param options.include_composables - when true, retains `heartbeat` /
449
+ * For genuinely cross-product enums the discriminator doesn't cover, use
450
+ * `generate_action_method_enum_block` caller owns the predicate, name,
451
+ * and jsdoc.
452
+ *
453
+ * @param options.emit - subset of enums to emit; defaults to all nine.
454
+ * @param options.include_protocol_actions - when true, retains `heartbeat` /
407
455
  * `cancel` in the emitted enums. Default `false`.
408
456
  */
409
457
  export const generate_action_method_enums = (specs, imports, options) => {
410
458
  const emit = options?.emit ?? ACTION_METHOD_ENUM_KINDS_ALL;
411
- const filtered = filter_composables(specs, options?.include_composables);
459
+ const filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
412
460
  const registry = new ActionRegistry([...filtered]);
413
461
  const blocks = [];
414
462
  const emit_block = (kind, name, methods, jsdoc) => {
@@ -418,21 +466,49 @@ export const generate_action_method_enums = (specs, imports, options) => {
418
466
  // Consumers that need a kind to exist should check their spec set, not the helper.
419
467
  if (methods.length === 0)
420
468
  return;
421
- const lines = methods.map((m) => `\t'${m}',`).join('\n');
422
- blocks.push(`/**\n * ${jsdoc}\n */\nexport const ${name} = z.enum([\n${lines}\n]);
423
- export type ${name} = z.infer<typeof ${name}>;`);
469
+ blocks.push(format_method_enum_block(name, jsdoc, methods));
424
470
  };
425
471
  emit_block('all', 'ActionMethod', registry.methods, 'All action method names. Request/response actions have two types per method.');
426
472
  emit_block('request_response', 'RequestResponseActionMethod', registry.request_response_methods, 'Names of all request_response actions.');
427
473
  emit_block('remote_notification', 'RemoteNotificationActionMethod', registry.remote_notification_methods, 'Names of all remote_notification actions.');
428
474
  emit_block('local_call', 'LocalCallActionMethod', registry.local_call_methods, 'Names of all local_call actions.');
429
- emit_block('frontend', 'FrontendActionMethod', registry.frontend_methods, 'Names of all actions that may be handled on the client.');
430
- emit_block('backend', 'BackendActionMethod', registry.backend_methods, 'Names of all actions that may be handled on the server.');
475
+ // Loose: every spec the side might encounter (call, receive, or execute).
476
+ // Drives the typed-Proxy method enum keyed by FrontendActionsApi.
477
+ emit_block('frontend', 'FrontendActionMethod', registry.methods_relevant_to_frontend, 'Names of all actions in the typed FrontendActionsApi surface — every spec the frontend may encounter (call, receive, or execute locally).');
478
+ emit_block('backend', 'BackendActionMethod', registry.methods_relevant_to_backend, 'Names of all actions the backend may encounter — request_response and remote_notification (local_call is frontend-only).');
479
+ // Narrow: request_response actions this side handles (receives).
480
+ emit_block('frontend_handled', 'FrontendRequestResponseMethod', registry.frontend_handled_methods, 'Names of request_response actions the frontend handles (initiator excludes frontend).');
481
+ emit_block('backend_handled', 'BackendRequestResponseMethod', registry.backend_handled_methods, 'Names of request_response actions the backend handles (initiator excludes backend).');
482
+ // Broadcast: backend-initiated remote_notification, excluding `streams` targets.
483
+ emit_block('broadcast', 'BroadcastActionMethod', registry.broadcast_methods, "Names of remote_notification actions exposed by the broadcast API (backend-initiated, excluding request-scoped progress notifications named by another action's `streams`).");
431
484
  if (blocks.length === 0)
432
485
  return '';
433
486
  imports.add('zod', 'z');
434
487
  return blocks.join('\n\n');
435
488
  };
489
+ /**
490
+ * Emit a single named `z.enum([...])` + `z.infer` block for an arbitrary
491
+ * spec subset. Lower-level escape hatch from `generate_action_method_enums` —
492
+ * for cross-product or domain-specific enums the built-in discriminator
493
+ * doesn't cover.
494
+ *
495
+ * Mirrors the built-in helper's contract: protocol actions filtered by
496
+ * default, empty subsets return `''` (skip rather than emit `z.enum([])`),
497
+ * `zod` import registered idempotently only when at least one method
498
+ * qualifies.
499
+ *
500
+ * The cross-product space is open-ended; rather than grow the
501
+ * `ActionMethodEnumKind` discriminator one cross-product at a time, callers
502
+ * own the subset shape — name, jsdoc, predicate.
503
+ */
504
+ export const generate_action_method_enum_block = (specs, imports, options) => {
505
+ const filtered = filter_protocol_actions(specs, options.include_protocol_actions);
506
+ const methods = filtered.filter(options.predicate).map((s) => s.method);
507
+ if (methods.length === 0)
508
+ return '';
509
+ imports.add('zod', 'z');
510
+ return format_method_enum_block(options.name, options.jsdoc, methods);
511
+ };
436
512
  /**
437
513
  * Emit the fixed-shape `TypedActionEvent` alias used by `FrontendActionHandlers`
438
514
  * to narrow `ActionEvent.data` against the consumer's generated `ActionEventDatas`
@@ -473,7 +549,7 @@ type TypedActionEvent<
473
549
  * is ignored when `qualify_spec` is set. Single-source consumers omit it.
474
550
  */
475
551
  export const generate_action_specs_record = (specs, imports, options) => {
476
- const filtered = filter_composables(specs, options?.include_composables);
552
+ const filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
477
553
  imports.add_type('@fuzdev/fuz_app/actions/action_spec.js', 'ActionSpecUnion');
478
554
  if (filtered.length === 0) {
479
555
  // Empty spec list — emit minimal valid output and skip the `* as specs`
@@ -517,7 +593,7 @@ export const action_specs: Array<ActionSpecUnion> = Object.values(ActionSpecs);`
517
593
  * and `specs_module` is ignored. Single-source consumers omit it.
518
594
  */
519
595
  export const generate_action_inputs_outputs = (specs, imports, options) => {
520
- const filtered = filter_composables(specs, options?.include_composables);
596
+ const filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
521
597
  if (filtered.length === 0) {
522
598
  // Empty spec list — emit minimal valid output and skip the `zod` /
523
599
  // `* as specs` imports that would have nothing to reference.
@@ -592,7 +668,7 @@ ${outputs_type}
592
668
  * path the import resolves to.
593
669
  */
594
670
  export const generate_action_event_datas = (specs, imports, options) => {
595
- const filtered = filter_composables(specs, options?.include_composables);
671
+ const filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
596
672
  if (filtered.length === 0) {
597
673
  // Empty spec list — emit `interface ActionEventDatas {}` and skip
598
674
  // the optional collections-path import that would be unused.
@@ -630,29 +706,37 @@ ${lines.join('\n')}
630
706
  }`;
631
707
  };
632
708
  /**
633
- * Emit the `ActionsApi` interface — one method signature per spec via
709
+ * Emit the `FrontendActionsApi` interface — one method signature per spec via
634
710
  * `generate_actions_api_method_signature`. Optionally filter the spec set
635
- * (e.g. omit composable methods) via `method_filter`.
711
+ * (e.g. omit additional methods alongside the default protocol-action
712
+ * filter) via `method_filter`.
636
713
  *
637
714
  * Adds the `Result`, `JsonrpcErrorObject`, and `RpcClientCallOptions` type
638
715
  * imports plus `ActionInputs` / `ActionOutputs` (sourced from `collections_path`).
716
+ *
717
+ * The interface name is fixed at `FrontendActionsApi` — the symmetric counterpart
718
+ * of `BackendActionsApi`. Earlier consumer-named variants (`MyActionsApi`,
719
+ * `VisionesActionsApi`) were retired in API review III to make the side-of-the-wire
720
+ * intent visible at every call site. If a consumer needs a different name they
721
+ * hand-roll the interface (the helper's job is the standard symmetric shape).
639
722
  */
640
- export const generate_actions_api = (specs, imports, options) => {
641
- const composable_filtered = filter_composables(specs, options?.include_composables);
723
+ export const generate_frontend_actions_api = (specs, imports, options) => {
724
+ const protocol_filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
642
725
  const filter = options?.method_filter;
643
- const filtered = filter ? composable_filtered.filter((s) => filter(s)) : composable_filtered;
726
+ const filtered = filter ? protocol_filtered.filter((s) => filter(s)) : protocol_filtered;
644
727
  const interface_doc = `/**
645
- * Interface for action dispatch functions.
646
- * Async methods (request_response, remote_notification, async local_call)
647
- * return \`Promise<Result<...>>\` and accept an optional \`RpcClientCallOptions\`
648
- * second arg that threads \`signal\`, \`transport_name\`, and \`queue\` through to
649
- * the peer. Sync local_call methods return values directly.
728
+ * Typed dispatch surface for the frontend's RPC client. Symmetric counterpart
729
+ * of \`BackendActionsApi\`. Async methods (request_response, remote_notification,
730
+ * async local_call) return \`Promise<Result<...>>\` and accept an optional
731
+ * \`RpcClientCallOptions\` second arg that threads \`signal\`, \`transport_name\`,
732
+ * and \`queue\` through to the peer. Sync local_call methods return values
733
+ * directly.
650
734
  */`;
651
735
  if (filtered.length === 0) {
652
- // Empty spec list — emit `ActionsApi {}` and skip every import. None
653
- // of the symbols would be referenced by the empty body.
736
+ // Empty spec list — emit `FrontendActionsApi {}` and skip every import.
737
+ // None of the symbols would be referenced by the empty body.
654
738
  return `${interface_doc}
655
- export interface ActionsApi {}`;
739
+ export interface FrontendActionsApi {}`;
656
740
  }
657
741
  const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
658
742
  imports.add_type('@fuzdev/fuz_util/result.js', 'Result');
@@ -666,7 +750,7 @@ export interface ActionsApi {}`;
666
750
  .map((line) => `\t${line}`)
667
751
  .join('\n');
668
752
  return `${interface_doc}
669
- export interface ActionsApi {
753
+ export interface FrontendActionsApi {
670
754
  ${lines}
671
755
  }`;
672
756
  };
@@ -677,7 +761,7 @@ ${lines}
677
761
  * matching `TypedActionEvent` alias) — call both in the same gen producer.
678
762
  */
679
763
  export const generate_frontend_action_handlers = (specs, imports, options) => {
680
- const filtered = filter_composables(specs, options?.include_composables);
764
+ const filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
681
765
  const interface_doc = `/**
682
766
  * Frontend action handlers organized by method and phase.
683
767
  * Generated using spec.initiator to determine valid phases:
@@ -712,12 +796,23 @@ ${lines};
712
796
  * each `(input) => Promise<void>`. The array bundles the matching specs as a
713
797
  * `ReadonlyArray<ActionSpecUnion>`.
714
798
  *
715
- * Filter: `kind === 'remote_notification' && initiator !== 'frontend'`.
799
+ * Filter: `kind === 'remote_notification' && initiator !== 'frontend'`,
800
+ * additionally excluding methods that are the target of another spec's
801
+ * `streams` field. Streams targets (e.g. `completion_progress`,
802
+ * `ollama_progress`) are request-scoped notifications invoked via
803
+ * `ctx.notify` inside their parent handler — they're never callable through
804
+ * the broadcast API. The discriminator is `ActionSpec.streams`, not a manual
805
+ * exclusion list.
716
806
  *
717
807
  * Adds the `* as specs` namespace import (from `specs_module`), the
718
808
  * `ActionInputs` type import (from `collections_path`), and the
719
809
  * `ActionSpecUnion` type import.
720
810
  *
811
+ * Method signature shape today is `(input) => Promise<void>` — matches the
812
+ * fire-and-forget runtime of `create_broadcast_api`. Generalizing per-kind
813
+ * via `generate_actions_api_method_signature` is deferred until a second
814
+ * backend runtime constructor lands (see SAES quest § API review III).
815
+ *
721
816
  * @param options.qualify_spec - per-spec qualified identifier callback for
722
817
  * multi-source consumers. When set, the helper emits the callback's return
723
818
  * value instead of ``specs.${method}_action_spec`` in the broadcast array
@@ -726,13 +821,17 @@ ${lines};
726
821
  * Single-source consumers omit it.
727
822
  */
728
823
  export const generate_backend_actions_api = (specs, imports, options) => {
729
- const composable_filtered = filter_composables(specs, options?.include_composables);
730
- const broadcast = composable_filtered.filter((s) => s.kind === 'remote_notification' && s.initiator !== 'frontend');
824
+ const protocol_filtered = filter_protocol_actions(specs, options?.include_protocol_actions);
825
+ const registry = new ActionRegistry([...protocol_filtered]);
826
+ const broadcast = registry.broadcast_specs;
731
827
  imports.add_type('@fuzdev/fuz_app/actions/action_spec.js', 'ActionSpecUnion');
732
828
  const interface_doc = `/**
733
- * Broadcast-style notifications from the backend to all connected clients.
734
- * Request-scoped streaming goes through \`ctx.notify\` instead — it's
735
- * socket-scoped, not a broadcast.
829
+ * Typed dispatch surface for backend-initiated calls. Symmetric counterpart
830
+ * of \`FrontendActionsApi\`. Today exposes broadcast-style \`remote_notification\`
831
+ * methods (1→N fan-out via \`create_broadcast_api\`); request-scoped streaming
832
+ * goes through \`ctx.notify\` inside a handler — it's socket-scoped, not a
833
+ * broadcast. Will widen when a second backend runtime constructor (targeted
834
+ * send, backend-initiated request_response) lands.
736
835
  */`;
737
836
  if (broadcast.length === 0) {
738
837
  // No backend-initiated remote_notifications — skip `* as specs` and
@@ -756,3 +855,142 @@ export interface BackendActionsApi {${interface_body}}
756
855
 
757
856
  export const broadcast_action_specs: ReadonlyArray<ActionSpecUnion> = [${array_body}];`;
758
857
  };
858
+ /**
859
+ * Emit the `BackendActionHandlers` mapped type — one entry per
860
+ * `BackendRequestResponseMethod`, each `(input, ctx) => output | Promise<output>`.
861
+ * Replaces the hand-maintained `Exclude<>` + parallel mapped-type pattern
862
+ * (zzz had this at `zzz/src/lib/server/zzz_action_handlers.ts:42-66`).
863
+ *
864
+ * The context type is consumer-defined (e.g. zzz's `ZzzHandlerContext`). Pass
865
+ * `context_type` to name it; the helper assumes it's importable or defined
866
+ * in the emitted module's scope (consumer's responsibility).
867
+ *
868
+ * Adds `ActionInputs` / `ActionOutputs` type imports from `collections_path`
869
+ * and the `BackendRequestResponseMethod` import from `metatypes_path`.
870
+ *
871
+ * @param options.type_name - default `'BackendActionHandlers'`.
872
+ * @param options.method_enum_name - default `'BackendRequestResponseMethod'`.
873
+ * Pair with `generate_action_method_enums` emitting the `'backend_handled'` kind.
874
+ * @param options.context_type - default `'BackendHandlerContext'`. Caller's
875
+ * handler context type — must be in scope at the emit site.
876
+ * @param options.collections_path - default `'./action_collections.js'`.
877
+ * @param options.metatypes_path - default `'./action_metatypes.js'`.
878
+ */
879
+ export const generate_backend_action_handlers_map = (imports, options) => {
880
+ const type_name = options?.type_name ?? 'BackendActionHandlers';
881
+ const method_enum_name = options?.method_enum_name ?? 'BackendRequestResponseMethod';
882
+ const context_type = options?.context_type ?? 'BackendHandlerContext';
883
+ const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
884
+ const metatypes_path = options?.metatypes_path ?? DEFAULT_METATYPES_PATH;
885
+ imports.add_types(collections_path, 'ActionInputs', 'ActionOutputs');
886
+ imports.add_type(metatypes_path, method_enum_name);
887
+ return `/**
888
+ * Typed handler map for request_response actions the backend handles.
889
+ * One entry per ${method_enum_name}; each handler receives the typed input
890
+ * and returns the typed output (sync or async).
891
+ */
892
+ export type ${type_name} = {
893
+ [K in ${method_enum_name}]: (
894
+ input: ActionInputs[K],
895
+ ctx: ${context_type},
896
+ ) => ActionOutputs[K] | Promise<ActionOutputs[K]>;
897
+ };`;
898
+ };
899
+ /**
900
+ * Multi-source consumer helper. Takes a list of `{ns, module, specs}` rows,
901
+ * registers `import * as ns from module` for each on `imports`, builds the
902
+ * `method_to_ns` lookup with duplicate-method detection, and returns
903
+ * `{qualify_spec, all_specs}` ready to thread through the high-level
904
+ * helpers.
905
+ *
906
+ * Closes the per-file boilerplate gap that kept tx + visiones on hand-rolled
907
+ * template strings even after `qualify_spec?` landed in API review II — the
908
+ * per-call callback wasn't enough; the import dance + dup-check was the
909
+ * real boilerplate.
910
+ *
911
+ * @example
912
+ * ```ts
913
+ * const sources = [
914
+ * {ns: 'tx_specs', module: './action_specs.js', specs: all_tx_action_specs},
915
+ * {ns: 'admin_specs', module: '@fuzdev/fuz_app/auth/admin_action_specs.js', specs: all_admin_action_specs},
916
+ * ];
917
+ *
918
+ * export const gen: Gen = ({origin_path}) => {
919
+ * const imports = new ImportBuilder();
920
+ * const {qualify_spec, all_specs} = create_namespace_qualifier(sources, imports);
921
+ * return compose_gen_file({
922
+ * origin_path,
923
+ * imports,
924
+ * blocks: [
925
+ * generate_action_specs_record(all_specs, imports, {qualify_spec}),
926
+ * generate_action_inputs_outputs(all_specs, imports, {qualify_spec}),
927
+ * ],
928
+ * });
929
+ * };
930
+ * ```
931
+ *
932
+ * @throws if two sources contain the same method name (same-method detection
933
+ * is the consumer's primary debugging signal).
934
+ */
935
+ export const create_namespace_qualifier = (sources, imports) => {
936
+ const method_to_ns = new Map();
937
+ const all_specs = [];
938
+ for (const { ns, module, specs } of sources) {
939
+ imports.add(module, `* as ${ns}`);
940
+ for (const spec of specs) {
941
+ if (method_to_ns.has(spec.method)) {
942
+ throw new Error(`duplicate action method across sources: ${spec.method} (in ${method_to_ns.get(spec.method)} and ${ns})`);
943
+ }
944
+ method_to_ns.set(spec.method, ns);
945
+ all_specs.push(spec);
946
+ }
947
+ }
948
+ const qualify_spec = (spec) => {
949
+ const ns = method_to_ns.get(spec.method);
950
+ if (!ns) {
951
+ throw new Error(`unknown action method passed to qualify_spec: ${spec.method} — not in any registered source`);
952
+ }
953
+ return `${ns}.${spec.method}_action_spec`;
954
+ };
955
+ return { qualify_spec, all_specs };
956
+ };
957
+ /**
958
+ * Wrap the per-`*.gen.ts` boilerplate (banner + `imports.build()` +
959
+ * blocks join + template literal) into one call. Returns the full file body
960
+ * as a string ready to return from a `Gen` function.
961
+ *
962
+ * Each consumer producer collapses to one `compose_gen_file` call wrapping
963
+ * the helper invocations.
964
+ *
965
+ * @example
966
+ * ```ts
967
+ * export const gen: Gen = ({origin_path}) => {
968
+ * const imports = new ImportBuilder();
969
+ * return compose_gen_file({
970
+ * origin_path,
971
+ * imports,
972
+ * blocks: [
973
+ * generate_action_specs_record(all_action_specs, imports),
974
+ * generate_action_inputs_outputs(all_action_specs, imports),
975
+ * generate_action_event_datas(all_action_specs, imports),
976
+ * ],
977
+ * });
978
+ * };
979
+ * ```
980
+ *
981
+ * Empty blocks (`''`) are filtered out so helpers that short-circuit on
982
+ * empty spec sets don't introduce stray double blank lines.
983
+ */
984
+ export const compose_gen_file = (input) => {
985
+ const banner = create_banner(input.origin_path);
986
+ const body = input.blocks.filter(Boolean).join('\n\n');
987
+ return `
988
+ // ${banner}
989
+
990
+ ${input.imports.build()}
991
+
992
+ ${body}
993
+
994
+ // ${banner}
995
+ `;
996
+ };
@@ -1,6 +1,29 @@
1
1
  /**
2
2
  * `ActionRegistry` — query and filter utility over `ActionSpecUnion[]`.
3
3
  *
4
+ * Vocabulary (set in API review III, see the `docs/` directory and the SAES quest):
5
+ * - `*_handled_*` — request_response specs the named side **receives**
6
+ * (so the named side owns the handler). Used by codegen to emit typed
7
+ * handler maps.
8
+ * - `*_relevant_to_*` — the loose "everything this side might encounter"
9
+ * set, used by the typed-Proxy method enums (`FrontendActionMethod`,
10
+ * `BackendActionMethod`).
11
+ * - `broadcast_*` — kind-narrow `remote_notification` set with the
12
+ * `streams`-target exclusion. Today this matches what the broadcast
13
+ * API exposes.
14
+ * - `backend_initiated_*` — forward-looking kind-agnostic version of the
15
+ * broadcast set. Same content today; will diverge when local_calls or
16
+ * backend `request_response` join the backend's typed surface.
17
+ *
18
+ * Cache discipline: `spec_by_method` (Map) and the internal streams-target
19
+ * set lazy-memoize because the Map is consulted per-RPC dispatch
20
+ * (`frontend_rpc_client.ts` wires it into `lookup_action_spec`) and the
21
+ * streams set is rebuilt by two public getters. Array-returning getters
22
+ * recompute on each call so callers can mutate the result freely
23
+ * (`.sort()`, `.push(injected)` on a copy, etc.) without affecting the
24
+ * registry — codegen is a build-time path where the extra `.filter` /
25
+ * `.map` work is negligible.
26
+ *
4
27
  * @module
5
28
  */
6
29
  import type { ActionSpecUnion, RequestResponseActionSpec, RemoteNotificationActionSpec, LocalCallActionSpec } from './action_spec.js';
@@ -9,26 +32,35 @@ import type { ActionSpecUnion, RequestResponseActionSpec, RemoteNotificationActi
9
32
  * Provides helper methods to get actions by various criteria.
10
33
  */
11
34
  export declare class ActionRegistry {
35
+ #private;
12
36
  readonly specs: Array<ActionSpecUnion>;
13
37
  constructor(specs: Array<ActionSpecUnion>);
14
38
  get spec_by_method(): Map<string, ActionSpecUnion>;
39
+ get methods(): Array<string>;
15
40
  get request_response_specs(): Array<RequestResponseActionSpec>;
16
41
  get remote_notification_specs(): Array<RemoteNotificationActionSpec>;
17
42
  get local_call_specs(): Array<LocalCallActionSpec>;
18
- get backend_specs(): Array<ActionSpecUnion>;
19
- get frontend_specs(): Array<ActionSpecUnion>;
20
- get backend_to_frontend_specs(): Array<ActionSpecUnion>;
21
- get frontend_to_backend_specs(): Array<ActionSpecUnion>;
22
- get public_specs(): Array<ActionSpecUnion>;
23
- get authenticated_specs(): Array<ActionSpecUnion>;
24
- get methods(): Array<string>;
25
43
  get request_response_methods(): Array<string>;
26
44
  get remote_notification_methods(): Array<string>;
27
45
  get local_call_methods(): Array<string>;
28
- get backend_methods(): Array<string>;
29
- get frontend_methods(): Array<string>;
46
+ get specs_relevant_to_frontend(): Array<ActionSpecUnion>;
47
+ get specs_relevant_to_backend(): Array<ActionSpecUnion>;
48
+ get methods_relevant_to_frontend(): Array<string>;
49
+ get methods_relevant_to_backend(): Array<string>;
50
+ get frontend_handled_specs(): Array<RequestResponseActionSpec>;
51
+ get backend_handled_specs(): Array<RequestResponseActionSpec>;
52
+ get frontend_handled_methods(): Array<string>;
53
+ get backend_handled_methods(): Array<string>;
54
+ get broadcast_specs(): Array<RemoteNotificationActionSpec>;
55
+ get broadcast_methods(): Array<string>;
56
+ get backend_initiated_specs(): Array<ActionSpecUnion>;
57
+ get backend_initiated_methods(): Array<string>;
58
+ get backend_to_frontend_specs(): Array<ActionSpecUnion>;
59
+ get frontend_to_backend_specs(): Array<ActionSpecUnion>;
30
60
  get frontend_to_backend_methods(): Array<string>;
31
61
  get backend_to_frontend_methods(): Array<string>;
62
+ get public_specs(): Array<ActionSpecUnion>;
63
+ get authenticated_specs(): Array<ActionSpecUnion>;
32
64
  get public_methods(): Array<string>;
33
65
  get authenticated_methods(): Array<string>;
34
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"action_registry.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_registry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACX,eAAe,EACf,yBAAyB,EACzB,4BAA4B,EAC5B,mBAAmB,EACnB,MAAM,kBAAkB,CAAC;AAQ1B;;;GAGG;AACH,qBAAa,cAAc;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;gBAE3B,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC;IAIzC,IAAI,cAAc,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAEjD;IAED,IAAI,sBAAsB,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAE7D;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAEnE;IAED,IAAI,gBAAgB,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAEjD;IAKD,IAAI,aAAa,IAAI,KAAK,CAAC,eAAe,CAAC,CAE1C;IAED,IAAI,cAAc,IAAI,KAAK,CAAC,eAAe,CAAC,CAE3C;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEtD;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEtD;IAED,IAAI,YAAY,IAAI,KAAK,CAAC,eAAe,CAAC,CAEzC;IAED,IAAI,mBAAmB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEhD;IAED,IAAI,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,CAE3B;IAED,IAAI,wBAAwB,IAAI,KAAK,CAAC,MAAM,CAAC,CAE5C;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAED,IAAI,kBAAkB,IAAI,KAAK,CAAC,MAAM,CAAC,CAEtC;IAED,IAAI,eAAe,IAAI,KAAK,CAAC,MAAM,CAAC,CAEnC;IAED,IAAI,gBAAgB,IAAI,KAAK,CAAC,MAAM,CAAC,CAEpC;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAED,IAAI,cAAc,IAAI,KAAK,CAAC,MAAM,CAAC,CAElC;IAED,IAAI,qBAAqB,IAAI,KAAK,CAAC,MAAM,CAAC,CAEzC;CACD"}
1
+ {"version":3,"file":"action_registry.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EACX,eAAe,EACf,yBAAyB,EACzB,4BAA4B,EAC5B,mBAAmB,EACnB,MAAM,kBAAkB,CAAC;AAS1B;;;GAGG;AACH,qBAAa,cAAc;;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;gBAE3B,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC;IAKzC,IAAI,cAAc,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAEjD;IAED,IAAI,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,CAE3B;IAID,IAAI,sBAAsB,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAE7D;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAEnE;IAED,IAAI,gBAAgB,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAEjD;IAED,IAAI,wBAAwB,IAAI,KAAK,CAAC,MAAM,CAAC,CAE5C;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAED,IAAI,kBAAkB,IAAI,KAAK,CAAC,MAAM,CAAC,CAEtC;IAOD,IAAI,0BAA0B,IAAI,KAAK,CAAC,eAAe,CAAC,CAEvD;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEtD;IAED,IAAI,4BAA4B,IAAI,KAAK,CAAC,MAAM,CAAC,CAEhD;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAOD,IAAI,sBAAsB,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAE7D;IAED,IAAI,qBAAqB,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAE5D;IAED,IAAI,wBAAwB,IAAI,KAAK,CAAC,MAAM,CAAC,CAE5C;IAED,IAAI,uBAAuB,IAAI,KAAK,CAAC,MAAM,CAAC,CAE3C;IASD,IAAI,eAAe,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAKzD;IAED,IAAI,iBAAiB,IAAI,KAAK,CAAC,MAAM,CAAC,CAErC;IAED,IAAI,uBAAuB,IAAI,KAAK,CAAC,eAAe,CAAC,CAKpD;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,MAAM,CAAC,CAE7C;IAID,IAAI,yBAAyB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEtD;IAED,IAAI,yBAAyB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEtD;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAED,IAAI,2BAA2B,IAAI,KAAK,CAAC,MAAM,CAAC,CAE/C;IAID,IAAI,YAAY,IAAI,KAAK,CAAC,eAAe,CAAC,CAEzC;IAED,IAAI,mBAAmB,IAAI,KAAK,CAAC,eAAe,CAAC,CAEhD;IAED,IAAI,cAAc,IAAI,KAAK,CAAC,MAAM,CAAC,CAElC;IAED,IAAI,qBAAqB,IAAI,KAAK,CAAC,MAAM,CAAC,CAEzC;CAaD"}