@opetope/react 0.14.0 → 0.16.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 (42) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +67 -48
  3. package/dist/act-delivery.d.ts +20 -0
  4. package/dist/command-hook-controller.d.ts +13 -3
  5. package/dist/command-hook.d.ts +8 -8
  6. package/dist/contribution-frame-DL0dyVP4.js +2 -0
  7. package/dist/contribution-frame-DL0dyVP4.js.map +1 -0
  8. package/dist/{contribution-isolation-Bzfbt4iM.js → contribution-isolation-Ig70xFiC.js} +2 -2
  9. package/dist/contribution-isolation-Ig70xFiC.js.map +1 -0
  10. package/dist/contribution-isolation.d.ts +3 -3
  11. package/dist/errors.d.ts +4 -3
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/integration.d.ts +0 -4
  16. package/dist/integration.js +1 -1
  17. package/dist/integration.js.map +1 -1
  18. package/dist/model-hook.d.ts +5 -4
  19. package/dist/model-selection-snapshot.d.ts +6 -7
  20. package/dist/readable-hooks.d.ts +5 -8
  21. package/dist/requires-models.d.ts +4 -7
  22. package/dist/resource-command.d.ts +25 -26
  23. package/dist/resource-hook.d.ts +3 -7
  24. package/dist/resource-verb-hook.d.ts +28 -0
  25. package/dist/scenario-diagnostics.d.ts +3 -1
  26. package/dist/scenario-mount.d.ts +2 -1
  27. package/dist/scenario-slot.d.ts +6 -15
  28. package/dist/scenario-types.d.ts +68 -11
  29. package/dist/scenario-wait.d.ts +8 -1
  30. package/dist/scenario.d.ts +5 -3
  31. package/dist/selection-hooks.d.ts +6 -4
  32. package/dist/selector-sources.d.ts +64 -0
  33. package/dist/slot.d.ts +9 -7
  34. package/dist/testing.d.ts +2 -8
  35. package/dist/testing.js +3 -3
  36. package/dist/testing.js.map +1 -1
  37. package/package.json +5 -5
  38. package/dist/contribution-frame-Bh104T5Z.js +0 -2
  39. package/dist/contribution-frame-Bh104T5Z.js.map +0 -1
  40. package/dist/contribution-isolation-Bzfbt4iM.js.map +0 -1
  41. package/dist/feature-boundary.d.ts +0 -29
  42. package/dist/feature-demand.d.ts +0 -21
@@ -1,6 +1,2 @@
1
1
  export { ContributionBoundary } from './contribution-isolation.js';
2
2
  export type { ContributionBoundaryProps, ContributionErrorContent, ContributionFailure, } from './contribution-isolation.js';
