@opetope/react 0.13.0 → 0.15.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 (40) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +5 -3
  3. package/dist/command-hook-controller.d.ts +13 -3
  4. package/dist/command-hook.d.ts +1 -8
  5. package/dist/contribution-frame-D7_DN6Y2.js +2 -0
  6. package/dist/contribution-frame-D7_DN6Y2.js.map +1 -0
  7. package/dist/{contribution-isolation-Bzfbt4iM.js → contribution-isolation-Ig70xFiC.js} +2 -2
  8. package/dist/contribution-isolation-Ig70xFiC.js.map +1 -0
  9. package/dist/contribution-isolation.d.ts +3 -3
  10. package/dist/errors.d.ts +4 -3
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/integration.d.ts +0 -4
  15. package/dist/integration.js +1 -1
  16. package/dist/integration.js.map +1 -1
  17. package/dist/model-hook.d.ts +5 -4
  18. package/dist/model-selection-snapshot.d.ts +6 -11
  19. package/dist/readable-hooks.d.ts +5 -8
  20. package/dist/requires-models.d.ts +4 -7
  21. package/dist/resource-command.d.ts +25 -26
  22. package/dist/resource-hook.d.ts +3 -7
  23. package/dist/resource-verb-hook.d.ts +28 -0
  24. package/dist/scenario-diagnostics.d.ts +3 -1
  25. package/dist/scenario-slot.d.ts +6 -15
  26. package/dist/scenario-types.d.ts +64 -11
  27. package/dist/scenario-wait.d.ts +8 -1
  28. package/dist/scenario.d.ts +5 -3
  29. package/dist/selection-hooks.d.ts +6 -4
  30. package/dist/selector-sources.d.ts +64 -0
  31. package/dist/slot.d.ts +9 -7
  32. package/dist/testing.d.ts +2 -8
  33. package/dist/testing.js +3 -3
  34. package/dist/testing.js.map +1 -1
  35. package/package.json +5 -5
  36. package/dist/contribution-frame-Bh104T5Z.js +0 -2
  37. package/dist/contribution-frame-Bh104T5Z.js.map +0 -1
  38. package/dist/contribution-isolation-Bzfbt4iM.js.map +0 -1
  39. package/dist/feature-boundary.d.ts +0 -29
  40. package/dist/feature-demand.d.ts +0 -21
@@ -1,21 +1,14 @@
1
- /**
2
- * D359: the verbs of a Resource, dressed as the commands a button already knows, and the recognition that answers
3
- * «is this selected field a Resource». A consumer keeps a status — `inFlight` until the operation settles, the
4
- * settled `outcome` — while the Resource stays owned by the model that declared it: cancelling this wait cancels the
5
- * wait and not the load.
6
- */
7
1
  import type { PaginationOutcome, PaginationState, Resource, ResourceOperationOutcome, ResourcePagination, ResourceState } from '@opetope/core';
8
2
  import type { ResourceKey } from '@opetope/core/internal';
9
- import type { CommandHook } from './command-hook.js';
10
- import type { CommandInvoker } from './command-hook-controller.js';
3
+ import type { ResourceVerbHook } from './resource-verb-hook.js';
11
4
  /**
12
5
  * D439: the widest Resource, and the name says so. `Resource<never, ResourceKey>` said the opposite in both
13
6
  * arguments — `never` is the narrowest `Data` a reader can be handed and a named `Key` *requires* `state.key`, so
14
7
  * hardly any member of the family fitted it. Nothing was refused, because every value reaches this name through a
15
8
  * cast, which is exactly why the name could drift. `ResourceKeyField<never>` requires no field, so
16
- * `Resource<unknown, never, 'consumers' | 'owner'>` is the one form every Resource satisfies — keyed or not, paginated or not.
9
+ * `Resource<unknown, never, 'consumers' | 'model'>` is the one form every Resource satisfies — keyed or not, paginated or not.
17
10
  */
18
- type AnyResource = Resource<unknown, never, 'consumers' | 'owner'>;
11
+ type AnyResource = Resource<unknown, never, 'consumers' | 'model'>;
19
12
  /**
20
13
  * D349, D359: a Resource is a structural contract a host may implement with an ordinary object, so the question the
21
14
  * type of a selected field asked is answered by the runtime that owns it — the registry of the materializations it
@@ -24,37 +17,43 @@ type AnyResource = Resource<unknown, never, 'consumers' | 'owner'>;
24
17
  * throws.
25
18
  */
26
19
  declare function isResource(value: unknown): boolean;
27
- /** D356: the nested capability, with `loadNext` as a command and the projected pagination state beside it. */
20
+ /**
21
+ * The nested capability — the projected pagination state and a stable `loadNext`.
22
+ */
28
23
  type ResourcePaginationHook<Cursor extends ResourceKey> = Readonly<{
29
- loadNext: CommandHook<void, PaginationOutcome<Cursor>>;
24
+ loadNext: (options?: {
25
+ readonly retry?: boolean;
26
+ }) => Promise<PaginationOutcome<Cursor>>;
30
27
  state: PaginationState<Cursor>;
31
28
  }>;
32
29
  /**
33
- * D359: what `useResource` answers, and what a field of a `useModel` selection whose value is a Resource answers.
34
- * The verbs never reject (a Resource answers an outcome), so the ordinary path of `outcome` is
35
- * `{ kind: 'ok', value }`; `failed` stays in the type because the controller turns a synchronous throw of an
36
- * invoker into it, and the wrapper below carries its own test that it never throws synchronously.
30
+ * What `useResource` answers, and what a field of a `useModel` selection whose value is a Resource answers.
37
31
  */
38
32
  type ResourceHook<Data, Key extends ResourceKey = never> = Readonly<{
39
- /** Present when the declaration carried `pagination`; `undefined` for every other Resource. */
33
+ /**
34
+ * Present when the Resource was paginated (`ctx.resource.paginate`); `undefined` for every other Resource.
35
+ */
40
36
  pagination: ResourcePaginationHook<ResourceKey> | undefined;
41
- refresh: CommandHook<void, ResourceOperationOutcome>;
42
- reset: CommandHook<void, ResourceOperationOutcome>;
43
- retry: CommandHook<void, ResourceOperationOutcome>;
37
+ refresh: ResourceVerbHook;
38
+ reset: ResourceVerbHook;
39
+ retry: ResourceVerbHook;
44
40
  state: ResourceState<Data, Key>;
45
41
  }>;
46
42
  type PaginatedResourceHook<Data, Key extends ResourceKey, Cursor extends ResourceKey> = Omit<ResourceHook<Data, Key>, 'pagination'> & Readonly<{
47
43
  pagination: ResourcePaginationHook<Cursor>;
48
44
  }>;
49
45
  type ResourceInvokers = Readonly<{
50
- loadNext: CommandInvoker;
51
- refresh: CommandInvoker;
52
- reset: CommandInvoker;
53
- retry: CommandInvoker;
46
+ /** D486: not a command — the stable function `pagination.loadNext` is, one per Resource identity. */
47
+ loadNext: (options?: {
48
+ readonly retry?: boolean;
49
+ }) => Promise<unknown>;
50
+ refresh: () => Promise<ResourceOperationOutcome>;
51
+ reset: () => Promise<ResourceOperationOutcome>;
52
+ retry: () => Promise<ResourceOperationOutcome>;
54
53
  }>;
