@fuzdev/fuz_app 0.47.0 → 0.49.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.
- package/dist/actions/CLAUDE.md +92 -47
- package/dist/actions/action_codegen.d.ts +199 -29
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +321 -70
- package/dist/actions/action_registry.d.ts +41 -9
- package/dist/actions/action_registry.d.ts.map +1 -1
- package/dist/actions/action_registry.js +109 -32
- package/package.json +1 -1
|
@@ -10,12 +10,12 @@ import { ActionRegistry } from './action_registry.js';
|
|
|
10
10
|
export const COMPOSABLE_ACTION_METHODS = ['heartbeat', 'cancel'];
|
|
11
11
|
const COMPOSABLE_METHOD_SET = new Set(COMPOSABLE_ACTION_METHODS);
|
|
12
12
|
/**
|
|
13
|
-
* Type predicate for filtering composable methods out of a typed `
|
|
13
|
+
* Type predicate for filtering composable methods out of a typed `FrontendActionsApi`
|
|
14
14
|
* `method_filter`. Avoids the `(... as never)` cast required to call
|
|
15
15
|
* `Array.prototype.includes` on the readonly tuple at narrow string types.
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
|
-
*
|
|
18
|
+
* generate_frontend_actions_api(specs, imports, {
|
|
19
19
|
* method_filter: (s) => !is_composable_action_method(s.method),
|
|
20
20
|
* });
|
|
21
21
|
*/
|
|
@@ -312,10 +312,10 @@ export const to_action_spec_identifier = (method) => `${method}_action_spec`;
|
|
|
312
312
|
export const to_action_spec_input_identifier = (method) => `${to_action_spec_identifier(method)}.input`;
|
|
313
313
|
export const to_action_spec_output_identifier = (method) => `${to_action_spec_identifier(method)}.output`;
|
|
314
314
|
/**
|
|
315
|
-
* Generates one method line of the typed `
|
|
316
|
-
* spec. Encapsulates the input/options/return-type signature shape so
|
|
317
|
-
* surface evolves in one place when fields like `signal` or
|
|
318
|
-
* are added to per-call options.
|
|
315
|
+
* Generates one method line of the typed `FrontendActionsApi` interface for a
|
|
316
|
+
* single spec. Encapsulates the input/options/return-type signature shape so
|
|
317
|
+
* the surface evolves in one place when fields like `signal` or
|
|
318
|
+
* `transport_name` are added to per-call options.
|
|
319
319
|
*
|
|
320
320
|
* Async methods (`request_response`, `remote_notification`, async
|
|
321
321
|
* `local_call`) get an optional second `options?: RpcClientCallOptions` arg
|
|
@@ -336,7 +336,7 @@ export const to_action_spec_output_identifier = (method) => `${to_action_spec_id
|
|
|
336
336
|
* @param options.sync_returns_value - when true (default), sync local_call
|
|
337
337
|
* methods return the output value directly; when false they're wrapped in
|
|
338
338
|
* `Result<{value, error}>` like async methods. Set to `false` if your
|
|
339
|
-
*
|
|
339
|
+
* FrontendActionsApi treats every method uniformly.
|
|
340
340
|
* @returns one line like `foo: (input: ActionInputs['foo'], options?: RpcClientCallOptions) => Promise<Result<...>>;`
|
|
341
341
|
*/
|
|
342
342
|
export const generate_actions_api_method_signature = (spec, options) => {
|
|
@@ -356,6 +356,40 @@ export const generate_actions_api_method_signature = (spec, options) => {
|
|
|
356
356
|
: result_return;
|
|
357
357
|
return `${spec.method}: (${input_param}${options_param}) => ${return_type};`;
|
|
358
358
|
};
|
|
359
|
+
// --------------------------------------------------------------------------
|
|
360
|
+
// High-level codegen helpers — compose the lower-level primitives above into
|
|
361
|
+
// the literal blocks consumer `*.gen.ts` producers emit. Tier 1 consumers
|
|
362
|
+
// (HTTP-only, e.g. tx) call the value-side helpers; Tier 2 (`TypedActionEvent`-
|
|
363
|
+
// aware, e.g. zzz) also call the typed-event + frontend-handlers helpers.
|
|
364
|
+
//
|
|
365
|
+
// **Multi-source consumers.** Helpers that reference specs at runtime
|
|
366
|
+
// (`generate_action_specs_record`, `generate_action_inputs_outputs`,
|
|
367
|
+
// `generate_backend_actions_api`) default to a single `* as specs from
|
|
368
|
+
// specs_module` namespace import and emit `specs.{method}_action_spec`. Pass
|
|
369
|
+
// `qualify_spec?: (spec) => string` to emit a per-spec qualified identifier
|
|
370
|
+
// (e.g. `admin_specs.account_list_action_spec`) for consumers stitching local
|
|
371
|
+
// specs together with multiple upstream sources (`all_admin_action_specs` /
|
|
372
|
+
// `all_permit_offer_action_specs` / `all_account_action_specs` /
|
|
373
|
+
// `all_self_service_role_action_specs` from fuz_app). When `qualify_spec` is
|
|
374
|
+
// set, the helper does NOT add a `* as specs` import — the consumer manages
|
|
375
|
+
// the multiple `* as ns` imports itself — and `specs_module` is ignored.
|
|
376
|
+
// `create_namespace_qualifier` automates the source-table → qualifier wiring.
|
|
377
|
+
// --------------------------------------------------------------------------
|
|
378
|
+
/**
|
|
379
|
+
* Format a `z.enum([...])` runtime const + matching `z.infer` type alias.
|
|
380
|
+
* Caller is responsible for ensuring `methods` is non-empty (`z.enum([])` is
|
|
381
|
+
* invalid) and registering the `zod` import on the `ImportBuilder`.
|
|
382
|
+
*/
|
|
383
|
+
const format_method_enum_block = (name, jsdoc, methods) => {
|
|
384
|
+
const lines = methods.map((m) => `\t'${m}',`).join('\n');
|
|
385
|
+
return `/**
|
|
386
|
+
* ${jsdoc}
|
|
387
|
+
*/
|
|
388
|
+
export const ${name} = z.enum([
|
|
389
|
+
${lines}
|
|
390
|
+
]);
|
|
391
|
+
export type ${name} = z.infer<typeof ${name}>;`;
|
|
392
|
+
};
|
|
359
393
|
/** Default emit set — every enum kind. */
|
|
360
394
|
export const ACTION_METHOD_ENUM_KINDS_ALL = new Set([
|
|
361
395
|
'all',
|
|
@@ -364,19 +398,41 @@ export const ACTION_METHOD_ENUM_KINDS_ALL = new Set([
|
|
|
364
398
|
'local_call',
|
|
365
399
|
'frontend',
|
|
366
400
|
'backend',
|
|
401
|
+
'frontend_handled',
|
|
402
|
+
'backend_handled',
|
|
403
|
+
'broadcast',
|
|
367
404
|
]);
|
|
368
405
|
/**
|
|
369
406
|
* Filter `heartbeat` / `cancel` out of `specs` unless the consumer opts back in.
|
|
370
407
|
* Composables ship from fuz_app and are spread into every consumer's `actions`
|
|
371
408
|
* array at registration time — they should not appear in consumer-owned typed
|
|
372
|
-
* surfaces (`ActionMethod`, `
|
|
409
|
+
* surfaces (`ActionMethod`, `FrontendActionsApi`, `ActionInputs`, etc.) by
|
|
410
|
+
* default.
|
|
373
411
|
*/
|
|
374
412
|
const filter_composables = (specs, include_composables) => include_composables ? specs : specs.filter((s) => !is_composable_action_method(s.method));
|
|
413
|
+
/**
|
|
414
|
+
* Resolve the per-spec identifier qualifier used by the multi-source helpers
|
|
415
|
+
* (`generate_action_specs_record`, `generate_action_inputs_outputs`,
|
|
416
|
+
* `generate_backend_actions_api`). When `qualify_spec` is set, returns the
|
|
417
|
+
* caller's callback verbatim — the consumer is managing its own namespace
|
|
418
|
+
* imports. Otherwise, registers the default `* as specs from specs_module`
|
|
419
|
+
* import (defaulting to `'./action_specs.js'`) and returns the matching
|
|
420
|
+
* `specs.${method}_action_spec` qualifier.
|
|
421
|
+
*/
|
|
422
|
+
const resolve_spec_qualifier = (imports, options) => {
|
|
423
|
+
if (options?.qualify_spec)
|
|
424
|
+
return options.qualify_spec;
|
|
425
|
+
const specs_module = options?.specs_module ?? DEFAULT_SPECS_MODULE;
|
|
426
|
+
imports.add(specs_module, '* as specs');
|
|
427
|
+
return (s) => `specs.${s.method}_action_spec`;
|
|
428
|
+
};
|
|
375
429
|
/**
|
|
376
430
|
* Emit one or more `z.enum([...])` declarations for action method names —
|
|
377
431
|
* `ActionMethod`, `RequestResponseActionMethod`, `RemoteNotificationActionMethod`,
|
|
378
|
-
* `LocalCallActionMethod`, `FrontendActionMethod`, `BackendActionMethod
|
|
379
|
-
*
|
|
432
|
+
* `LocalCallActionMethod`, `FrontendActionMethod`, `BackendActionMethod`,
|
|
433
|
+
* `FrontendRequestResponseMethod`, `BackendRequestResponseMethod`,
|
|
434
|
+
* `BroadcastActionMethod`. Pairs each runtime const with a `z.infer` type
|
|
435
|
+
* alias under the same identifier.
|
|
380
436
|
*
|
|
381
437
|
* Composable methods (`heartbeat`, `cancel`) are filtered out by default —
|
|
382
438
|
* pass `include_composables: true` if a consumer genuinely wants them on
|
|
@@ -386,7 +442,11 @@ const filter_composables = (specs, include_composables) => include_composables ?
|
|
|
386
442
|
* Adds `import {z} from 'zod';` to `imports` only when at least one block
|
|
387
443
|
* is emitted (idempotent).
|
|
388
444
|
*
|
|
389
|
-
*
|
|
445
|
+
* For genuinely cross-product enums the discriminator doesn't cover, use
|
|
446
|
+
* `generate_action_method_enum_block` — caller owns the predicate, name,
|
|
447
|
+
* and jsdoc.
|
|
448
|
+
*
|
|
449
|
+
* @param options.emit - subset of enums to emit; defaults to all nine.
|
|
390
450
|
* @param options.include_composables - when true, retains `heartbeat` /
|
|
391
451
|
* `cancel` in the emitted enums. Default `false`.
|
|
392
452
|
*/
|
|
@@ -402,21 +462,48 @@ export const generate_action_method_enums = (specs, imports, options) => {
|
|
|
402
462
|
// Consumers that need a kind to exist should check their spec set, not the helper.
|
|
403
463
|
if (methods.length === 0)
|
|
404
464
|
return;
|
|
405
|
-
|
|
406
|
-
blocks.push(`/**\n * ${jsdoc}\n */\nexport const ${name} = z.enum([\n${lines}\n]);
|
|
407
|
-
export type ${name} = z.infer<typeof ${name}>;`);
|
|
465
|
+
blocks.push(format_method_enum_block(name, jsdoc, methods));
|
|
408
466
|
};
|
|
409
467
|
emit_block('all', 'ActionMethod', registry.methods, 'All action method names. Request/response actions have two types per method.');
|
|
410
468
|
emit_block('request_response', 'RequestResponseActionMethod', registry.request_response_methods, 'Names of all request_response actions.');
|
|
411
469
|
emit_block('remote_notification', 'RemoteNotificationActionMethod', registry.remote_notification_methods, 'Names of all remote_notification actions.');
|
|
412
470
|
emit_block('local_call', 'LocalCallActionMethod', registry.local_call_methods, 'Names of all local_call actions.');
|
|
413
|
-
|
|
414
|
-
|
|
471
|
+
// Loose: every spec the side might encounter (call, receive, or execute).
|
|
472
|
+
// Drives the typed-Proxy method enum keyed by FrontendActionsApi.
|
|
473
|
+
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).');
|
|
474
|
+
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).');
|
|
475
|
+
// Narrow: request_response actions this side handles (receives).
|
|
476
|
+
emit_block('frontend_handled', 'FrontendRequestResponseMethod', registry.frontend_handled_methods, 'Names of request_response actions the frontend handles (initiator excludes frontend).');
|
|
477
|
+
emit_block('backend_handled', 'BackendRequestResponseMethod', registry.backend_handled_methods, 'Names of request_response actions the backend handles (initiator excludes backend).');
|
|
478
|
+
// Broadcast: backend-initiated remote_notification, excluding `streams` targets.
|
|
479
|
+
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`).");
|
|
415
480
|
if (blocks.length === 0)
|
|
416
481
|
return '';
|
|
417
482
|
imports.add('zod', 'z');
|
|
418
483
|
return blocks.join('\n\n');
|
|
419
484
|
};
|
|
485
|
+
/**
|
|
486
|
+
* Emit a single named `z.enum([...])` + `z.infer` block for an arbitrary
|
|
487
|
+
* spec subset. Lower-level escape hatch from `generate_action_method_enums` —
|
|
488
|
+
* for cross-product or domain-specific enums the built-in discriminator
|
|
489
|
+
* doesn't cover.
|
|
490
|
+
*
|
|
491
|
+
* Mirrors the built-in helper's contract: composables filtered by default,
|
|
492
|
+
* empty subsets return `''` (skip rather than emit `z.enum([])`), `zod`
|
|
493
|
+
* import registered idempotently only when at least one method qualifies.
|
|
494
|
+
*
|
|
495
|
+
* The cross-product space is open-ended; rather than grow the
|
|
496
|
+
* `ActionMethodEnumKind` discriminator one cross-product at a time, callers
|
|
497
|
+
* own the subset shape — name, jsdoc, predicate.
|
|
498
|
+
*/
|
|
499
|
+
export const generate_action_method_enum_block = (specs, imports, options) => {
|
|
500
|
+
const filtered = filter_composables(specs, options.include_composables);
|
|
501
|
+
const methods = filtered.filter(options.predicate).map((s) => s.method);
|
|
502
|
+
if (methods.length === 0)
|
|
503
|
+
return '';
|
|
504
|
+
imports.add('zod', 'z');
|
|
505
|
+
return format_method_enum_block(options.name, options.jsdoc, methods);
|
|
506
|
+
};
|
|
420
507
|
/**
|
|
421
508
|
* Emit the fixed-shape `TypedActionEvent` alias used by `FrontendActionHandlers`
|
|
422
509
|
* to narrow `ActionEvent.data` against the consumer's generated `ActionEventDatas`
|
|
@@ -449,11 +536,12 @@ type TypedActionEvent<
|
|
|
449
536
|
* Array<ActionSpecUnion>` value bundling every spec. Adds the `* as specs`
|
|
450
537
|
* namespace import + the `ActionSpecUnion` type import.
|
|
451
538
|
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
539
|
+
* @param options.qualify_spec - per-spec qualified identifier callback for
|
|
540
|
+
* multi-source consumers (e.g. ``(s) => `admin_specs.${s.method}_action_spec` ``).
|
|
541
|
+
* When set, the helper emits the callback's return value instead of
|
|
542
|
+
* ``specs.${method}_action_spec`` and skips the default `* as specs`
|
|
543
|
+
* import — the consumer manages its own namespace imports. `specs_module`
|
|
544
|
+
* is ignored when `qualify_spec` is set. Single-source consumers omit it.
|
|
457
545
|
*/
|
|
458
546
|
export const generate_action_specs_record = (specs, imports, options) => {
|
|
459
547
|
const filtered = filter_composables(specs, options?.include_composables);
|
|
@@ -470,14 +558,9 @@ export interface ActionSpecs {}
|
|
|
470
558
|
|
|
471
559
|
export const action_specs: Array<ActionSpecUnion> = Object.values(ActionSpecs);`;
|
|
472
560
|
}
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
.map((s) => `\t${s.method}: specs.${s.method}_action_spec,`)
|
|
477
|
-
.join('\n');
|
|
478
|
-
const type_lines = filtered
|
|
479
|
-
.map((s) => `\t${s.method}: typeof specs.${s.method}_action_spec;`)
|
|
480
|
-
.join('\n');
|
|
561
|
+
const qualify = resolve_spec_qualifier(imports, options);
|
|
562
|
+
const value_lines = filtered.map((s) => `\t${s.method}: ${qualify(s)},`).join('\n');
|
|
563
|
+
const type_lines = filtered.map((s) => `\t${s.method}: typeof ${qualify(s)};`).join('\n');
|
|
481
564
|
return `/**
|
|
482
565
|
* Action specifications indexed by method name.
|
|
483
566
|
* These represent the complete action spec definitions.
|
|
@@ -498,8 +581,11 @@ export const action_specs: Array<ActionSpecUnion> = Object.values(ActionSpecs);`
|
|
|
498
581
|
*
|
|
499
582
|
* Adds `import {z} from 'zod';` and the `* as specs` namespace import.
|
|
500
583
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
584
|
+
* @param options.qualify_spec - per-spec qualified identifier callback for
|
|
585
|
+
* multi-source consumers. The helper appends `.input` / `.output` to the
|
|
586
|
+
* callback's return value. When set, the helper skips the default
|
|
587
|
+
* `* as specs` import — the consumer manages its own namespace imports —
|
|
588
|
+
* and `specs_module` is ignored. Single-source consumers omit it.
|
|
503
589
|
*/
|
|
504
590
|
export const generate_action_inputs_outputs = (specs, imports, options) => {
|
|
505
591
|
const filtered = filter_composables(specs, options?.include_composables);
|
|
@@ -523,19 +609,14 @@ export const ActionOutputs = {} as const;
|
|
|
523
609
|
export interface ActionOutputs {}`;
|
|
524
610
|
}
|
|
525
611
|
imports.add('zod', 'z');
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
const inputs_value = filtered
|
|
529
|
-
.map((s) => `\t${s.method}: specs.${s.method}_action_spec.input,`)
|
|
530
|
-
.join('\n');
|
|
612
|
+
const qualify = resolve_spec_qualifier(imports, options);
|
|
613
|
+
const inputs_value = filtered.map((s) => `\t${s.method}: ${qualify(s)}.input,`).join('\n');
|
|
531
614
|
const inputs_type = filtered
|
|
532
|
-
.map((s) => `\t${s.method}: z.infer<typeof
|
|
533
|
-
.join('\n');
|
|
534
|
-
const outputs_value = filtered
|
|
535
|
-
.map((s) => `\t${s.method}: specs.${s.method}_action_spec.output,`)
|
|
615
|
+
.map((s) => `\t${s.method}: z.infer<typeof ${qualify(s)}.input>;`)
|
|
536
616
|
.join('\n');
|
|
617
|
+
const outputs_value = filtered.map((s) => `\t${s.method}: ${qualify(s)}.output,`).join('\n');
|
|
537
618
|
const outputs_type = filtered
|
|
538
|
-
.map((s) => `\t${s.method}: z.infer<typeof
|
|
619
|
+
.map((s) => `\t${s.method}: z.infer<typeof ${qualify(s)}.output>;`)
|
|
539
620
|
.join('\n');
|
|
540
621
|
return `/**
|
|
541
622
|
* Action parameter schemas indexed by method name.
|
|
@@ -570,12 +651,16 @@ ${outputs_type}
|
|
|
570
651
|
*
|
|
571
652
|
* Adds the per-kind data type imports (only the kinds that appear in `specs`).
|
|
572
653
|
*
|
|
573
|
-
* @param options.
|
|
574
|
-
* `ActionOutputs`
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
654
|
+
* @param options.same_file - when `true` (default), assumes `ActionInputs` /
|
|
655
|
+
* `ActionOutputs` are in the same module as the emitted `ActionEventDatas`
|
|
656
|
+
* and adds no import (the zzz pattern, where `generate_action_inputs_outputs`
|
|
657
|
+
* and this helper feed the same `action_collections.ts` output). When
|
|
658
|
+
* `false`, adds `ActionInputs` / `ActionOutputs` type imports from
|
|
659
|
+
* `collections_path`.
|
|
660
|
+
* @param options.collections_path - import path used when `same_file: false`.
|
|
661
|
+
* Defaults to `'./action_collections.js'`. Ignored when `same_file: true`
|
|
662
|
+
* — `same_file` is the file-layout switch; `collections_path` is just the
|
|
663
|
+
* path the import resolves to.
|
|
579
664
|
*/
|
|
580
665
|
export const generate_action_event_datas = (specs, imports, options) => {
|
|
581
666
|
const filtered = filter_composables(specs, options?.include_composables);
|
|
@@ -589,8 +674,10 @@ export const generate_action_event_datas = (specs, imports, options) => {
|
|
|
589
674
|
*/
|
|
590
675
|
export interface ActionEventDatas {}`;
|
|
591
676
|
}
|
|
592
|
-
|
|
593
|
-
|
|
677
|
+
const same_file = options?.same_file ?? true;
|
|
678
|
+
if (!same_file) {
|
|
679
|
+
const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
|
|
680
|
+
imports.add_types(collections_path, 'ActionInputs', 'ActionOutputs');
|
|
594
681
|
}
|
|
595
682
|
const lines = filtered.map((spec) => {
|
|
596
683
|
const data_type = spec.kind === 'request_response'
|
|
@@ -614,29 +701,36 @@ ${lines.join('\n')}
|
|
|
614
701
|
}`;
|
|
615
702
|
};
|
|
616
703
|
/**
|
|
617
|
-
* Emit the `
|
|
704
|
+
* Emit the `FrontendActionsApi` interface — one method signature per spec via
|
|
618
705
|
* `generate_actions_api_method_signature`. Optionally filter the spec set
|
|
619
706
|
* (e.g. omit composable methods) via `method_filter`.
|
|
620
707
|
*
|
|
621
708
|
* Adds the `Result`, `JsonrpcErrorObject`, and `RpcClientCallOptions` type
|
|
622
709
|
* imports plus `ActionInputs` / `ActionOutputs` (sourced from `collections_path`).
|
|
710
|
+
*
|
|
711
|
+
* The interface name is fixed at `FrontendActionsApi` — the symmetric counterpart
|
|
712
|
+
* of `BackendActionsApi`. Earlier consumer-named variants (`MyActionsApi`,
|
|
713
|
+
* `VisionesActionsApi`) were retired in API review III to make the side-of-the-wire
|
|
714
|
+
* intent visible at every call site. If a consumer needs a different name they
|
|
715
|
+
* hand-roll the interface (the helper's job is the standard symmetric shape).
|
|
623
716
|
*/
|
|
624
|
-
export const
|
|
717
|
+
export const generate_frontend_actions_api = (specs, imports, options) => {
|
|
625
718
|
const composable_filtered = filter_composables(specs, options?.include_composables);
|
|
626
719
|
const filter = options?.method_filter;
|
|
627
720
|
const filtered = filter ? composable_filtered.filter((s) => filter(s)) : composable_filtered;
|
|
628
721
|
const interface_doc = `/**
|
|
629
|
-
*
|
|
630
|
-
* Async methods (request_response, remote_notification,
|
|
631
|
-
* return \`Promise<Result<...>>\` and accept an optional
|
|
632
|
-
* second arg that threads \`signal\`, \`transport_name\`,
|
|
633
|
-
* the peer. Sync local_call methods return values
|
|
722
|
+
* Typed dispatch surface for the frontend's RPC client. Symmetric counterpart
|
|
723
|
+
* of \`BackendActionsApi\`. Async methods (request_response, remote_notification,
|
|
724
|
+
* async local_call) return \`Promise<Result<...>>\` and accept an optional
|
|
725
|
+
* \`RpcClientCallOptions\` second arg that threads \`signal\`, \`transport_name\`,
|
|
726
|
+
* and \`queue\` through to the peer. Sync local_call methods return values
|
|
727
|
+
* directly.
|
|
634
728
|
*/`;
|
|
635
729
|
if (filtered.length === 0) {
|
|
636
|
-
// Empty spec list — emit `
|
|
637
|
-
// of the symbols would be referenced by the empty body.
|
|
730
|
+
// Empty spec list — emit `FrontendActionsApi {}` and skip every import.
|
|
731
|
+
// None of the symbols would be referenced by the empty body.
|
|
638
732
|
return `${interface_doc}
|
|
639
|
-
export interface
|
|
733
|
+
export interface FrontendActionsApi {}`;
|
|
640
734
|
}
|
|
641
735
|
const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
|
|
642
736
|
imports.add_type('@fuzdev/fuz_util/result.js', 'Result');
|
|
@@ -650,7 +744,7 @@ export interface ActionsApi {}`;
|
|
|
650
744
|
.map((line) => `\t${line}`)
|
|
651
745
|
.join('\n');
|
|
652
746
|
return `${interface_doc}
|
|
653
|
-
export interface
|
|
747
|
+
export interface FrontendActionsApi {
|
|
654
748
|
${lines}
|
|
655
749
|
}`;
|
|
656
750
|
};
|
|
@@ -696,23 +790,42 @@ ${lines};
|
|
|
696
790
|
* each `(input) => Promise<void>`. The array bundles the matching specs as a
|
|
697
791
|
* `ReadonlyArray<ActionSpecUnion>`.
|
|
698
792
|
*
|
|
699
|
-
* Filter: `kind === 'remote_notification' && initiator !== 'frontend'
|
|
793
|
+
* Filter: `kind === 'remote_notification' && initiator !== 'frontend'`,
|
|
794
|
+
* additionally excluding methods that are the target of another spec's
|
|
795
|
+
* `streams` field. Streams targets (e.g. `completion_progress`,
|
|
796
|
+
* `ollama_progress`) are request-scoped notifications invoked via
|
|
797
|
+
* `ctx.notify` inside their parent handler — they're never callable through
|
|
798
|
+
* the broadcast API. The discriminator is `ActionSpec.streams`, not a manual
|
|
799
|
+
* exclusion list.
|
|
700
800
|
*
|
|
701
801
|
* Adds the `* as specs` namespace import (from `specs_module`), the
|
|
702
802
|
* `ActionInputs` type import (from `collections_path`), and the
|
|
703
803
|
* `ActionSpecUnion` type import.
|
|
704
804
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
805
|
+
* Method signature shape today is `(input) => Promise<void>` — matches the
|
|
806
|
+
* fire-and-forget runtime of `create_broadcast_api`. Generalizing per-kind
|
|
807
|
+
* via `generate_actions_api_method_signature` is deferred until a second
|
|
808
|
+
* backend runtime constructor lands (see SAES quest § API review III).
|
|
809
|
+
*
|
|
810
|
+
* @param options.qualify_spec - per-spec qualified identifier callback for
|
|
811
|
+
* multi-source consumers. When set, the helper emits the callback's return
|
|
812
|
+
* value instead of ``specs.${method}_action_spec`` in the broadcast array
|
|
813
|
+
* and skips the default `* as specs` import — the consumer manages its own
|
|
814
|
+
* namespace imports. `specs_module` is ignored when `qualify_spec` is set.
|
|
815
|
+
* Single-source consumers omit it.
|
|
707
816
|
*/
|
|
708
817
|
export const generate_backend_actions_api = (specs, imports, options) => {
|
|
709
818
|
const composable_filtered = filter_composables(specs, options?.include_composables);
|
|
710
|
-
const
|
|
819
|
+
const registry = new ActionRegistry([...composable_filtered]);
|
|
820
|
+
const broadcast = registry.broadcast_specs;
|
|
711
821
|
imports.add_type('@fuzdev/fuz_app/actions/action_spec.js', 'ActionSpecUnion');
|
|
712
822
|
const interface_doc = `/**
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
823
|
+
* Typed dispatch surface for backend-initiated calls. Symmetric counterpart
|
|
824
|
+
* of \`FrontendActionsApi\`. Today exposes broadcast-style \`remote_notification\`
|
|
825
|
+
* methods (1→N fan-out via \`create_broadcast_api\`); request-scoped streaming
|
|
826
|
+
* goes through \`ctx.notify\` inside a handler — it's socket-scoped, not a
|
|
827
|
+
* broadcast. Will widen when a second backend runtime constructor (targeted
|
|
828
|
+
* send, backend-initiated request_response) lands.
|
|
716
829
|
*/`;
|
|
717
830
|
if (broadcast.length === 0) {
|
|
718
831
|
// No backend-initiated remote_notifications — skip `* as specs` and
|
|
@@ -722,18 +835,156 @@ export interface BackendActionsApi {}
|
|
|
722
835
|
|
|
723
836
|
export const broadcast_action_specs: ReadonlyArray<ActionSpecUnion> = [];`;
|
|
724
837
|
}
|
|
725
|
-
const specs_module = options?.specs_module ?? DEFAULT_SPECS_MODULE;
|
|
726
838
|
const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
|
|
727
|
-
imports.add(specs_module, '* as specs');
|
|
728
839
|
imports.add_type(collections_path, 'ActionInputs');
|
|
840
|
+
const qualify = resolve_spec_qualifier(imports, options);
|
|
729
841
|
const interface_body = '\n' +
|
|
730
842
|
broadcast
|
|
731
843
|
.map((s) => `\t${s.method}: (input: ActionInputs['${s.method}']) => Promise<void>;`)
|
|
732
844
|
.join('\n') +
|
|
733
845
|
'\n';
|
|
734
|
-
const array_body = '\n' + broadcast.map((s) => `\
|
|
846
|
+
const array_body = '\n' + broadcast.map((s) => `\t${qualify(s)},`).join('\n') + '\n';
|
|
735
847
|
return `${interface_doc}
|
|
736
848
|
export interface BackendActionsApi {${interface_body}}
|
|
737
849
|
|
|
738
850
|
export const broadcast_action_specs: ReadonlyArray<ActionSpecUnion> = [${array_body}];`;
|
|
739
851
|
};
|
|
852
|
+
/**
|
|
853
|
+
* Emit the `BackendActionHandlers` mapped type — one entry per
|
|
854
|
+
* `BackendRequestResponseMethod`, each `(input, ctx) => output | Promise<output>`.
|
|
855
|
+
* Replaces the hand-maintained `Exclude<>` + parallel mapped-type pattern
|
|
856
|
+
* (zzz had this at `zzz/src/lib/server/zzz_action_handlers.ts:42-66`).
|
|
857
|
+
*
|
|
858
|
+
* The context type is consumer-defined (e.g. zzz's `ZzzHandlerContext`). Pass
|
|
859
|
+
* `context_type` to name it; the helper assumes it's importable or defined
|
|
860
|
+
* in the emitted module's scope (consumer's responsibility).
|
|
861
|
+
*
|
|
862
|
+
* Adds `ActionInputs` / `ActionOutputs` type imports from `collections_path`
|
|
863
|
+
* and the `BackendRequestResponseMethod` import from `metatypes_path`.
|
|
864
|
+
*
|
|
865
|
+
* @param options.type_name - default `'BackendActionHandlers'`.
|
|
866
|
+
* @param options.method_enum_name - default `'BackendRequestResponseMethod'`.
|
|
867
|
+
* Pair with `generate_action_method_enums` emitting the `'backend_handled'` kind.
|
|
868
|
+
* @param options.context_type - default `'BackendHandlerContext'`. Caller's
|
|
869
|
+
* handler context type — must be in scope at the emit site.
|
|
870
|
+
* @param options.collections_path - default `'./action_collections.js'`.
|
|
871
|
+
* @param options.metatypes_path - default `'./action_metatypes.js'`.
|
|
872
|
+
*/
|
|
873
|
+
export const generate_backend_action_handlers_map = (imports, options) => {
|
|
874
|
+
const type_name = options?.type_name ?? 'BackendActionHandlers';
|
|
875
|
+
const method_enum_name = options?.method_enum_name ?? 'BackendRequestResponseMethod';
|
|
876
|
+
const context_type = options?.context_type ?? 'BackendHandlerContext';
|
|
877
|
+
const collections_path = options?.collections_path ?? DEFAULT_COLLECTIONS_PATH;
|
|
878
|
+
const metatypes_path = options?.metatypes_path ?? DEFAULT_METATYPES_PATH;
|
|
879
|
+
imports.add_types(collections_path, 'ActionInputs', 'ActionOutputs');
|
|
880
|
+
imports.add_type(metatypes_path, method_enum_name);
|
|
881
|
+
return `/**
|
|
882
|
+
* Typed handler map for request_response actions the backend handles.
|
|
883
|
+
* One entry per ${method_enum_name}; each handler receives the typed input
|
|
884
|
+
* and returns the typed output (sync or async).
|
|
885
|
+
*/
|
|
886
|
+
export type ${type_name} = {
|
|
887
|
+
[K in ${method_enum_name}]: (
|
|
888
|
+
input: ActionInputs[K],
|
|
889
|
+
ctx: ${context_type},
|
|
890
|
+
) => ActionOutputs[K] | Promise<ActionOutputs[K]>;
|
|
891
|
+
};`;
|
|
892
|
+
};
|
|
893
|
+
/**
|
|
894
|
+
* Multi-source consumer helper. Takes a list of `{ns, module, specs}` rows,
|
|
895
|
+
* registers `import * as ns from module` for each on `imports`, builds the
|
|
896
|
+
* `method_to_ns` lookup with duplicate-method detection, and returns
|
|
897
|
+
* `{qualify_spec, all_specs}` ready to thread through the high-level
|
|
898
|
+
* helpers.
|
|
899
|
+
*
|
|
900
|
+
* Closes the per-file boilerplate gap that kept tx + visiones on hand-rolled
|
|
901
|
+
* template strings even after `qualify_spec?` landed in API review II — the
|
|
902
|
+
* per-call callback wasn't enough; the import dance + dup-check was the
|
|
903
|
+
* real boilerplate.
|
|
904
|
+
*
|
|
905
|
+
* @example
|
|
906
|
+
* ```ts
|
|
907
|
+
* const sources = [
|
|
908
|
+
* {ns: 'tx_specs', module: './action_specs.js', specs: all_tx_action_specs},
|
|
909
|
+
* {ns: 'admin_specs', module: '@fuzdev/fuz_app/auth/admin_action_specs.js', specs: all_admin_action_specs},
|
|
910
|
+
* ];
|
|
911
|
+
*
|
|
912
|
+
* export const gen: Gen = ({origin_path}) => {
|
|
913
|
+
* const imports = new ImportBuilder();
|
|
914
|
+
* const {qualify_spec, all_specs} = create_namespace_qualifier(sources, imports);
|
|
915
|
+
* return compose_gen_file({
|
|
916
|
+
* origin_path,
|
|
917
|
+
* imports,
|
|
918
|
+
* blocks: [
|
|
919
|
+
* generate_action_specs_record(all_specs, imports, {qualify_spec}),
|
|
920
|
+
* generate_action_inputs_outputs(all_specs, imports, {qualify_spec}),
|
|
921
|
+
* ],
|
|
922
|
+
* });
|
|
923
|
+
* };
|
|
924
|
+
* ```
|
|
925
|
+
*
|
|
926
|
+
* @throws if two sources contain the same method name (same-method detection
|
|
927
|
+
* is the consumer's primary debugging signal).
|
|
928
|
+
*/
|
|
929
|
+
export const create_namespace_qualifier = (sources, imports) => {
|
|
930
|
+
const method_to_ns = new Map();
|
|
931
|
+
const all_specs = [];
|
|
932
|
+
for (const { ns, module, specs } of sources) {
|
|
933
|
+
imports.add(module, `* as ${ns}`);
|
|
934
|
+
for (const spec of specs) {
|
|
935
|
+
if (method_to_ns.has(spec.method)) {
|
|
936
|
+
throw new Error(`duplicate action method across sources: ${spec.method} (in ${method_to_ns.get(spec.method)} and ${ns})`);
|
|
937
|
+
}
|
|
938
|
+
method_to_ns.set(spec.method, ns);
|
|
939
|
+
all_specs.push(spec);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const qualify_spec = (spec) => {
|
|
943
|
+
const ns = method_to_ns.get(spec.method);
|
|
944
|
+
if (!ns) {
|
|
945
|
+
throw new Error(`unknown action method passed to qualify_spec: ${spec.method} — not in any registered source`);
|
|
946
|
+
}
|
|
947
|
+
return `${ns}.${spec.method}_action_spec`;
|
|
948
|
+
};
|
|
949
|
+
return { qualify_spec, all_specs };
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* Wrap the per-`*.gen.ts` boilerplate (banner + `imports.build()` +
|
|
953
|
+
* blocks join + template literal) into one call. Returns the full file body
|
|
954
|
+
* as a string ready to return from a `Gen` function.
|
|
955
|
+
*
|
|
956
|
+
* Each consumer producer collapses to one `compose_gen_file` call wrapping
|
|
957
|
+
* the helper invocations.
|
|
958
|
+
*
|
|
959
|
+
* @example
|
|
960
|
+
* ```ts
|
|
961
|
+
* export const gen: Gen = ({origin_path}) => {
|
|
962
|
+
* const imports = new ImportBuilder();
|
|
963
|
+
* return compose_gen_file({
|
|
964
|
+
* origin_path,
|
|
965
|
+
* imports,
|
|
966
|
+
* blocks: [
|
|
967
|
+
* generate_action_specs_record(all_action_specs, imports),
|
|
968
|
+
* generate_action_inputs_outputs(all_action_specs, imports),
|
|
969
|
+
* generate_action_event_datas(all_action_specs, imports),
|
|
970
|
+
* ],
|
|
971
|
+
* });
|
|
972
|
+
* };
|
|
973
|
+
* ```
|
|
974
|
+
*
|
|
975
|
+
* Empty blocks (`''`) are filtered out so helpers that short-circuit on
|
|
976
|
+
* empty spec sets don't introduce stray double blank lines.
|
|
977
|
+
*/
|
|
978
|
+
export const compose_gen_file = (input) => {
|
|
979
|
+
const banner = create_banner(input.origin_path);
|
|
980
|
+
const body = input.blocks.filter(Boolean).join('\n\n');
|
|
981
|
+
return `
|
|
982
|
+
// ${banner}
|
|
983
|
+
|
|
984
|
+
${input.imports.build()}
|
|
985
|
+
|
|
986
|
+
${body}
|
|
987
|
+
|
|
988
|
+
// ${banner}
|
|
989
|
+
`;
|
|
990
|
+
};
|
|
@@ -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
|
|
29
|
-
get
|
|
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
|
|
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"}
|