3
- export { FeatureBoundary, FeatureBoundaryError, useFeatureRetry } from './feature-boundary.js';
4
- export type { FeatureBoundaryProps } from './feature-boundary.js';
5
- export { useFeature } from './feature-demand.js';
6
- export type { FeatureDemandLease, FeatureDemandResult, FeatureDemandSource, FeatureDemandState, } from './feature-demand.js';
@@ -1,2 +1,2 @@
1
- import{a as M}from"./contribution-isolation-Bzfbt4iM.js";import{jsx as C}from"react/jsx-runtime";import{useCallback as f,useSyncExternalStore as E,useMemo as h,useEffect as x,useState as S,createContext as B,useContext as g}from"react";const y=()=>{};function b(e){const r=f(t=>e.subscribe(t),[e]),u=f(()=>e.getState(),[e]),d=E(r,u,u),n=h(()=>({current:null,source:e}),[e]);x(()=>{const t=e.acquire();return t.settled.catch(y),()=>{t.release().catch(y)}},[e]);const o=f(()=>{if(n.current!==null)return n.current.settled;let t=()=>{};const s={lease:null,settled:new Promise(p=>{t=p})},c=()=>{n.current===s&&(n.current=null),t()};n.current=s;let i;try{i=n.source.acquire()}catch{return c(),s.settled}return s.lease=i,i.settled.catch(y).finally(c),i.release().catch(y),s.settled},[n]);return h(()=>Object.freeze({retry:o,state:d}),[o,d])}const F=B(void 0);class v extends Error{code;name="FeatureBoundaryError";constructor(r,u){super(u),this.code=r}}function w(){const e=g(F);if(e===void 0)throw new v("missing","Feature error hooks may only be used by the error subtree of a FeatureBoundary.");return e}function q(){return w().retry}function j({children:e,demand:r,error:u,fallback:d}){const{retry:n,state:o}=b(r),[t,s]=S(),c=f(()=>{const a={},m=()=>s(l=>(l==null?void 0:l.attempt)===a?void 0:l);s({attempt:a,demand:r});try{n().finally(m)}catch(l){throw m(),l}},[r,n]),i=h(()=>({retry:c}),[c]),p=(t==null?void 0:t.demand)===r;if(x(()=>{s(a=>a===void 0||a.demand===r&&o.instance===null?a:void 0)},[r,o.instance]),o.instance!==null)return typeof e=="function"?e(o.instance):e;if(o.error!==null&&!p){const a=typeof u=="function"?u({error:o.error,retry:c}):u;return C(F,{value:i,children:a})}return d}export{M as ContributionBoundary,j as FeatureBoundary,v as FeatureBoundaryError,b as useFeature,q as useFeatureRetry};
1
+ import{C as n}from"./contribution-isolation-Ig70xFiC.js";export{n as ContributionBoundary};
2
2
  //# sourceMappingURL=integration.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","sources":["../src/feature-demand.ts","../src/feature-boundary.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';\n\ninterface FeatureDemandLease {\n readonly release: () => Promise<void>;\n readonly settled: Promise<void>;\n}\n\ninterface FeatureDemandState<Ready> {\n readonly error: unknown | null;\n readonly instance: Ready | null;\n}\n\ninterface FeatureDemandSource<Ready> {\n acquire(): FeatureDemandLease;\n getState(): FeatureDemandState<Ready>;\n subscribe(listener: () => void): () => void;\n}\n\ninterface FeatureDemandResult<Ready> {\n /** Resolves when the attempt it started has settled, in success and in failure alike (D198). */\n readonly retry: () => Promise<void>;\n readonly state: FeatureDemandState<Ready>;\n}\n\nconst ignoreFailure = (): void => undefined;\n\nfunction useFeature<Ready>(source: FeatureDemandSource<Ready>): FeatureDemandResult<Ready> {\n const subscribe = useCallback((listener: () => void) => source.subscribe(listener), [source]);\n const getSnapshot = useCallback(() => source.getState(), [source]);\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n const retrying = useMemo<{\n current: { lease: FeatureDemandLease | null; readonly settled: Promise<void> } | null;\n readonly source: FeatureDemandSource<Ready>;\n }>(() => ({ current: null, source }), [source]);\n\n useEffect(() => {\n const demand = source.acquire();\n void demand.settled.catch(ignoreFailure);\n\n return () => void demand.release().catch(ignoreFailure);\n }, [source]);\n\n const retry = useCallback((): Promise<void> => {\n // Single-flight per source: a second call joins the attempt already running instead of starting another.\n if (retrying.current !== null) return retrying.current.settled;\n\n // D198: `acquire` runs foreign code, and a host that answers it synchronously may call `retry` again. The\n // attempt is reserved with its own promise before that call, so the reentrant caller joins this attempt\n // instead of taking a second lease of its own.\n let settle = (): void => undefined;\n const attempt = {\n lease: null as FeatureDemandLease | null,\n settled: new Promise<void>(resolve => {\n settle = resolve;\n }),\n };\n const finish = (): void => {\n if (retrying.current === attempt) retrying.current = null;\n\n settle();\n };\n retrying.current = attempt;\n\n let lease: FeatureDemandLease;\n\n try {\n lease = retrying.source.acquire();\n } catch {\n // D198: `retry` answers with the promise of the attempt it started, and an acquisition that refused is an\n // attempt that is over. The refusal belongs to the state the source publishes, which is where the caller\n // reads it; throwing it back here would leave every caller of `retry` waiting for an attempt that ended.\n finish();\n\n return attempt.settled;\n }\n\n attempt.lease = lease;\n void lease.settled.catch(ignoreFailure).finally(finish);\n void lease.release().catch(ignoreFailure);\n\n return attempt.settled;\n }, [retrying]);\n\n return useMemo(() => Object.freeze({ retry, state }), [retry, state]);\n}\n\nexport { useFeature };\nexport type { FeatureDemandLease, FeatureDemandResult, FeatureDemandSource, FeatureDemandState };\n","import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport { useFeature } from './feature-demand';\nimport type { FeatureDemandSource } from './feature-demand';\n\n/**\n * D177: a branch may be a node or a render callback. The callback receives what only the boundary knows — the ready\n * instance, or the failure with the retry that belongs to this demand — so a ready consumer needs no second\n * `useFeature` and no second lease. A function is never a `ReactNode`, so the two forms cannot be confused.\n */\ntype FeatureBoundaryChildren<Ready> = ((instance: Ready) => ReactNode) | ReactNode;\ntype FeatureBoundaryErrorContent =\n | ((context: { readonly error: unknown; readonly retry: () => void }) => ReactNode)\n | ReactNode;\n\n/** The boundary keeps the feature open while it is mounted; its UI enters the host through slots (D85). */\ntype FeatureBoundaryProps<Ready = unknown> = {\n readonly children: FeatureBoundaryChildren<Ready>;\n readonly demand: FeatureDemandSource<Ready>;\n readonly error: FeatureBoundaryErrorContent;\n readonly fallback: ReactNode;\n};\n\n/** Only `retry` is read: the error itself belongs to the `error` subtree a host already renders (D142). */\ntype FeatureBoundaryErrorContext = {\n readonly retry: () => void;\n};\n\n/** D198: the error is hidden for the life of the attempt that was started, not until the error object changes. */\ntype SuppressedError = {\n readonly attempt: object;\n readonly demand: object;\n};\n\nconst ErrorContext = createContext<FeatureBoundaryErrorContext | undefined>(undefined);\n\n/** One class per subject with the state in `code` (D69): the boundary is its own subject, not a feature failure. */\nclass FeatureBoundaryError extends Error {\n override readonly name = 'FeatureBoundaryError';\n\n constructor(\n readonly code: 'missing',\n message: string,\n ) {\n super(message);\n }\n}\n\nfunction useBoundaryErrorContext(): FeatureBoundaryErrorContext {\n const context = useContext(ErrorContext);\n\n if (context === undefined) {\n throw new FeatureBoundaryError(\n 'missing',\n 'Feature error hooks may only be used by the error subtree of a FeatureBoundary.',\n );\n }\n\n return context;\n}\n\nfunction useFeatureRetry(): () => void {\n return useBoundaryErrorContext().retry;\n}\n\nfunction FeatureBoundary<Ready>({ children, demand, error, fallback }: FeatureBoundaryProps<Ready>): ReactNode {\n const { retry: requestRetry, state } = useFeature(demand);\n const [suppressed, setSuppressed] = useState<SuppressedError>();\n const retry = useCallback(() => {\n const attempt = {};\n // The attempt is over: whatever it ended with, including the very same error object, is the answer to show.\n const reveal = (): void => setSuppressed(current => (current?.attempt === attempt ? undefined : current));\n setSuppressed({ attempt, demand });\n\n try {\n void requestRetry().finally(reveal);\n } catch (error) {\n // A retry that refuses synchronously has no promise to settle, and an error hidden by it would never be\n // shown again: the suppression is lifted here before the refusal goes back to whoever asked (D198).\n reveal();\n\n throw error;\n }\n }, [demand, requestRetry]);\n const errorContext = useMemo(() => ({ retry }), [retry]);\n const errorIsSuppressed = suppressed?.demand === demand;\n\n useEffect(() => {\n // A suppression outlives neither its source nor a ready feature: only the attempt of this demand may hide it.\n setSuppressed(current =>\n current === undefined || (current.demand === demand && state.instance === null) ? current : undefined,\n );\n }, [demand, state.instance]);\n\n // Only the branch that is shown runs its callback: an abandoned branch renders nothing and computes nothing.\n if (state.instance !== null) return typeof children === 'function' ? children(state.instance) : children;\n\n if (state.error !== null && !errorIsSuppressed) {\n const content = typeof error === 'function' ? error({ error: state.error, retry }) : error;\n\n return <ErrorContext value={errorContext}>{content}</ErrorContext>;\n }\n\n return fallback;\n}\n\nexport { FeatureBoundary, FeatureBoundaryError, useFeatureRetry };\nexport type { FeatureBoundaryProps };\n"],"names":["ignoreFailure","useFeature","source","subscribe","useCallback","listener","getSnapshot","state","useSyncExternalStore","retrying","useMemo","useEffect","demand","retry","settle","attempt","resolve","finish","lease","ErrorContext","createContext","FeatureBoundaryError","code","message","useBoundaryErrorContext","context","useContext","useFeatureRetry","FeatureBoundary","children","error","fallback","requestRetry","suppressed","setSuppressed","useState","reveal","current","errorContext","errorIsSuppressed","content","_jsx"],"mappings":"4OAwBA,MAAMA,EAAgB,IAAA,GAEtB,SAASC,EAAkBC,EAAkC,CAC3D,MAAMC,EAAYC,EAAaC,GAAyBH,EAAO,UAAUG,CAAQ,EAAG,CAACH,CAAM,CAAC,EACtFI,EAAcF,EAAY,IAAMF,EAAO,SAAQ,EAAI,CAACA,CAAM,CAAC,EAC3DK,EAAQC,EAAqBL,EAAWG,EAAaA,CAAW,EAChEG,EAAWC,EAGd,KAAO,CAAE,QAAS,KAAM,OAAAR,CAAM,GAAK,CAACA,CAAM,CAAC,EAE9CS,EAAU,IAAK,CACb,MAAMC,EAASV,EAAO,QAAO,EAC7B,OAAKU,EAAO,QAAQ,MAAMZ,CAAa,EAEhC,IAAA,CAAWY,EAAO,QAAO,EAAG,MAAMZ,CAAa,EACxD,EAAG,CAACE,CAAM,CAAC,EAEX,MAAMW,EAAQT,EAAY,IAAoB,CAE5C,GAAIK,EAAS,UAAY,KAAM,OAAOA,EAAS,QAAQ,QAKvD,IAAIK,EAAS,IAAA,GACb,MAAMC,EAAU,CACd,MAAO,KACP,QAAS,IAAI,QAAcC,GAAU,CACnCF,EAASE,CACX,CAAC,GAEGC,EAAS,IAAW,CACpBR,EAAS,UAAYM,IAASN,EAAS,QAAU,MAErDK,EAAM,CACR,EACAL,EAAS,QAAUM,EAEnB,IAAIG,EAEJ,GAAI,CACFA,EAAQT,EAAS,OAAO,QAAO,CACjC,MAAQ,CAIN,OAAAQ,EAAM,EAECF,EAAQ,OACjB,CAEA,OAAAA,EAAQ,MAAQG,EACXA,EAAM,QAAQ,MAAMlB,CAAa,EAAE,QAAQiB,CAAM,EACjDC,EAAM,UAAU,MAAMlB,CAAa,EAEjCe,EAAQ,OACjB,EAAG,CAACN,CAAQ,CAAC,EAEb,OAAOC,EAAQ,IAAM,OAAO,OAAO,CAAE,MAAAG,EAAO,MAAAN,CAAK,CAAE,EAAG,CAACM,EAAON,CAAK,CAAC,CACtE,CCjDA,MAAMY,EAAeC,EAAuD,MAAS,EAGrF,MAAMC,UAA6B,KAAK,CAI3B,KAHO,KAAO,uBAEzB,YACWC,EACTC,EAAe,CAEf,MAAMA,CAAO,EAHJ,KAAA,KAAAD,CAIX,CACD,CAED,SAASE,GAAuB,CAC9B,MAAMC,EAAUC,EAAWP,CAAY,EAEvC,GAAIM,IAAY,OACd,MAAM,IAAIJ,EACR,UACA,iFAAiF,EAIrF,OAAOI,CACT,CAEA,SAASE,GAAe,CACtB,OAAOH,EAAuB,EAAG,KACnC,CAEA,SAASI,EAAuB,CAAE,SAAAC,EAAU,OAAAjB,EAAQ,MAAAkB,EAAO,SAAAC,CAAQ,EAA+B,CAChG,KAAM,CAAE,MAAOC,EAAc,MAAAzB,CAAK,EAAKN,EAAWW,CAAM,EAClD,CAACqB,EAAYC,CAAa,EAAIC,EAAQ,EACtCtB,EAAQT,EAAY,IAAK,CAC7B,MAAMW,EAAU,CAAA,EAEVqB,EAAS,IAAYF,EAAcG,IAAYA,GAAA,YAAAA,EAAS,WAAYtB,EAAU,OAAYsB,CAAQ,EACxGH,EAAc,CAAE,QAAAnB,EAAS,OAAAH,EAAQ,EAEjC,GAAI,CACGoB,EAAY,EAAG,QAAQI,CAAM,CACpC,OAASN,EAAO,CAGd,MAAAM,EAAM,EAEAN,CACR,CACF,EAAG,CAAClB,EAAQoB,CAAY,CAAC,EACnBM,EAAe5B,EAAQ,KAAO,CAAE,MAAAG,CAAK,GAAK,CAACA,CAAK,CAAC,EACjD0B,GAAoBN,GAAA,YAAAA,EAAY,UAAWrB,EAUjD,GARAD,EAAU,IAAK,CAEbuB,EAAcG,GACZA,IAAY,QAAcA,EAAQ,SAAWzB,GAAUL,EAAM,WAAa,KAAQ8B,EAAU,MAAS,CAEzG,EAAG,CAACzB,EAAQL,EAAM,QAAQ,CAAC,EAGvBA,EAAM,WAAa,KAAM,OAAO,OAAOsB,GAAa,WAAaA,EAAStB,EAAM,QAAQ,EAAIsB,EAEhG,GAAItB,EAAM,QAAU,MAAQ,CAACgC,EAAmB,CAC9C,MAAMC,EAAU,OAAOV,GAAU,WAAaA,EAAM,CAAE,MAAOvB,EAAM,MAAO,MAAAM,CAAK,CAAE,EAAIiB,EAErF,OAAOW,EAACtB,EAAY,CAAC,MAAOmB,EAAY,SAAGE,EAAO,CACpD,CAEA,OAAOT,CACT"}