55
- declare function paginationOf(resource: AnyResource): ResourcePagination<ResourceKey, 'consumers' | 'owner'> | undefined;
54
+ declare function paginationOf(resource: AnyResource): ResourcePagination<ResourceKey, 'consumers' | 'model'> | undefined;
56
55
  /**
57
- * One set per Resource identity. `selectEntry` and `useCommandSlot` both pick their controller by the identity of the
56
+ * One set per Resource identity. Selected and standalone hooks both pick their controller by the identity of the
58
57
  * invoker, so a set rebuilt on every render would reset `inFlight`, drop `outcome` and change `run` — which is why it
59
58
  * is memoised against the Resource itself and never assembled during a render.
60
59
  */
@@ -1,11 +1,7 @@
1
1
  import type { PaginatedResource, Resource } from '@opetope/core';
2
2
  import type { ResourceKey } from '@opetope/core/internal';
3
3
  import type { PaginatedResourceHook, ResourceHook } from './resource-command.js';
4
- /**
5
- * D359: the state of a Resource, the lease that keeps it open for as long as this component is mounted, and its verbs
6
- * as commands. The number of React hooks here does not depend on the Resource: a declaration without `pagination`
7
- * subscribes to a constant source and keeps a `loadNext` that answers `skipped`.
8
- */
9
- declare function useResource<Data, Key extends ResourceKey, Cursor extends ResourceKey>(resource: PaginatedResource<Data, Key, Cursor, 'consumers' | 'owner'>): PaginatedResourceHook<Data, Key, Cursor>;
10
- declare function useResource<Data, Key extends ResourceKey>(resource: Resource<Data, Key, 'consumers' | 'owner'>): ResourceHook<Data, Key>;
4
+ /** Lease a Resource for this committed component and read its state and optional pagination. */
5
+ declare function useResource<Data, Key extends ResourceKey, Cursor extends ResourceKey>(resource: PaginatedResource<Data, Key, Cursor, 'consumers' | 'model'>): PaginatedResourceHook<Data, Key, Cursor>;
6
+ declare function useResource<Data, Key extends ResourceKey>(resource: Resource<Data, Key, 'consumers' | 'model'>): ResourceHook<Data, Key>;
11
7
  export { useResource };
@@ -0,0 +1,28 @@
1
+ import type { ResourceOperationOutcome } from '@opetope/core';
2
+ /** A Resource verb answers its own outcome; only its consumer owns this wait and status. */
3
+ type ResourceVerbHook = Readonly<{
4
+ inFlight: boolean;
5
+ outcome: ResourceOperationOutcome | undefined;
6
+ run: () => Promise<ResourceOperationOutcome>;
7
+ }>;
8
+ type VerbNotification = (inFlight: boolean, outcome?: ResourceOperationOutcome) => void;
9
+ /**
10
+ * Only Resource verbs use this controller. A detached consumer settles its own waits without cancelling the
11
+ * materialization's work. The linked waiters add no retained collection to a Resource hook that never runs a verb.
12
+ */
13
+ declare class ResourceVerbController {
14
+ private readonly start;
15
+ private inFlight;
16
+ private notify;
17
+ private pending;
18
+ readonly run: () => Promise<ResourceOperationOutcome>;
19
+ constructor(start: () => Promise<ResourceOperationOutcome>);
20
+ attach(notify: VerbNotification): () => void;
21
+ synchronize(): void;
22
+ private unlink;
23
+ private finish;
24
+ private fail;
25
+ }
26
+ declare function useResourceVerbStatus(slot: ResourceVerbController): ResourceVerbHook;
27
+ export { ResourceVerbController, useResourceVerbStatus };
28
+ export type { ResourceVerbHook };
@@ -1,6 +1,8 @@
1
1
  import type { RuntimeGraphSnapshot } from '@opetope/runtime/internal';
2
2
  import type { ScenarioOwnership } from './scenario-types.js';
