@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
package/docs/spec.ru.md
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
# Opetope — спецификация фреймворка
|
|
2
|
+
|
|
3
|
+
Нормативная спецификация фреймворка композиции фич Opetope: `@opetope/core`, `@opetope/runtime`, `@opetope/react`.
|
|
4
|
+
Всё ниже описывает фреймворк таким, какой он есть. Там, где правило иначе выглядело бы произвольным, один указатель
|
|
5
|
+
в конце называет решение, которое его зафиксировало.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Цель и принципы
|
|
10
|
+
|
|
11
|
+
Opetope композирует фичи: граф фич с явными зависимостями, владеемое состояние с точным lifecycle, типизированные
|
|
12
|
+
вызовы с политиками, точки расширения с cardinality и React-слой без service locator. Слоты и явный граф образуют
|
|
13
|
+
небольшой авторский словарь. Runtime владеет lifecycle, подписками и гардами вызовов; наблюдаемые данные следуют
|
|
14
|
+
времени жизни своих читателей без самодельных `useEffect`.
|
|
15
|
+
|
|
16
|
+
Явный non-goal: SSR, hydration и персистентность. Данные живут в моделях и ресурсах экземпляра и не сериализуются —
|
|
17
|
+
ни снимка состояния для сервера, ни восстановления из него фреймворк не предлагает; пересмотр этой границы это
|
|
18
|
+
отдельный RFC (см. decisions.md, D155).
|
|
19
|
+
|
|
20
|
+
Всё остальное выведено из этих принципов.
|
|
21
|
+
|
|
22
|
+
1. **Один семантический факт объявляется один раз.** Механическое выводится нормализацией и проверяется валидацией.
|
|
23
|
+
2. **Одно понятие, одно слово, на всех стадиях.** Модель объявляется `defineModel`, создаётся `model(...)`, читается
|
|
24
|
+
`useModel`.
|
|
25
|
+
3. **Автор фичи видит только авторский словарь.** Kernel, машина экземпляров, IR и authority живут в `internal` и
|
|
26
|
+
доступны реализации фреймворка и интеграции хоста. Потолок — 40 value-экспортов и 60 типов на трёх безопасных входах.
|
|
27
|
+
4. **Обязателен только `id`.** Секции появляются по потребности, пустых секций автор не пишет.
|
|
28
|
+
5. **Слои разделены физически.** UI фичи импортирует только собственные контракты фичи. Модель не знает о фиче,
|
|
29
|
+
портах и импортах: свои зависимости она объявляет собственным интерфейсом, фича их подставляет. Чужая фича
|
|
30
|
+
доступна только через `imports` или `requires`.
|
|
31
|
+
6. **Sugar lower-ится в один IR.** Любая короткая форма имеет compile-тест эквивалентности длинной и не создаёт
|
|
32
|
+
второго runtime-пути.
|
|
33
|
+
7. **Фреймворк независим от хоста.** Адаптеры приложения зависят от `@opetope/*`; пакеты фреймворка не зависят от
|
|
34
|
+
кода приложения. Сервисы хоста входят через явные контракты.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Как выглядит фича
|
|
39
|
+
|
|
40
|
+
Фича это один `defineFeature` с семью секциями. Пример на двух фичах: `catalog.catalog` владеет каталогом активов и
|
|
41
|
+
отдаёт порт «разрешить актив», `checkout.createForm` требует его и показывает форму.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// features/catalog/integration/platform/catalog/feature.ts
|
|
45
|
+
const catalogCatalogFeature = defineFeature({
|
|
46
|
+
id: 'catalog.catalog',
|
|
47
|
+
imports: { platform: catalogHost }, // defineHostContract<CatalogHost>('catalog.catalog.platform')
|
|
48
|
+
own: ({ imports, resource, calls }) => ({
|
|
49
|
+
// async-материализация с retention; источник и селектор цели позиционные
|
|
50
|
+
catalog: resource(imports.platform, source => source.target, {
|
|
51
|
+
key: target => target,
|
|
52
|
+
load: (_target, { signal, source }) => source.load(signal),
|
|
53
|
+
retention: scoped({ capacity: 1 }),
|
|
54
|
+
}),
|
|
55
|
+
...calls(imports.platform, ['resolveItem']), // методы хоста как Call, один к одному
|
|
56
|
+
}),
|
|
57
|
+
exports: ({ own }) => ({ resolveItem: own.resolveItem }), // для фич, импортирующих это определение
|
|
58
|
+
provides: ({ port, own }) => ({
|
|
59
|
+
resolve: port(resolveItemPort, own.resolveItem), // порт для тех, кто его требует
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// features/checkout/models/orderModel.ts — не знает о фиче
|
|
66
|
+
type OrderPlatform = {
|
|
67
|
+
resolveItem(input: { itemId: string }, signal: AbortSignal): Promise<Item | null>;
|
|
68
|
+
submit(intent: Intent, signal: AbortSignal): Promise<Receipt>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const OrderModel = defineModel<{
|
|
72
|
+
amount: Readable<number>;
|
|
73
|
+
item: Readable<Item | null>;
|
|
74
|
+
resolveItem: Call<{ itemId: string }, Item | null>;
|
|
75
|
+
submit: Call<Intent, Receipt>;
|
|
76
|
+
}>('checkout.order');
|
|
77
|
+
|
|
78
|
+
export const createOrderModel = (ctx: ModelContext, platform: OrderPlatform): ModelOf<typeof OrderModel> => {
|
|
79
|
+
const amount = ctx.state(0); // владеемое состояние; запись только через ctx.update
|
|
80
|
+
const item = ctx.state<Item | null>(null);
|
|
81
|
+
// метод зависимости формы `(input, signal)` становится `Call` одной строкой
|
|
82
|
+
const { resolveItem } = ctx.calls(platform, ['resolveItem']);
|
|
83
|
+
const submit = ctx.call({
|
|
84
|
+
run: async (intent: Intent, { invoke, signal }) => {
|
|
85
|
+
const resolved = await invoke(resolveItem, { itemId: intent.itemId }); // вызов через контекст
|
|
86
|
+
ctx.update(item, resolved);
|
|
87
|
+
return platform.submit({ ...intent, precision: resolved?.precision }, signal);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { amount, item, resolveItem, submit };
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// features/checkout/integration/platform/createForm/feature.tsx — единственное место, знающее обе стороны
|
|
97
|
+
const createFormFeature = defineFeature({
|
|
98
|
+
id: 'checkout.createForm',
|
|
99
|
+
imports: { platform: createFormHost, catalog: catalogCatalogFeature }, // хост через контракт, фича через определение
|
|
100
|
+
requires: { resolveItem: resolveItemPort }, // порт; провайдера выберет приложение
|
|
101
|
+
own: ({ model, imports, requires }) => ({
|
|
102
|
+
// запись зависимостей разрешает refs: фабрика получает материализованное значение хоста
|
|
103
|
+
order: model(OrderModel, { platform: imports.platform }, (ctx, { platform }) => createOrderModel(ctx, platform)),
|
|
104
|
+
lookup: requires.resolveItem, // в `own` порт остаётся ref-ом
|
|
105
|
+
}),
|
|
106
|
+
// здесь `own` уже материализован: и порт, и поле модели пришли живыми `Call`
|
|
107
|
+
exports: ({ own }) => ({ lookup: own.lookup, submit: own.order.submit }),
|
|
108
|
+
provides: ({ slot, pipe }) => ({
|
|
109
|
+
// вклад это значение, фабрика экземпляра или дескриптор pipe; `priority` и `when` в опциях третьим
|
|
110
|
+
content: slot(orderFormContentSlot, ({ exports, model }) => ({
|
|
111
|
+
Component: CreateForm,
|
|
112
|
+
// UI-модель вклада: создаётся на каждое монтирование и видит `props` именно этого монтирования
|
|
113
|
+
models: [
|
|
114
|
+
model(OrderActions, (ctx, props: Readable<OrderFormProps>) => ({
|
|
115
|
+
submit: ctx.call({
|
|
116
|
+
run: (amount: number, { invoke }) => invoke(exports.submit, { amount, itemId: props.getSnapshot().itemId }),
|
|
117
|
+
}),
|
|
118
|
+
})),
|
|
119
|
+
],
|
|
120
|
+
props: ({ itemId }: OrderFormProps) => ({ itemId }), // адаптер пропсов слота под пропсы компонента
|
|
121
|
+
})),
|
|
122
|
+
toolbar: slot(
|
|
123
|
+
orderHeaderToolbar,
|
|
124
|
+
{ Component: SubmitToolbarButton },
|
|
125
|
+
// предикат этого экземпляра: `read` записывает, от чего зависит ответ, а `false` удерживает вклад
|
|
126
|
+
{ priority: 10, when: ({ imports, read }) => read(imports.platform.submitAllowed) },
|
|
127
|
+
),
|
|
128
|
+
// pipe объявляет обработчик дескриптором: `fold` выполняется на свертке, с контекстом этого экземпляра
|
|
129
|
+
amount: pipe(orderAmountFormatter, { fold: value => value.trim() }),
|
|
130
|
+
}),
|
|
131
|
+
when: [authorized], // фича живёт, пока условие истинно; без `when` живёт с приложением
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// features/checkout/ui/createForm/contracts.ts — контракты UI
|
|
137
|
+
export const orderFormContentSlot = defineSlot<OrderFormProps>({ id: 'checkout.createForm.content' });
|
|
138
|
+
|
|
139
|
+
export const OrderActions = defineModel<{ submit: Call<number, Receipt> }>('checkout.createForm.actions');
|
|
140
|
+
|
|
141
|
+
// features/checkout/ui/createForm/CreateForm.tsx — компонент импортирует только контракты своей фичи
|
|
142
|
+
export const CreateForm = requiresModels([OrderActions])(({ itemId }: { readonly itemId: string }) => {
|
|
143
|
+
const order = useModel(OrderModel); // модель `own`: её служит сам вклад
|
|
144
|
+
const actions = useModel(OrderActions); // UI-модель вклада: она перечислена в `requiresModels`
|
|
145
|
+
const amount = useReadable(order.amount);
|
|
146
|
+
const { run, inFlight, result, lastError } = useCommand(actions.submit);
|
|
147
|
+
|
|
148
|
+
return <Form amount={amount} itemId={itemId} busy={inFlight} error={lastError} receipt={result} onSubmit={run} />;
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Фичи собираются в манифесте приложения, который хост может загрузить за dynamic boundary. Хост передаёт
|
|
153
|
+
platform-адаптеры через `bind` и монтирует consumer-owned targets:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const sampleAppApplication = defineApplication({
|
|
157
|
+
id: 'sampleApp',
|
|
158
|
+
features: [catalogCatalogFeature, createFormFeature],
|
|
159
|
+
reporter: error => sentry.capture(error),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const execution = openApplication(sampleAppApplication, {
|
|
163
|
+
conditions,
|
|
164
|
+
imports: [bind(catalogHost, catalogSource), bind(createFormHost, createFormSource)],
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<Slot props={{ itemId }} target={orderFormContentSlot} />
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Что здесь не видно и не должно быть видно автору: module, fence, retire, drain, quarantine, lane-lease,
|
|
173
|
+
publication barrier. Всё это принадлежит runtime и его integration-слою.
|
|
174
|
+
|
|
175
|
+
### 2.1 Секции `defineFeature`
|
|
176
|
+
|
|
177
|
+
| Секция | Отвечает на вопрос | Форма |
|
|
178
|
+
| ----------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `id` | Как фича называется | `'feature.entity'`, только точки, без суффиксов |
|
|
180
|
+
| `imports` | Что мне дадут снаружи | запись из `defineHostContract` контрактов и определений других фич; `optional(x)` это слабое ребро, `onDemand(hostContract)` откладывает подключение до первого вызова |
|
|
181
|
+
| `requires` | Какой порт мне нужен | запись `port` или `optional(port)`; каждое `requires.x` это ref, а `Call` появляется у экземпляра |
|
|
182
|
+
| ref vs Call | Что видно в какой стадии | `own` и внешняя фабрика `provides` видят refs; `exports` и вложенные фабрики вкладов видят материализованные значения экземпляра |
|
|
183
|
+
| `own` | Чем я владею | builder: `model`, `call`, `calls`, `lane`, `effect`, `event`, `resource`, `stream`, `scope`, `attach` |
|
|
184
|
+
| `exports` | Что я отдаю фичам, которые меня импортируют | `({ own }) => запись Call, Readable и Resource` на открытии экземпляра |
|
|
185
|
+
| `provides` | Что я предлагаю точкам расширения | `({ port, slot, pipe, register, own }) => ...` |
|
|
186
|
+
| `when` | Сколько фича живёт | список `Condition`; пустой или отсутствующий это постоянная фича |
|
|
187
|
+
|
|
188
|
+
Обязателен только `id`. Стадии видят только предыдущие: `imports`, `requires` → `own` → `exports`, `provides`.
|
|
189
|
+
Секции `ui` нет: UI входит в приложение вкладами.
|
|
190
|
+
|
|
191
|
+
Фичу можно написать и двумя файлами (D186, D207). Заголовок держит `id`, `imports`, `requires`, `provides`
|
|
192
|
+
как метаданные — цель каждого вклада с его приоритетом, порт каждого провайдера — а также `when` и `body: () => import('./feature.body')`;
|
|
193
|
+
файл тела пишет `defineFeature.body(header, { own, exports, provides })`, а loader заголовка называет границу
|
|
194
|
+
экспортов как `FeatureBody<Exports>`, поэтому заголовок не зависит от типа собственного тела. Consumers и приложение
|
|
195
|
+
импортируют только заголовок: вся топология, её циклы и вложенность времён жизни компилируются до того, как загружен
|
|
196
|
+
хоть байт тела. Тело загружается при открытии экземпляра, один раз на фичу и совместно для одновременных
|
|
197
|
+
потребителей; загруженное тело переиспользуется следующими экземплярами, неудачная загрузка не кэшируется, а
|
|
198
|
+
закрытие или фенс до прихода кода означают, что экземпляр не откроется вовсе. `defineFeature.preload(feature)`
|
|
199
|
+
подтягивает код, ничего не открывая, — для хоста, который уже знает, что фича вот-вот понадобится.
|
|
200
|
+
|
|
201
|
+
### 2.2 Слой моделей
|
|
202
|
+
|
|
203
|
+
Модель это единица владеемого состояния и поведения фичи. Объявление `defineModel<Shape>(id)` живёт в
|
|
204
|
+
`features/x/models/` или в контрактах UI и задаёт запись из `Readable` и `Call`; класс в качестве `Shape` не
|
|
205
|
+
проходит типом. Реализация это фабрика `(ctx: ModelContext, deps) => Shape` или класс `implements ModelOf<typeof X>`,
|
|
206
|
+
получающий `ctx` и `deps` в конструктор. `ModelContext` без generic: `state`, `update`, `call`, `calls`, `lane`,
|
|
207
|
+
`effect`, `event`, `resource`, `stream`, `scope`, `timers`, `cleanup`, `signal`. `ctx.calls(deps, [...], { lane, policy })`
|
|
208
|
+
выбирает из зависимостей методы форм `() => Output`, `(input) => Output` и `(input, signal: AbortSignal) => Output`
|
|
209
|
+
и отдаёт по одному `Call` на ключ — тот же выбор и
|
|
210
|
+
тот же вывод типов, что у `own.calls`, плюс необязательная lane модели и политика, с которой создаётся каждый
|
|
211
|
+
выбранный вызов. Метод без параметров получает input `void`; необязательный input сохраняет `undefined`. Второй
|
|
212
|
+
параметр должен быть точно `AbortSignal` или `AbortSignal | undefined`, в том числе необязательный signal.
|
|
213
|
+
Поля данных, сигнатуры с `any`, неограниченный rest и сигнатуры с возможным третьим аргументом не выбираются.
|
|
214
|
+
Ограниченный tuple rest длиной до двух параметров подчиняется тому же правилу. Для overload действует вывод
|
|
215
|
+
TypeScript по последней сигнатуре; для другой перегрузки нужен явный адаптер (D244). Политика вызова пишется там, где он создаётся —
|
|
216
|
+
`ctx.call`, `ctx.calls`, `own.call`, — и действует для всех вызывающих, которых вызов допускает, включая
|
|
217
|
+
`context.invoke(target, input)` (D185). `ctx.cleanup(disposer)` регистрирует владеемую уборку: диспозер выполняется в том же drain, что и
|
|
218
|
+
диспозеры `effect`, `event`, `resource`, `stream` и `scope`, в обратном порядке регистрации, его отказ уходит в
|
|
219
|
+
`reporter`, а вызов после фенса это `TypeError`, как у `timers` после закрытия. `ctx.state` возвращает
|
|
220
|
+
`OwnedState<Value>`, и только его принимает `ctx.update`: производный `Readable` не проходит типом. Зависимости
|
|
221
|
+
модель объявляет своим интерфейсом, фича подставляет их в `own`. UI получает модель через `useModel(Decl)` только
|
|
222
|
+
внутри монтирования вклада, который её выдал.
|
|
223
|
+
|
|
224
|
+
**Объявление и исполнение (D243).** `ctx.call(options)` в контексте фабрики модели и `own.call(options)` объявляют
|
|
225
|
+
команду; `ctx.calls(source, keys, options?)` или `own.calls(source, keys)` объявляет несколько. Обработчик `run` получает другой контекст:
|
|
226
|
+
`context.invoke(target, input)` исполняет существующую команду с текущим авторитетом, отменой и стеком lane.
|
|
227
|
+
Деструктуризация поддерживается: `run: (input, { invoke }) => invoke(deps.submit, input)`. Политика остаётся в
|
|
228
|
+
объявлении; `invoke` не добавляет планировщик или переопределение политики.
|
|
229
|
+
|
|
230
|
+
Это breaking-переименование без алиаса `call` в контексте исполнения. Одновременно перенесите обращения к полю,
|
|
231
|
+
деструктуризацию и собственные аннотации типов контекста. Методы фабрики и типы `Call` и `CallInvocation` сохраняют имена:
|
|
232
|
+
|
|
233
|
+
```diff
|
|
234
|
+
const submit = ctx.call({
|
|
235
|
+
- run: (input: Input, { call }) => call(deps.submit, input),
|
|
236
|
+
+ run: (input: Input, { invoke }) => invoke(deps.submit, input),
|
|
237
|
+
});
|
|
238
|
+
-await context.call(submit, input);
|
|
239
|
+
+await context.invoke(submit, input);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
То же переименование действует для существующего права вызова в `ModelCallContext`, `FeatureCallContext` и
|
|
243
|
+
доверенных контекстах исполнения attachment, включая `ModuleAttachmentExecutionContext`, передаваемый в `context`
|
|
244
|
+
шага `step`. Контексты без права вызова его не получают; callbacks сохраняют свои документированные возможности.
|
|
245
|
+
|
|
246
|
+
**Время жизни сигналов (D244).** Используйте сигнал callback, который выполняет работу:
|
|
247
|
+
|
|
248
|
+
| Сигнал | Время жизни |
|
|
249
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
250
|
+
| `ModelContext.signal` | Вся владеемая модель. Он не подходит для отмены одного запроса. |
|
|
251
|
+
| `signal` контекста `run` команды | Одно физическое исполнение команды. Вложенный `invoke` наследует отмену, авторитет и стек lane; у ожидающих `singleFlight` сохраняются отдельные логические исходы. |
|
|
252
|
+
| `signal` контекста `Resource.load` | Активная загрузка текущего поколения target; замена, refresh, retirement или закрытие владельца могут отменить её, пока модель жива. |
|
|
253
|
+
| `signal` контекста `Stream.connect` / `consume` | Текущее исполнение соединения; замена, retry или retirement отзывают его право работать и дожидаются допущенной работы перед открытием преемника. |
|
|
254
|
+
|
|
255
|
+
`calls` всегда вызывает выбранный метод с `(input, signal)` и сохраняет его receiver. Обычный метод может
|
|
256
|
+
игнорировать лишние аргументы; фиктивный `_signal` не требуется. Единственный параметр с именем `signal` остаётся
|
|
257
|
+
input, а не автоматически переданным токеном отмены. Options клиента и дополнительные аргументы преобразуйте
|
|
258
|
+
небольшим обычным адаптером, например `load: (input: Input, signal: AbortSignal) => client.load(input, { signal })`.
|
|
259
|
+
Нет проверки `Function.length`, угадывания аргументов или дополнительного primitive адаптера. Метод, игнорирующий
|
|
260
|
+
отмену, может продолжить работу после логической отмены вызывающего. Runtime отвергает поздний результат Call и
|
|
261
|
+
управляет публикацией завершённых Resource/Stream executions; он не может остановить произвольные записи состояния
|
|
262
|
+
и внешние побочные эффекты внутри метода. Для них сам метод должен проверять signal. Физический drain продолжает
|
|
263
|
+
ждать уже запущенную работу.
|
|
264
|
+
|
|
265
|
+
Фича собирает зависимости через `model(Decl, { platform: imports.platform, lookup: requires.lookup }, create)`;
|
|
266
|
+
второй аргумент фабрики — readonly-запись разрешённых значений с выведенными типами. Для одного источника
|
|
267
|
+
используется та же запись, без зависимостей — `model(Decl, create)`. Принимаются только подлинные импорты и call
|
|
268
|
+
refs текущей фичи; optional-импорты сохраняют `Readable<Lookup<…>>`. Сырые объекты хоста, декларации портов,
|
|
269
|
+
чужие refs и refs моделей не являются элементами зависимостей. Именованная фабрика может объявить свой интерфейс
|
|
270
|
+
зависимостей, не видя контекста фичи. `provides.port(SubmitPort, { from: own.order, select: order => order.submit })`
|
|
271
|
+
один раз выбирает подлинный вызов из готовой модели до публикации и добавляет фенс провайдера, включая
|
|
272
|
+
переданные напрямую вызовы. Локальный call ref по-прежнему использует `port(SubmitPort, own.submit)` (D169).
|
|
273
|
+
|
|
274
|
+
Моделей у вклада два вида, и обе объявлены одним `defineModel`. Модели `own` живут с экземпляром фичи и служатся
|
|
275
|
+
монтированию автоматически. UI-модель вклада живёт с монтированием: она перечислена в `models` вклада, её фабрика
|
|
276
|
+
получает вторым аргументом `Readable` пропсов этого монтирования и берёт поля как есть (`submit: exports.submit`)
|
|
277
|
+
или оборачивает их в `ctx.call`, когда контракт UI отличается входом, формой результата или числом фичевых вызовов.
|
|
278
|
+
Отдельного понятия View нет.
|
|
279
|
+
|
|
280
|
+
### 2.3 UI
|
|
281
|
+
|
|
282
|
+
UI входит в приложение одним путём: `provides.slot(target, { Component, props?, models? })` публикует компонент, а
|
|
283
|
+
`Slot` рендерит его внутри авторитета фичи-контрибьютора. Внутри вклада работают `useModel` на модели его экземпляра и
|
|
284
|
+
на UI-модели, объявленные вкладом, а поверх их полей — `useCommand`, `useReadable`, `useSelector`, `useResource`.
|
|
285
|
+
Пропсы слота типизированы целью: без адаптера компонент принимает их как есть, с адаптером `props` — ровно результат
|
|
286
|
+
адаптера. `requiresModels([...])(Component)` перечисляет UI-модели, которые компонент читает: тип проверяет, что вклад
|
|
287
|
+
выдал каждую из них. Выдача гибридная: модели `own` фичи служатся неявно любому компоненту поддерева монтирования, а
|
|
288
|
+
per-mount UI-модель обязан объявить тот компонент или хук того же модуля, который читает её через `useModel`.
|
|
289
|
+
Объявление принадлежит каждому читателю, а не только месту вклада (см. decisions.md, D158).
|
|
290
|
+
|
|
291
|
+
`opetope/require-declared-models` из recommended `@opetope/lint` проверяет видимые `slot`-вклады и readers моделей в пределах одного модуля. Он учитывает aliases импортов и локальные bindings, включая именованные компоненты. Импортированные реализации и динамические объявления остаются непроверенными; отсутствие диагностики не доказывает полноту их требований (D250).
|
|
292
|
+
|
|
293
|
+
Хуки: `useModel`, `useReadable`, `useSelector`, `useCommand`, `useCommands`, `useResource`. Компоненты: `Slot` и `FeatureBoundary`
|
|
294
|
+
из `@opetope/react/integration`, который держит фичу открытой и показывает `fallback` или `error`. Обе ветви
|
|
295
|
+
принимают либо узел, либо render-колбэк: `children` получает готовый экземпляр, типизированный по `demand`, `error`
|
|
296
|
+
получает `{ error, retry }`, выполняется только показанная ветвь, и готовому потребителю не нужны второй
|
|
297
|
+
`useFeature` и второе удержание (D177). Команда через
|
|
298
|
+
`useCommand` следует объявленной политике Call, не превращает отмену экземпляра в ошибку пользователя и держит последний
|
|
299
|
+
исход в `result` и `lastError`; режима наблюдения у команды нет, состояние чужой инвокации модель отдаёт полем
|
|
300
|
+
`Readable`. Тесты компонента используют `renderSlot(target, { props, models, contribution })` и `command(run)` из
|
|
301
|
+
`@opetope/react/testing`.
|
|
302
|
+
|
|
303
|
+
`run` сохраняет ссылку, пока invoker тот же; объект результата хука меняется со статусом.
|
|
304
|
+
Эффект, вызывающий команду, зависит от `run`; `run` сохраняет идентичность, пока не меняется invoker. Своей политики
|
|
305
|
+
у потребителя нет: каждый `run` доходит до вызова, и решает политика, с которой вызов создан (D203). За потребителем
|
|
306
|
+
остаётся только то, что знает он один: уже отменённый входной signal отвергается как `cancelled`, не доходя до
|
|
307
|
+
вызова, `run` размонтированного потребителя отвергается так же, `inFlight` истинен, пока не завершился хоть один
|
|
308
|
+
начатый им прогон, и у каждого прогона свои исход, callbacks и signal. Два потребителя одного вызова держат
|
|
309
|
+
раздельные статусы.
|
|
310
|
+
|
|
311
|
+
`useModel(Declaration, (model, { read }) => ({ ... }))` выбирает данные и command consumers одним hook (D205, D214).
|
|
312
|
+
|
|
313
|
+
Результат выбора модели может быть именованным interface без index signature (D217). Результат — плоская запись данных;
|
|
314
|
+
arrays, functions, constructors и встроенные объекты коллекций, дат и promises не являются selection records.
|
|
315
|
+
Readonly record с выведенными типами преобразует authentic Call в `CommandHook`; остальные поля сохраняют свои типы.
|
|
316
|
+
`read(readable, project?)` читает явно, объединяет подписки на один источник и не делает автоматический Resource retain.
|
|
317
|
+
Поля сравниваются через `Object.is`; новый вложенный объект считается изменением. Чистый callback не вызывает hooks,
|
|
318
|
+
команды или side effects. Замена подписок и допуск команд происходят в commit, не в abandoned render. Ключи, aliases,
|
|
319
|
+
локальные статусы и `.run` следуют `useCommands`. Создание модели, feature acquire и новый scheduler не подразумеваются.
|
|
320
|
+
`useModel(Declaration)` по-прежнему возвращает выданную модель; структура React hooks постоянна при смене selection.
|
|
321
|
+
Общий hook улучшает DX, но сам по себе не гарантирует ускорение.
|
|
322
|
+
|
|
323
|
+
`useCommands({ save: model.save, remove: other.remove })` возвращает `CommandHook` на каждом ключе, с точными
|
|
324
|
+
разнородными типами (D174). Ключи — локальные имена; команды берутся из разрешённого авторитета моделей. Общей
|
|
325
|
+
очереди, транзакции или busy нет, и опции нет: политика принадлежит каждому вызову. Пока ключ и invoker те же,
|
|
326
|
+
`.run` стабилен, а объект поля меняется только с его статусом. Новый объект выбора и перестановка ключей не меняют
|
|
327
|
+
ничего. Удаление ключа отбрасывает его потребителя и статус; повторное добавление создаёт нового. Два псевдонима
|
|
328
|
+
одного Call — независимые потребители. Выбираются собственные enumerable-поля (включая symbols); число React hooks
|
|
329
|
+
постоянно при изменении набора ключей. Только commit активирует новый выбор: отброшенный рендер не трогает
|
|
330
|
+
закоммиченных потребителей. Одиночный `useCommand` остаётся.
|
|
331
|
+
|
|
332
|
+
Прямые пропсы компонента и props-адаптер читают тот же снимок монтирования, что читатели модели; входящие пропсы
|
|
333
|
+
слота публикуются в layout до paint, без обновления состояния в insertion effects. Retry спроса `FeatureBoundary`
|
|
334
|
+
принадлежит своему источнику: замена A на B не заставляет B ждать незавершённого retry A (D170). `useFeature(...).retry`
|
|
335
|
+
возвращает промис, который разрешается по завершении начатой им попытки, а второй вызов во время работы присоединяется
|
|
336
|
+
к той же попытке, а не начинает новую. Он отвечает этим промисом и тогда, когда источник отказал в acquisition
|
|
337
|
+
синхронно: отказавшая acquisition это завершившаяся попытка, а сам отказ принадлежит состоянию, которое публикует
|
|
338
|
+
источник, а не вызывающему `retry` (D226). Граница показывает `fallback` всю жизнь попытки и перечитывает состояние по её
|
|
339
|
+
завершении, поэтому попытка, упавшая с тем же объектом ошибки, который хост уже держит, возвращает поддерево `error`
|
|
340
|
+
(D198). `retry` поддерева ошибки остаётся `() => void`: хост вправе отдать его прямо в `onClick`.
|
|
341
|
+
|
|
342
|
+
`resource(from, target, options)` и `stream(from, target, options)` принимают селектор, возвращающий
|
|
343
|
+
`Readable<T | null | undefined>`, и в контексте фичи, и у модели. Скалярный snapshot недопустим; nullish target
|
|
344
|
+
закрывает материализацию. Retention необязателен; `scoped({ capacity })` выбирает ограниченное LRU-удержание.
|
|
345
|
+
`effect({ from, when, run })` проверяет `when(current, previous)` для каждого выбранного запуска.
|
|
346
|
+
`calls(imports.x, keys)` выбирает методы хоста, экспортированные вызовы жёсткого импорта фичи или — поверх
|
|
347
|
+
`optional(feature)` — экспортированные вызовы того провайдера, который есть сейчас: выбранный вызов доходит до живого
|
|
348
|
+
экземпляра, а пока провайдера нет, отвечает `CallError` с кодом `unavailable` (D187). Данные слабого ребра читаются
|
|
349
|
+
через `fromOptional(source, select, { missing })`: `select` выполняется только на `found` и вправе вернуть значение
|
|
350
|
+
или собственный `Readable` провайдера, вложенный readable отслеживается только пока провайдер есть, а всё остальное
|
|
351
|
+
время ответ это `missing`.
|
|
352
|
+
|
|
353
|
+
### 2.4 Приложение и хост
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
const authorized = defineCondition({ id: 'workspace.authorized' }); // факт, который привязывает хост
|
|
357
|
+
const premium = defineCondition({ from: authFeature, id: 'workspace.premium', select: e => e.premium });
|
|
358
|
+
|
|
359
|
+
const sessionsFeature = defineFeature({ id: 'workspace.sessions', when: [authorized] /* … */ });
|
|
360
|
+
|
|
361
|
+
const workspaceApp = defineApplication({
|
|
362
|
+
id: 'workspace', // runtime-идентичность приложения
|
|
363
|
+
features: [authFeature, sessionsFeature, catalogFeature], // включённые фичи; мир замыкается по импортам
|
|
364
|
+
reporter: error => sentry.capture(error),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
openApplication(workspaceApp, {
|
|
368
|
+
conditions: {
|
|
369
|
+
'workspace.authorized': derive({ from: example.session, select: session => session.status === 'authorized' }),
|
|
370
|
+
},
|
|
371
|
+
imports: [bind(authHost, example), bind(sessionsHost, agents)],
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Время жизни объявляет сама фича. `defineCondition({ id })` это факт, который привязывает хост;
|
|
376
|
+
`defineCondition({ from, id, select })` вычисляет факт из экспортов одной фичи, и такой id хост привязывать не
|
|
377
|
+
имеет права. `defineFeature({ when: [условия] })` говорит, что фича живёт, пока все её условия истинны; пустой или
|
|
378
|
+
отсутствующий `when` это постоянная фича. `defineApplication({ id, features, reporter })` компилирует статическую
|
|
379
|
+
топологию до старта: `requires ↔ provides`, `imports`, вклады. `features` это список включённых фич, а мир это они
|
|
380
|
+
плюс транзитивное замыкание их жёстких импортов, поэтому провайдер порта должен быть включён, иначе порт остаётся
|
|
381
|
+
без провайдера. Фичи с одинаковым набором условий образуют группу условий: она даёт порядок активации и узел в
|
|
382
|
+
devtools-плане, отдельного объявления группы нет. `openApplication` поднимает приложение: `conditions` это запись
|
|
383
|
+
`Readable<boolean>` по id, типизированная из `Application<Features>` как объединение id условий без `from`,
|
|
384
|
+
`imports` это массив `bind(contract, value)`, проверяющий значение в месте вызова. Необязательный `cleanupFailure`
|
|
385
|
+
несёт те же два слова, что у `openFeature`, и по умолчанию равен `report`: хост, выбравший `quarantine`, оставляет
|
|
386
|
+
провалившуюся уборку точным фронтиром на `FeatureError.retryCleanup` вместо отчёта, и порт управления devtools может
|
|
387
|
+
её повторить (D182).
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 3. Публичный словарь
|
|
392
|
+
|
|
393
|
+
Правила имён: `define*` объявляет контракт без поведения; `use*` читает в React; `open*` поднимает lifetime; методы
|
|
394
|
+
builder-а без префикса; ошибки `<Subject>Error` с полем `code`. Kernel-слово это имя механики, которой автор фичи не
|
|
395
|
+
управляет: `Module`, `Attachment`, `Executor`, `Authority`, `Owner`, `Blueprint`, `IR`. Такое слово живёт только в
|
|
396
|
+
`internal`, не появляется ни в одном имени на безопасном входе и не появляется в тексте авторской ошибки — там
|
|
397
|
+
говорят «feature» и «instance». `module` это единица lowering, из которой kernel собирает фичи и детей динамического
|
|
398
|
+
scope: отдельный концепт, а не второе имя фичи. Оба запрета держит гейт `ci:public-surface`.
|
|
399
|
+
|
|
400
|
+
**`@opetope/core`.** Значения `computed`, `derive`, `fromOptional`, `externalReadable`, `collection`, `selectByKey`,
|
|
401
|
+
`declarationId`, `defineModel`, `definePort`, `definePipe`, `defineRegistry`. Типы `Readable`, `State`, `OwnedState`, `Collection`,
|
|
402
|
+
`Lookup`, `Equality`, `Model`, `ModelOf`, `ModelContext`, `Call`, `Port`, `PortRef`, `Pipe`, `Registry`,
|
|
403
|
+
`DeclarationId`, `Resource`, `ResourceSnapshot`, `ScopedRetention`, `LatestBackpressure`. Ошибки
|
|
404
|
+
`CancellationError`, `FeatureError`, `ReadableError`, `DeclarationError`, `CallError`; коды `CallError` это
|
|
405
|
+
`cancelled`, `closed`, `publication-rejected`, `unavailable`. `createState` и `isCancellation` живут на
|
|
406
|
+
`@opetope/core/internal`: у автора нет пилота ни на своё состояние вне модели, ни на распознавание отмены руками —
|
|
407
|
+
состояние приходит из `ctx.state`, а отмену читает `useCommand`.
|
|
408
|
+
|
|
409
|
+
**`@opetope/runtime`.** Значения `defineFeature`, `defineHostContract`, `onDemand`, `optional`,
|
|
410
|
+
`openFeature`, `defineCondition`, `defineApplication`, `bind`, `openApplication` и политики `scoped`, `latest`.
|
|
411
|
+
Типы `Feature`, `FeatureInstance`, `HostContract`, `Condition`, `Application`, `ApplicationExecution`,
|
|
412
|
+
`ApplicationImportBinding`, `FeatureBody`, `FeatureBodyOf`, `Resource`, `ResourceSnapshot`, `ScopedRetention`, `LatestBackpressure`, `ErrorReporter`,
|
|
413
|
+
`CleanupFailurePolicy`, а также re-export `Call` и `Readable`. Ошибки `FeatureError` (re-export) и
|
|
414
|
+
`ApplicationError`.
|
|
415
|
+
|
|
416
|
+
**`@opetope/react`.** Значения `useModel`, `useReadable`, `useSelector`, `useCommand`, `useCommands`, `useResource`,
|
|
417
|
+
`requiresModels`, `Slot`, `defineSlot`, `defineSwitchSlot`. Типы `SlotTarget`, `SwitchSlotTarget`,
|
|
418
|
+
`SlotContribution`, `CommandHook`, `CommandOutcome`. Ошибка `ContributionError` несёт коды `binding-invalid`,
|
|
419
|
+
`duplicate`, `inactive`, `missing`: её субъект это монтирование вклада, которое и выдаёт модели, и фенсит команды,
|
|
420
|
+
поэтому отдельных `ModelError`, `RootError` и `ActionError` нет.
|
|
421
|
+
|
|
422
|
+
**`@opetope/react/integration`.** `FeatureBoundary`, `useFeature`, `useFeatureRetry`,
|
|
423
|
+
`FeatureBoundaryError`. **`@opetope/react/testing`.** `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` и типы fixtures/scenarios. `useFeatureError`-а нет: ошибку,
|
|
424
|
+
которую хост сам передал в `error`, он уже знает, а контекст boundary остаётся внутренним для `useFeatureRetry`.
|
|
425
|
+
|
|
426
|
+
**Возможности lifecycle.** `FeatureInstance` предоставляет `ready` и `close()`. Закрытие фенсит немедленно и
|
|
427
|
+
присоединяется к одному promise завершения. `FeatureError.retryCleanup?: () => Promise<void>` существует только
|
|
428
|
+
у карантинной ошибки с повторяемой уборкой; перед вызовом проверяется наличие capability. Внутренний термин
|
|
429
|
+
и код отмены `retired` сохраняются. Retry ресурса и retry спроса фичи сохраняют свой смысл. Прежние авторские
|
|
430
|
+
имена `retire`, `FeatureError.retry`, `effect.condition`, `required(port)` и опция `scoped` `evict` не имеют
|
|
431
|
+
алиасов (D168).
|
|
432
|
+
|
|
433
|
+
**Внутри `defineModel`.** `ModelContext` по §2.2, где `scope` читается как `scope.while`, `scope.switch`,
|
|
434
|
+
`scope.keyed`.
|
|
435
|
+
|
|
436
|
+
**Внутри `defineFeature`.** Секции §2.1; методы `own`: `model`, `call`, `calls`, `lane`, `effect`,
|
|
437
|
+
`event`, `resource`, `stream`, `scope.while`, `scope.switch`, `scope.keyed`, `attach`. У `resource`,
|
|
438
|
+
`stream` и `event` источник и носитель вывода типа позиционные: `resource(from, target, {…})`,
|
|
439
|
+
`stream(from, target, {…})`, `event(from, subscribe, {…})`; порядок ключей в объекте опций свободен. Одна опция
|
|
440
|
+
несёт для обоих одно слово: `stream` требует `backpressure: latest()`, а `event` его допускает. Методы `provides`:
|
|
441
|
+
`port`, `slot`, `pipe`, `register`; у вкладов цель первым аргументом, значение или фабрика экземпляра вторым, опции
|
|
442
|
+
третьим: `slot(target, contribution, { priority, when })`, `pipe(target, { fold }, { priority, when })`,
|
|
443
|
+
`register(target, entry, { priority, when })`, причём у `pipe` второй аргумент это дескриптор, потому что голую
|
|
444
|
+
функцию было бы не отличить от фабрики экземпляра. Контекст фабрики вклада это `{ exports, imports, instance, model, own }`, и его
|
|
445
|
+
`model(Decl, create)` строит UI-модель монтирования, где `create` получает `(ctx, props)`. `when` это
|
|
446
|
+
`Readable<boolean>` или предикат экземпляра — `({ exports, imports, own, read }) => boolean`, — чей `read`
|
|
447
|
+
записывает, от чего зависит ответ: пока ответ `false`, вклад не входит в `entries` цели. В этом контексте
|
|
448
|
+
вычисления `own` дан в материализованной форме, которую видит `exports`: поле модели это `Readable`, а не ref из
|
|
449
|
+
контекста фабрики. `fold` дескриптора `pipe` получает третьим аргументом тот же контекст вычисления —
|
|
450
|
+
`(value, meta, { exports, imports, own, read }) => …`, — связанный с экземпляром один раз при публикации вклада и
|
|
451
|
+
вызываемый только сверткой, а не при объявлении или предзагрузке; а `target.fold(value, meta, read?)` принимает
|
|
452
|
+
читателя третьим параметром, поэтому `computed({ read: read => target.fold(0, undefined, read) })` подписывается и
|
|
453
|
+
на `entries`, и на всё, что прочитали обработчики; без читателя обработчики читают текущий snapshot. Контексты:
|
|
454
|
+
`source`, `signal`, `invoke`, `update`, `timers.delay`, `timers.interval`, `cleanup`, `current`, `previous`, `emit`,
|
|
455
|
+
`payload`.
|
|
456
|
+
|
|
457
|
+
Итого: 39 value-экспортов и 35 типов на трёх безопасных входах по гейту `ci:public-surface` при цели 40 и 60.
|
|
458
|
+
Ежедневный словарь автора фичи около 20 слов.
|
|
459
|
+
|
|
460
|
+
**`@opetope/core/internal` и `@opetope/runtime/internal`**, для реализации фреймворка и интеграции хоста: `defineModule`,
|
|
461
|
+
`instantiateModule`, все `Module*` и `Attachment*`, call-kernel `defineCallTarget`, `createCallExecutor`,
|
|
462
|
+
`defineCallLane`, публикация вкладов `publishContributions`, `bindFeatureResource`, `getApplicationPlan`,
|
|
463
|
+
`createState` и `isCancellation`. Состав входов ограничен фактом потребления: value-экспортов 43 в ядре и 20 в
|
|
464
|
+
рантайме, и всё, что не импортируют вне пакета, осталось модульным (см. decisions.md, D159). У `@opetope/react`
|
|
465
|
+
внутреннего входа нет.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
`@opetope/devtools/react` экспортирует `defaultTheme` — нейтральную самостоятельную палитру. Приложение настраивает панель через `DevtoolsTheme`; переопределения ограничены корнем панели (D249).
|
|
470
|
+
|
|
471
|
+
## 4. Законы ядра
|
|
472
|
+
|
|
473
|
+
Каждый закон доказан тестами пакетов и не меняется переименованиями.
|
|
474
|
+
|
|
475
|
+
- **Ownership и экземпляр фичи.** Всё созданное в `own` живёт и умирает с экземпляром фичи. Каждое вложение
|
|
476
|
+
подключает импорт или host-контракт: владеемых scope-ов у фичи нет, поэтому «на экземпляр» значение приходит
|
|
477
|
+
ровно одним путём — через `imports` (см. decisions.md, D141). Retire сначала делает экземпляр stale и fence-ит
|
|
478
|
+
команды, затем уведомляет, затем закрывает своих детей. Порядка между вложениями одного scope-а нет: они
|
|
479
|
+
открываются и закрываются одной волной, а карантинный фронтир докладывается в каноническом порядке слотов
|
|
480
|
+
(см. decisions.md, D163). Порядок между фичами разных групп условий держит порядок групп, а не этот закон. Поздний результат старого экземпляра не публикуется.
|
|
481
|
+
- **Физический дренаж и откат.** Логическая отмена быстро завершает ожидание вызывающего, но не означает, что
|
|
482
|
+
пользовательская работа остановилась. Дренаж ждёт допущенную работу, включая незавершённое открытие и consume
|
|
483
|
+
стрима, до освобождения источника. Модели регистрируют владельца отката до вызова фабрики, включая per-mount
|
|
484
|
+
модели вкладов. Отказ фабрики убирает частично созданные узлы. Фенс закрывает всё владеемое состояние и
|
|
485
|
+
отвергает новые узлы того же контекста; `OwnedState` соседней модели недоступен ему для записи (D170).
|
|
486
|
+
|
|
487
|
+
- **Координированное закрытие.** Закрытие, уже идущее у одного вложения, не отменяет закрытия остальных:
|
|
488
|
+
координированный fence присоединяется к этому drain-у и докладывает его исход, поэтому один член в откате не стоит
|
|
489
|
+
соседям их закрытия.
|
|
490
|
+
- **Cleanup.** По умолчанию `report`: ошибка cleanup уходит в reporter приложения, экземпляр считается закрытым.
|
|
491
|
+
`quarantine` это явный opt-in для ресурсов с обязательным физическим release.
|
|
492
|
+
- **Данные.** Один scheduler стабилизирует `derive` и `computed`, уведомление один раз на транзакцию, `selectByKey`
|
|
493
|
+
не создаёт cross-key уведомлений.
|
|
494
|
+
- **Осевшее чтение.** Чтение активного чистого узла не обходит граф: оно отдаёт осевшее значение, потому что вверх
|
|
495
|
+
по графу всё уже опубликовалось (см. decisions.md, D130).
|
|
496
|
+
- **Транзакционная активация.** Отказ на середине откатывает узлы, которые активация успела пройти, в обратном
|
|
497
|
+
порядке, и отказы диспозеров при откате приезжают вместе с исходной причиной.
|
|
498
|
+
- **Распространение закрытия.** `close()` у `State` это одно уведомление зависимым узлам, после которого `derive`,
|
|
499
|
+
`computed`, `collection` и `selectByKey` отвечают `ReadableError` с кодом `closed` вместо последнего значения, и
|
|
500
|
+
дальше молчат; неподписанный производный узел получает тот же отказ на следующем чтении закрытого источника.
|
|
501
|
+
Закрытие терминально и для того кода, который его вызвал: апдейтер, закрывший состояние по ходу вычисления,
|
|
502
|
+
получает отказ на свой результат, а состояние остаётся закрытым (D201).
|
|
503
|
+
- **Lookup.** `Lookup` литерален: `{ kind: 'found', value } | { kind: 'missing' }`, и та же форма проецирует слабое
|
|
504
|
+
ребро.
|
|
505
|
+
- **Вызовы.** `call` по умолчанию `queue`, `parallel` явно, `latest` держит одно ожидающее место, `once` кэширует,
|
|
506
|
+
`singleFlight` дедуплицирует по ключу; вложенный вызов только через `context.invoke`, отмена и lane наследуются.
|
|
507
|
+
- **Latest.** Вызов, объявленный `latest`, идёт по lane как `queue`, и пока его тело исполняется, новый вход
|
|
508
|
+
заменяет ожидающий: вытесненный invocation завершается отменой со своими колбэками и сигналом, а тело в полёте не
|
|
509
|
+
прерывается. Замена останавливается на границе lane: если за ожидающим уже встал другой вызов, ожидающий остаётся
|
|
510
|
+
на своём месте, а новый вход ждёт после него. Цена 100, отправка и цена 200 на одной lane поэтому отправляют 100
|
|
511
|
+
и затем применяют 200 (D185).
|
|
512
|
+
- **Доставка событий.** `event` исполняет один payload за раз, а за ним слот ещё на один. `backpressure: latest()`
|
|
513
|
+
отдаёт этот слот новому payload и ничего не репортит; `event`, написанный без опции, оставляет слот тому payload,
|
|
514
|
+
который занял его первым, и отбрасывает новый `emit` записью отказа `queue-capacity`. Ни одна политика не
|
|
515
|
+
прерывает идущий `run`, ни одна не держит больше одного ожидающего payload, а закрытие владельца отбрасывает то,
|
|
516
|
+
что ждёт. Очередь без потерь это отдельный вопрос (D183, D247).
|
|
517
|
+
- **Исходы команды.** `useCommand` не превращает отмену в ошибку и держит последний исход: `result` от последнего
|
|
518
|
+
`ok`, `lastError` от последнего `failed`, `cancelled` не двигает ни то, ни другое, и старт прогона не чистит ни
|
|
519
|
+
то, ни другое, поэтому прежний отказ читается, пока повтор в полёте. Отсутствие провайдера и отклонённая
|
|
520
|
+
публикация приходят исходом `failed` и оседают в `lastError`.
|
|
521
|
+
- **Публикация.** Вклады публикуются атомарно после critical-барьера открытия владельца и до разрешения публичного
|
|
522
|
+
`instance.ready`, затем снимаются при retire; цели вкладов статичны,
|
|
523
|
+
поэтому топология компилируется до старта. Цель держит один вклад на ключ фичи, поэтому два живых экземпляра одной
|
|
524
|
+
фичи не могут одновременно вкладываться в одну цель.
|
|
525
|
+
- **Удержанный вклад.** Вклад с `when` в `false` опубликован, но не входит в `entries` цели, поэтому `Slot`, `fold`,
|
|
526
|
+
`select` и проверки пустоты его не видят; смена значения пересчитывает `entries` одной транзакцией. Предикат
|
|
527
|
+
понижается в один computed-readable своего экземпляра до начала публикации, поэтому дальнейшие пути одинаковы для
|
|
528
|
+
обеих форм, а ответ не-boolean роняет это чтение, а не публикует truthy-объект как видимость (D220). Уникальность
|
|
529
|
+
id и ключей проверяется по всем опубликованным вкладам, поэтому смена `when` не может провалить валидацию.
|
|
530
|
+
- **Атомарность публикации.** Публикация держит чужой код вне своих записей. Подписка на `when` и его чтение идут до
|
|
531
|
+
того, как хоть одна цель тронула свои `entries`, — чтение после подписки, поэтому источник, перевернувший своё
|
|
532
|
+
значение изнутри `subscribe`, публикуется в том состоянии, в котором остался, — а освобождение наблюдателя,
|
|
533
|
+
оставшегося от ушедшей записи, идёт после того, как зафиксированы все цели этой публикации: снятие, сделанное из
|
|
534
|
+
такого disposer, удаляет из уже зафиксированного состояния, а не отменяется им. Публикация читает `when` только
|
|
535
|
+
своих записей; каждый другой владелец сохраняет видимость, с которой был опубликован в последний раз, поэтому
|
|
536
|
+
обязательное снятие не читает `when` вообще, а нечитаемый предикат не может удержать в цели записи другого
|
|
537
|
+
владельца (D225). `when`, подавший сигнал во время проекции публикации, делает эту проекцию недействительной, и
|
|
538
|
+
затронутые цели проецируются заново; источник, подающий сигнал на каждое чтение, роняет публикацию громко, а не
|
|
539
|
+
коммитит то, что он ответил до сигнала. Цель, отказавшая в публикации, откатывает подписки, сделанные предыдущими
|
|
540
|
+
целями, поэтому несостоявшаяся публикация не оставляет ни одного слушателя, а отказ снятия убирает и то, что успело
|
|
541
|
+
лечь. Смена `when`, сделанная изнутри слушателя `entries`, не теряется: охранник от реентрантности её запоминает, и
|
|
542
|
+
цикл её публикует.
|
|
543
|
+
Последующий пересчёт видимости также сверяет точный состав entries и ревизию видимости после каждого getter.
|
|
544
|
+
Устаревшая проекция отбрасывается до чтения снятого соседа и публикации старых entries; после 100 повторов
|
|
545
|
+
пересчёт завершается с `TypeError` и освобождает guard, чтобы следующее стабильное уведомление могло восстановить
|
|
546
|
+
снимок (D242).
|
|
547
|
+
- **Открытие и закрытие фичи.** Eager-определение и lazy-заголовок проверяют и копируют запись options, imports и
|
|
548
|
+
requirements в `openFeature`, до вызова loader; последующая замена этих полей не меняет экземпляр. `close()`
|
|
549
|
+
фенсит синхронно, а параллельные и реентерабельные вызовы получают один Promise завершения. Ошибка loader,
|
|
550
|
+
включая синхронный throw, отклоняет `ready` или `preload`. Закрытие до прихода кода отклоняет `ready` этого
|
|
551
|
+
экземпляра как `retired` и завершается, не ожидая общей загрузки. Загрузка остаётся доступна другим экземплярам
|
|
552
|
+
и preload; поздний успех не открывает закрытый экземпляр, а отказ допускает повтор. Если материализация уже
|
|
553
|
+
началась, close дожидается владеемой работы и сохраняет восстановление из quarantine (D211).
|
|
554
|
+
- **Запуск приложения.** Закрытие из подписки условия или её disposer получает тот же Promise, что и остальные
|
|
555
|
+
вызовы close. Оно фенсит уже созданные группы, снимает подписку, вернувшуюся после закрытия, не создаёт следующих
|
|
556
|
+
групп и отклоняет готовность приложения; `close()` дожидается всей начатой уборки (D210).
|
|
557
|
+
- **Порты.** У порта ровно один провайдер, выбор на уровне приложения, два провайдера это ошибка компиляции
|
|
558
|
+
топологии.
|
|
559
|
+
- **Время жизни как условия.** Время жизни объявляет фича: она живёт, пока истинны все условия её `when`, и пустой
|
|
560
|
+
`when` значит «пока живёт приложение». Фичи с одинаковым набором условий образуют одну группу, поэтому набор, а не
|
|
561
|
+
порядок, определяет владельца. Условие это желаемое состояние, а не событие: рантайм следует последнему снапшоту,
|
|
562
|
+
поэтому значение, успевшее стать `false` и снова `true` в одном turn, сохраняет живое поколение, а конец сессии
|
|
563
|
+
выражается фактом в самом условии — например её id, — а не кратким `false` (D190).
|
|
564
|
+
- **Закон включения.** Жёсткое ребро законно только когда набор условий провайдера это подмножество набора
|
|
565
|
+
потребителя: провайдер обязан жить не меньше, иначе компилятор отказывает и указывает на `optional`. Тот же закон
|
|
566
|
+
держит условие с `from`: фича-источник обязана жить не меньше того, что условие гейтит. Группа открывается в
|
|
567
|
+
каноническом порядке своих фич и закрывается в обратном, а условие, вычисленное из закрытой фичи, читается как
|
|
568
|
+
`false`.
|
|
569
|
+
- **Слабое ребро.** `optional(x)` в `imports` и `optional(port)` в `requires` это одно слово и одно значение: ребро,
|
|
570
|
+
на котором потребитель может жить дольше провайдера. Экспорты приходят проекцией `Readable<Lookup<Exports>>`,
|
|
571
|
+
ресурсы за ней недостижимы, пока проекция говорит `missing`, а вызов слабого порта оседает исходом `CallError` с
|
|
572
|
+
кодом `unavailable`, пока живого провайдера нет, — это ошибка, а не отмена. Причина отсутствия не различается:
|
|
573
|
+
провайдера нет в приложении или его группа условий закрыта — выглядит одинаково.
|
|
574
|
+
- **Сахар слабого ребра.** У двух половин слабого ребра по одному слову: `calls(imports.x, keys)` для
|
|
575
|
+
экспортированных вызовов провайдера и `fromOptional(source, select, { missing })` для его данных. Обе читают
|
|
576
|
+
провайдера в момент использования, поэтому новый экземпляр подхватывается без пересборки, вызов в полёте
|
|
577
|
+
завершается фенсом самого провайдера, а от отсутствующего провайдера не удерживается ничего (D187).
|
|
578
|
+
- **Инертность слабого ребра.** Слабое ребро не втягивает провайдера в мир приложения и никогда не влияет на порядок
|
|
579
|
+
активации: провайдер открывается со своей группой условий, кто бы его ни импортировал. Закон включения держится
|
|
580
|
+
только для жёстких рёбер, а сообщение компилятора при его нарушении указывает на `optional`
|
|
581
|
+
(см. decisions.md, D105).
|
|
582
|
+
- **Экспорт.** `exports` вычисляется на открытии экземпляра из уже материализованного `own`, поэтому запись несёт
|
|
583
|
+
живые значения: `Call`, `Readable` и `Resource` того самого экземпляра, а не снимок и не ref. Модель целиком не
|
|
584
|
+
экспортируется — только её поля, потому что модель это запись значений, а не значение. Владеемое состояние выходит
|
|
585
|
+
наружу суженным до `Readable`: писатель остаётся у владельца, и это проверяет тип.
|
|
586
|
+
- **Видимость на жёстком ребре.** На жёстком ребре провайдер открывается раньше потребителя, а потребитель
|
|
587
|
+
закрывается раньше провайдера, поэтому потребитель никогда не видит закрытых значений. После retire провайдера
|
|
588
|
+
экспортированный `Readable` закрыт на фенсе, поэтому подписчик слышит это один раз, а чтение даёт `ReadableError`
|
|
589
|
+
с кодом `closed`.
|
|
590
|
+
- **UI authority.** `useModel` разрешает только модели, выданные монтированием вклада: модели его собственного
|
|
591
|
+
экземпляра плюс UI-модели, объявленные вкладом. Выдача гибридная: модели `own` доходят до любого компонента
|
|
592
|
+
поддерева неявно, per-mount модель объявляет тот компонент или хук, который её читает, и эту границу держит
|
|
593
|
+
lint-правило, потому что кадр монтирования один и вложенных читателей сам по себе не различает. Обхода дерева и
|
|
594
|
+
доступа к чужим фичам нет, frame стабилен, update модели не меняет Context.
|
|
595
|
+
- **Совместимость React.** `@opetope/react` поддерживает React `>=19.0.0 <20`; UI и браузерная интеграция Devtools поддерживают согласованные версии React/React DOM в этом диапазоне. Приёмка релиза устанавливает отдельные наборы React/React DOM и их типов версии `19.0.0` и версий contributor toolchain, затем проверяет типы NodeNext/Bundler, рендеринг с моделями, команды, demand и монтирование/размонтирование Devtools на Node 20.19+ (D251).
|
|
596
|
+
- **Время жизни монтирования.** Экземпляры планов `models` создаются в коммите и никогда в рендере (D188): первый
|
|
597
|
+
проход вклада не рендерит ничего, layout-эффект с ключом по идентичности вклада собирает монтирование из
|
|
598
|
+
закоммитившихся пропсов, а React сбрасывает этот один дополнительный синхронный рендер до отрисовки. Брошенный
|
|
599
|
+
рендер — вторая копия StrictMode, прерванный concurrent-проход, поддерево, приостановленное ленивым соседом, — не
|
|
600
|
+
создаёт ничего, поэтому подметать, усыновлять и пересобирать нечего, а layout-эффект ребёнка никогда не видит
|
|
601
|
+
закрытое монтирование.
|
|
602
|
+
- **Структурные читатели.** `Readable`, `Resource` и источник спроса это структурные контракты, поэтому хост вправе
|
|
603
|
+
реализовать их объектом, члены которого — методы. Любой хук зовёт их через тот объект, который ему передали, и
|
|
604
|
+
никогда как оторванную функцию, поэтому адаптер вправе опираться на свой `this` (D199).
|
|
605
|
+
- **Закреплённое монтирование.** Идентичность закоммиченного вклада владеет набором моделей. Повтор эффектов
|
|
606
|
+
StrictMode и скрытие/раскрытие Suspense используют тот же живой набор и состояние; отключение layout-эффектов
|
|
607
|
+
его не закрывает. Реальная замена идентичности или размонтирование освобождает его один раз. Размонтирование
|
|
608
|
+
скрытого поддерева освобождает набор в микрозадаче, потому что React уже отключил его layout-эффекты;
|
|
609
|
+
уведомления readable никогда не выполняются в insertion effects (D209). После освобождения вызов это отмена,
|
|
610
|
+
а не ошибка продукта.
|
|
611
|
+
- **Один Call — одна команда.** У монтирования одна команда на различимый Call, сколько бы полей модели его ни
|
|
612
|
+
называли: два поля одной модели это алиасы, они делят запись, активацию и фенс (D197). Две модели одного
|
|
613
|
+
монтирования с одним id модели по-прежнему дубликат. Статус остаётся локальным для потребителя: `useCommand` и
|
|
614
|
+
каждый ключ `useCommands` держат свои `inFlight`, `result` и `lastError`, даже когда называют один Call.
|
|
615
|
+
- **Ошибки.** Один класс на субъект авторского словаря, состояние в поле `code`: `FeatureError`, `CallError`,
|
|
616
|
+
`ReadableError`, `ContributionError`, `ApplicationError`, `DeclarationError`. Отмена это маркер по коду, а не
|
|
617
|
+
место в иерархии: он истинен для `CallError` с `cancelled` и `closed`, `FeatureError` с `retired` и
|
|
618
|
+
`ContributionError` с `inactive`. `CallError` с `unavailable` или `publication-rejected` такого бренда не несёт:
|
|
619
|
+
это ответ продукту, а не брошенный вызов. Тому же закону подчиняется `FeatureBoundaryError` с кодом `missing`: он
|
|
620
|
+
живёт в `@opetope/react/integration` вне бюджета трёх безопасных входов.
|
|
621
|
+
- **Границы.** `@opetope/*` не импортирует хосты приложений, их адаптеры и UI-дизайн-системы. Production-файл в `@opetope/*/src` не
|
|
622
|
+
больше 600 строк. На hot path методы вызываются напрямую, без `Reflect.apply`, bind-обёрток и массивов аргументов;
|
|
623
|
+
успешный путь не создаёт `Error`, циклы не добавляют защитный freeze. Новые per-operation записи требуют
|
|
624
|
+
точечного измерения аллокаций в пределах бюджетов. Hostile-boundary валидация выполняется на входе внешних данных.
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
Повторный вход из callbacks сохраняет эти законы (D218): close приложения фенсит готовых участников ещё открывающейся
|
|
629
|
+
condition group, включая member, вернувшийся после close. Abort/getter/key callbacks ресурса не могут затереть новую
|
|
630
|
+
selection или восстановить состояние после fence. Latest admission резервирует waiter и место в lane до уведомлений
|
|
631
|
+
о замещении; reentrant close/retire завершает этого waiter. Физический cleanup сохраняет порядок зависимостей.
|
|
632
|
+
|
|
633
|
+
Выбор target потока следует тому же правилу актуальности (D219): если getter источника или key callback синхронно
|
|
634
|
+
вызывает новую selection, прежний callback не может заменить её после возврата. Актуальность selection отделена
|
|
635
|
+
от равенства request key: новый target с тем же ключом остаётся доступен следующему refresh.
|
|
636
|
+
|
|
637
|
+
## 5. Гейты и бюджеты
|
|
638
|
+
|
|
639
|
+
Бюджеты производительности и размера — ratchet: повышением предела нельзя скрывать регрессию. Добавление
|
|
640
|
+
публичного слова в пределах цели требует явного решения и compile-checked пилота приложения; гейт фиксирует
|
|
641
|
+
согласованное изменение, как для D187 и D207.
|
|
642
|
+
|
|
643
|
+
### 5.1 `ci:public-surface`
|
|
644
|
+
|
|
645
|
+
Считает авторский словарь трёх безопасных входов — `opetope/core/src/index.ts`, `opetope/runtime/src/index.ts`,
|
|
646
|
+
`opetope/react/src/index.ts` — при текущем бюджете из
|
|
647
|
+
[контракт публичной поверхности](spec.ru.md#5-гейты-и-бюджеты), итогах и целях §3. Имена дедуплицируются по
|
|
648
|
+
входам, поэтому re-export ничего не стоит.
|
|
649
|
+
|
|
650
|
+
Тот же гейт держит ещё четыре проверки на внутренних входах `core/src/internal.ts`, `runtime/src/internal.ts`,
|
|
651
|
+
`react/src/integration.ts` и `react/src/testing.tsx`:
|
|
652
|
+
|
|
653
|
+
| Проверка | Что валит гейт |
|
|
654
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
655
|
+
| Изъятые слова | `[Tt]ask[A-Z]`, `^Task$`, `[Ss]ignal[A-Z]`, `Domain[A-Z]`, `^Domain`, `View[A-Z]`, `^Setup[A-Z]` в любой позиции идентификатора на внутреннем входе |
|
|
656
|
+
| Утечки в подписях | публичное значение, чей возвращаемый тип называет internal-only тип, вне allowlist из трёх входов (`onDemand`, `optional`, `selectByKey`) |
|
|
657
|
+
| Kernel-слова в глубине | обход глубины 4 по свойствам и результатам вызова каждого публичного типа, отказ на `Module`, `Attachment`, `Executor`, `Authority`, `Owner`, `Blueprint`, `IR` |
|
|
658
|
+
| Тексты авторских ошибок | строка или template literal, доезжающая до класса авторской ошибки и называющая module, attachment, executor, authority, owner, blueprint, generation, lifetime, task, setup или domain |
|
|
659
|
+
|
|
660
|
+
Классы авторских ошибок это `ApplicationError`, `CallError`, `ContributionError`, `DeclarationError`,
|
|
661
|
+
`FeatureError`, `ReadableError`.
|
|
662
|
+
|
|
663
|
+
### 5.2 `ci:size-limit`
|
|
664
|
+
|
|
665
|
+
Бюджетов пять, и все пять меряют то, что видит автор. Фикстуры это собранные consumer-ы, а не пакеты целиком, кроме
|
|
666
|
+
`@opetope/react`, который меряется на `dist`.
|
|
667
|
+
|
|
668
|
+
| Бюджет | Пакет | Предел |
|
|
669
|
+
| ---------------------------- | ------------------ | ------ |
|
|
670
|
+
| `author primitives consumer` | `@opetope/core` | 1.1 кБ |
|
|
671
|
+
| `data layer consumer` | `@opetope/core` | 4.9 кБ |
|
|
672
|
+
| `public feature consumer` | `@opetope/runtime` | 32 кБ |
|
|
673
|
+
| `application graph consumer` | `@opetope/runtime` | 48 кБ |
|
|
674
|
+
| `dist/**/*.js` | `@opetope/react` | 18 кБ |
|
|
675
|
+
|
|
676
|
+
Фикстура `feature-consumer.ts` держит текущую форму: условие в `when`, слабый импорт и слабый порт через `optional`,
|
|
677
|
+
живые `exports`, вклады `port`, `register` и `pipe`. `public feature consumer` определяется самим фичевым рантаймом, а не богатством композиции фикстуры: минимальная возможная фича — один host-контракт, один `calls`, один `openFeature` — весит уже большую часть бюджета, а снятие `resource`, `model`, `port` или `register` меняет вес на десятки байт, потому что граф фичевого рантайма почти монолитен.
|
|
678
|
+
|
|
679
|
+
npm-артефакты используют минифицированный ESM с `keepNames: false`, сохраняют декларации `.d.ts` и поставляют source
|
|
680
|
+
maps с исходным TypeScript в `sourcesContent` (D254). Имена функций и конструкторов, полученные через рефлексию, не
|
|
681
|
+
являются стабильным API; явные ID деклараций, публичные имена/коды ошибок и идентичность для `instanceof` сохраняются.
|
|
682
|
+
Карты помогают перейти к исходнику и раскрывают реализацию. Меньший несжатый JavaScript не гарантирует меньший
|
|
683
|
+
tarball или итоговый бандл приложения; см. [содержимое пакетов](releases.ru.md#содержимое-пакетов).
|
|
684
|
+
|
|
685
|
+
### 5.3 Проверка сборки приложения
|
|
686
|
+
|
|
687
|
+
Бюджеты бандла приложения и размещение чанков принадлежат хосту-потребителю. Когда приложение динамически
|
|
688
|
+
загружает манифест или UI фичи, проверка его сборки подтверждает наличие ожидаемых чанков и отсутствие кода,
|
|
689
|
+
предназначенного для отложенной загрузки, в initial-чанках страниц. Сам по себе dynamic import в исходнике не доказывает, что собранный бандл сохраняет эту
|
|
690
|
+
границу. Size-фикстуры пакетов измеряют потребителей фреймворка; хост дополнительно измеряет полный бандл
|
|
691
|
+
своего приложения.
|
|
692
|
+
|
|
693
|
+
### 5.4 `ci:perf-memory`
|
|
694
|
+
|
|
695
|
+
Гоняет нагрузки на размерах 1, 100 и 10 000 (application: 1, 100 и 1 000) против
|
|
696
|
+
`perf-memory-budgets.json`, schema 11. Гейт проверяет схему reference baseline; числовой enforcement использует
|
|
697
|
+
абсолютные пределы и scaling guards внутри текущего прогона, а не сравнение с историческими таймингами baseline.
|
|
698
|
+
Требуется Node `--expose-gc`.
|
|
699
|
+
|
|
700
|
+
Каждая нагрузка и размер проходят один полный прогрев, три замера времени и три отдельных замера памяти.
|
|
701
|
+
`results.*.*.durationMs` — медиана wall time полного workload: setup harness, ожидание cleanup и естественный GC
|
|
702
|
+
остаются внутри окна. Колбэки sampling heap и измерительного harness в timing-проходах ничего не делают;
|
|
703
|
+
принудительная сборка перед каждым проходом завершается до старта часов. Время обвязки не вычитается из смешанного
|
|
704
|
+
замера. Memory-проходы сохраняют sampling после GC, baseline после открытия harness и финальную сборку после cleanup;
|
|
705
|
+
peak-live и retained memory берутся по максимуму трёх проходов. `timingSamples` и `memorySamples` сохраняют все
|
|
706
|
+
результаты, включая `instrumentedDurationMs` и счётчики проб. У каждого прохода должен быть конечный checksum,
|
|
707
|
+
совпадающий с прогревом, и валидные измерения; ошибка завершает гейт без повторов до прохождения.
|
|
708
|
+
|
|
709
|
+
Schema 11 заменяет смешанную методику D252. Пределы 500 мс для command и 1 200 мс для application, остальные
|
|
710
|
+
численные бюджеты, размеры и тела нагрузок не меняются. Старые тайминги schema 10 несопоставимы;
|
|
711
|
+
полный reference переснимается явно. Уменьшение времени после удаления проб не является ускорением runtime.
|
|
712
|
+
Разделение относится к `results`; отдельные allocation, adapter, graph и listener workloads сохраняют свои
|
|
713
|
+
документированные методы измерения (D253).
|
|
714
|
+
|
|
715
|
+
`allocationRate.*.estimatedAllocatedBytesPerOperation` использует Poisson sampling V8 со средним интервалом
|
|
716
|
+
16 384 байта, включая объекты, собранные minor и major GC. Это медиана трёх прогонов с измерительной обвязкой,
|
|
717
|
+
без external backing stores; оценка, а не точный учёт. При нуле samples гейт проверяет 95%-ную верхнюю границу
|
|
718
|
+
`-log(0.05) * samplingInterval / operations`. GC observer ждёт два оборота event loop и фильтрует записи по окну
|
|
719
|
+
нагрузки; калибровка на 500 000 объектов проверяет собранные аллокации независимо от retained heap.
|
|
720
|
+
`peakLiveBytesPerOperation` измеряет живой heap после GC, а не allocation rate. Baseline пишется только после
|
|
721
|
+
успешного enforcement (D172).
|
|
722
|
+
|
|
723
|
+
Насыщенная CallLane использует production-очередь с 1k, 10k и 100k ожидающих запросов. Она проверяет FIFO-дренаж
|
|
724
|
+
и произвольную отмену чередующихся внутренних записей; enqueue измеряется отдельно от drain/cancel, берётся
|
|
725
|
+
медиана пяти прогонов и проверяется рост цены операции. Браузерная проверка отдельно создаёт настоящее ребро
|
|
726
|
+
приложения и блокирует уборку потребителя, доказывая, что его провайдер остаётся физически открыт.
|
|
727
|
+
|
|
728
|
+
`applicationGraphCompile` отдельно измеряет `defineApplication` на цепочке, звезде, графе групп и независимых
|
|
729
|
+
фичах при 256, 1 024 и 4 096 фичах. Определения строятся вне измерения; после двух прогревов идут семь samples.
|
|
730
|
+
У каждой формы потолок 100 мс на 4 096 фичах и guard роста цены на фичу 3× с нижней границей времени 1 мс.
|
|
731
|
+
Дополнение schema 9 измеряет компиляцию приложения, сохраняя измерение аллокаций и прежние лимиты (D213).
|
|
732
|
+
|
|
733
|
+
`hotPathScaling` добавляет batch/append/withdraw в одном target вкладов при 2k/4k/8k/16k entries,
|
|
734
|
+
чередующиеся admission `latest` и контрольный `queue` за заблокированной общей lane при 1k/2k/4k/8k парах,
|
|
735
|
+
а также idle-чтение inspection при 256/1 024/4 096 фичах: без импортов и с host-import у каждой фичи.
|
|
736
|
+
Итог — медиана пяти samples после двух прогревов, без setup, forced GC и cleanup.
|
|
737
|
+
Проверяются результат публикации, блокировка исполнения, уборка и неизменный sequence idle-инспекции.
|
|
738
|
+
У каждой формы абсолютный потолок на наибольшем размере и guard роста цены операции 3× с нижней
|
|
739
|
+
границей 1 мс для малого размера (D245).
|
|
740
|
+
Гейт требует все заявленные размеры, ровно пять конечных samples на строку и соответствие записанной медианы.
|
|
741
|
+
|
|
742
|
+
Perf- и TypeScript-гейты до нагрузок проверяют все обязательные числовые поля бюджетов по независимой схеме.
|
|
743
|
+
Отсутствующие и неизвестные ключи, нечисловые, отрицательные и не конечные пределы вызывают отказ;
|
|
744
|
+
enforced-сравнения также отвергают отсутствие измерения. При `--enforce --write-baseline` неуспешный вердикт
|
|
745
|
+
оставляет прежний baseline; успешный отчёт заменяет его атомарно. `ci:gate-config` проверяет эти ошибочные пути (D245).
|
|
746
|
+
|
|
747
|
+
Ключи бюджетов и форма каждой группы:
|
|
748
|
+
|
|
749
|
+
| Группа | Ключи |
|
|
750
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
751
|
+
| `workloads` | `command`, `definitionCompile`, `reusedDefinitionLifecycle`, `contribution` — у каждого `durationMsAt10k`, `peakLiveBytesPerOperationAt10k`, `retainedMbAt10k` |
|
|
752
|
+
| `applicationGraphCompile` | цепочка, звезда, граф групп и независимые фичи: `medianMsAt4096` 100, `perFeatureGrowthFactor` 3 |
|
|
753
|
+
| `hotPathScaling` | каждая из семи форм: `medianMsAtLargest` 50 для вкладов, 150 для admission/inspection; `perOperationGrowthFactor` 3 |
|
|
754
|
+
| `listenerMultiplicity` | `contributionDurationMsAt10k` 3 000, `callDurationMsAt10k` 3 000, `peakLiveBytesPerOperationAt10k` 32 768, `retainedMbAfterUnmountAt10k` 14.5 |
|
|
755
|
+
| `coreData` | `graph`, `leaseChurn`, `settledRead` — у последнего `microsecondsPerReadAtDepth1000` 1 и `depthGrowthFactor` 3 |
|
|
756
|
+
| `allocationRate` | `command` 5 120, `contribution` 3 072 байта на операцию на 10 к, `unobservedWrite` 8 байт на операцию на 100 к |
|
|
757
|
+
| `adapterDepth` | режимы `syncPrimitive`, `plainObject`, `nativePromise`, `customThenable`, `neverSettlingCancel` |
|
|
758
|
+
| `lifecycleStepDepth` | `sync` и `async`, у каждого открытие, закрытие, байты на шаг и удержанные МБ на 1 000 |
|
|
759
|
+
| группы одной формы | `cleanupRetry`, `coldInternalImport`, `mailbox`, `readableSelector` |
|
|
760
|
+
|
|
761
|
+
`workloads.contribution` публикует и снимает 10 000 вкладов под бюджетом 1 000 мс полного workload без проб памяти и 16 384 peak-live байта на операцию;
|
|
762
|
+
`workloads.command` держит 2 048 peak-live байт на операцию на 10 к. `listenerMultiplicity` проверяет контракт слушателей: у контроллера вызова нет членов подписки (`getSnapshot`/`subscribe` отсутствуют, остались ровно `activate`, `close`, `diagnostics`, `run` — команду вызывают, а не наблюдают, см. decisions.md, D162), у смонтированного `useReadable` ровно один listener на его `Readable`, после размонтирования ноль. `coreData.settledRead` читает активную чистую цепочку
|
|
763
|
+
20 000 раз на глубинах 1, 100 и 1 000 под двумя бюджетами: одна микросекунда на чтение на глубине 1 000 и рост не
|
|
764
|
+
больше 3× между глубинами 1 и 1 000.
|
|
765
|
+
|
|
766
|
+
### 5.5 `ci:type-stress`
|
|
767
|
+
|
|
768
|
+
Компилирует три сгенерированные фикстуры — `large-feature-01`, `-20`, `-40` — с `--extendedDiagnostics` и опрашивает
|
|
769
|
+
на каждой языковую службу. Schema version 4; гейт сравнивает с baseline той же схемы, а
|
|
770
|
+
`type-stress-budgets.json` на каждом срезе называет, какие числа `enforced`, а какие `informational`.
|
|
771
|
+
|
|
772
|
+
| Enforced budget | 01 | 20 | 40 |
|
|
773
|
+
| ------------------------------ | -----: | -----: | ------: |
|
|
774
|
+
| `tscInstantiations` | 10 000 | 80 000 | 155 000 |
|
|
775
|
+
| `tscMemoryMb` | 160 | 220 | 260 |
|
|
776
|
+
| `ideRssMb` | 400 | 600 | 650 |
|
|
777
|
+
| `ideDiagnosticMaxCount` | 4 | 4 | 4 |
|
|
778
|
+
| `ideDiagnosticMaxLineDistance` | 30 | 30 | 30 |
|
|
779
|
+
|
|
780
|
+
| Informational reference | 01 | 20 | 40 |
|
|
781
|
+
| ------------------------ | --: | --: | ----: |
|
|
782
|
+
| `tscCheckMs` | 150 | 350 | 550 |
|
|
783
|
+
| `tscTotalMs` | 500 | 800 | 1 000 |
|
|
784
|
+
| `ideColdCompletionMs` | 300 | 350 | 400 |
|
|
785
|
+
| `ideWarmCompletionP95Ms` | 10 | 10 | 12 |
|
|
786
|
+
| `ideQuickInfoP95Ms` | 10 | 10 | 12 |
|
|
787
|
+
| `ideDiagnosticMs` | 60 | 200 | 400 |
|
|
788
|
+
|
|
789
|
+
**Правило таймингов (D224):** каждая миллисекунда этого гейта информационна. Она печатается как
|
|
790
|
+
`actual of reference` и ничего не останавливает, включая два временных ratchet — время проверки против удвоенного
|
|
791
|
+
записанного baseline и время проверки на 40 секциях против max(2.75 × время проверки на 20 секциях, 1 000 мс), —
|
|
792
|
+
потому что на загруженной машине та же фикстура качается вокруг них без единой правки строки. Гейт держит то, что
|
|
793
|
+
решает сам код: instantiations по таблице и по `1.15 ×` записанного baseline, память компилятора, RSS языковой
|
|
794
|
+
службы, форму каскада диагностик и объявленную поверхность среза против baseline, с которого она записана.
|
|
795
|
+
|
|
796
|
+
### 5.6 `ci:browser-floor`
|
|
797
|
+
|
|
798
|
+
Два прохода по production-исходникам `core`, `runtime` и `react`, без тестов. Объявленный пол это Chrome 82,
|
|
799
|
+
Firefox 110, Safari 15, iOS 15, Android 82.
|
|
800
|
+
|
|
801
|
+
Проход по поверхности сопоставляет каждое обращение к члену, каждый глобальный конструктор и каждую глобальную
|
|
802
|
+
функцию с lib-декларациями TypeScript и отказывает всему, что выше пола, — `Object.hasOwn`, `Promise.any`, добавкам
|
|
803
|
+
`AbortSignal`, `AbortController.abort(reason)`, `String.prototype.at`/`replaceAll`, новым методам массива,
|
|
804
|
+
`Error.cause`, `Error(options)`, `AggregateError`, `FinalizationRegistry`, `WeakRef` — вне двух проверенных
|
|
805
|
+
исключений: `core/src/abort-compat.ts` для abort-хелперов и `core/src/platform-compat.ts` для `AggregateError`.
|
|
806
|
+
|
|
807
|
+
Проход по сырому dist трансформирует каждый выпущенный `.js` esbuild-ом с целями `chrome82`, `firefox110` и
|
|
808
|
+
`safari15` и падает на первом байте, где трансформация расходится с отгружаемым выводом.
|
|
809
|
+
|
|
810
|
+
### 5.7 `ci:inspection`
|
|
811
|
+
|
|
812
|
+
Граница наблюдения проверяет `ringCapacity` как неотрицательное безопасное целое до подключения (по умолчанию 256;
|
|
813
|
+
ноль сохраняет снимки, но не историю delta). Диагностические id приложений используют namespace `application:`,
|
|
814
|
+
поэтому одинаковые id объявлений приложения и фичи не сливаются. Необязательный `cause.at` — неотрицательное целое
|
|
815
|
+
в домене Date, не больше `8_640_000_000_000_000` миллисекунд эпохи. Замена activity является изменением кадра,
|
|
816
|
+
даже когда graph operations пусты (D242).
|
|
817
|
+
|
|
818
|
+
Гоняет исполняемую приёмку `assertInspectionSessionContract` из `@opetope/devtools/testing` против настоящего
|
|
819
|
+
продюсера `@opetope/runtime`, на фикстуре приложения с фичей-провайдером, фичей-потребителем со слабым импортом,
|
|
820
|
+
слабым портом, жёстким портом и вкладом `pipe`, и одним условием, привязанным хостом. Корпус это настоящие
|
|
821
|
+
транзакции: смена условия единственная, которую рантайм коммитит синхронно, поэтому синхронная приёмка видит именно
|
|
822
|
+
её, а всё остальное успевает лечь. Ровно одна проверка имеет право быть помеченной `skipped` — resync выпавшего
|
|
823
|
+
кадра, который харнесс уронить не умеет и который закрывает собственный тест кольца в рантайме. Второй пропуск валит
|
|
824
|
+
гейт. После приёмки фикстура на своём харнессе проверяет два факта D167: переключает `when` одного вклада
|
|
825
|
+
туда-обратно и ждёт `withheld` и обратно `published`, затем закрывает приложение при живых экземплярах и ждёт, что не
|
|
826
|
+
осталось ни живого экземпляра, ни живого вклада, а группа закрыта. Гейт валит любая упавшая проверка, а не только
|
|
827
|
+
второй пропуск. Затем гейт прогоняет порт управления D176 на живой группе с зависимым: suspend обязан примениться,
|
|
828
|
+
показать `force-inactive` у группы и увести зависимого, resume — снять и то и другое, неизвестное условие и здоровый
|
|
829
|
+
экземпляр — получить отказ данными, а `close()` — снять override. Гейт живёт в `tooling/stress`, чтобы рантайм не
|
|
830
|
+
держал зависимости на devtools; единственное ребро в обратную сторону это шов подключения devtools, который
|
|
831
|
+
открывает оба порта за хоста (D248).
|
|
832
|
+
|
|
833
|
+
### 5.8 `ci:docs` — ссылки на решения
|
|
834
|
+
|
|
835
|
+
Каждый `Dnnn` в `opetope/**/src` и `tooling/stress/scripts/*.mjs` обязан разрешаться в
|
|
836
|
+
заголовок `### Dnnn —` в `docs/decisions.md`. Именно это делает обещание архива — номера стабильны, существующие
|
|
837
|
+
строки не редактируются — проверяемым, а не заявленным: номера нельзя двигать как раз потому, что на них ссылается
|
|
838
|
+
код.
|
|
839
|
+
|
|
840
|
+
Коды находок ревью (`L3-F17`, `R1-1`) в тех же исходниках запрещены; гейт проверяет формы `L<digits>-F<digits>`
|
|
841
|
+
и `R<digits>-<digits>`. Они называют строку отчёта, которого у читателя кода
|
|
842
|
+
нет; находка, которую стоит держать в комментарии, — это либо решение, и тогда она ссылается на него, либо
|
|
843
|
+
рассуждение, и тогда оно написано словами. Проход сделан по всем пакетам, поэтому карта записанных чисел пуста, а
|
|
844
|
+
код теперь просто ошибка, где бы он ни встретился.
|
|
845
|
+
|
|
846
|
+
## Сценарные тесты и физическая активность
|
|
847
|
+
|
|
848
|
+
`createScenario(application, options)` из `@opetope/react/testing` открывает настоящее приложение и его существующую
|
|
849
|
+
inspection session (D206, D215). Передайте обычные `imports`/`conditions` и принадлежащий тесту адаптер
|
|
850
|
+
`host.mount(Component)`, возвращающий handle с `unmount()`. Пакет не добавляет зависимость от DOM renderer или test runner.
|
|
851
|
+
`scenario.mount(target, { props })` использует опубликованные Slot contributions и возвращает
|
|
852
|
+
`{ host, updateProps, unmount }`; `host` — исходный результат renderer. Typed targets требуют `options.props`,
|
|
853
|
+
а targets без props опускают его, как в `Slot` (D217). Fixtures не обходят authority команд.
|
|
854
|
+
|
|
855
|
+
Синхронный конструктор возвращает `ready`, поэтому pending lazy body можно исследовать до готовности.
|
|
856
|
+
`waitFor(snapshot => predicate, { label, timeoutMs, pollIntervalMs })` просыпается от inspection и дополнительно
|
|
857
|
+
опрашивает predicates внешнего UI; `notify()` будит его после изменения управляемой fixture. По умолчанию deadline
|
|
858
|
+
равен 1000ms, polling predicate — 10ms. `ScenarioTimeoutError` содержит data-only snapshot, ограниченную историю и
|
|
859
|
+
наблюдаемые conditions, фазы feature, body load, lane blockers и удержания ресурсов. Причины внутри repository
|
|
860
|
+
или сети не выводятся из догадок. `getSnapshot()` и `history()` используют ту же модель наблюдения; по умолчанию
|
|
861
|
+
хранятся 64 снимка, activity ограничена 256 записями. Capacities — целые от 1 до 10000.
|
|
862
|
+
Не заменяйте predicates фиксированным числом ticks.
|
|
863
|
+
|
|
864
|
+
`close()` синхронно ставит fence admission приложения, размонтирует зарегистрированные экраны и ждёт их cleanup
|
|
865
|
+
вместе с physical application drain. Deadline не отменяет cleanup: последующий `close()` может дождаться того же drain.
|
|
866
|
+
Deadline готовности также оставляет приложение доступным для inspection и явного закрытия.
|
|
867
|
+
`ownership()` описывает только зарегистрированное владение runtime; при отсутствующих, stale или усечённых данных
|
|
868
|
+
возвращается `unknown`. Терминальный stale-снимок считается полным лишь после подтверждённого сценарием успешного
|
|
869
|
+
физического cleanup. Это допускает проверку нулевых счётчиков в данном scope, но не доказывает отсутствие произвольных
|
|
870
|
+
host/UI/GC-утечек. Успешный cleanup очищает imports приложения и внутренние ссылки на renderer. Promise отказавшего
|
|
871
|
+
cleanup может удерживать исходные ошибки и retry capabilities; сохранённый пользователем `mounted.host` удерживает его renderer result.
|
|
872
|
+
|
|
873
|
+
Inspection graph/frame имеют схему `/3` и optional snapshots `opetope.runtime-activity/1`. В пределах одной session
|
|
874
|
+
frame без `activity` сохраняет предыдущую activity; полный snapshot/reset без этого поля очищает наблюдение (D216).
|
|
875
|
+
Frame с `activity` заменяет предыдущую activity целиком.
|
|
876
|
+
Используйте согласованные версии runtime/devtools: readers `/2` отвергают новую revision. Activity указывает execution,
|
|
877
|
+
фактическое поколение feature, физические Calls, точных текущих lane blockers, зарегистрированные leases и попытки загрузок.
|
|
878
|
+
Спрос хоста и UI-модели неизвестны; у stream наблюдается state, но не физические identities загрузок. `freshness`
|
|
879
|
+
и `truncated` отличают полное live-наблюдение от усечённого или отключённого. Закрытая session имеет stale-снимок;
|
|
880
|
+
`closed: true` требует успешного физического drain приложения. Control authority и продуктовые payload не добавляются.
|
|
881
|
+
Размер activity ограничен capacity записей. Сбор снимка обходит зарегистрированных owners, executors и resources,
|
|
882
|
+
поэтому capacity не ограничивает стоимость обхода. Сбор останавливается после доказанного truncation;
|
|
883
|
+
idle executors могут требовать обхода, чтобы подтвердить полноту данных. Обычный call dispatch не создаёт диагностических записей при
|
|
884
|
+
выключенном наблюдении. Frames ограничены существующей ring capacity.
|