1
+ {"version":3,"file":"integration.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -1,18 +1,19 @@
1
1
  import type { Command, ModelOf, PaginatedResource, Resource } from '@opetope/core';
2
2
  import type { CommandHook } from './command-hook.js';
3
3
  import type { AnyModel } from './model-binding.js';
4
- import type { SelectionContext, SelectionShape } from './model-selection-snapshot.js';
4
+ import type { SelectionContext } from './model-selection-snapshot.js';
5
5
  import type { PaginatedResourceHook, ResourceHook, ResourceKey } from './resource-command.js';
6
+ import type { RefusedSelection, SelectorModel } from './selector-sources.js';
6
7
  /**
7
8
  * D359: a field whose value is a Resource answers the hook `useResource` answers, and the mount leases it exactly as
8
9
  * `useResource` does. `read(resource.state)` is unchanged — it hands out the state and leases nothing — so naming one
9
10
  * Resource both ways registers it twice: the subscription and the lease are independent.
10
11
  */
11
- type SelectedValue<Value> = Value extends Command<infer Input, infer Output> ? CommandHook<Input, Output> : Value extends PaginatedResource<infer Data, infer Key extends ResourceKey, infer Cursor extends ResourceKey, 'consumers' | 'owner'> ? PaginatedResourceHook<Data, Key, Cursor> : Value extends Resource<infer Data, infer Key extends ResourceKey, 'consumers' | 'owner'> ? ResourceHook<Data, Key> : Value;
12
+ type SelectedValue<Value> = Value extends Command<infer Input, infer Output> ? CommandHook<Input, Output> : Value extends PaginatedResource<infer Data, infer Key extends ResourceKey, infer Cursor extends ResourceKey, 'consumers' | 'model'> ? PaginatedResourceHook<Data, Key, Cursor> : Value extends Resource<infer Data, infer Key extends ResourceKey, 'consumers' | 'model'> ? ResourceHook<Data, Key> : Value;
12
13
  type ModelSelection<Selected> = {
13
14
  readonly [Key in keyof Selected]: SelectedValue<Selected[Key]>;
14
15
  };
15
- /** D205: resolve only a granted model, explicitly read data, and reuse the ordinary per-key command consumers. */
16
+ /** Read a granted model; a selection reads sources through `read` and binds Command or Resource fields. */
16
17
  declare function useModel<Declaration extends AnyModel>(declaration: Declaration): ModelOf<Declaration>;
17
- declare function useModel<Declaration extends AnyModel, const Selected extends object>(declaration: Declaration, select: (model: ModelOf<Declaration>, context: SelectionContext) => Selected & SelectionShape<Selected>): ModelSelection<Selected>;
18
+ declare function useModel<Declaration extends AnyModel, const Selected extends object = RefusedSelection>(declaration: Declaration, select: (model: SelectorModel<ModelOf<Declaration>>, context: SelectionContext) => Selected): ModelSelection<Selected>;
18
19
  export { useModel };
@@ -2,17 +2,16 @@ import type { Readable, ResourceView } from '@opetope/core';
2
2
  import type { ResourceDemand } from '@opetope/core/internal';
3
3
  import type { AnyCommand } from './model-binding.js';
4
4
  import type { AnyResource } from './resource-command.js';
5
- type SelectionValue<Source> = Source extends ResourceView<infer Value> ? Value : Source extends Readable<infer Value> ? Value : never;
5
+ type SelectionValue<Candidate> = Candidate extends ResourceView<infer Value> ? Value : Candidate extends Readable<infer Value> ? Value : never;
6
6
  interface SelectionRead {
7
- <Source extends Readable<unknown> | ResourceView<unknown>>(source: Source): SelectionValue<Source>;
8
- <Source extends Readable<unknown> | ResourceView<unknown>, Selected>(source: Source, select: (value: SelectionValue<Source>) => Selected): Selected;
7
+ <Candidate extends Readable<unknown> | ResourceView<unknown>>(source: Candidate): SelectionValue<Candidate>;
8
+ <Candidate extends Readable<unknown> | ResourceView<unknown>, Selected>(source: Candidate, select: (value: SelectionValue<Candidate>) => Selected): Selected;
9
9
  }
10
+ /** The tracked reader passed to a `useModel` selector. Reading a view records demand for commit. */
10
11
  type SelectionContext = Readonly<{
12
+ /** Read a source for this selection, tracking it so the selection updates when it changes. */
11
13
  read: SelectionRead;
12
14
  }>;
13
- type NonSelectionRecord = readonly unknown[] | ((...arguments_: never[]) => unknown) | (abstract new (...arguments_: never[]) => unknown) | Date | PromiseLike<unknown> | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | RegExp | WeakMap<object, unknown> | WeakSet<object>;
14
- /** A named interface needs no open index signature; container and callable results are not field selections. */
15
- type SelectionShape<Selected extends object> = Selected extends NonSelectionRecord ? never : Selected;
16
15
  type SelectionRecord = Readonly<Record<PropertyKey, unknown>>;
17
16
  /**
18
17
  * D359: the Resource a field selected, and what this selection read from it — its state, and the state of its
@@ -61,4 +60,4 @@ declare function createSelectionMemory(): SelectionMemory;
61
60
  */
62
61
  declare function createSelectionSnapshot<Model>(model: Model, selectRef: SelectionReaderRef<Model>, memory: SelectionMemory): () => SelectionSnapshot;
63
62
  export { createSelectionMemory, createSelectionSnapshot, emptySelectionSnapshot };
64
- export type { ModelSelector, SelectedResource, SelectionContext, SelectionMemory, SelectionShape, SelectionSnapshot };
63
+ export type { ModelSelector, SelectedResource, SelectionContext, SelectionMemory, SelectionSnapshot };
@@ -1,10 +1,7 @@
1
- import type { Readable } from '@opetope/core';
2
- import type { EqualityOption } from '@opetope/core/internal';
1
+ import type { Readable, Resource, ResourceKey, ResourceView } from '@opetope/core';
2
+ type SourceValue<Source> = Source extends ResourceView<infer Value> ? Value : Source extends Readable<infer Value> ? Value : never;
3
3
  /**
4
- * D279: the source and the selection are positional; `equals` is the one modifier, spelled as in Core — including
5
- * D348's rule for where the comparator is read, so a named non-generic one compiles here as it does in `derive`.
4
+ * Subscribe to a Readable or lease a ResourceView for this committed mount.
6
5
  */
7
- export declare function useSelector<Value, Selected>(readable: Readable<Value>, select: (value: NoInfer<Value>) => Selected, options?: {
8
- readonly equals?: EqualityOption<NoInfer<Selected>>;
9
- }): Selected;
10
- export declare function useReadable<Value>(readable: Readable<Value>): Value;
6
+ export declare function useReadable<Source extends Readable<unknown> | Resource<unknown, ResourceKey, 'consumers' | 'model'> | ResourceView<unknown>>(source: Source extends Readable<unknown> | ResourceView<unknown> ? Source : 'read resource.state, or select the Resource in useModel'): SourceValue<Source>;
7
+ export {};
@@ -2,16 +2,13 @@ import type { FunctionComponent } from 'react';
2
2
  import type { ModelIdentity } from '@opetope/core/internal';
3
3
  type AnyModel = ModelIdentity;
4
4
  /**
5
- * The marker a component uses to state which UI models of its contribution it reads. `slot` checks by type that the
6
- * contribution grants every model of this list — it may grant more, because the mount serves its whole instance.
7
- *
8
- * The runtime authority is the mount frame, not this marker: `useModel` resolves against what the mount was given
9
- * and refuses anything else. The lint rule `opetope/require-declared-models` checks visible contribution sites and
10
- * each reader of a per-mount model within the same module. Imported implementations and dynamic declaration lists
11
- * remain unknown to this syntactic check (D85, D158, D250).
5
+ * The marker a component uses to state which UI models of its contribution it reads.
12
6
  */
13
7
  type ComponentRequiringModels<Props, Models extends readonly AnyModel[]> = FunctionComponent<Props> & {
14
8
  readonly requires: Models;
15
9
  };
10
+ /**
11
+ * Declares the UI models a component reads and returns the same component marked with them, so `slot` can check by type that the contribution grants each one.
12
+ */
16
13
  declare function requiresModels<const Models extends readonly AnyModel[]>(models: Models): <Props>(component: FunctionComponent<Props>) => ComponentRequiringModels<Props, Models>;
17
14
  export { requiresModels };
@@ -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;
@@ -2,13 +2,14 @@ import type { ScenarioHostMount } from './scenario-types.js';
2
2
  /** A reservation exists before the host callback, so reentrant close also owns a mount returned later. */
3
3
  declare class ScenarioMountLease<Mounted extends ScenarioHostMount> {
4
4
  private readonly released;
5
+ private readonly act;
5
6
  get closed(): boolean;
6
7
  private available;
7
8
  private completion;
8
9
  private handle;
9
10
  private reject;
10
11
  private resolve;
11
- constructor(released: (failed: boolean, error?: unknown) => void);
12
+ constructor(released: (failed: boolean, error?: unknown) => void, act: ((callback: () => unknown) => PromiseLike<unknown>) | undefined);
12
13
  readonly close: () => Promise<void>;
13
14
  provide(handle: Mounted | undefined): void;
14
15
  private finish;
@@ -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,50 @@
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
+ * The renderer's `act`: mount, props, unmount, close and every delivery to a mounted tree run inside it.
22
+ */
23
+ readonly act?: (callback: () => unknown) => PromiseLike<unknown>;
24
+ /**
25
+ * Records of physical activity an observation keeps, an integer from 1 to 10000.
26
+ * @default 256
27
+ */
14
28
  readonly activityCapacity?: number;
29
+ /**
30
+ * Snapshots `history()` keeps, an integer from 1 to 10000.
31
+ * @default 64
32
+ */
15
33
  readonly historyCapacity?: number;
34
+ /** The test-owned renderer: `mount(Component)` answers a handle with `unmount()`. */
16
35
  readonly host: ScenarioHost<Mounted>;
36
+ /** Deadline of readiness and of every wait without its own; the `configureTesting` default otherwise. */
17
37
  readonly timeoutMs?: number;
18
38
  };
19
39
  interface ScenarioWaitOptions {
40
+ /** Names the wait in its `ScenarioTimeoutError`. */
20
41
  readonly label?: string;
21
- /** Polls a predicate that also reads external UI state; inspection notifications always wake it immediately. */
42
+ /**
43
+ * Polls a predicate that also reads external UI state; inspection notifications always wake it immediately.
44
+ * @default 10
45
+ */
22
46
  readonly pollIntervalMs?: number;
47
+ /** Deadline of this wait; the scenario's `timeoutMs`, then the `configureTesting` default otherwise. */
23
48
  readonly timeoutMs?: number;
24
49
  }
25
50
  interface ScenarioMount<Props extends SlotProperties, Mounted extends ScenarioHostMount> {
@@ -38,20 +63,52 @@ type ScenarioOwnership = Readonly<{
38
63
  features: 'unknown' | number;
39
64
  reasons: readonly string[];
40
65
  resources: 'unknown' | number;
41
- /** This is not a GC leak assertion and does not claim coverage of arbitrary host or UI resources. */
66
+ /**
67
+ * This is not a GC leak assertion and does not claim coverage of arbitrary host or UI resources.
68
+ */
42
69
  scope: 'registered-runtime';
43
70
  status: 'complete' | 'unknown';
44
71
  }>;
45
- interface Scenario<Mounted extends ScenarioHostMount> {
72
+ type ScenarioAnswer<Value> = Exclude<Value, false | null | undefined>;
73
+ type ScenarioWaitSource<Value> = Readable<Value> | ResourceView<Value>;
74
+ /**
75
+ * A source whose every value answers needs a decision.
76
+ */
77
+ type ScenarioNullableAnswer<Value> = unknown extends Value ? unknown : [Extract<Value, false | null | undefined>] extends [never] ? {
78
+ readonly 'waitFor(source) requires accept when every source value is an answer': true;
79
+ } : unknown;
80
+ /**
81
+ * A read answers now or not at all.
82
+ */
83
+ type ScenarioSyncOnly<Value> = 0 extends 1 & Value ? unknown : [Extract<Value, PromiseLike<unknown>>] extends [never] ? unknown : {
84
+ readonly 'waitFor reads synchronously and a promise would answer its first attempt: await the read inside eventually': never;
85
+ };
86
+ /**
87
+ * Without a decision, the function and the source share one signature, so the refusal of either half is the one
88
+ * printed. The name stands in the first line: spelled out, the snapshot type would fill it before the refusal.
89
+ */
90
+ type ScenarioReadWithoutAccept<Value> = (((snapshot: RuntimeGraphSnapshot) => Value) & ScenarioSyncOnly<Value>) | (ScenarioWaitSource<Value> & ScenarioNullableAnswer<Value> & ScenarioSyncOnly<Value>);
91
+ interface ScenarioBase<Mounted extends ScenarioHostMount> {
46
92
  close(): Promise<void>;
47
93
  getSnapshot(): RuntimeGraphSnapshot;
48
94
  history(): readonly RuntimeGraphSnapshot[];
49
95
  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. */
96
+ /**
97
+ * Wakes predicates after an external fixture changes; it publishes no runtime event.
98
+ */
51
99
  notify(): void;
52
100
  ownership(): ScenarioOwnership;
53
- /** A deadline does not close the application: a test can inspect a loading body and then release its fixture. */
101
+ /**
102
+ * A deadline does not close the application: a test can inspect a loading body and then release its fixture.
103
+ */
54
104
  readonly ready: Promise<void>;
55
- waitFor(predicate: (snapshot: RuntimeGraphSnapshot) => boolean, options?: ScenarioWaitOptions): Promise<void>;
105
+ waitFor<Value>(read: ScenarioReadWithoutAccept<Value>, options?: ScenarioWaitOptions): Promise<ScenarioAnswer<Value>>;
106
+ waitFor<Value, Narrowed extends Value>(source: ScenarioWaitSource<Value> & ScenarioSyncOnly<Value>, accept: (value: Value) => value is Narrowed, options?: ScenarioWaitOptions): Promise<Narrowed>;
107
+ waitFor<Value>(source: ScenarioWaitSource<Value> & ScenarioSyncOnly<Value>, accept: (value: Value) => boolean, options?: ScenarioWaitOptions): Promise<Value>;
108
+ waitFor<Value, Narrowed extends Value>(read: ((snapshot: RuntimeGraphSnapshot) => Value) & ScenarioSyncOnly<Value>, accept: (value: Value) => value is Narrowed, options?: ScenarioWaitOptions): Promise<Narrowed>;
109
+ waitFor<Value>(read: ((snapshot: RuntimeGraphSnapshot) => Value) & ScenarioSyncOnly<Value>, accept: (value: Value) => boolean, options?: ScenarioWaitOptions): Promise<Value>;
56
110
  }
57
- export type { Scenario, ScenarioFeatures, ScenarioHost, ScenarioHostMount, ScenarioMount, ScenarioOptions, ScenarioOwnership, ScenarioWaitOptions, };
111
+ type Scenario<Mounted extends ScenarioHostMount, Choice extends ErrorReporter | 'collect' = ErrorReporter> = ScenarioBase<Mounted> & (Choice extends 'collect' ? Readonly<{
112
+ reported: readonly unknown[];
113
+ }> : {});
114
+ 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 };