3
- /** D206: the failure carries the producer's same data-only graph/activity facts, without product payloads. */
3
+ /**
4
+ * The failure carries the producer's same data-only graph/activity facts, without product payloads.
5
+ */
4
6
  declare class ScenarioTimeoutError extends Error {
5
7
  readonly label: string;
6
8
  readonly timeoutMs: number;
@@ -1,18 +1,15 @@
1
1
  import type { FC } from 'react';
2
- import type { Command } from '@opetope/core';
3
2
  import type { ModelFixture } from './contribution-frame.js';
4
3
  import type { SlotComponentProperties, SlotContribution, SlotProperties, SlotTarget } from './slot.js';
5
4
  /**
6
- * Two forms, as `renderRoot` had (D19): without `contribution` the harness mounts what the target published, with it
7
- * the harness mounts one fixture contribution so a component test needs no generation.
5
+ * Two forms, as `renderRoot` had: without `contribution` the harness mounts what the target published.
8
6
  */
9
7
  type SlotTestFixture<Props extends SlotProperties> = {
10
8
  readonly contribution?: SlotContribution<Props>;
11
9
  readonly models?: readonly ModelFixture[];
12
10
  readonly props?: SlotComponentProperties<Props>;
13
11
  /**
14
- * Where a contained mount failure is reported (D256). A component test has no feature to report to, so without it
15
- * the failure is detached and the assertion has nothing to read.
12
+ * Where a contained mount failure is reported.
16
13
  */
17
14
  readonly reporter?: (error: unknown) => void;
18
15
  };
@@ -20,15 +17,6 @@ interface SlotTestHarness<Props extends SlotProperties> {
20
17
  readonly Slot: FC;
21
18
  readonly updateProps: (props: SlotComponentProperties<Props>) => void;
22
19
  }
23
- /**
24
- * A command a fixture publishes: the test writes the body, the harness gives it the identity a model requires. The
25
- * Command itself is the core fixture (D300); what this word adds, and what keeps it in the React package, is the
26
- * contribution binding a mount reads (D285).
27
- */
28
- declare function command<Input = void, Output = void>(run: (context: {
29
- readonly input: Input;
30
- readonly signal: AbortSignal;
31
- }) => Output | PromiseLike<Output>): Command<Input, Output>;
32
20
  /**
33
21
  * Mounts the contributions of one slot the way a host does, with optional model fixtures for a component test. D19 and D85: the same
34
22
  * ContributionMount is shared by component tests and the application scenario harness (D206).
@@ -36,6 +24,9 @@ declare function command<Input = void, Output = void>(run: (context: {
36
24
  declare function createSlotHarness<Props extends SlotProperties>(target: SlotTarget<Props>, fixture?: SlotTestFixture<Props>): SlotTestHarness<Props> & {
37
25
  readonly close: () => void;
38
26
  };
27
+ /**
28
+ * Mounts one slot contribution under the React test harness.
29
+ */
39
30
  declare function renderSlot<Props extends SlotProperties>(target: SlotTarget<Props>, fixture?: SlotTestFixture<Props>): SlotTestHarness<Props>;
40
- export { command, createSlotHarness, renderSlot };
31
+ export { createSlotHarness, renderSlot };
41
32
  export type { SlotTestFixture, SlotTestHarness };
@@ -1,25 +1,46 @@
1
1
  import type { FC } from 'react';
2
- import type { Application, ApplicationConditionBinding } from '@opetope/runtime';
3
- import type { OpenApplicationOptions, RuntimeGraphSnapshot } from '@opetope/runtime/internal';
2
+ import type { Readable, ResourceView } from '@opetope/core';
3
+ import type { Application, ApplicationConditionBinding, ApplicationImportBinding, ErrorReporter } from '@opetope/runtime';
4
+ import type { ApplicationBindingOptions, OpenApplicationOptions, RuntimeGraphSnapshot } from '@opetope/runtime/internal';
4
5
  import type { SlotComponentProperties, SlotProperties, SlotTarget } from './slot.js';
5
6
  type ScenarioFeatures = Application extends Application<infer Features> ? Features : never;
6
7
  interface ScenarioHostMount {
7
8
  unmount(): PromiseLike<void> | void;
8
9
  }
9
- /** The test owns its renderer; the package has no dependency on a DOM renderer or a test runner. */
10
+ /**
11
+ * The test owns its renderer; the package has no dependency on a DOM renderer or a test runner.
12
+ */
10
13
  interface ScenarioHost<Mounted extends ScenarioHostMount> {
11
14
  mount(Component: FC): Mounted;
12
15
  }
13
- type ScenarioOptions<Features extends ScenarioFeatures, Mounted extends ScenarioHostMount, Bindings extends readonly ApplicationConditionBinding[] = readonly ApplicationConditionBinding[]> = OpenApplicationOptions<Features, Bindings> & {
16
+ type ScenarioOptions<Features extends ScenarioFeatures, Mounted extends ScenarioHostMount, Bindings extends readonly ApplicationConditionBinding[] = readonly ApplicationConditionBinding[], Imports extends readonly ApplicationImportBinding[] = readonly ApplicationImportBinding[], Choice extends ErrorReporter | 'collect' = ErrorReporter | 'collect'> = Omit<OpenApplicationOptions<Features, Bindings, Imports>, 'reporter' | 'conditions' | 'imports'> & ApplicationBindingOptions<Features, Bindings, Imports> & {
17
+ /** Where the application's failures go: a function, or `'collect'` for a readonly `reported` list. */
18
+ readonly reporter: Choice;
19
+ } & {
20
+ /**
21
+ * Records of physical activity an observation keeps, an integer from 1 to 10000.
22
+ * @default 256
23
+ */
14
24
  readonly activityCapacity?: number;
25
+ /**
26
+ * Snapshots `history()` keeps, an integer from 1 to 10000.
27
+ * @default 64
28
+ */
15
29
  readonly historyCapacity?: number;
30
+ /** The test-owned renderer: `mount(Component)` answers a handle with `unmount()`. */
16
31
  readonly host: ScenarioHost<Mounted>;
32
+ /** Deadline of readiness and of every wait without its own; the `configureTesting` default otherwise. */
17
33
  readonly timeoutMs?: number;
18
34
  };
19
35
  interface ScenarioWaitOptions {
36
+ /** Names the wait in its `ScenarioTimeoutError`. */
20
37
  readonly label?: string;
21
- /** Polls a predicate that also reads external UI state; inspection notifications always wake it immediately. */
38
+ /**
39
+ * Polls a predicate that also reads external UI state; inspection notifications always wake it immediately.
40
+ * @default 10
41
+ */
22
42
  readonly pollIntervalMs?: number;
43
+ /** Deadline of this wait; the scenario's `timeoutMs`, then the `configureTesting` default otherwise. */
23
44
  readonly timeoutMs?: number;
24
45
  }
25
46
  interface ScenarioMount<Props extends SlotProperties, Mounted extends ScenarioHostMount> {
@@ -38,20 +59,52 @@ type ScenarioOwnership = Readonly<{
38
59
  features: 'unknown' | number;
39
60
  reasons: readonly string[];
40
61
  resources: 'unknown' | number;
41
- /** This is not a GC leak assertion and does not claim coverage of arbitrary host or UI resources. */
62
+ /**
63
+ * This is not a GC leak assertion and does not claim coverage of arbitrary host or UI resources.
64
+ */
42
65
  scope: 'registered-runtime';
43
66
  status: 'complete' | 'unknown';
44
67
  }>;
45
- interface Scenario<Mounted extends ScenarioHostMount> {
68
+ type ScenarioAnswer<Value> = Exclude<Value, false | null | undefined>;
69
+ type ScenarioWaitSource<Value> = Readable<Value> | ResourceView<Value>;
70
+ /**
71
+ * A source whose every value answers needs a decision.
72
+ */
73
+ type ScenarioNullableAnswer<Value> = unknown extends Value ? unknown : [Extract<Value, false | null | undefined>] extends [never] ? {
74
+ readonly 'waitFor(source) requires accept when every source value is an answer': true;
75
+ } : unknown;
76
+ /**
77
+ * A read answers now or not at all.
78
+ */
79
+ type ScenarioSyncOnly<Value> = 0 extends 1 & Value ? unknown : [Extract<Value, PromiseLike<unknown>>] extends [never] ? unknown : {
80
+ readonly 'waitFor reads synchronously and a promise would answer its first attempt: await the read inside eventually': never;
81
+ };
82
+ /**
83
+ * Without a decision, the function and the source share one signature, so the refusal of either half is the one
84
+ * printed. The name stands in the first line: spelled out, the snapshot type would fill it before the refusal.
85
+ */
86
+ type ScenarioReadWithoutAccept<Value> = (((snapshot: RuntimeGraphSnapshot) => Value) & ScenarioSyncOnly<Value>) | (ScenarioWaitSource<Value> & ScenarioNullableAnswer<Value> & ScenarioSyncOnly<Value>);
87
+ interface ScenarioBase<Mounted extends ScenarioHostMount> {
46
88
  close(): Promise<void>;
47
89
  getSnapshot(): RuntimeGraphSnapshot;
48
90
  history(): readonly RuntimeGraphSnapshot[];
49
91
  mount<Props extends SlotProperties>(target: SlotTarget<Props>, ...arguments_: ScenarioMountArguments<Props>): ScenarioMount<Props, Mounted>;
50
- /** Wakes predicates after an external fixture changes; it publishes no runtime event. */
92
+ /**
93
+ * Wakes predicates after an external fixture changes; it publishes no runtime event.
94
+ */
51
95
  notify(): void;
52
96
  ownership(): ScenarioOwnership;
53
- /** A deadline does not close the application: a test can inspect a loading body and then release its fixture. */
97
+ /**
98
+ * A deadline does not close the application: a test can inspect a loading body and then release its fixture.
99
+ */
54
100
  readonly ready: Promise<void>;
55
- waitFor(predicate: (snapshot: RuntimeGraphSnapshot) => boolean, options?: ScenarioWaitOptions): Promise<void>;
101
+ waitFor<Value>(read: ScenarioReadWithoutAccept<Value>, options?: ScenarioWaitOptions): Promise<ScenarioAnswer<Value>>;
102
+ waitFor<Value, Narrowed extends Value>(source: ScenarioWaitSource<Value> & ScenarioSyncOnly<Value>, accept: (value: Value) => value is Narrowed, options?: ScenarioWaitOptions): Promise<Narrowed>;
103
+ waitFor<Value>(source: ScenarioWaitSource<Value> & ScenarioSyncOnly<Value>, accept: (value: Value) => boolean, options?: ScenarioWaitOptions): Promise<Value>;
104
+ waitFor<Value, Narrowed extends Value>(read: ((snapshot: RuntimeGraphSnapshot) => Value) & ScenarioSyncOnly<Value>, accept: (value: Value) => value is Narrowed, options?: ScenarioWaitOptions): Promise<Narrowed>;
105
+ waitFor<Value>(read: ((snapshot: RuntimeGraphSnapshot) => Value) & ScenarioSyncOnly<Value>, accept: (value: Value) => boolean, options?: ScenarioWaitOptions): Promise<Value>;
56
106
  }
57
- export type { Scenario, ScenarioFeatures, ScenarioHost, ScenarioHostMount, ScenarioMount, ScenarioOptions, ScenarioOwnership, ScenarioWaitOptions, };
107
+ type Scenario<Mounted extends ScenarioHostMount, Choice extends ErrorReporter | 'collect' = ErrorReporter> = ScenarioBase<Mounted> & (Choice extends 'collect' ? Readonly<{
108
+ reported: readonly unknown[];
109
+ }> : {});
110
+ export type { Scenario, ScenarioFeatures, ScenarioHost, ScenarioHostMount, ScenarioMount, ScenarioOptions, ScenarioOwnership, ScenarioReadWithoutAccept, ScenarioWaitOptions, };
@@ -6,6 +6,13 @@ interface ScenarioWaitSource {
6
6
  readonly history: () => readonly RuntimeGraphSnapshot[];
7
7
  readonly timeoutMs: number;
8
8
  }
9
+ type ScenarioWaitRead = {
10
+ readonly kind: 'source';
11
+ readonly read: () => unknown;
12
+ } | {
13
+ readonly kind: 'graph';
14
+ readonly read: (snapshot: RuntimeGraphSnapshot) => unknown;
15
+ };
9
16
  declare class ScenarioWaiters {
10
17
  private readonly source;
11
18
  private readonly listeners;
@@ -14,7 +21,7 @@ declare class ScenarioWaiters {
14
21
  cancel(): void;
15
22
  deadline<Value>(result: PromiseLike<Value>, label: string, timeoutMs?: number): Promise<Value>;
16
23
  readonly notify: () => void;
17
- waitFor(predicate: (snapshot: RuntimeGraphSnapshot) => boolean, options: ScenarioWaitOptions): Promise<void>;
24
+ waitFor(request: ScenarioWaitRead, accept: ((value: unknown) => boolean) | undefined, options: ScenarioWaitOptions): Promise<unknown>;
18
25
  private timeout;
19
26
  }
20
27
  export { positiveDuration, ScenarioWaiters };
@@ -1,5 +1,7 @@
1
- import type { Application, ApplicationConditionBinding } from '@opetope/runtime';
1
+ import type { Application, ApplicationConditionBinding, ApplicationImportBinding, ErrorReporter } from '@opetope/runtime';
2
2
  import type { Scenario, ScenarioFeatures, ScenarioHostMount, ScenarioOptions } from './scenario-types.js';
3
- /** D206: one real application, its existing inspection session and renderer-owned Slot ingress. */
4
- declare function createScenario<const Features extends ScenarioFeatures, Mounted extends ScenarioHostMount, const Bindings extends readonly ApplicationConditionBinding[] = readonly []>(application: Application<Features>, options: ScenarioOptions<NoInfer<Features>, Mounted, Bindings>): Scenario<Mounted>;
3
+ /**
4
+ * One real application, its existing inspection session and renderer-owned Slot ingress.
5
+ */
6
+ declare function createScenario<const Features extends ScenarioFeatures, Mounted extends ScenarioHostMount, const Bindings extends readonly ApplicationConditionBinding[] = readonly [], const Imports extends readonly ApplicationImportBinding[] = readonly [], const Choice extends ErrorReporter | 'collect' = ErrorReporter>(application: Application<Features>, options: ScenarioOptions<NoInfer<Features>, Mounted, Bindings, Imports, Choice>): Scenario<Mounted, Choice>;
5
7
  export { createScenario };
@@ -1,17 +1,19 @@
1
1
  import type { Command } from '@opetope/core';
2
2
  import type { CommandHook } from './command-hook.js';
3
3
  import type { SelectedResource } from './model-selection-snapshot.js';
4
+ import type { ResourceInvokers } from './resource-command.js';
5
+ import type { ResourceVerbHook } from './resource-verb-hook.js';
4
6
  type CommandSelection = Readonly<Record<string, Command<never, unknown>>>;
5
7
  type ResourceSelection = ReadonlyMap<PropertyKey, SelectedResource>;
6
8
  /** D359: what one selected Resource answers — its three verbs, its state, and the pagination capability if it has one. */
7
9
  type SelectedResourceHook = Readonly<{
8
10
  pagination: Readonly<{
9
- loadNext: CommandHook<never, unknown>;
11
+ loadNext: ResourceInvokers['loadNext'];
10
12
  state: unknown;
11
13
  }> | undefined;
12
- refresh: CommandHook<never, unknown>;
13
- reset: CommandHook<never, unknown>;
14
- retry: CommandHook<never, unknown>;
14
+ refresh: ResourceVerbHook;
15
+ reset: ResourceVerbHook;
16
+ retry: ResourceVerbHook;
15
17
  state: unknown;
16
18
  }>;
17
19
  type SelectionHooks = Readonly<Record<PropertyKey, CommandHook<never, unknown> | SelectedResourceHook>>;
@@ -0,0 +1,64 @@
1
+ import type { Readable, ResourceView } from '@opetope/core';
2
+ import type { State } from '@opetope/core/internal';
3
+ /**
4
+ * D476: the refusals of a selector. Each is the instruction, as D401 writes a refusal; a string literal behind an alias
5
+ * prints as the literal, so the sentence reaches the message. A string and not a record, because a `this` of a record
6
+ * type reaches TS7 as a missing property that prints the sentence twice and never says `this`.
7
+ */
8
+ type SnapshotRefusal = 'getSnapshot() in a selector never updates: use read(source)';
9
+ type SubscribeRefusal = 'a selector subscribes to a source through read(source)';
10
+ type LeaseRefusal = 'select the Resource as a field, or read(view), and the component leases it';
11
+ type StateWriteRefusal = 'a selector reads; write in a command';
12
+ /**
13
+ * D476: what a selector sees in place of a `Readable` field. It is still a `Readable` — `this` is not compared where
14
+ * the target signature declares none — so `derive`, `selectByKey` and a selector annotated with the model keep taking
15
+ * it, while a direct read refuses its receiver: `read` is the one read in a selector that subscribes the component to
16
+ * what it read, and `getSnapshot()` there answers the first value forever. An alias of an object type and not an
17
+ * interface, so a declaration file that meets it prints its structure instead of refusing an unexported name.
18
+ */
19
+ type Source<Value> = {
20
+ getSnapshot(this: SnapshotRefusal): Value;
21
+ subscribe(this: SubscribeRefusal, listener: () => void): () => void;
22
+ };
23
+ type Guarded<Member, Refusal> = Member extends (...arguments_: infer Arguments) => infer Result ? (this: Refusal, ...arguments_: Arguments) => Result : Member;
24
+ /** D476, D519: the members a selector may not call on a source, each with its refusal. */
25
+ type ReadRefusals = {
26
+ readonly acquire: LeaseRefusal;
27
+ readonly getSnapshot: SnapshotRefusal;
28
+ readonly subscribe: SubscribeRefusal;
29
+ };
30
+ type GuardedMembers = ReadRefusals & {
31
+ readonly close: StateWriteRefusal;
32
+ readonly set: StateWriteRefusal;
33
+ readonly update: StateWriteRefusal;
34
+ };
35
+ /**
36
+ * D476: a source with more members than `Readable` — a Resource, a view, a collection, a pagination capability — keeps
37
+ * every one of them, and only the reads and the lease refuse a direct call. `observe()`, `state` and the members of a
38
+ * collection answer sources of the same kind. What `read` hands back is a value and is not guarded, and neither is
39
+ * what a factory field answers: a factory keeps its own signature, generic and overloaded ones included. D519: the
40
+ * writes of a `State` refuse too. The six refusals are one lookup and not a chain of six conditionals, which costs
41
+ * the `selectors` workload 141 instantiations less for the same refusals.
42
+ */
43
+ type InSelector<Field> = {
44
+ readonly [Key in keyof Field]: Key extends keyof GuardedMembers ? Key extends keyof ReadRefusals ? Guarded<Field[Key], ReadRefusals[Key]> : Field extends State<infer _Value> ? Guarded<Field[Key], StateWriteRefusal> : FieldInSelector<Field[Key]> : Key extends 'observe' ? Field[Key] extends () => infer Observed ? () => FieldInSelector<Observed> : Field[Key] : FieldInSelector<Field[Key]>;
45
+ };
46
+ type FieldInSelector<Member> = Member extends Readable<infer Value> ? [Exclude<keyof Member, keyof Readable<Value>>] extends [never] ? Source<Value> : InSelector<Member> : Member extends ResourceView<unknown> | Readonly<{
47
+ state: Readable<unknown>;
48
+ }> ? InSelector<Member> : Member;
49
+ /**
50
+ * The model a selector receives.
51
+ */
52
+ type SelectorModel<Model> = {
53
+ readonly [Key in keyof Model]: FieldInSelector<Model[Key]>;
54
+ };
55
+ /**
56
+ * D476: what a refused call answers. TypeScript resolves a refused call again without its selector, so the selection
57
+ * type is not inferred there: destructuring stays silent, and a field that is used says the selection was refused.
58
+ */
59
+ type RefusedSelection = {
60
+ readonly [Field in PropertyKey]: {
61
+ readonly 'this selection is refused: fix the error its selector reports': never;
62
+ };
63
+ };
64
+ export type { RefusedSelection, SelectorModel };
package/dist/slot.d.ts CHANGED
@@ -6,8 +6,7 @@ type SlotProperties = object | undefined;
6
6
  type EmptySlotProperties = Readonly<Record<string, never>>;
7
7
  type SlotComponentProperties<Props extends SlotProperties> = [Props] extends [undefined] ? EmptySlotProperties : Extract<Props, object>;
8
8
  /**
9
- * What a contribution publishes: the component, the optional props adapter and the optional UI models the mount
10
- * creates. Without an adapter the component takes the slot props; with one it takes exactly the adapter result (D85).
9
+ * Contribution component and adapter published for a Slot target.
11
10
  */
12
11
  type SlotContribution<Props extends SlotProperties = undefined> = {
13
12
  readonly Component: ComponentType<never>;
@@ -19,19 +18,22 @@ type SwitchSlotTarget<Route extends string, Props extends SlotProperties = undef
19
18
  readonly id: DeclarationId;
20
19
  };
21
20
  type SlotRenderProps<Props extends SlotProperties> = [Props] extends [undefined] ? {
22
- readonly props?: never;
21
+ readonly props?: 'this slot has no props: omit the props field';
23
22
  readonly target: SlotTarget<Props>;
24
23
  } : {
25
24
  readonly props: SlotComponentProperties<Props>;
26
25
  readonly target: SlotTarget<Props>;
27
26
  };
28
- /** D280: a slot is a name, so it takes its id as the first argument. */
27
+ /**
28
+ * Declare a typed Slot target by its id.
29
+ */
29
30
  declare function defineSlot<Props extends SlotProperties = undefined>(id: string): SlotTarget<Props>;
30
31
  declare function defineSwitchSlot<Route extends string, Props extends SlotProperties = undefined>(id: string): SwitchSlotTarget<Route, Props>;
31
32
  /**
32
- * Every contribution renders inside its own mount: the models its generation owns plus the UI models it declared,
33
- * created on mount and closed on unmount (D70, D85).
33
+ * Render the published contributions of a Slot target within their owning mounts.
34
34
  */
35
- declare function Slot<Props extends SlotProperties>(options: SlotRenderProps<Props>): ReactNode;
35
+ declare function Slot<Props extends SlotProperties>(options: SlotRenderProps<NoInfer<Props>> & {
36
+ readonly target: SlotTarget<Props>;
37
+ }): ReactNode;
36
38
  export { defineSlot, defineSwitchSlot, Slot };
37
39
  export type { SlotComponentProperties, SlotContribution, SlotProperties, SlotTarget, SwitchSlotTarget };
package/dist/testing.d.ts CHANGED
@@ -1,12 +1,6 @@
1
- /**
2
- * Every word of the entry below, except one: `command` is shadowed here by the fixture that also registers the
3
- * contribution binding a mount reads, so the React name keeps meaning the stronger thing it has always meant. The
4
- * core fixture is the same Command without that binding, and a test that wants it names `@opetope/core/testing` (D300).
5
- */
6
- export { CommandTimeoutError, eventually, openModel, runCommand, settled, testReadable, testResourceEpoch, waitFor, WaitTimeoutError, yieldTurn, } from '@opetope/runtime/testing';
7
- export type { CommandInputArgs, EventuallyOptions, ModelInstance, OpenModelOptions, RunCommandOptions, SettleTarget, WaitOptions, } from '@opetope/runtime/testing';
1
+ /** React test entry for scenario ownership and slot rendering. */
8
2
  export { createScenario } from './scenario.js';
9
3
  export { ScenarioTimeoutError } from './scenario-diagnostics.js';
10
- export { command, renderSlot } from './scenario-slot.js';
4
+ export { renderSlot } from './scenario-slot.js';
11
5
  export type { SlotTestFixture, SlotTestHarness } from './scenario-slot.js';
12
6
  export type { Scenario, ScenarioHost, ScenarioHostMount, ScenarioMount, ScenarioOptions, ScenarioOwnership, ScenarioWaitOptions, } from './scenario-types.js';
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import{CommandTimeoutError as ye,WaitTimeoutError as ge,eventually as Se,openModel as ve,runCommand as $e,settled as ke,testReadable as Me,testResourceEpoch as Ce,waitFor as je,yieldTurn as Ee}from"@opetope/runtime/testing";import{hasOwn as E,createAggregateError as x}from"@opetope/core/internal";import{openApplication as _}from"@opetope/runtime";import{createInspectionSession as N}from"@opetope/runtime/internal";import{createElement as B}from"react";import{declarationId as H}from"@opetope/core";import{command as W}from"@opetope/core/testing";import{e as q,a as P,b as D,M as L}from"./contribution-frame-Bh104T5Z.js";function U(e){if(e.state.kind==="ready")return[];const t=e.state.kind==="waiting"?` (${e.state.reason})`:"";return[`Feature ${e.label}: ${e.state.kind}${t}.`]}function G(e){if(e.lane===null)return`Command ${e.declaration}: ${e.state}; lane parallel; blockers none (parallel).`;const t=e.lane.blockedBy==="unknown"?"unknown":e.lane.blockedBy.join(", ");return`Command ${e.declaration}: ${e.state}; lane ${e.lane.declaration}; blockers ${t}.`}function J(e){if(e===void 0)return["Runtime activity: unknown."];const t=[];return(e.freshness==="stale"||e.truncated)&&t.push(`Activity: ${e.freshness}${e.truncated?", truncated":""}; completeness unknown.`),[...t,...e.features.map(n=>`Feature ${n.feature}: ${n.phase}; body ${n.body}; host demand unknown.`),...e.calls.map(G),...e.resources.map(n=>`Resource ${n.id}: ${n.state}; leases ${String(n.leases)}.`)]}function K(e){const t=[...e.runtime.conditions.flatMap(n=>n.state.kind==="true"?[]:[`Condition ${n.label}: ${n.state.kind}.`]),...e.runtime.instances.flatMap(U),...J(e.activity)];return t.length===0?"No registered runtime blocker observed; external UI and host work are unknown.":t.join(`
2
- `)}class T extends Error{label;timeoutMs;snapshot;history;code="timeout";constructor(t,n,r,o){super(`Scenario timed out after ${String(n)}ms: ${t}.
3
- ${K(r)}`),this.label=t,this.timeoutMs=n,this.snapshot=r,this.history=o,this.name="ScenarioTimeoutError"}}function Q(e,t){return!t||e===void 0?!1:e.closed&&!e.truncated}function V(e,t,n){if(e===void 0)return["Runtime activity is unavailable."];const r=[];return e.truncated&&r.push("The activity snapshot is truncated."),e.freshness==="stale"&&!t&&r.push("The activity snapshot is stale."),n||r.push("Successful application and mount cleanup has not been observed."),Object.freeze(r)}function X(e,t){return e===void 0||e.truncated||e.freshness==="stale"&&!t?{calls:"unknown",features:"unknown",resources:"unknown"}:{calls:e.calls.length,features:e.features.length,resources:e.resources.length}}function Y(e,t){const n=e.activity,r=Q(n,t);return Object.freeze({...X(n,r),closed:t,reasons:V(n,r,t),scope:"registered-runtime",status:r?"complete":"unknown"})}class Z{released;get closed(){return this.completion!==void 0}available=!1;completion;handle;reject;resolve;constructor(t){this.released=t}close=()=>this.completion!==void 0?this.completion:(this.completion=new Promise((t,n)=>{this.resolve=t,this.reject=n}),this.available&&this.releaseAvailable(),this.completion);provide(t){this.available=!0,this.handle=t,this.closed&&this.releaseAvailable()}finish(t,n){const r=this.resolve,o=this.reject;this.resolve=void 0,this.reject=void 0,this.released(t,n),t?o==null||o(n):r==null||r()}releaseAvailable(){const t=this.handle;this.handle=void 0,this.available=!1;try{Promise.resolve(t==null?void 0:t.unmount()).then(()=>this.finish(!1),n=>this.finish(!0,n))}catch(n){this.finish(!0,n)}}}const ee=H("testing.contribution");function te(e){return q(W(e))}function O(e,t={}){const n=t.models??[];let r=t.props??{};const o=new Set,w={getSnapshot:()=>r,subscribe:i=>(o.add(i),()=>o.delete(i))},c=t.contribution,s=t.reporter,l=s===void 0?void 0:{models:[],reporter:s};return Object.freeze({close:()=>{r={},o.clear()},Slot:()=>{const i=P(e.entries),d=P(w);return(c===void 0?i.map(p=>[p.id,p.value,D(p)??l]):[[ee,c,l]]).map(([p,f,k])=>B(L,{authority:k,contribution:f,contributionId:p,fixtures:n,key:p,slotProps:d,targetId:e.id}))},updateProps:i=>{r=i;for(const d of[...o])d()}})}function ne(e,t={}){const n=O(e,t);return Object.freeze({Slot:n.Slot,updateProps:n.updateProps})}function C(e,t){if(!Number.isFinite(e)||e<=0||e>2147483647)throw new RangeError(`${t} must be a finite duration from 0 (exclusive) to 2147483647ms.`);return e}class re{source;listeners=new Set;pending=new Set;constructor(t){this.source=t}cancel(){for(const t of[...this.pending])t(new Error("Scenario is closing."))}deadline(t,n,r=this.source.timeoutMs){return C(r,"Scenario timeoutMs"),new Promise((o,w)=>{const c=setTimeout(()=>w(this.timeout(n,r)),r);Promise.resolve(t).then(s=>{clearTimeout(c),o(s)},s=>{clearTimeout(c),w(s)})})}notify=()=>{for(const t of[...this.listeners])t()};waitFor(t,n){const r=C(n.timeoutMs??this.source.timeoutMs,"Scenario timeoutMs"),o=C(n.pollIntervalMs??10,"Scenario pollIntervalMs");return new Promise((w,c)=>{let s=!1,l=()=>{};const y=(f,k=f!==void 0)=>{s||(s=!0,l(),k?c(f):w())},i=()=>{if(!s)try{t(this.source.getSnapshot())&&y()}catch(f){y(f,!0)}},d=f=>y(f),v=setTimeout(()=>y(this.timeout(n.label??"waitFor",r)),r),p=setInterval(i,o);l=()=>{clearTimeout(v),clearInterval(p),this.listeners.delete(i),this.pending.delete(d)},this.listeners.add(i),this.pending.add(d),i()})}timeout(t,n){return new T(t,n,this.source.getSnapshot(),this.source.history())}}function R(e,t){if(!Number.isSafeInteger(e)||e<1||e>1e4)throw new RangeError(`${t} must be an integer from 1 to 10000.`);return e}function oe(e){if(e===null||typeof e!="object"||!("mount"in e)||typeof e.mount!="function")throw new TypeError("Scenario requires a host.mount(Component) adapter.")}function se(e){return{...e.cleanupFailure===void 0?{}:{cleanupFailure:e.cleanupFailure},...E(e,"conditions")?{conditions:e.conditions}:{},...E(e,"grace")?{grace:e.grace}:{},...E(e,"imports")?{imports:e.imports}:{},reporter:e.reporter}}function ie(e,t){const n=C(t.timeoutMs??1e3,"Scenario timeoutMs"),r=R(t.historyCapacity??64,"Scenario historyCapacity"),o=R(t.activityCapacity??256,"Scenario activityCapacity");let w=t.host;oe(w);let c=_(e,se(t)),s;try{s=N(c,{activityCapacity:o,ringCapacity:r})}catch(u){throw c.ready.catch(()=>{}),c.close().catch(()=>{}),u}let l="open",y=!1,i=!1;const d=[],v=new Set,p=[],f=()=>s.getSnapshot(),k=()=>Object.freeze([...d]),$=new re({getSnapshot:f,history:k,timeoutMs:n}),j=()=>{if(!i){i=!0;try{const u=f(),m=d[d.length-1];((m==null?void 0:m.stateRevision)!==u.stateRevision||m.activity!==u.activity)&&(d.push(u),d.length>r&&d.shift())}finally{i=!1}$.notify()}},I=s.subscribe(j);j();const F=$.deadline(c.ready,"application ready");F.catch(()=>{});let M,g;const A=()=>{if(M!==void 0)return M;let u,m;M=new Promise((a,h)=>{u=a,m=h}),l="closing",$.cancel();const S=[];try{S.push(c.close())}catch(a){S.push(Promise.reject(a))}return S.push(...[...v].map(a=>a.close())),Promise.allSettled(S).then(a=>{const h=[...new Set([...p,...a.flatMap(b=>b.status==="rejected"?[b.reason]:[])])];p.length=0,l="closed";try{j()}catch(b){h.push(b)}finally{I(),s.close(),v.clear(),c=void 0,w=void 0}y=h.length===0,h.length===0?u():m(x(h,"Scenario cleanup failed."))}),M};return Object.freeze({close:()=>{if(g!==void 0)return g;const u=A();return g!==void 0||(g=$.deadline(u,"scenario close"),g.catch(()=>{l==="closing"&&(g=void 0)})),g},getSnapshot:f,history:k,mount:(u,m={})=>{if(l!=="open")throw new Error("Scenario is closing.");const S=O(u,m),a=new Z((b,z)=>{v.delete(a),S.close(),b&&p.push(z)});v.add(a);let h;try{if(h=w.mount(S.Slot),h===null||typeof h!="object"||typeof h.unmount!="function")throw new TypeError("Scenario host.mount must return an unmount() handle.");a.provide(h)}catch(b){throw a.provide(void 0),a.close().catch(()=>{}),b}return Object.freeze({host:h,unmount:a.close,updateProps:b=>{if(l!=="open"||a.closed)throw new Error("Scenario mount is closed.");S.updateProps(b),$.notify()}})},notify:()=>{l==="open"&&$.notify()},ownership:()=>Y(f(),y),ready:F,waitFor:(u,m={})=>l!=="open"?Promise.reject(new Error("Scenario is closing.")):$.waitFor(u,m)})}export{ye as CommandTimeoutError,T as ScenarioTimeoutError,ge as WaitTimeoutError,te as command,ie as createScenario,Se as eventually,ve as openModel,ne as renderSlot,$e as runCommand,ke as settled,Me as testReadable,Ce as testResourceEpoch,je as waitFor,Ee as yieldTurn};
1
+ import{hasOwn as E,isResourceView as R,createAggregateError as D}from"@opetope/core/internal";import{openApplication as L}from"@opetope/runtime";import{createInspectionSession as U,testingTimeoutMs as G}from"@opetope/runtime/internal";import{createElement as J}from"react";import{declarationId as K}from"@opetope/core";import{u as I,c as Q,M as X}from"./contribution-frame-D7_DN6Y2.js";function Y(e){if(e.state.kind==="ready")return[];const t=e.state.kind==="waiting"?` (${e.state.reason})`:"";return[`Feature ${e.label}: ${e.state.kind}${t}.`]}function Z(e){if(e.lane===null)return`Command ${e.declaration}: ${e.state}; lane parallel; blockers none (parallel).`;const t=e.lane.blockedBy==="unknown"?"unknown":e.lane.blockedBy.join(", ");return`Command ${e.declaration}: ${e.state}; lane ${e.lane.declaration}; blockers ${t}.`}function q(e){if(e===void 0)return["Runtime activity: unknown."];const t=[];return(e.freshness==="stale"||e.truncated)&&t.push(`Activity: ${e.freshness}${e.truncated?", truncated":""}; completeness unknown.`),[...t,...e.features.map(n=>`Feature ${n.feature}: ${n.phase}; body ${n.body}; host demand unknown.`),...e.calls.map(Z),...e.resources.map(n=>`Resource ${n.id}: ${n.state}; leases ${String(n.leases)}.`)]}function ee(e){const t=[...e.runtime.conditions.flatMap(n=>n.state.kind==="true"?[]:[`Condition ${n.label}: ${n.state.kind}.`]),...e.runtime.instances.flatMap(Y),...q(e.activity)];return t.length===0?"No registered runtime blocker observed; external UI and host work are unknown.":t.join(`
2
+ `)}class z extends Error{label;timeoutMs;snapshot;history;code="timeout";constructor(t,n,r,a){super(`Scenario timed out after ${String(n)}ms: ${t}.
3
+ ${ee(r)}`),this.label=t,this.timeoutMs=n,this.snapshot=r,this.history=a,this.name="ScenarioTimeoutError"}}function te(e,t){return!t||e===void 0?!1:e.closed&&!e.truncated}function ne(e,t,n){if(e===void 0)return["Runtime activity is unavailable."];const r=[];return e.truncated&&r.push("The activity snapshot is truncated."),e.freshness==="stale"&&!t&&r.push("The activity snapshot is stale."),n||r.push("Successful application and mount cleanup has not been observed."),Object.freeze(r)}function re(e,t){return e===void 0||e.truncated||e.freshness==="stale"&&!t?{calls:"unknown",features:"unknown",resources:"unknown"}:{calls:e.calls.length,features:e.features.length,resources:e.resources.length}}function oe(e,t){const n=e.activity,r=te(n,t);return Object.freeze({...re(n,r),closed:t,reasons:ne(n,r,t),scope:"registered-runtime",status:r?"complete":"unknown"})}class se{released;get closed(){return this.completion!==void 0}available=!1;completion;handle;reject;resolve;constructor(t){this.released=t}close=()=>this.completion!==void 0?this.completion:(this.completion=new Promise((t,n)=>{this.resolve=t,this.reject=n}),this.available&&this.releaseAvailable(),this.completion);provide(t){this.available=!0,this.handle=t,this.closed&&this.releaseAvailable()}finish(t,n){const r=this.resolve,a=this.reject;this.resolve=void 0,this.reject=void 0,this.released(t,n),t?a==null||a(n):r==null||r()}releaseAvailable(){const t=this.handle;this.handle=void 0,this.available=!1;try{Promise.resolve(t==null?void 0:t.unmount()).then(()=>this.finish(!1),n=>this.finish(!0,n))}catch(n){this.finish(!0,n)}}}const ie=K("testing.contribution");function A(e,t={}){const n=t.models??[];let r=t.props??{};const a=new Set,m={getSnapshot:()=>r,subscribe:o=>(a.add(o),()=>a.delete(o))},w=t.contribution,h=t.reporter,u=h===void 0?void 0:{models:[],reporter:h};return Object.freeze({close:()=>{r={},a.clear()},Slot:()=>{const o=I(e.entries),S=I(m);return(w===void 0?o.map(s=>[s.id,s.value,Q(s)??u]):[[ie,w,u]]).map(([s,$,M])=>J(X,{authority:M,contribution:$,contributionId:s,fixtures:n,key:s,slotProps:S,targetId:e.id}))},updateProps:o=>{r=o;for(const S of[...a])S()}})}function ce(e,t={}){const n=A(e,t);return Object.freeze({Slot:n.Slot,updateProps:n.updateProps})}function F(e,t){if(!Number.isFinite(e)||e<=0||e>2147483647)throw new RangeError(`${t} must be a finite duration from 0 (exclusive) to 2147483647ms.`);return e}class ae{source;listeners=new Set;pending=new Set;constructor(t){this.source=t}cancel(){for(const t of[...this.pending])t(new Error("Scenario is closing."))}deadline(t,n,r=this.source.timeoutMs){return F(r,"Scenario timeoutMs"),new Promise((a,m)=>{const w=setTimeout(()=>m(this.timeout(n,r)),r);Promise.resolve(t).then(h=>{clearTimeout(w),a(h)},h=>{clearTimeout(w),m(h)})})}notify=()=>{for(const t of[...this.listeners])t()};waitFor(t,n,r){const a=F(r.timeoutMs??this.source.timeoutMs,"Scenario timeoutMs"),m=F(r.pollIntervalMs??10,"Scenario pollIntervalMs"),w=t.kind==="source"?t.read:()=>t.read(this.source.getSnapshot()),h=(u,g)=>{if(u!==null&&(typeof u=="object"||typeof u=="function")&&typeof Reflect.get(u,"then")=="function")throw new TypeError(`Scenario.waitFor ${g} synchronously; put an awaited read inside eventually.`);return u};return new Promise((u,g)=>{let o=!1,S=()=>{};const k=(l,y=!1)=>{o||(o=!0,S(),y?g(l):u(l))},s=()=>{if(!o)try{const l=h(w(),"reads");(n===void 0?l!==!1&&l!==null&&l!==void 0:h(n(l),"decides"))&&k(l)}catch(l){k(l,!0)}},$=l=>k(l,!0),M=setTimeout(()=>k(this.timeout(r.label??(t.kind==="source"?"waitFor(source)":"waitFor"),a),!0),a),v=setInterval(s,m);S=()=>{clearTimeout(M),clearInterval(v),this.listeners.delete(s),this.pending.delete($)},this.listeners.add(s),this.pending.add($),s()})}timeout(t,n){return new z(t,n,this.source.getSnapshot(),this.source.history())}}function x(e,t){if(!Number.isSafeInteger(e)||e<1||e>1e4)throw new RangeError(`${t} must be an integer from 1 to 10000.`);return e}function ue(e){if(e===null||typeof e!="object"||!("mount"in e)||typeof e.mount!="function")throw new TypeError("Scenario requires a host.mount(Component) adapter.")}function le(e){return R(e)?!0:(typeof e!="object"||e===null)&&typeof e!="function"?!1:"getSnapshot"in e&&typeof e.getSnapshot=="function"}function de(e,t){return{...e.cleanupFailure===void 0?{}:{cleanupFailure:e.cleanupFailure},...E(e,"conditions")?{conditions:e.conditions}:{},...E(e,"grace")?{grace:e.grace}:{},...E(e,"imports")?{imports:e.imports}:{},reporter:t}}function fe(e){if(e!=="collect")return{reporter:e};const t=[];return{reported:t,reporter:n=>{t.push(n)}}}function pe(e,t){const n=t.timeoutMs;n!==void 0&&F(n,"Scenario timeoutMs");const r=x(t.historyCapacity??64,"Scenario historyCapacity"),a=x(t.activityCapacity??256,"Scenario activityCapacity");let m=t.host;ue(m);const{reported:w,reporter:h}=fe(t.reporter);let u=L(e,de(t,h)),g;try{g=U(u,{activityCapacity:a,ringCapacity:r})}catch(i){throw u.ready.catch(()=>{}),u.close().catch(()=>{}),i}let o="open",S=!1,k=!1;const s=[],$=new Set,M=[],v=()=>g.getSnapshot(),l=()=>Object.freeze([...s]),y=new ae({getSnapshot:v,history:l,get timeoutMs(){return n??G()}}),P=()=>{if(!k){k=!0;try{const i=v(),f=s[s.length-1];((f==null?void 0:f.stateRevision)!==i.stateRevision||f.activity!==i.activity)&&(s.push(i),s.length>r&&s.shift())}finally{k=!1}y.notify()}},_=g.subscribe(P);P();const O=y.deadline(u.ready,"application ready");O.catch(()=>{});let C,j;const N=()=>{if(C!==void 0)return C;let i,f;C=new Promise((c,d)=>{i=c,f=d}),o="closing",y.cancel();const b=[];try{b.push(u.close())}catch(c){b.push(Promise.reject(c))}return b.push(...[...$].map(c=>c.close())),Promise.allSettled(b).then(c=>{const d=[...new Set([...M,...c.flatMap(p=>p.status==="rejected"?[p.reason]:[])])];M.length=0,o="closed";try{P()}catch(p){d.push(p)}finally{_(),g.close(),$.clear(),u=void 0,m=void 0}S=d.length===0,d.length===0?i():f(D(d,"Scenario cleanup failed."))}),C},W=()=>{if(j!==void 0)return j;const i=N();return j!==void 0||(j=y.deadline(i,"scenario close"),j.catch(()=>{o==="closing"&&(j=void 0)})),j},B=(i,f={})=>{if(o!=="open")throw new Error("Scenario is closing.");const b=A(i,f),c=new se((p,V)=>{$.delete(c),b.close(),p&&M.push(V)});$.add(c);let d;try{if(d=m.mount(b.Slot),d===null||typeof d!="object"||typeof d.unmount!="function")throw new TypeError("Scenario host.mount must return an unmount() handle.");c.provide(d)}catch(p){throw c.provide(void 0),c.close().catch(()=>{}),p}return Object.freeze({host:d,unmount:c.close,updateProps:p=>{if(o!=="open"||c.closed)throw new Error("Scenario mount is closed.");b.updateProps(p),y.notify()}})};function H(i,f,b){if(o!=="open")return Promise.reject(new Error("Scenario is closing."));const c=typeof f=="function"?f:void 0,d=(c===void 0?f:b)??{},p=R(i)?{kind:"source",read:()=>i.observe().getSnapshot()}:le(i)?{kind:"source",read:()=>i.getSnapshot()}:{kind:"graph",read:i};return y.waitFor(p,c,d)}const T={close:W,getSnapshot:v,history:l,mount:B,notify:()=>{o==="open"&&y.notify()},ownership:()=>oe(v(),S),ready:O,waitFor:H};return w!==void 0&&Object.defineProperty(T,"reported",{enumerable:!0,get:()=>Object.freeze([...w])}),Object.freeze(T)}export{z as ScenarioTimeoutError,pe as createScenario,ce as renderSlot};
4
4
  //# sourceMappingURL=testing.js.map