@opetope/runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +345 -0
- package/README.ru.md +344 -0
- package/dist/application-compiler-edges.d.ts +3 -0
- package/dist/application-compiler-edges.js +2 -0
- package/dist/application-compiler-edges.js.map +1 -0
- package/dist/application-compiler-graph.d.ts +8 -0
- package/dist/application-compiler-graph.js +2 -0
- package/dist/application-compiler-graph.js.map +1 -0
- package/dist/application-compiler.d.ts +116 -0
- package/dist/application-compiler.js +2 -0
- package/dist/application-compiler.js.map +1 -0
- package/dist/application-conditions.d.ts +18 -0
- package/dist/application-conditions.js +2 -0
- package/dist/application-conditions.js.map +1 -0
- package/dist/application-definition.d.ts +30 -0
- package/dist/application-definition.js +2 -0
- package/dist/application-definition.js.map +1 -0
- package/dist/application-error.d.ts +10 -0
- package/dist/application-error.js +2 -0
- package/dist/application-error.js.map +1 -0
- package/dist/application-execution.d.ts +43 -0
- package/dist/application-execution.js +2 -0
- package/dist/application-execution.js.map +1 -0
- package/dist/application-feature-bindings.d.ts +11 -0
- package/dist/application-feature-bindings.js +2 -0
- package/dist/application-feature-bindings.js.map +1 -0
- package/dist/application-feature-instance.d.ts +5 -0
- package/dist/application-feature-instance.js +2 -0
- package/dist/application-feature-instance.js.map +1 -0
- package/dist/application-group-order.d.ts +29 -0
- package/dist/application-group-order.js +2 -0
- package/dist/application-group-order.js.map +1 -0
- package/dist/application-instance-retirement.d.ts +36 -0
- package/dist/application-instance-retirement.js +2 -0
- package/dist/application-instance-retirement.js.map +1 -0
- package/dist/application-open-options.d.ts +29 -0
- package/dist/application-open-options.js +2 -0
- package/dist/application-open-options.js.map +1 -0
- package/dist/application-port-compiler.d.ts +28 -0
- package/dist/application-port-compiler.js +2 -0
- package/dist/application-port-compiler.js.map +1 -0
- package/dist/attachment-call-declaration.d.ts +43 -0
- package/dist/attachment-call-declaration.js +2 -0
- package/dist/attachment-call-declaration.js.map +1 -0
- package/dist/attachment-declaration.d.ts +68 -0
- package/dist/attachment-declaration.js +2 -0
- package/dist/attachment-declaration.js.map +1 -0
- package/dist/attachment-execution.d.ts +9 -0
- package/dist/attachment-execution.js +2 -0
- package/dist/attachment-execution.js.map +1 -0
- package/dist/attachment-retirement-scheduler.d.ts +14 -0
- package/dist/attachment-retirement-scheduler.js +2 -0
- package/dist/attachment-retirement-scheduler.js.map +1 -0
- package/dist/call-option-snapshot.d.ts +20 -0
- package/dist/call-option-snapshot.js +2 -0
- package/dist/call-option-snapshot.js.map +1 -0
- package/dist/compile-call-target-bindings.d.ts +14 -0
- package/dist/compile-call-target-bindings.js +2 -0
- package/dist/compile-call-target-bindings.js.map +1 -0
- package/dist/compile-module-template.d.ts +38 -0
- package/dist/compile-module-template.js +2 -0
- package/dist/compile-module-template.js.map +1 -0
- package/dist/condition-group-execution.d.ts +44 -0
- package/dist/condition-group-execution.js +2 -0
- package/dist/condition-group-execution.js.map +1 -0
- package/dist/condition-override.d.ts +28 -0
- package/dist/condition-override.js +2 -0
- package/dist/condition-override.js.map +1 -0
- package/dist/condition-source.d.ts +10 -0
- package/dist/condition-source.js +2 -0
- package/dist/condition-source.js.map +1 -0
- package/dist/condition-types.d.ts +14 -0
- package/dist/condition.d.ts +28 -0
- package/dist/condition.js +2 -0
- package/dist/condition.js.map +1 -0
- package/dist/control-registry.d.ts +56 -0
- package/dist/control-registry.js +2 -0
- package/dist/control-registry.js.map +1 -0
- package/dist/dynamic-scope-child.d.ts +23 -0
- package/dist/dynamic-scope-child.js +2 -0
- package/dist/dynamic-scope-child.js.map +1 -0
- package/dist/dynamic-scope-controller.d.ts +16 -0
- package/dist/dynamic-scope-controller.js +2 -0
- package/dist/dynamic-scope-controller.js.map +1 -0
- package/dist/feature-attachment-authoring-types.d.ts +51 -0
- package/dist/feature-attachment-lowering.d.ts +18 -0
- package/dist/feature-attachment-lowering.js +2 -0
- package/dist/feature-attachment-lowering.js.map +1 -0
- package/dist/feature-attachment.d.ts +45 -0
- package/dist/feature-attachment.js +2 -0
- package/dist/feature-attachment.js.map +1 -0
- package/dist/feature-authoring-types.d.ts +196 -0
- package/dist/feature-authoring.d.ts +15 -0
- package/dist/feature-authoring.js +2 -0
- package/dist/feature-authoring.js.map +1 -0
- package/dist/feature-body.d.ts +53 -0
- package/dist/feature-body.js +2 -0
- package/dist/feature-body.js.map +1 -0
- package/dist/feature-call-authority.d.ts +8 -0
- package/dist/feature-call-authority.js +2 -0
- package/dist/feature-call-authority.js.map +1 -0
- package/dist/feature-call-types.d.ts +39 -0
- package/dist/feature-call.d.ts +18 -0
- package/dist/feature-call.js +2 -0
- package/dist/feature-call.js.map +1 -0
- package/dist/feature-calls.d.ts +6 -0
- package/dist/feature-calls.js +2 -0
- package/dist/feature-calls.js.map +1 -0
- package/dist/feature-contract.d.ts +70 -0
- package/dist/feature-contract.js +2 -0
- package/dist/feature-contract.js.map +1 -0
- package/dist/feature-contribution-model.d.ts +38 -0
- package/dist/feature-contribution-model.js +2 -0
- package/dist/feature-contribution-model.js.map +1 -0
- package/dist/feature-contribution.d.ts +126 -0
- package/dist/feature-contribution.js +2 -0
- package/dist/feature-contribution.js.map +1 -0
- package/dist/feature-definition-api.d.ts +53 -0
- package/dist/feature-definition-support.d.ts +21 -0
- package/dist/feature-definition-support.js +2 -0
- package/dist/feature-definition-support.js.map +1 -0
- package/dist/feature-effect.d.ts +26 -0
- package/dist/feature-effect.js +2 -0
- package/dist/feature-effect.js.map +1 -0
- package/dist/feature-event.d.ts +32 -0
- package/dist/feature-event.js +2 -0
- package/dist/feature-event.js.map +1 -0
- package/dist/feature-generation.d.ts +34 -0
- package/dist/feature-generation.js +2 -0
- package/dist/feature-generation.js.map +1 -0
- package/dist/feature-lazy-generation.d.ts +6 -0
- package/dist/feature-lazy-generation.js +2 -0
- package/dist/feature-lazy-generation.js.map +1 -0
- package/dist/feature-lazy.d.ts +44 -0
- package/dist/feature-lazy.js +2 -0
- package/dist/feature-lazy.js.map +1 -0
- package/dist/feature-materialization-binding.d.ts +31 -0
- package/dist/feature-materialization-binding.js +2 -0
- package/dist/feature-materialization-binding.js.map +1 -0
- package/dist/feature-materialization-types.d.ts +45 -0
- package/dist/feature-model-dependencies.d.ts +23 -0
- package/dist/feature-model-dependencies.js +2 -0
- package/dist/feature-model-dependencies.js.map +1 -0
- package/dist/feature-model.d.ts +60 -0
- package/dist/feature-model.js +2 -0
- package/dist/feature-model.js.map +1 -0
- package/dist/feature-optional.d.ts +15 -0
- package/dist/feature-optional.js +2 -0
- package/dist/feature-optional.js.map +1 -0
- package/dist/feature-own-lowering.d.ts +22 -0
- package/dist/feature-own-lowering.js +2 -0
- package/dist/feature-own-lowering.js.map +1 -0
- package/dist/feature-port-binding.d.ts +6 -0
- package/dist/feature-port-binding.js +2 -0
- package/dist/feature-port-binding.js.map +1 -0
- package/dist/feature-port.d.ts +66 -0
- package/dist/feature-port.js +2 -0
- package/dist/feature-port.js.map +1 -0
- package/dist/feature-record.d.ts +6 -0
- package/dist/feature-record.js +2 -0
- package/dist/feature-record.js.map +1 -0
- package/dist/feature-resource.d.ts +29 -0
- package/dist/feature-resource.js +2 -0
- package/dist/feature-resource.js.map +1 -0
- package/dist/feature-scope-types.d.ts +35 -0
- package/dist/feature-scope.d.ts +29 -0
- package/dist/feature-scope.js +2 -0
- package/dist/feature-scope.js.map +1 -0
- package/dist/feature-stream.d.ts +37 -0
- package/dist/feature-stream.js +2 -0
- package/dist/feature-stream.js.map +1 -0
- package/dist/feature-timers.d.ts +14 -0
- package/dist/feature-timers.js +2 -0
- package/dist/feature-timers.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection-activity-protocol.d.ts +63 -0
- package/dist/inspection-activity.d.ts +54 -0
- package/dist/inspection-activity.js +2 -0
- package/dist/inspection-activity.js.map +1 -0
- package/dist/inspection-diff.d.ts +10 -0
- package/dist/inspection-diff.js +2 -0
- package/dist/inspection-diff.js.map +1 -0
- package/dist/inspection-module-activity.d.ts +4 -0
- package/dist/inspection-module-activity.js +2 -0
- package/dist/inspection-module-activity.js.map +1 -0
- package/dist/inspection-observer.d.ts +38 -0
- package/dist/inspection-observer.js +2 -0
- package/dist/inspection-observer.js.map +1 -0
- package/dist/inspection-plan.d.ts +33 -0
- package/dist/inspection-plan.js +2 -0
- package/dist/inspection-plan.js.map +1 -0
- package/dist/inspection-protocol.d.ts +333 -0
- package/dist/inspection-protocol.js +2 -0
- package/dist/inspection-protocol.js.map +1 -0
- package/dist/inspection-registry.d.ts +41 -0
- package/dist/inspection-registry.js +2 -0
- package/dist/inspection-registry.js.map +1 -0
- package/dist/inspection-session.d.ts +92 -0
- package/dist/inspection-session.js +2 -0
- package/dist/inspection-session.js.map +1 -0
- package/dist/inspection-snapshot.d.ts +4 -0
- package/dist/inspection-snapshot.js +2 -0
- package/dist/inspection-snapshot.js.map +1 -0
- package/dist/inspection-state.d.ts +91 -0
- package/dist/inspection-state.js +2 -0
- package/dist/inspection-state.js.map +1 -0
- package/dist/instance-demand.d.ts +31 -0
- package/dist/instance-demand.js +2 -0
- package/dist/instance-demand.js.map +1 -0
- package/dist/internal.d.ts +41 -0
- package/dist/internal.js +2 -0
- package/dist/internal.js.map +1 -0
- package/dist/keyed-scope-controller.d.ts +11 -0
- package/dist/keyed-scope-controller.js +2 -0
- package/dist/keyed-scope-controller.js.map +1 -0
- package/dist/model-kernel.d.ts +18 -0
- package/dist/model-kernel.js +2 -0
- package/dist/model-kernel.js.map +1 -0
- package/dist/module-call-context.d.ts +7 -0
- package/dist/module-call-context.js +2 -0
- package/dist/module-call-context.js.map +1 -0
- package/dist/module-call-runtime.d.ts +21 -0
- package/dist/module-call-runtime.js +2 -0
- package/dist/module-call-runtime.js.map +1 -0
- package/dist/module-generation.d.ts +57 -0
- package/dist/module-generation.js +2 -0
- package/dist/module-generation.js.map +1 -0
- package/dist/module-instance-types.d.ts +156 -0
- package/dist/module-instance.d.ts +19 -0
- package/dist/module-instance.js +2 -0
- package/dist/module-instance.js.map +1 -0
- package/dist/module-runtime-identity.d.ts +4 -0
- package/dist/module-runtime-identity.js +2 -0
- package/dist/module-runtime-identity.js.map +1 -0
- package/dist/module-scope-open.d.ts +4 -0
- package/dist/module-scope-open.js +2 -0
- package/dist/module-scope-open.js.map +1 -0
- package/dist/module-scope-retirement.d.ts +3 -0
- package/dist/module-scope-retirement.js +2 -0
- package/dist/module-scope-retirement.js.map +1 -0
- package/dist/module-template-ir.d.ts +59 -0
- package/dist/owner-generation-retirement.d.ts +6 -0
- package/dist/owner-generation-retirement.js +2 -0
- package/dist/owner-generation-retirement.js.map +1 -0
- package/dist/owner-generation-state.d.ts +123 -0
- package/dist/owner-generation-state.js +2 -0
- package/dist/owner-generation-state.js.map +1 -0
- package/dist/owner-generation.d.ts +16 -0
- package/dist/owner-generation.js +2 -0
- package/dist/owner-generation.js.map +1 -0
- package/dist/public-module-definition.d.ts +9 -0
- package/dist/public-module-definition.js +2 -0
- package/dist/public-module-definition.js.map +1 -0
- package/dist/public-module-instance.d.ts +6 -0
- package/dist/public-module-instance.js +2 -0
- package/dist/public-module-instance.js.map +1 -0
- package/dist/public-module-retirement-diagnostics.d.ts +5 -0
- package/dist/public-module-retirement-diagnostics.js +2 -0
- package/dist/public-module-retirement-diagnostics.js.map +1 -0
- package/dist/public-module-retirement.d.ts +3 -0
- package/dist/public-module-retirement.js +2 -0
- package/dist/public-module-retirement.js.map +1 -0
- package/dist/public-module-scope.d.ts +5 -0
- package/dist/public-module-scope.js +2 -0
- package/dist/public-module-scope.js.map +1 -0
- package/dist/public-module-state.d.ts +28 -0
- package/dist/public-module-state.js +2 -0
- package/dist/public-module-state.js.map +1 -0
- package/dist/public-module-types.d.ts +295 -0
- package/dist/public-module.d.ts +4 -0
- package/dist/resource-cache.d.ts +12 -0
- package/dist/resource-cache.js +2 -0
- package/dist/resource-cache.js.map +1 -0
- package/dist/resource-controller.d.ts +29 -0
- package/dist/resource-controller.js +2 -0
- package/dist/resource-controller.js.map +1 -0
- package/dist/resource-policy.d.ts +16 -0
- package/dist/resource-policy.js +2 -0
- package/dist/resource-policy.js.map +1 -0
- package/dist/resource-snapshot.d.ts +12 -0
- package/dist/resource-snapshot.js +2 -0
- package/dist/resource-snapshot.js.map +1 -0
- package/dist/resource-types.d.ts +3 -0
- package/dist/runtime-error-reporting.d.ts +4 -0
- package/dist/runtime-error-reporting.js +2 -0
- package/dist/runtime-error-reporting.js.map +1 -0
- package/dist/stream-backpressure.d.ts +18 -0
- package/dist/stream-backpressure.js +2 -0
- package/dist/stream-backpressure.js.map +1 -0
- package/dist/stream-cleanup.d.ts +17 -0
- package/dist/stream-cleanup.js +2 -0
- package/dist/stream-cleanup.js.map +1 -0
- package/dist/stream-controller-types.d.ts +51 -0
- package/dist/stream-controller.d.ts +5 -0
- package/dist/stream-controller.js +2 -0
- package/dist/stream-controller.js.map +1 -0
- package/docs/agent-guide.md +214 -0
- package/docs/agent-guide.ru.md +208 -0
- package/docs/cookbook.md +734 -0
- package/docs/cookbook.ru.md +729 -0
- package/docs/decisions.md +1437 -0
- package/docs/devtools.md +423 -0
- package/docs/devtools.ru.md +419 -0
- package/docs/how-it-works.md +521 -0
- package/docs/how-it-works.ru.md +495 -0
- package/docs/releases.md +78 -0
- package/docs/releases.ru.md +78 -0
- package/docs/spec.md +874 -0
- package/docs/spec.ru.md +884 -0
- package/package.json +72 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# How Opetope works: creation, computation, cleanup
|
|
2
|
+
|
|
3
|
+
Status: **DRAFT**, 2026-09-04; describes branch `tech-opetope`. This document explains in plain language what happens
|
|
4
|
+
inside `@opetope/core`, `@opetope/runtime` and `@opetope/react` while an application runs: what is created, when it is
|
|
5
|
+
recomputed, and how it is cleaned up. The normative source is [spec.md](spec.md); here you get only the mechanics and the reasons for them, and the `(Dnnn)` pointers lead to [decisions.md](decisions.md). File references are given for anyone who wants to check a statement against the code.
|
|
6
|
+
|
|
7
|
+
## 0. Map of concepts
|
|
8
|
+
|
|
9
|
+
| Word | What it is | Who creates it | When it dies |
|
|
10
|
+
| ------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
11
|
+
| application | a list of features, an error sink, host-bound contracts and conditions | `defineApplication` + `openApplication` | `close()` |
|
|
12
|
+
| condition (`Condition`) | a named `Readable<boolean>` a group of features lives by | `defineCondition`; the value comes from the host or a source feature | with the application |
|
|
13
|
+
| feature | a definition: `id`, `when`, `imports`, `requires`, `own`, `exports`, `provides` | `defineFeature`, once per module load | never, it is a description |
|
|
14
|
+
| feature instance | the live copy of a definition: models, calls, resources, contributions | the runtime, when the feature is needed and its conditions are true | retire, when a condition turns false or demand is released |
|
|
15
|
+
| model | a record of `Readable` and `Call` an instance owns | the factory from `own.model()`, when the instance opens | the fence and drain of the instance |
|
|
16
|
+
| call (`Call`) | an operation with a queue, a lane, cancellation and single-flight | `ctx.call` in a model, or `call` in `own` | cancelled at retire |
|
|
17
|
+
| resource, stream, event | external data materialized with a key, a retention and cancellation | `ctx.resource`, `ctx.stream`, `ctx.event` | closed in drain |
|
|
18
|
+
| contribution | what a feature contributes to someone else's target: a slot, a pipe, a registry, a port | `provides`, when the instance opens | withdrawn in fence |
|
|
19
|
+
| target (`SlotTarget`, `Pipe`, `Registry`, `Port`) | the place a contribution lands; it lives outside features, in a contracts file | `defineSlot`, `definePipe`, `defineRegistry`, `definePort` | never |
|
|
20
|
+
|
|
21
|
+
Three cross-cutting rules explain half of the behaviour:
|
|
22
|
+
|
|
23
|
+
1. **Authenticity is checked through a `WeakMap`, not by shape.** A definition, an instance, a target, a lane and a
|
|
24
|
+
binding live in closed `WeakMap`/`WeakSet` registries; anything that never entered one is refused with
|
|
25
|
+
`TypeError('… is not authentic.')`. A framework value therefore cannot be forged with an object literal.
|
|
26
|
+
2. **Everything the author supplies is snapshotted at declaration**, with an exact key-set check, and frozen. A
|
|
27
|
+
mutation of the source object after that is invisible.
|
|
28
|
+
3. **Order is always canonical.** Features by id, contributions by id, imports by key, attachments by slot; every
|
|
29
|
+
teardown walks the reverse of the assembly order.
|
|
30
|
+
|
|
31
|
+
## 1. The application
|
|
32
|
+
|
|
33
|
+
### 1.1 Compilation: `defineApplication({ id, features, reporter })`
|
|
34
|
+
|
|
35
|
+
Compilation is synchronous and happens once, before anything is opened. This is the "compile time" of the graph laws:
|
|
36
|
+
a violation fails in the first manifest test.
|
|
37
|
+
|
|
38
|
+
| Step | What happens | Where |
|
|
39
|
+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
40
|
+
| 1 | exact key set, `id` is a string and unique among the live definitions of the process, `reporter` is a function | `runtime/src/application-definition.ts` |
|
|
41
|
+
| 2 | the world closes: from the enabled features along `imports` to their providers, through the export contract | same file |
|
|
42
|
+
| 3 | features are sorted by id; a duplicate object and a duplicate id are refused | `runtime/src/application-compiler.ts` |
|
|
43
|
+
| 4 | import edges: a host contract becomes an application input, a feature definition resolves to its provider | same file |
|
|
44
|
+
| 5 | port edges `requires ↔ provides`: exactly one provider per port | `application-port-compiler.ts` |
|
|
45
|
+
| 6 | a Kahn topological sort with a lexicographic tie-break; a cycle of hard imports is an error | `application-compiler-graph.ts` |
|
|
46
|
+
| 7 | the lifetime check: a hard edge requires `provider.when ⊆ consumer.when`; a weak `optional` edge is free of the inclusion law (D105) | `application-compiler.ts` |
|
|
47
|
+
| 8 | groups by identical condition sets, the activation order inside a group and the order between groups from hard edges (D123); the graph plan for the observation port | same file, `application-group-order.ts` |
|
|
48
|
+
|
|
49
|
+
The laws checked here: one provider per contract and per port, one contract identity per id, no provider is an error,
|
|
50
|
+
no cycles, and the provider of a hard edge lives at least as long as its consumer.
|
|
51
|
+
|
|
52
|
+
### 1.2 Opening: `openApplication(app, { cleanupFailure, conditions, imports })`
|
|
53
|
+
|
|
54
|
+
| Step | What happens |
|
|
55
|
+
| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| 1 | `imports` accepts only the results of `bind(contract, value)`; every host contract is bound exactly once, and a contract that only weak imports name may be left unbound |
|
|
57
|
+
| 2 | `conditions` supplies a `Readable<boolean>` for every condition without `from`; the record type is derived from the application, so TypeScript catches a missing and an extra key, and the runtime repeats both checks |
|
|
58
|
+
| 3 | `cleanupFailure` is optional and defaults to `report`; `quarantine` makes every instance of this application keep a failed cleanup as a retryable frontier instead of a report (D182) |
|
|
59
|
+
| 4 | the controller starts in its constructor: opening begins synchronously, before the first `await` |
|
|
60
|
+
| 5 | features with an empty `when` open **sequentially**, in compilation order: `open()`, then `await ready`, then the next one |
|
|
61
|
+
| 6 | conditions with `from` are computed from the exports of the source features that opened |
|
|
62
|
+
| 7 | every condition group gets a reconciler: the desired state is `all(conditions) === true`; before it opens, a group waits for the providers of its hard edges in other groups to become ready, and before it closes it waits for their consumers to be gone (D123) |
|
|
63
|
+
| 8 | an `ApplicationExecution` with `ready` and `close()` is returned |
|
|
64
|
+
|
|
65
|
+
A group reconciler is one serialized worker with epochs: a condition change raises the epoch, fences the unfinished
|
|
66
|
+
opening and schedules a reconcile on a microtask; members open sequentially with an epoch check before each one, and
|
|
67
|
+
closing always runs in reverse order and tolerates failures. A condition source must publish a boolean synchronously
|
|
68
|
+
when it is installed, otherwise the error is immediate.
|
|
69
|
+
|
|
70
|
+
`close()` runs once: it releases the condition subscriptions in reverse order, fences the unfinished opening,
|
|
71
|
+
closes the groups in reverse order, awaits `ready`, retires the permanent features in reverse order, and collects
|
|
72
|
+
failures into one error. The observation session closes in `finally`, after the drain, so it sees the final group,
|
|
73
|
+
instance and contribution transitions (D167). The shared close promise exists before any source disposer runs.
|
|
74
|
+
If a condition subscription closes the application before returning its disposer, that disposer is released
|
|
75
|
+
immediately on return, no further group is installed, readiness rejects and close drains all started work (D210).
|
|
76
|
+
|
|
77
|
+
### 1.3 Lifetime: `when`
|
|
78
|
+
|
|
79
|
+
A feature declares the conditions it lives under: `when: [authorized, miningEnabled]`. An empty `when` is a permanent
|
|
80
|
+
feature. Features with an identical condition set open and close together, and imports order them inside the group.
|
|
81
|
+
The set-inclusion law says something simple: you may not hard-depend on something that can die before you do. A
|
|
82
|
+
consumer that wants to read the data of a shorter-lived feature takes the weak `optional` edge and gets a `Lookup`
|
|
83
|
+
projection with a `missing` state (§2.4); a weak edge neither affects the activation of its provider nor pulls it into
|
|
84
|
+
the application world (D106).
|
|
85
|
+
|
|
86
|
+
## 2. The feature instance
|
|
87
|
+
|
|
88
|
+
### 2.1 At definition, once
|
|
89
|
+
|
|
90
|
+
The eager `defineFeature` form runs the `own` factory once and turns it into an intermediate representation made
|
|
91
|
+
of three kinds of kernel node: scope, attachment, call. A split header instead declares topology; its
|
|
92
|
+
`defineFeature.body` builds that representation when the body code loads (D186, D207). Everything authored reduces
|
|
93
|
+
to these nodes:
|
|
94
|
+
|
|
95
|
+
| In `own` | What it lowers to |
|
|
96
|
+
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `attach(source, { open, close })` | a scope with one attachment |
|
|
98
|
+
| `call({ run, within, lane, once, singleFlight, policy })` | a call node bound to the attachment from `within` |
|
|
99
|
+
| `calls(imports.x, [...])` | an implicit attachment on the source with no lifecycle, plus one call per key |
|
|
100
|
+
| `lane({ within })` | a lane record, materialized per instance |
|
|
101
|
+
| `effect`, `event`, `resource`, `stream`, `scope.*` | an attachment with a controller inside |
|
|
102
|
+
| `requires.x` | one call on the hidden requirements attachment, which forwards to the port provider; with `optional(port)` it settles a `CallError` with code `unavailable` while there is no provider (D105) |
|
|
103
|
+
| `model(...)` | **not a kernel node** but a data descriptor; the model factory does not run here; owned state lives only inside the model (`ctx.state`, D139) |
|
|
104
|
+
|
|
105
|
+
`exports` is a factory of the form `({ own }) => …`. It runs when the instance opens, against a live `own`, and every
|
|
106
|
+
field of the record it returns must be a live value form — a `Call`, a `Readable` or a `Resource` — checked key by key
|
|
107
|
+
(D88). `provides` runs once, synchronously, and is split into ports and contributions by the builder that produced each
|
|
108
|
+
entry. Every attachment of a feature is critical: the instance is not ready until all of them are.
|
|
109
|
+
|
|
110
|
+
### 2.2 Opening an instance
|
|
111
|
+
|
|
112
|
+
| Step | What happens | Where |
|
|
113
|
+
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ |
|
|
114
|
+
| 1 | options are snapshotted; `imports` and `requirements` hold exactly the declared fields; an attachment takes its value only from an import or a requirement; a port value is checked against its declaration | `runtime/src/feature-generation.ts` |
|
|
115
|
+
| 2 | an instance record is created in state `assembly` | `public-module-instance.ts` |
|
|
116
|
+
| 3 | **`prepare` runs synchronously, before the first scope opens**: the model rollback participant is registered before any factory runs; then the factories create their owned nodes; then the export facade is built (it materializes `own` values lazily); then the contribution participant is registered | `module-generation.ts`, `feature-model.ts` |
|
|
117
|
+
| 4 | scopes open sequentially; before each one the runtime checks whether cancellation was requested | `module-generation.ts` |
|
|
118
|
+
| 5 | inside a scope the attachments form one wave with no order among them (D163): critical ones start before the owner commits, readiness is counted with counters and never recurses into promises; when no critical one is left the scope publishes the owner authority, calls become live, and deferred attachments start | `module-scope-open.ts` |
|
|
119
|
+
| 6 | the internal ready barrier yields `{ exports }`; contributions are then published, and only after successful publication does public `instance.ready` resolve | `feature-generation.ts` |
|
|
120
|
+
|
|
121
|
+
The laws: `prepare` must return synchronously; retire participants are registered before the first scope opens; the failure of a critical attachment moves the scope straight into retire, while the failure of a deferred one leaves the others and the scope's readiness untouched; a failed attachment lands in the quarantine frontier in canonical slot order.
|
|
122
|
+
|
|
123
|
+
Seen from the inside, an attachment is a small machine with a mailbox: at most one active and one waiting intent; a new
|
|
124
|
+
intent either joins a compatible one, replaces one that has not started, or queues up and cancels the active one.
|
|
125
|
+
Opening takes a permit from the readiness controller, collects a cleanup list (the first entry is always `close`), and
|
|
126
|
+
any error rolls the attempt back in the reverse order of that list.
|
|
127
|
+
|
|
128
|
+
### 2.3 Retire: fence, drain, settle
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
retire() → fence (synchronous, one pass) → drain (asynchronous, in parallel) → settle (policy)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
1. **Fence.** The state becomes `retiring`. Participants run in registration order: contributions are withdrawn from
|
|
135
|
+
their targets, the data scope is fenced — models are made inactive, the `AbortController` fires, `state` cells are
|
|
136
|
+
closed. Then every scope is fenced. Fencing is what turns any later work into a cancellation: the call authority is
|
|
137
|
+
invalid, and a new call through that attachment settles with a `CallError` as a cancellation rather than as a product
|
|
138
|
+
failure. Nothing is thrown; only the cleanup frontiers are collected.
|
|
139
|
+
2. **Drain.** All frontiers run in parallel; the attachments of one scope drain as one wave, with no order among them (D163). Calls: pending ones are cancelled, executing ones get an
|
|
140
|
+
abort, the `once` cache and the single-flight map are cleared, lane leases are released. Timers: the registry is
|
|
141
|
+
closed, and after that `delay` and `interval` throw. Model subscriptions and disposers run, and failures are
|
|
142
|
+
collected.
|
|
143
|
+
3. **Settle.** With no failures: state `retired`, every call target revoked, references dropped, the outcome is
|
|
144
|
+
`closed`. With failures under the `report` policy (the default): the instance is workspace anyway, and a
|
|
145
|
+
`FeatureError` with code `cleanup-failed` goes to the `reporter`. Under the `quarantine` policy: the outcome is
|
|
146
|
+
`quarantined`, with a `FeatureError` carrying code `quarantined` and an optional `retryCleanup()` capability that repeats exactly the
|
|
147
|
+
remaining cleanup frontier under a re-entry guard.
|
|
148
|
+
|
|
149
|
+
Retire is idempotent and cannot be started from its own callback.
|
|
150
|
+
|
|
151
|
+
The public instance names this operation `close()`; internal coordinators keep `retire`. Logical cancellation
|
|
152
|
+
settles a waiter without proving that an in-progress `open` or call physically stopped. The drain joins the work
|
|
153
|
+
already admitted and its late cleanup before releasing the source. A stream also joins its active `consume`
|
|
154
|
+
before closing the connection and releasing the imported value (D170).
|
|
155
|
+
|
|
156
|
+
### 2.4 Edges between features
|
|
157
|
+
|
|
158
|
+
| Edge | What the consumer sees | Who guarantees safety |
|
|
159
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
160
|
+
| `imports: { x: feature }` | the live `Call`, `Readable` and `Resource` values of the provider instance | order: the provider opens first, the consumer closes first |
|
|
161
|
+
| `optional(feature)` | `Readable<Lookup<Exports>>`: `found` while the provider instance is open, `missing` while it is closed or absent from the application | one subscription on the consumer side; the edge neither affects the provider's activation nor pulls it into the world (D105, D106) |
|
|
162
|
+
| `optional(port)` | a `Call` that settles a `CallError` with code `unavailable` while there is no live port provider | the same weak law (D105) |
|
|
163
|
+
| a host contract | the value bound with `bind` at `openApplication`; `optional(contract)` gives `Readable<Lookup<T>>` with `missing` when the host did not bind it, while `onDemand(contract)` keeps the plain value and only marks the edge as deferred | the host |
|
|
164
|
+
| a port, `port` | a `Call`; the application picked the provider | the compiler: exactly one provider |
|
|
165
|
+
|
|
166
|
+
Models never cross a feature boundary: the UI law "a model is read only inside its own feature" rests precisely on
|
|
167
|
+
that.
|
|
168
|
+
|
|
169
|
+
## 3. The model and its kernel
|
|
170
|
+
|
|
171
|
+
### 3.1 What is created when
|
|
172
|
+
|
|
173
|
+
| Step | What happens |
|
|
174
|
+
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| 1 | at definition, `own.model(Decl, deps?, factory)` only records a descriptor; the factory does not run |
|
|
176
|
+
| 2 | when the instance opens, inside `prepare`, the descriptors are walked in declaration order and each `model` receives a fresh kernel with the id `<feature>.model.<n>` |
|
|
177
|
+
| 3 | the kernel is a readiness controller (which is also the fence of the model), a timer registry on a shared `AbortSignal`, and the lists of executors and disposers |
|
|
178
|
+
| 4 | the factory runs **synchronously**; every `ctx.call`, `ctx.calls(source, keys, { lane? })` (the same selection rules as `calls` in `own`: a source method `(input, signal) => Output` becomes a `Call`, D143), `ctx.effect`, `ctx.resource`, `ctx.stream`, `ctx.event`, `ctx.scope`, `ctx.lane` and `ctx.state` creates its node immediately with the id `<model>.<kind>.<n>`; `ctx.cleanup(disposer)` registers a model cleanup (D107) |
|
|
179
|
+
| 5 | the returned record is frozen and stored under the descriptor ref |
|
|
180
|
+
| 6 | the model fence: readiness is withdrawn and the timers are closed, then the abort fires and the cells close; the drain: executors are retired, then every model disposer runs in the reverse order of registration and failures are reported; registering anything after the fence is a `TypeError` |
|
|
181
|
+
|
|
182
|
+
The dependency descriptor is a map of authentic current-feature import and call refs. Materialization resolves
|
|
183
|
+
it once to a readonly map before calling the factory; optional imports stay lookup readables. The zero-dependency
|
|
184
|
+
form is `model(Decl, factory)`. Model-to-model refs are not a dependency graph. A model-backed port selects its
|
|
185
|
+
call once after model creation and wraps even a passthrough call with the provider instance's fence (D169).
|
|
186
|
+
|
|
187
|
+
The kernel owns rollback before entering user code, so a factory that creates nodes and then throws still drains
|
|
188
|
+
them. The same rule holds for per-mount UI models. A fence synchronously closes every owned state and rejects
|
|
189
|
+
all subsequent node constructors; no late call, timer or subscription can escape that lifetime (D170).
|
|
190
|
+
|
|
191
|
+
### 3.2 `state` and `update`
|
|
192
|
+
|
|
193
|
+
`ctx.state(initial)` returns an `OwnedState<Value>`: a `Readable` with an ownership brand. `ctx.update(state, value)`
|
|
194
|
+
accepts only that — a derived `Readable` does not compile, and a sibling model's `OwnedState` is rejected at runtime
|
|
195
|
+
by a private owner lookup. A write after
|
|
196
|
+
the fence does not throw at the writer; it is reported as a `FeatureError` with code `retired`. Writes that are equal
|
|
197
|
+
under `Object.is` stay silent.
|
|
198
|
+
|
|
199
|
+
### 3.3 The call: `ctx.call({ run, lane?, once?, singleFlight?, policy? })`
|
|
200
|
+
|
|
201
|
+
`ctx.call(options)` creates the command; the `run` callback's `context.invoke(target, input)` executes an existing
|
|
202
|
+
one. The execution member is `invoke` in every context that provides it, including trusted attachment steps;
|
|
203
|
+
the factory `call` and the selected-method sugar `calls` keep their names (D243).
|
|
204
|
+
|
|
205
|
+
Selected host methods can take no parameters, one input, or an input followed by `AbortSignal`, including optional
|
|
206
|
+
input/signal (D244). The runtime calls each method with `(input, signal)` and its original receiver. It does not
|
|
207
|
+
inspect function arity; a lone parameter is always the input. Physical work that ignores cancellation still drains,
|
|
208
|
+
even if its caller has already settled as cancelled.
|
|
209
|
+
|
|
210
|
+
The default `queue` policy serializes calls through a lane; `latest` serializes them the same way but keeps one
|
|
211
|
+
waiting place; `parallel` starts them immediately, and `parallel` with a `lane` is refused. Per call:
|
|
212
|
+
|
|
213
|
+
1. if the parent signal or its own signal is already aborted, a cancelled call is returned;
|
|
214
|
+
2. a closed executor or a fenced instance yields a cancellation;
|
|
215
|
+
3. a hit in the `once` cache returns the already settled result;
|
|
216
|
+
4. a hit on a `singleFlight` key joins the live execution;
|
|
217
|
+
5. otherwise a new execution: `parallel` starts at once, `queue` and `latest` take a lane lease in FIFO order, and if
|
|
218
|
+
the lane is already on top of the caller's stack it is reused without waiting;
|
|
219
|
+
6. `latest` first frees its waiting place: the invocation of the same call that still sits last on the lane is
|
|
220
|
+
cancelled before this one asks for the lease, so one waiting input survives and lane order is untouched (D185).
|
|
221
|
+
|
|
222
|
+
A nested `context.invoke(target, input)` inherits the lifetime of the authority and the lane stack, so cancelling the
|
|
223
|
+
parent cancels the child, and a lane cannot form a cycle: requesting a lane that sits below the top of the stack is a
|
|
224
|
+
cycle error, and so is joining your own execution.
|
|
225
|
+
|
|
226
|
+
### 3.4 `effect` and `timers`
|
|
227
|
+
|
|
228
|
+
`ctx.effect({ from, run, when?, onDispose? })` subscribes to the source and runs immediately for the initial
|
|
229
|
+
value. Inside it is one serialized loop: a new value cancels the current run, waits for its disposer, checks
|
|
230
|
+
`when`, creates a new `AbortController` and timer registry, and starts `run`. Intermediate values are lost: only
|
|
231
|
+
the latest one lives. `run` returns `void` or a disposer.
|
|
232
|
+
|
|
233
|
+
`ctx.timers.delay(ms, run)` and `ctx.timers.interval(ms, run)` register their cancellation in the model registry; once
|
|
234
|
+
the registry is closed, both throw.
|
|
235
|
+
|
|
236
|
+
### 3.5 The UI models of a contribution
|
|
237
|
+
|
|
238
|
+
A slot contribution may declare `models: [model(Decl, (ctx, props) => …)]`: plans that the contribution frame
|
|
239
|
+
instantiates on every mount. `props` is a `Readable` of the slot props of that particular mount, and the kernel of
|
|
240
|
+
such a model lives from mount to unmount. The law: instances are created in commit and never in render (D188). The
|
|
241
|
+
first pass of a contribution renders nothing, a `useLayoutEffect` keyed by the identity of the contribution builds the
|
|
242
|
+
mount from the props that committed and publishes it, and React flushes that extra synchronous render before paint;
|
|
243
|
+
the contribution identity owns it until actual replacement or unmount. A render that is thrown away — StrictMode's double invoke, an abandoned
|
|
244
|
+
concurrent pass, a subtree suspended by a lazy sibling — therefore creates nothing, so there is no sweep, no adoption
|
|
245
|
+
and no rebuild, and a child layout effect always sees a live model. Incoming slot props reach the live mount through
|
|
246
|
+
its own layout effect, so an interrupted render still cannot write to a model. Granting is hybrid (D158): the `own` models of the feature reach every component of the mount's subtree implicitly, while a per-mount UI model is declared with `requiresModels([X])` by the component or hook of the same module that reads it with `useModel(X)`. The declaration belongs to each reader, not only to the contribution site (D117).
|
|
247
|
+
|
|
248
|
+
StrictMode effect replay and Suspense hide/reveal reuse the same committed model bundle and preserve state.
|
|
249
|
+
Actual replacement or unmount releases it once; when a hidden subtree unmounts, a microtask releases it because
|
|
250
|
+
React has already disconnected its layout effects. Insertion effects mark retirement without notifying Readables
|
|
251
|
+
(D209).
|
|
252
|
+
|
|
253
|
+
A mounted model reads props from the mount-owned state. Direct props and the optional props adapter read that
|
|
254
|
+
same snapshot; incoming slot props are published in layout before paint. No state update runs in insertion
|
|
255
|
+
effects. A demand retry keeps its source identity, so an unfinished retry from a replaced source cannot block
|
|
256
|
+
or overwrite the new source's retry (D170).
|
|
257
|
+
|
|
258
|
+
## 4. Reactivity
|
|
259
|
+
|
|
260
|
+
### 4.1 The contract and the sources
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
interface Readable<T> {
|
|
264
|
+
getSnapshot(): T;
|
|
265
|
+
subscribe(listener: () => void): () => void;
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
A listener is handed nothing; it re-reads. A state cell (`createState`, internal only since D142; the author receives
|
|
270
|
+
it as `ctx.state` of a model) holds the value and a set of listeners; `set` and `update` stay silent on an `Object.is`
|
|
271
|
+
match; after `close()` both reading and writing raise a `ReadableError` with code `closed`. Closing propagates:
|
|
272
|
+
`close()` is one notification to the dependents, after which active `derive`, `computed`, `collection` and
|
|
273
|
+
`selectByKey` answer `closed` instead of the last value and then stay silent; an unsubscribed derived node gets the
|
|
274
|
+
same failure on its next read (D146).
|
|
275
|
+
|
|
276
|
+
### 4.2 `derive` and `computed`
|
|
277
|
+
|
|
278
|
+
Both are nodes of one graph with a "push invalidation, pull recomputation" model: a node has a freshness of `clean`,
|
|
279
|
+
`check` or `dirty` and a monotonic revision. An unsubscribed node recomputes on demand in `getSnapshot()`, with no
|
|
280
|
+
upstream subscription needed. A node becomes active when it has listeners or dependents, and deactivates with a
|
|
281
|
+
one-microtask delay so that resubscribing within the same tick does not drop the upstream lease.
|
|
282
|
+
|
|
283
|
+
`computed({ read: get => … })` rebuilds its dependency map on every recomputation from the `get` calls and remembers
|
|
284
|
+
their revisions; in the `check` state the body does not run if no revision moved. Re-entering a computation is a
|
|
285
|
+
"cycle" error. A transition from `error` to `value` counts as a change even when the value is equal.
|
|
286
|
+
|
|
287
|
+
### 4.3 One scheduler
|
|
288
|
+
|
|
289
|
+
Glitches are impossible by construction: a single scheduler stabilizes every pending node in topological order before
|
|
290
|
+
it publishes, and only then calls the listeners. Invalidation and traversal are iterative, so a chain of a thousand
|
|
291
|
+
nodes does not overflow the stack. Reading an active clean node returns the settled value with no graph walk, O(1)
|
|
292
|
+
instead of O(graph), and that is pinned by the `coreData.settledRead` budget (D130). Activation is transactional: if a
|
|
293
|
+
node fails to activate, the already activated nodes roll back in reverse order, and disposer failures during the
|
|
294
|
+
rollback are aggregated with the original cause, so no part of the graph stays active without a subscription owner
|
|
295
|
+
(D134).
|
|
296
|
+
|
|
297
|
+
When listeners learn about a change:
|
|
298
|
+
|
|
299
|
+
| Source | When |
|
|
300
|
+
| ------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
301
|
+
| the direct listeners of `State.set` | synchronously, inside the write transaction |
|
|
302
|
+
| `derive` and `computed` downstream of that write | synchronously at the end of `set()`: the transaction closes and drains the queue |
|
|
303
|
+
| an external source with no open transaction | it opens its own transaction, then the same |
|
|
304
|
+
| an invalidation with no transaction | the next microtask |
|
|
305
|
+
| nested writes | one publication at the outer transaction boundary |
|
|
306
|
+
|
|
307
|
+
### 4.4 Collections
|
|
308
|
+
|
|
309
|
+
`collection({ from, key, project })` keeps a projection cache keyed by the item object, stable lookups by key, and
|
|
310
|
+
listeners per key: `selectByKey(source, key)` wakes only when its own key changes. Duplicate keys are an error. Unused
|
|
311
|
+
selectors are swept through a microtask.
|
|
312
|
+
|
|
313
|
+
### 4.5 Hooks
|
|
314
|
+
|
|
315
|
+
| Hook | Mechanics |
|
|
316
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
317
|
+
| `useReadable`, `useSelector` | `useSyncExternalStore` with a cache of the selected value; the same getter for client and server |
|
|
318
|
+
| `useResource` | `useSyncExternalStore` over the snapshot plus `retain()` in an effect: the mount is the lease |
|
|
319
|
+
| `useModel` | the nearest contribution mount frame; outside a frame a `ContributionError` `missing`, an inactive mount `inactive`, a foreign model `missing` |
|
|
320
|
+
| `useCommand` | per-consumer state, with no listener on the invoker: `run` returns a promise of the outcome, `inFlight` for the duration of the call, `result` the last `ok` outcome, which survives the next run, `lastError` the error of the last `failed`, cleared only by a success and never reset by starting a retry (D116); a cancellation changes neither of them |
|
|
321
|
+
|
|
322
|
+
`useCommand` keeps status per hook and invoker, and schedules nothing: every `run` reaches the call, whose declared
|
|
323
|
+
policy decides whether it queues, replaces a waiting input or runs in parallel (D203). Each request owns its
|
|
324
|
+
callbacks and abort subscription, an already-aborted signal is refused without reaching the call, and a run of an
|
|
325
|
+
unmounted consumer is refused the same way. `inFlight` is true while any run this consumer started is unsettled.
|
|
326
|
+
|
|
327
|
+
## 5. Resources, streams, events
|
|
328
|
+
|
|
329
|
+
Both the feature and the model select their target with `(source) => Readable<T | null | undefined>`; the
|
|
330
|
+
returned object is checked before work starts. A scalar snapshot does not satisfy the contract. `retention` is
|
|
331
|
+
optional in either context and `scoped({ capacity })` fixes LRU eviction (D169).
|
|
332
|
+
|
|
333
|
+
### 5.1 `resource(from, target, { key, load, retention, retry })`
|
|
334
|
+
|
|
335
|
+
| Step | What happens |
|
|
336
|
+
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
337
|
+
| 1 | the retention `scoped({ capacity })` activates the controller immediately, so the resource is warm with no observer; the `observer` retention — the literal `{ kind: 'observer' }` and the default — activates on the first lease (D160) |
|
|
338
|
+
| 2 | activation subscribes to `target` and reads it; a `null` target closes the selection |
|
|
339
|
+
| 3 | a key change: the same key updates the target without a reload; a new key fences the execution, advances the key epoch and takes the value from the LRU cache if it is there, otherwise it publishes `opening` (no data) or `refreshing` (stale data) and loads |
|
|
340
|
+
| 4 | `load(target, { signal, source })`; the result is accepted only if the execution, the key epoch and the activity did not change |
|
|
341
|
+
| 5 | on failure: a retry per `retry: { attempts, delayMs }`, then `status: 'error'` with `retryable: true` and a report to the `reporter` |
|
|
342
|
+
| 6 | the last lease released under `observer`: deactivation through a microtask, guarded by a revision |
|
|
343
|
+
| 7 | closing in drain: fence, unsubscribe, clear the cache, `idle`, await the unfinished operations |
|
|
344
|
+
|
|
345
|
+
`refresh()` reloads the same key past the cache and keeps the data; `retry()` works only from `error`; `invalidate()`
|
|
346
|
+
clears the cache and reloads without keeping the data.
|
|
347
|
+
|
|
348
|
+
### 5.2 `stream(from, target, { connect, consume, key, backpressure })`
|
|
349
|
+
|
|
350
|
+
The same key-and-epoch machine plus a one-slot admission layer: the only policy, `latest()`, keeps the newest pending value (D160). Every value is cached and published as `ready` first, then handed to the
|
|
351
|
+
author's `consume`. On a key change the new connection waits for the disposer of the old one: connections never
|
|
352
|
+
overlap. `backpressure` is required at runtime.
|
|
353
|
+
|
|
354
|
+
### 5.3 `event(from, subscribe, { run, backpressure })`
|
|
355
|
+
|
|
356
|
+
A one-slot queue in front of a sequential `run`, and the option says who owns the slot. Without `backpressure` it
|
|
357
|
+
belongs to the payload that reached it first: while `run` is busy and one payload waits, a newer `emit` is dropped and
|
|
358
|
+
reported as a `queue-capacity` failure record — drop-newest, the shape `exhaustMap` and `takeLeading` have. With
|
|
359
|
+
`backpressure: latest()` the newest payload replaces the waiting one and nothing is reported — conflation, the shape
|
|
360
|
+
`conflate` has; unlike `switchMap` it never interrupts the `run` that is already going (D247). A lossless queue is
|
|
361
|
+
neither policy and stays an open question (D183). `run` executes sequentially, must resolve to `undefined`, and
|
|
362
|
+
errors after a cancellation are swallowed. Cancelling the owner closes the queue and drops whatever waits in it, and
|
|
363
|
+
closing waits for the disposer and for the queue to drain.
|
|
364
|
+
|
|
365
|
+
## 6. Contributions and slots
|
|
366
|
+
|
|
367
|
+
### 6.1 Publication
|
|
368
|
+
|
|
369
|
+
| Step | What happens |
|
|
370
|
+
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
371
|
+
| 1 | at definition `provides` yields opaque declarations `{ target, value, priority, when }`; the value is always normalized to an instance-scoped factory |
|
|
372
|
+
| 2 | every contribution gets the id `<feature>.<key>`, one key per declaration |
|
|
373
|
+
| 3 | the compiler enumerates the contributions by id |
|
|
374
|
+
| 4 | in `prepare` a retire participant is registered whose fence withdraws the publication; the authority is captured: the models of the instance |
|
|
375
|
+
| 5 | **after `ready`** the factories run with the instance context, the value is marked with its owner, and `when` is resolved: a predicate becomes one computed readable of that instance |
|
|
376
|
+
| 6 | every affected target is updated **in one transaction**, and listeners are called afterwards |
|
|
377
|
+
|
|
378
|
+
Entry order: `priority` ascending, ties broken by id. `when` (a `Readable<boolean>`, or a predicate of the instance
|
|
379
|
+
that the runtime lowers to one): an entry whose value is `false`
|
|
380
|
+
is not part of `entries`, so `Slot`, `fold`, `select` and emptiness checks never see it; a flip publishes in one
|
|
381
|
+
transaction; id uniqueness is checked across everything published, so a flip can never fail validation. Publication is
|
|
382
|
+
atomic: subscribing a `when` and reading it happen in a preflight before any target is mutated, releasing the watcher
|
|
383
|
+
of a departing entry happens after every target is committed, and a publication reads only the `when` of its own
|
|
384
|
+
entries, so a withdrawal reads none at all (D225). A failure rolls back the observers that were added and leaves the
|
|
385
|
+
targets untouched, a `when` that signals mid-projection makes the affected targets project again, and withdrawing a
|
|
386
|
+
publication that never reached `published` removes whatever did land (D124).
|
|
387
|
+
|
|
388
|
+
### 6.2 Rendering a contribution
|
|
389
|
+
|
|
390
|
+
`Slot` reads the `entries` of the target through `useReadable`, returns `null` for an empty list, and otherwise renders
|
|
391
|
+
the `Component` of every entry with the slot props, wrapped in the authority frame of the contributing feature: inside
|
|
392
|
+
a contribution, `useModel` reaches the models of its own feature and nothing foreign. A contribution with no models
|
|
393
|
+
does not pay for the frame. A contribution is `{ Component, props?, models? }`: `props` maps the slot props into the
|
|
394
|
+
component's own props, `models` supplies the per-mount UI models, and a component built with `requiresModels([...])`
|
|
395
|
+
declares which of them it needs, which is checked by the type at the contribution boundary. A mount is memoized:
|
|
396
|
+
publishing or withdrawing one contribution does not re-render the other mounts of the same target (D130).
|
|
397
|
+
|
|
398
|
+
### 6.3 `fold` on a pipe
|
|
399
|
+
|
|
400
|
+
`fold(value, meta, read?)` walks the handlers in order; with a reader passed inside a `computed`, the dependencies
|
|
401
|
+
include both the entry list and everything the handlers read through their `{ read }`, so the result recomputes when
|
|
402
|
+
their sources change rather than only when contributions are re-registered. Without a reader the reads are untracked.
|
|
403
|
+
|
|
404
|
+
A handler is declared as a descriptor, `pipe(target, { fold })`, and the runtime binds one evaluation context to it
|
|
405
|
+
when the contribution publishes: `exports`, `imports` and the materialized `own` are resolved once and the reader is
|
|
406
|
+
the only thing that changes from call to call, so folding over many handlers allocates no per-handler context
|
|
407
|
+
(D223). A `fold` is the only thing that runs a handler: declaring, preloading and publishing do not.
|
|
408
|
+
|
|
409
|
+
### 6.4 The registry
|
|
410
|
+
|
|
411
|
+
`defineRegistry` is a target built on top of `collection`: `byId`, `list`, and `select(key)` with
|
|
412
|
+
`{ kind: 'found' | 'missing' }`. A duplicate key is an error.
|
|
413
|
+
|
|
414
|
+
## 7. Errors
|
|
415
|
+
|
|
416
|
+
One class per subject, with the state in the `code` field.
|
|
417
|
+
|
|
418
|
+
| Class | Codes | Who throws it |
|
|
419
|
+
| ---------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
420
|
+
| `FeatureError` | `retired`, `not-ready`, `quarantined`, `cleanup-failed` | the instance fence, a failed opening, the settle of a retire |
|
|
421
|
+
| `CallError` | `cancelled`, `closed`, `publication-rejected`, `unavailable` | the call kernel; cancellation is only `cancelled` and `closed` (D138) |
|
|
422
|
+
| `ReadableError` | `closed` | reading and writing a closed cell |
|
|
423
|
+
| `DeclarationError` | `invalid-id` | `declarationId` |
|
|
424
|
+
| `ApplicationError` | `closed` | the application closed before it was ready |
|
|
425
|
+
| `ContributionError` | `binding-invalid`, `duplicate`, `inactive`, `missing` | a contribution mount: a hook outside a frame, an inactive frame, a duplicate model, an invalid model or command binding |
|
|
426
|
+
| `CancellationError` | no code, only the cancellation brand | an author cancellation, which `isCancellation` recognizes |
|
|
427
|
+
| `FeatureBoundaryError` | `missing` | `useFeatureRetry` outside the error subtree of a boundary |
|
|
428
|
+
|
|
429
|
+
Cancellation is a property of the error, not a class: the predicate `isCancellation` (internal, the author does not
|
|
430
|
+
need it — cancellation is read by `useCommand` and `resource`) looks at the brand carried by `CancellationError`, by
|
|
431
|
+
`CallError` with codes `cancelled` and `closed`, by `FeatureError` with code `retired`, and by `ContributionError` with
|
|
432
|
+
code `inactive`. `unavailable` and `publication-rejected` are product answers: `useCommand` shows them as a `failed`
|
|
433
|
+
outcome with the error in `lastError`, and `resource` shows them as an error (D138). Neither `useCommand` nor
|
|
434
|
+
`resource` shows a cancellation as a user-facing error.
|
|
435
|
+
|
|
436
|
+
The path an error takes to the application: the `reporter` from `defineApplication` is passed to every `openFeature`,
|
|
437
|
+
and from there into model kernels, timers and controllers; the single funnel `reportRuntimeFailure` falls back to the
|
|
438
|
+
global `reportError` or to a deferred `throw` when the `reporter` is absent or itself fails. A condition group reports
|
|
439
|
+
its failures while it is running and holds only the ones raised after `close()` started, which `close()` then throws.
|
|
440
|
+
|
|
441
|
+
## 8. Boundaries and the host
|
|
442
|
+
|
|
443
|
+
The table names the everyday entries; the exhaustive export lists live in spec.md §3.
|
|
444
|
+
|
|
445
|
+
| Entry | For whom | What is there |
|
|
446
|
+
| ---------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
447
|
+
| `@opetope/core` | the feature author | `defineModel`, `definePort`, `definePipe`, `defineRegistry`, `declarationId`, `derive`, `computed`, `externalReadable`, `collection`, `selectByKey`, the errors, and the types `Readable`, `Lookup`, `Call`, `Port`, `ModelContext`, `OwnedState`, `Resource` |
|
|
448
|
+
| `@opetope/runtime` | the feature author and the host | `defineFeature`, `defineHostContract`, `onDemand`, `optional`, `openFeature`, `defineCondition`, `defineApplication`, `openApplication`, `bind`, `scoped`, `latest` |
|
|
449
|
+
| `@opetope/react` | the component author | `useModel`, `useReadable`, `useSelector`, `useCommand`, `useResource`, `defineSlot`, `defineSwitchSlot`, `Slot`, `requiresModels` |
|
|
450
|
+
| `@opetope/react/integration` | the host | `FeatureBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` |
|
|
451
|
+
| `@opetope/react/testing` | tests | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` |
|
|
452
|
+
| `@opetope/*/internal` | framework and host integration | the kernel, the compiler, the controllers, `createState`, `isCancellation`, the observation port `createInspectionSession` and the control port `createControlSession`; unavailable to authors (D142) |
|
|
453
|
+
|
|
454
|
+
The `exports` field of `package.json` exposes exact entry points without wildcards. Feature authors use the safe
|
|
455
|
+
entries; host integration and testing use their dedicated entries. Application import rules preserve this
|
|
456
|
+
separation between roles.
|
|
457
|
+
|
|
458
|
+
A demand-driven host shows feature UI like this: `FeatureBoundary` takes a lease on the instance through `useFeature`,
|
|
459
|
+
opens it if needed, shows `fallback` while it opens and `error` on failure, and inside renders `<Slot target props />`,
|
|
460
|
+
which is where the feature contributed its UI. Unmounting the boundary releases the lease; the last released lease
|
|
461
|
+
leads to a retire.
|
|
462
|
+
|
|
463
|
+
### 8.1 Observation
|
|
464
|
+
|
|
465
|
+
The observation port (D122) lives on `@opetope/runtime/internal`:
|
|
466
|
+
`createInspectionSession(execution, { ringCapacity })`, where `execution` is the result of `openApplication`. A
|
|
467
|
+
`WeakMap` holds the link; there is no global hook and no public value, and an application has at most one session.
|
|
468
|
+
Until a session is opened the application records nothing; `close()` detaches the observer without touching the
|
|
469
|
+
application. The consumer gets exactly four methods — `getSnapshot`, `readSince`, `subscribe` and `close` — and cannot
|
|
470
|
+
steer the graph it reads. The runtime implements the `@opetope/devtools` protocol rather than importing it, and the
|
|
471
|
+
`ci:inspection` gate holds the correspondence.
|
|
472
|
+
|
|
473
|
+
Both ports stay on `/internal` because the package that calls them is `@opetope/devtools`, not the application. A
|
|
474
|
+
host attaches the panel with `installDevtools({ execution, … })` and touches this entry for nothing else (D248).
|
|
475
|
+
|
|
476
|
+
### 8.2 Control
|
|
477
|
+
|
|
478
|
+
The write port is a second, separate factory on the same entry: `createControlSession(execution)` (D176). Holding an
|
|
479
|
+
inspection session does not mean holding the right to write, so the two have their own registries and their own
|
|
480
|
+
authority. The only controllable subject is a condition and, through it, its group: `suspend(conditionId)` sets a
|
|
481
|
+
debug override that forces the condition to read `false`, `resume(conditionId)` removes it, and the group follows its
|
|
482
|
+
own source again. A feature without `when` has no condition and is therefore not addressable at all — devtools never
|
|
483
|
+
becomes a source of lifetime the plan does not have.
|
|
484
|
+
|
|
485
|
+
The override wraps the condition's own `Readable`, so a suspended group deactivates through the ordinary
|
|
486
|
+
transactional path: the same scheduler, the same atomic frame, and the dependency law of
|
|
487
|
+
[devtools.md §5.3](devtools.md) —
|
|
488
|
+
suspending a provider takes its hard dependents with it through their own semantics, while suspending a consumer
|
|
489
|
+
leaves its provider alone. The frame the transition publishes names the control command as its cause, so «what I did»
|
|
490
|
+
and «what happened» read as one line of the timeline, and the group snapshot reports the override next to the product
|
|
491
|
+
truth: `desired.source` is what the condition says, `desired.effective` is what the runtime acts on, and
|
|
492
|
+
`desired.override` is `force-inactive` while the override stands.
|
|
493
|
+
|
|
494
|
+
`retryCleanup(instanceId)` repeats the cleanup of an exact quarantined frontier through the existing retry contract;
|
|
495
|
+
an instance that is not quarantined answers `not-controllable`. A refusal is data, never an exception:
|
|
496
|
+
`{ kind: 'rejected', reason: 'not-controllable' | 'stale' | 'unknown-target' }`, and an applied command answers with
|
|
497
|
+
the sequence of the frame its effect is visible in. `close()` releases every override the session holds, so closing a
|
|
498
|
+
panel never leaves the application in a debugged state, and an override lives only in the current execution — it does
|
|
499
|
+
not survive a reload or HMR.
|
|
500
|
+
|
|
501
|
+
## 9. How to verify this
|
|
502
|
+
|
|
503
|
+
- A feature instance in a test: `openFeature(feature, { imports, reporter })`, then `ready`, then `close()`; fakes of
|
|
504
|
+
host contracts are ordinary objects.
|
|
505
|
+
- A contribution component: `renderSlot(target, { props, models })` mounts the contribution with a fixture of per-mount
|
|
506
|
+
models and gives back `updateProps`; `command(run)` mints an authentic `Call` for a fixture.
|
|
507
|
+
- The application graph: `defineApplication` in a manifest test catches a missing provider, a cycle and a violation of
|
|
508
|
+
the condition-set inclusion law before anything starts.
|
|
509
|
+
- Budgets and gates: `ci:public-surface` counts the public words and forbids kernel and retired words on the safe
|
|
510
|
+
entries; `ci:perf-memory` measures retained memory, listeners and the linearity of operation cost, including
|
|
511
|
+
`coreData.settledRead` and the allocations of a write with no listeners (`unobservedWrite`, the median of three runs
|
|
512
|
+
of 100k operations); `ci:type-stress` measures the cost of the types, and language-service latency is printed for
|
|
513
|
+
information only; instantiations, memory and diagnostics are enforced (D224); `ci:browser-floor` holds the syntax and API level
|
|
514
|
+
of Chrome 82; `ci:inspection` checks the contract of the observation port; `ci:size-limit` holds five budgets the
|
|
515
|
+
author sees (core `author primitives consumer` 1.1 kB and `data layer consumer` 4.9 kB, runtime
|
|
516
|
+
`public feature consumer` 32 kB and `application graph consumer` 48 kB, and `@opetope/react` 18 kB).
|
|
517
|
+
- Authoring review: every component and hook of the module that reads a per-mount contribution model with
|
|
518
|
+
`useModel(X)` declares `requiresModels([X])`; owner models need no declaration (D117, D158). Imports use the
|
|
519
|
+
package entry points permitted for their role.
|
|
520
|
+
- Application build verification: the consuming host checks its bundle budgets and verifies that intended dynamic
|
|
521
|
+
boundaries survive bundling; package size fixtures do not measure the complete application.
|