@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.
Files changed (314) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +345 -0
  4. package/README.ru.md +344 -0
  5. package/dist/application-compiler-edges.d.ts +3 -0
  6. package/dist/application-compiler-edges.js +2 -0
  7. package/dist/application-compiler-edges.js.map +1 -0
  8. package/dist/application-compiler-graph.d.ts +8 -0
  9. package/dist/application-compiler-graph.js +2 -0
  10. package/dist/application-compiler-graph.js.map +1 -0
  11. package/dist/application-compiler.d.ts +116 -0
  12. package/dist/application-compiler.js +2 -0
  13. package/dist/application-compiler.js.map +1 -0
  14. package/dist/application-conditions.d.ts +18 -0
  15. package/dist/application-conditions.js +2 -0
  16. package/dist/application-conditions.js.map +1 -0
  17. package/dist/application-definition.d.ts +30 -0
  18. package/dist/application-definition.js +2 -0
  19. package/dist/application-definition.js.map +1 -0
  20. package/dist/application-error.d.ts +10 -0
  21. package/dist/application-error.js +2 -0
  22. package/dist/application-error.js.map +1 -0
  23. package/dist/application-execution.d.ts +43 -0
  24. package/dist/application-execution.js +2 -0
  25. package/dist/application-execution.js.map +1 -0
  26. package/dist/application-feature-bindings.d.ts +11 -0
  27. package/dist/application-feature-bindings.js +2 -0
  28. package/dist/application-feature-bindings.js.map +1 -0
  29. package/dist/application-feature-instance.d.ts +5 -0
  30. package/dist/application-feature-instance.js +2 -0
  31. package/dist/application-feature-instance.js.map +1 -0
  32. package/dist/application-group-order.d.ts +29 -0
  33. package/dist/application-group-order.js +2 -0
  34. package/dist/application-group-order.js.map +1 -0
  35. package/dist/application-instance-retirement.d.ts +36 -0
  36. package/dist/application-instance-retirement.js +2 -0
  37. package/dist/application-instance-retirement.js.map +1 -0
  38. package/dist/application-open-options.d.ts +29 -0
  39. package/dist/application-open-options.js +2 -0
  40. package/dist/application-open-options.js.map +1 -0
  41. package/dist/application-port-compiler.d.ts +28 -0
  42. package/dist/application-port-compiler.js +2 -0
  43. package/dist/application-port-compiler.js.map +1 -0
  44. package/dist/attachment-call-declaration.d.ts +43 -0
  45. package/dist/attachment-call-declaration.js +2 -0
  46. package/dist/attachment-call-declaration.js.map +1 -0
  47. package/dist/attachment-declaration.d.ts +68 -0
  48. package/dist/attachment-declaration.js +2 -0
  49. package/dist/attachment-declaration.js.map +1 -0
  50. package/dist/attachment-execution.d.ts +9 -0
  51. package/dist/attachment-execution.js +2 -0
  52. package/dist/attachment-execution.js.map +1 -0
  53. package/dist/attachment-retirement-scheduler.d.ts +14 -0
  54. package/dist/attachment-retirement-scheduler.js +2 -0
  55. package/dist/attachment-retirement-scheduler.js.map +1 -0
  56. package/dist/call-option-snapshot.d.ts +20 -0
  57. package/dist/call-option-snapshot.js +2 -0
  58. package/dist/call-option-snapshot.js.map +1 -0
  59. package/dist/compile-call-target-bindings.d.ts +14 -0
  60. package/dist/compile-call-target-bindings.js +2 -0
  61. package/dist/compile-call-target-bindings.js.map +1 -0
  62. package/dist/compile-module-template.d.ts +38 -0
  63. package/dist/compile-module-template.js +2 -0
  64. package/dist/compile-module-template.js.map +1 -0
  65. package/dist/condition-group-execution.d.ts +44 -0
  66. package/dist/condition-group-execution.js +2 -0
  67. package/dist/condition-group-execution.js.map +1 -0
  68. package/dist/condition-override.d.ts +28 -0
  69. package/dist/condition-override.js +2 -0
  70. package/dist/condition-override.js.map +1 -0
  71. package/dist/condition-source.d.ts +10 -0
  72. package/dist/condition-source.js +2 -0
  73. package/dist/condition-source.js.map +1 -0
  74. package/dist/condition-types.d.ts +14 -0
  75. package/dist/condition.d.ts +28 -0
  76. package/dist/condition.js +2 -0
  77. package/dist/condition.js.map +1 -0
  78. package/dist/control-registry.d.ts +56 -0
  79. package/dist/control-registry.js +2 -0
  80. package/dist/control-registry.js.map +1 -0
  81. package/dist/dynamic-scope-child.d.ts +23 -0
  82. package/dist/dynamic-scope-child.js +2 -0
  83. package/dist/dynamic-scope-child.js.map +1 -0
  84. package/dist/dynamic-scope-controller.d.ts +16 -0
  85. package/dist/dynamic-scope-controller.js +2 -0
  86. package/dist/dynamic-scope-controller.js.map +1 -0
  87. package/dist/feature-attachment-authoring-types.d.ts +51 -0
  88. package/dist/feature-attachment-lowering.d.ts +18 -0
  89. package/dist/feature-attachment-lowering.js +2 -0
  90. package/dist/feature-attachment-lowering.js.map +1 -0
  91. package/dist/feature-attachment.d.ts +45 -0
  92. package/dist/feature-attachment.js +2 -0
  93. package/dist/feature-attachment.js.map +1 -0
  94. package/dist/feature-authoring-types.d.ts +196 -0
  95. package/dist/feature-authoring.d.ts +15 -0
  96. package/dist/feature-authoring.js +2 -0
  97. package/dist/feature-authoring.js.map +1 -0
  98. package/dist/feature-body.d.ts +53 -0
  99. package/dist/feature-body.js +2 -0
  100. package/dist/feature-body.js.map +1 -0
  101. package/dist/feature-call-authority.d.ts +8 -0
  102. package/dist/feature-call-authority.js +2 -0
  103. package/dist/feature-call-authority.js.map +1 -0
  104. package/dist/feature-call-types.d.ts +39 -0
  105. package/dist/feature-call.d.ts +18 -0
  106. package/dist/feature-call.js +2 -0
  107. package/dist/feature-call.js.map +1 -0
  108. package/dist/feature-calls.d.ts +6 -0
  109. package/dist/feature-calls.js +2 -0
  110. package/dist/feature-calls.js.map +1 -0
  111. package/dist/feature-contract.d.ts +70 -0
  112. package/dist/feature-contract.js +2 -0
  113. package/dist/feature-contract.js.map +1 -0
  114. package/dist/feature-contribution-model.d.ts +38 -0
  115. package/dist/feature-contribution-model.js +2 -0
  116. package/dist/feature-contribution-model.js.map +1 -0
  117. package/dist/feature-contribution.d.ts +126 -0
  118. package/dist/feature-contribution.js +2 -0
  119. package/dist/feature-contribution.js.map +1 -0
  120. package/dist/feature-definition-api.d.ts +53 -0
  121. package/dist/feature-definition-support.d.ts +21 -0
  122. package/dist/feature-definition-support.js +2 -0
  123. package/dist/feature-definition-support.js.map +1 -0
  124. package/dist/feature-effect.d.ts +26 -0
  125. package/dist/feature-effect.js +2 -0
  126. package/dist/feature-effect.js.map +1 -0
  127. package/dist/feature-event.d.ts +32 -0
  128. package/dist/feature-event.js +2 -0
  129. package/dist/feature-event.js.map +1 -0
  130. package/dist/feature-generation.d.ts +34 -0
  131. package/dist/feature-generation.js +2 -0
  132. package/dist/feature-generation.js.map +1 -0
  133. package/dist/feature-lazy-generation.d.ts +6 -0
  134. package/dist/feature-lazy-generation.js +2 -0
  135. package/dist/feature-lazy-generation.js.map +1 -0
  136. package/dist/feature-lazy.d.ts +44 -0
  137. package/dist/feature-lazy.js +2 -0
  138. package/dist/feature-lazy.js.map +1 -0
  139. package/dist/feature-materialization-binding.d.ts +31 -0
  140. package/dist/feature-materialization-binding.js +2 -0
  141. package/dist/feature-materialization-binding.js.map +1 -0
  142. package/dist/feature-materialization-types.d.ts +45 -0
  143. package/dist/feature-model-dependencies.d.ts +23 -0
  144. package/dist/feature-model-dependencies.js +2 -0
  145. package/dist/feature-model-dependencies.js.map +1 -0
  146. package/dist/feature-model.d.ts +60 -0
  147. package/dist/feature-model.js +2 -0
  148. package/dist/feature-model.js.map +1 -0
  149. package/dist/feature-optional.d.ts +15 -0
  150. package/dist/feature-optional.js +2 -0
  151. package/dist/feature-optional.js.map +1 -0
  152. package/dist/feature-own-lowering.d.ts +22 -0
  153. package/dist/feature-own-lowering.js +2 -0
  154. package/dist/feature-own-lowering.js.map +1 -0
  155. package/dist/feature-port-binding.d.ts +6 -0
  156. package/dist/feature-port-binding.js +2 -0
  157. package/dist/feature-port-binding.js.map +1 -0
  158. package/dist/feature-port.d.ts +66 -0
  159. package/dist/feature-port.js +2 -0
  160. package/dist/feature-port.js.map +1 -0
  161. package/dist/feature-record.d.ts +6 -0
  162. package/dist/feature-record.js +2 -0
  163. package/dist/feature-record.js.map +1 -0
  164. package/dist/feature-resource.d.ts +29 -0
  165. package/dist/feature-resource.js +2 -0
  166. package/dist/feature-resource.js.map +1 -0
  167. package/dist/feature-scope-types.d.ts +35 -0
  168. package/dist/feature-scope.d.ts +29 -0
  169. package/dist/feature-scope.js +2 -0
  170. package/dist/feature-scope.js.map +1 -0
  171. package/dist/feature-stream.d.ts +37 -0
  172. package/dist/feature-stream.js +2 -0
  173. package/dist/feature-stream.js.map +1 -0
  174. package/dist/feature-timers.d.ts +14 -0
  175. package/dist/feature-timers.js +2 -0
  176. package/dist/feature-timers.js.map +1 -0
  177. package/dist/index.d.ts +19 -0
  178. package/dist/index.js +2 -0
  179. package/dist/index.js.map +1 -0
  180. package/dist/inspection-activity-protocol.d.ts +63 -0
  181. package/dist/inspection-activity.d.ts +54 -0
  182. package/dist/inspection-activity.js +2 -0
  183. package/dist/inspection-activity.js.map +1 -0
  184. package/dist/inspection-diff.d.ts +10 -0
  185. package/dist/inspection-diff.js +2 -0
  186. package/dist/inspection-diff.js.map +1 -0
  187. package/dist/inspection-module-activity.d.ts +4 -0
  188. package/dist/inspection-module-activity.js +2 -0
  189. package/dist/inspection-module-activity.js.map +1 -0
  190. package/dist/inspection-observer.d.ts +38 -0
  191. package/dist/inspection-observer.js +2 -0
  192. package/dist/inspection-observer.js.map +1 -0
  193. package/dist/inspection-plan.d.ts +33 -0
  194. package/dist/inspection-plan.js +2 -0
  195. package/dist/inspection-plan.js.map +1 -0
  196. package/dist/inspection-protocol.d.ts +333 -0
  197. package/dist/inspection-protocol.js +2 -0
  198. package/dist/inspection-protocol.js.map +1 -0
  199. package/dist/inspection-registry.d.ts +41 -0
  200. package/dist/inspection-registry.js +2 -0
  201. package/dist/inspection-registry.js.map +1 -0
  202. package/dist/inspection-session.d.ts +92 -0
  203. package/dist/inspection-session.js +2 -0
  204. package/dist/inspection-session.js.map +1 -0
  205. package/dist/inspection-snapshot.d.ts +4 -0
  206. package/dist/inspection-snapshot.js +2 -0
  207. package/dist/inspection-snapshot.js.map +1 -0
  208. package/dist/inspection-state.d.ts +91 -0
  209. package/dist/inspection-state.js +2 -0
  210. package/dist/inspection-state.js.map +1 -0
  211. package/dist/instance-demand.d.ts +31 -0
  212. package/dist/instance-demand.js +2 -0
  213. package/dist/instance-demand.js.map +1 -0
  214. package/dist/internal.d.ts +41 -0
  215. package/dist/internal.js +2 -0
  216. package/dist/internal.js.map +1 -0
  217. package/dist/keyed-scope-controller.d.ts +11 -0
  218. package/dist/keyed-scope-controller.js +2 -0
  219. package/dist/keyed-scope-controller.js.map +1 -0
  220. package/dist/model-kernel.d.ts +18 -0
  221. package/dist/model-kernel.js +2 -0
  222. package/dist/model-kernel.js.map +1 -0
  223. package/dist/module-call-context.d.ts +7 -0
  224. package/dist/module-call-context.js +2 -0
  225. package/dist/module-call-context.js.map +1 -0
  226. package/dist/module-call-runtime.d.ts +21 -0
  227. package/dist/module-call-runtime.js +2 -0
  228. package/dist/module-call-runtime.js.map +1 -0
  229. package/dist/module-generation.d.ts +57 -0
  230. package/dist/module-generation.js +2 -0
  231. package/dist/module-generation.js.map +1 -0
  232. package/dist/module-instance-types.d.ts +156 -0
  233. package/dist/module-instance.d.ts +19 -0
  234. package/dist/module-instance.js +2 -0
  235. package/dist/module-instance.js.map +1 -0
  236. package/dist/module-runtime-identity.d.ts +4 -0
  237. package/dist/module-runtime-identity.js +2 -0
  238. package/dist/module-runtime-identity.js.map +1 -0
  239. package/dist/module-scope-open.d.ts +4 -0
  240. package/dist/module-scope-open.js +2 -0
  241. package/dist/module-scope-open.js.map +1 -0
  242. package/dist/module-scope-retirement.d.ts +3 -0
  243. package/dist/module-scope-retirement.js +2 -0
  244. package/dist/module-scope-retirement.js.map +1 -0
  245. package/dist/module-template-ir.d.ts +59 -0
  246. package/dist/owner-generation-retirement.d.ts +6 -0
  247. package/dist/owner-generation-retirement.js +2 -0
  248. package/dist/owner-generation-retirement.js.map +1 -0
  249. package/dist/owner-generation-state.d.ts +123 -0
  250. package/dist/owner-generation-state.js +2 -0
  251. package/dist/owner-generation-state.js.map +1 -0
  252. package/dist/owner-generation.d.ts +16 -0
  253. package/dist/owner-generation.js +2 -0
  254. package/dist/owner-generation.js.map +1 -0
  255. package/dist/public-module-definition.d.ts +9 -0
  256. package/dist/public-module-definition.js +2 -0
  257. package/dist/public-module-definition.js.map +1 -0
  258. package/dist/public-module-instance.d.ts +6 -0
  259. package/dist/public-module-instance.js +2 -0
  260. package/dist/public-module-instance.js.map +1 -0
  261. package/dist/public-module-retirement-diagnostics.d.ts +5 -0
  262. package/dist/public-module-retirement-diagnostics.js +2 -0
  263. package/dist/public-module-retirement-diagnostics.js.map +1 -0
  264. package/dist/public-module-retirement.d.ts +3 -0
  265. package/dist/public-module-retirement.js +2 -0
  266. package/dist/public-module-retirement.js.map +1 -0
  267. package/dist/public-module-scope.d.ts +5 -0
  268. package/dist/public-module-scope.js +2 -0
  269. package/dist/public-module-scope.js.map +1 -0
  270. package/dist/public-module-state.d.ts +28 -0
  271. package/dist/public-module-state.js +2 -0
  272. package/dist/public-module-state.js.map +1 -0
  273. package/dist/public-module-types.d.ts +295 -0
  274. package/dist/public-module.d.ts +4 -0
  275. package/dist/resource-cache.d.ts +12 -0
  276. package/dist/resource-cache.js +2 -0
  277. package/dist/resource-cache.js.map +1 -0
  278. package/dist/resource-controller.d.ts +29 -0
  279. package/dist/resource-controller.js +2 -0
  280. package/dist/resource-controller.js.map +1 -0
  281. package/dist/resource-policy.d.ts +16 -0
  282. package/dist/resource-policy.js +2 -0
  283. package/dist/resource-policy.js.map +1 -0
  284. package/dist/resource-snapshot.d.ts +12 -0
  285. package/dist/resource-snapshot.js +2 -0
  286. package/dist/resource-snapshot.js.map +1 -0
  287. package/dist/resource-types.d.ts +3 -0
  288. package/dist/runtime-error-reporting.d.ts +4 -0
  289. package/dist/runtime-error-reporting.js +2 -0
  290. package/dist/runtime-error-reporting.js.map +1 -0
  291. package/dist/stream-backpressure.d.ts +18 -0
  292. package/dist/stream-backpressure.js +2 -0
  293. package/dist/stream-backpressure.js.map +1 -0
  294. package/dist/stream-cleanup.d.ts +17 -0
  295. package/dist/stream-cleanup.js +2 -0
  296. package/dist/stream-cleanup.js.map +1 -0
  297. package/dist/stream-controller-types.d.ts +51 -0
  298. package/dist/stream-controller.d.ts +5 -0
  299. package/dist/stream-controller.js +2 -0
  300. package/dist/stream-controller.js.map +1 -0
  301. package/docs/agent-guide.md +214 -0
  302. package/docs/agent-guide.ru.md +208 -0
  303. package/docs/cookbook.md +734 -0
  304. package/docs/cookbook.ru.md +729 -0
  305. package/docs/decisions.md +1437 -0
  306. package/docs/devtools.md +423 -0
  307. package/docs/devtools.ru.md +419 -0
  308. package/docs/how-it-works.md +521 -0
  309. package/docs/how-it-works.ru.md +495 -0
  310. package/docs/releases.md +78 -0
  311. package/docs/releases.ru.md +78 -0
  312. package/docs/spec.md +874 -0
  313. package/docs/spec.ru.md +884 -0
  314. package/package.json +72 -0
@@ -0,0 +1,495 @@
1
+ # Как работает Opetope: создание, вычисление, очистка
2
+
3
+ Статус: **DRAFT**, 2026-09-04. Документ объясняет доступным языком, что происходит в `@opetope/core`, `@opetope/runtime`
4
+ и `@opetope/react` во время работы приложения: что создаётся, когда пересчитывается и как убирается. Нормативный источник — [spec.md](spec.md); здесь только механика и её причины, а пометки `(Dnnn)` ведут в [decisions.md](decisions.md). Ссылки на файлы даны для того, кто захочет проверить утверждение по коду.
5
+
6
+ ## 0. Карта понятий
7
+
8
+ | Слово | Что это | Кто создаёт | Когда умирает |
9
+ | ----------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------ |
10
+ | приложение | список фич, сток ошибок, связанные хостом контракты и условия | `defineApplication` + `openApplication` | `close()` |
11
+ | условие (`Condition`) | именованный `Readable<boolean>`, от которого зависит жизнь группы фич | `defineCondition`, значение даёт хост или фича-источник | вместе с приложением |
12
+ | фича | определение: `id`, `when`, `imports`, `requires`, `own`, `exports`, `provides` | `defineFeature`, один раз на загрузку модуля | никогда, это описание |
13
+ | экземпляр фичи | живая копия определения: модели, вызовы, ресурсы, вклады | runtime, когда фича нужна и её условия истинны | retire, когда условия ложны или требование снято |
14
+ | модель | запись из `Readable` и `Call`, которой владеет экземпляр | фабрика из `own.model()` при открытии экземпляра | fence и drain экземпляра |
15
+ | вызов (`Call`) | операция с очередью, lane, отменой и single-flight | `ctx.call` в модели или `call` в `own` | отмена при retire |
16
+ | ресурс, поток, событие | материализация внешних данных с ключом, retention и отменой | `ctx.resource`, `ctx.stream`, `ctx.event` | закрытие в drain |
17
+ | вклад | что фича вкладывает в чужую цель: слот, pipe, реестр, порт | `provides` при открытии экземпляра | withdraw в fence |
18
+ | цель (`SlotTarget`, `Pipe`, `Registry`, `Port`) | место, куда вкладывают; живёт вне фич, в файле контрактов | `defineSlot`, `definePipe`, `defineRegistry`, `definePort` | никогда |
19
+
20
+ Три сквозных правила, которые объясняют половину поведения:
21
+
22
+ 1. **Подлинность проверяется по `WeakMap`, а не по форме.** Определение, экземпляр, цель, lane, binding хранятся в
23
+ закрытых `WeakMap`/`WeakSet`; всё, что туда не попало, отвергается `TypeError('… is not authentic.')`. Поэтому
24
+ значение фреймворка нельзя подделать литералом.
25
+ 2. **Всё, что даёт автор, снимается снапшотом при объявлении** с проверкой точного набора ключей и замораживается.
26
+ Мутация исходного объекта после этого невидима.
27
+ 3. **Порядок всегда канонический.** Фичи по id, вклады по id, импорты по ключу, attachment-ы по слоту; любой разбор
28
+ идёт в обратном порядке сборки.
29
+
30
+ ## 1. Приложение
31
+
32
+ ### 1.1 Компиляция: `defineApplication({ id, features, reporter })`
33
+
34
+ Компиляция синхронная и происходит один раз, до открытия чего бы то ни было. Это и есть «compile time» для законов
35
+ графа: ошибка здесь падает в первом же тесте манифеста.
36
+
37
+ | Шаг | Что происходит | Где |
38
+ | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
39
+ | 1 | точный набор ключей, `id` строка и уникален среди живых определений процесса (реестр слабый, D127), `reporter` функция | `runtime/src/application-definition.ts` |
40
+ | 2 | мир замыкается: от включённых фич по `imports` до провайдеров, через контракт экспорта | там же |
41
+ | 3 | фичи сортируются по id, дубли объекта и id отвергаются | `runtime/src/application-compiler.ts` |
42
+ | 4 | рёбра импортов: контракт хоста становится входом приложения, определение фичи резолвится в провайдера | там же |
43
+ | 5 | рёбра портов `requires ↔ provides`: ровно один провайдер на порт | `application-port-compiler.ts` |
44
+ | 6 | топологическая сортировка Кана с лексикографическим разрывом связей; цикл жёстких импортов это ошибка | `application-compiler-graph.ts` |
45
+ | 7 | проверка времени жизни: для жёсткого ребра `provider.when ⊆ consumer.when`; слабое ребро `optional` от закона включения свободно (D105) | `application-compiler.ts` |
46
+ | 8 | группы по одинаковому набору условий, порядок активации внутри группы и порядок между группами по жёстким рёбрам (D123); план графа для порта наблюдения | там же, `application-group-order.ts` |
47
+
48
+ Законы, которые здесь проверяются: один провайдер на контракт и на порт, одна идентичность контракта на id, нет
49
+ провайдера это ошибка, нет циклов, провайдер жёсткого ребра живёт не короче потребителя.
50
+
51
+ ### 1.2 Открытие: `openApplication(app, { cleanupFailure, conditions, imports })`
52
+
53
+ | Шаг | Что происходит |
54
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | 1 | `imports` принимает только результаты `bind(contract, value)`; каждый контракт хоста связан ровно один раз |
56
+ | 2 | `conditions` даёт `Readable<boolean>` на каждое условие без `from`; тип записи выведен из приложения, лишнее и недостающее ловит TypeScript |
57
+ | 3 | `cleanupFailure` необязателен и по умолчанию `report`; `quarantine` заставляет каждый экземпляр этого приложения оставлять провалившуюся уборку повторяемым фронтиром вместо отчёта (D182) |
58
+ | 4 | контроллер стартует в конструкторе: открытие начинается синхронно до первого `await` |
59
+ | 5 | фичи с пустым `when` открываются **последовательно** в порядке компиляции: `open()`, затем `await ready`, затем следующая |
60
+ | 6 | условия с `from` вычисляются из экспортов открывшихся фич-источников |
61
+ | 7 | для каждой группы условий ставится reconciler: желаемое состояние `all(conditions) === true`; перед открытием группа ждёт готовности провайдеров своих жёстких рёбер из других групп, перед закрытием ждёт ухода их потребителей (D123) |
62
+ | 8 | возвращается `ApplicationExecution` с `ready` и `close()` |
63
+
64
+ Reconciler группы это один сериализованный worker с эпохами: смена условия поднимает эпоху, ограждает незавершённое
65
+ открытие и планирует сверку на микрозадаче; открытие членов идёт последовательно с проверкой актуальности эпохи перед
66
+ каждым, закрытие всегда в обратном порядке и терпимо к ошибкам. Источник условия обязан отдать булево значение
67
+ синхронно при подключении, иначе ошибка сразу.
68
+
69
+ `close()` однократный: снять подписки условий в обратном порядке, оградить незавершённое открытие, закрыть группы
70
+ в обратном порядке, дождаться `ready`, закрыть постоянные фичи в обратном порядке и собрать отказы в одну ошибку.
71
+ Сессия наблюдения закрывается в `finally` после дренажа и поэтому видит финальные переходы групп, экземпляров
72
+ и вкладов (D167). Общий Promise закрытия существует до вызова любого disposer источника. Если подписка условия
73
+ закрывает приложение до возврата своего disposer, тот снимается сразу после возврата, следующие группы не
74
+ создаются, готовность отклоняется, а close дожидается всей начатой работы (D210).
75
+
76
+ ### 1.3 Время жизни: `when`
77
+
78
+ Фича объявляет, при каких условиях живёт: `when: [authorized, miningEnabled]`. Пустой `when` это постоянная фича.
79
+ Группа фич с одинаковым набором условий открывается и закрывается вместе, порядок внутри группы задают импорты. Закон
80
+ включения наборов означает простую вещь: нельзя жёстко зависеть от того, кто может умереть раньше тебя. Кто хочет
81
+ читать данные более короткоживущей фичи, берёт слабое ребро `optional` и получает проекцию `Lookup` с состоянием
82
+ `missing` (§2.4); слабое ребро не влияет на активацию провайдера и не тянет его в мир приложения (D106).
83
+
84
+ ## 2. Экземпляр фичи
85
+
86
+ ### 2.1 При определении, один раз
87
+
88
+ Синхронная форма `defineFeature` выполняет фабрику `own` один раз и строит промежуточное представление из трёх
89
+ видов узлов ядра: scope, attachment, call. Разделённый заголовок объявляет топологию; его `defineFeature.body`
90
+ строит это представление при загрузке кода body (D186, D207). Всё авторское сводится к этим узлам:
91
+
92
+ | В `own` | Во что опускается |
93
+ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
94
+ | `attach(source, { open, close })` | scope с одним attachment-ом |
95
+ | `call({ run, within, lane, once, singleFlight, policy })` | узел call, привязанный к attachment-у из `within` |
96
+ | `calls(imports.x, [...])` | неявный attachment на источник без lifecycle плюс один call на ключ |
97
+ | `lane({ within })` | запись lane, материализуется на экземпляр |
98
+ | `effect`, `event`, `resource`, `stream`, `scope.*` | attachment с контроллером внутри |
99
+ | `requires.x` | один call на скрытом attachment-е требований, который пересылает в провайдера порта; у `optional(port)` он оседает `CallError` `unavailable`, пока провайдера нет (D105) |
100
+ | `model(...)` | **не узел kernel-а**, а описатель данных; фабрика модели здесь не выполняется; владеемое состояние живёт только в модели (`ctx.state`, D139) |
101
+
102
+ `exports` проверяется по форме каждого поля (`Call`, `Readable` или `Resource`), идентичность с `own` намеренно не проверяется; фасад экспортов вычисляется при открытии экземпляра с живым `own` (D88). `provides` выполняется один раз, синхронно, и делится на порты и вклады по тому,
103
+ какой builder породил запись. Каждый attachment фичи критичен: экземпляр не готов, пока не готовы все.
104
+
105
+ ### 2.2 Открытие экземпляра
106
+
107
+ | Шаг | Что происходит | Где |
108
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
109
+ | 1 | снапшот опций; `imports` и `requirements` содержат ровно нужные поля; вложение берёт значение только из импорта или требования; значение порта проверяется по объявлению | `runtime/src/feature-generation.ts` |
110
+ | 2 | создаётся запись экземпляра в состоянии `assembly` | `public-module-instance.ts` |
111
+ | 3 | **`prepare` синхронно, до открытия первого scope**: регистрация участника отката моделей до запуска фабрик; затем фабрики создают свои узлы, затем фасад экспортов (значения `own` он материализует лениво), затем участник вкладов | `module-generation.ts`, `feature-model.ts` |
112
+ | 4 | scope открываются последовательно; перед каждым проверяется, не запрошена ли отмена | `module-generation.ts` |
113
+ | 5 | внутри scope вложения одна волна без порядка между ними (D163): критичные стартуют до коммита владельца, готовность считается счётчиками, без рекурсии в промисы; когда критичных не осталось, scope публикует авторитет владельца, вызовы становятся живыми, затем стартуют отложенные | `module-scope-open.ts` |
114
+ | 6 | внутренняя ready barrier отдаёт `{ exports }`; затем публикуются вклады, и только после успешной публикации разрешается публичный `instance.ready` | `feature-generation.ts` |
115
+
116
+ Законы: `prepare` обязан вернуться синхронно; участники retire регистрируются до первого открытия scope; отказ критичного attachment-а сразу переводит scope в retire, отказ отложенного не трогает остальные и оставляет scope готовым; отказавшее вложение попадает в карантинный фронтир в каноническом порядке слотов.
117
+
118
+ Attachment изнутри это маленькая машина с почтовым ящиком: не больше одного активного и одного ожидающего намерения;
119
+ новое намерение либо присоединяется к совместимому, либо заменяет ещё не начатое, либо встаёт в очередь и отменяет
120
+ активное. Открытие берёт разрешение у контроллера готовности, собирает список уборок (первая всегда `close`), и любая
121
+ ошибка откатывает попытку в обратном порядке уборок.
122
+
123
+ ### 2.3 Retire: fence, drain, settle
124
+
125
+ ```text
126
+ retire() → fence (синхронно, один проход) → drain (асинхронно, параллельно) → settle (политика)
127
+ ```
128
+
129
+ 1. **Fence.** Состояние `retiring`. Участники в порядке регистрации: вклады снимаются с целей, scope данных
130
+ ограждается: модели переводятся в неактивные, `AbortController` срабатывает, ячейки `state` закрываются. Затем
131
+ ограждается каждый scope. Ограждение это то, что превращает любую последующую работу в отмену: авторитет вызовов
132
+ недействителен, новый вызов через этот attachment завершается `CallError` как отмена, а не как продуктовая ошибка.
133
+ Ничего не бросается, только собираются фронты уборки.
134
+ 2. **Drain.** Все фронты запускаются параллельно; вложения одного scope дренируются целиком одной волной, порядка между ними нет (D163). Вызовы: ожидающие отменяются, исполняющиеся
135
+ получают abort, кэш `once` и карта single-flight очищаются, аренды lane отпускаются. Таймеры: реестр закрывается,
136
+ после закрытия `delay` и `interval` бросают. Подписки моделей и disposer-ы выполняются, отказы собираются.
137
+ 3. **Settle.** Без отказов: состояние `retired`, все цели вызовов отозваны, ссылки сброшены, результат `closed`. С
138
+ отказами при политике `report` (по умолчанию): экземпляр всё равно терминальный, а `FeatureError` с кодом
139
+ `cleanup-failed` уходит в `reporter`. При политике `quarantine`: результат `quarantined`, `FeatureError` с кодом
140
+ `quarantined` и необязательной capability `retryCleanup()`, повторяющей ровно оставшийся фронт уборок под защитой от повторного входа.
141
+
142
+ Retire идемпотентен и не может быть запущен из собственного колбэка.
143
+
144
+ Публичный экземпляр называет эту операцию `close()`; внутренние координаторы сохраняют `retire`. Логическая
145
+ отмена завершает ожидание, но не доказывает, что выполняющийся `open` или вызов физически остановился.
146
+ Дренаж присоединяется к уже допущенной работе и её поздней уборке до освобождения источника. Стрим также ждёт
147
+ активный `consume` до закрытия соединения и освобождения импортированного значения (D170).
148
+
149
+ ### 2.4 Рёбра между фичами
150
+
151
+ | Ребро | Что видит потребитель | Кто гарантирует безопасность |
152
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
153
+ | `imports: { x: feature }` | живые `Call`, `Readable`, `Resource` экземпляра провайдера | порядок: провайдер раньше, потребитель закрывается раньше |
154
+ | `optional(feature)` | `Readable<Lookup<Exports>>`: `found`, пока экземпляр провайдера открыт, `missing`, пока он закрыт или его нет в приложении | одна подписка у потребителя; ребро не влияет на активацию провайдера и не тянет его в мир (D105, D106) |
155
+ | `optional(port)` | `Call`, который оседает `CallError` с кодом `unavailable`, пока нет живого провайдера порта | тот же слабый закон (D105) |
156
+ | контракт хоста | значение, связанное `bind` при `openApplication`; `optional(contract)` даёт `Readable<Lookup<T>>` с `missing`, если хост его не связал; `onDemand(contract)` отдаёт значение как есть и откладывает связывание до первого использования | хост |
157
+ | порт `port` | `Call`, провайдера выбрало приложение | компилятор: ровно один провайдер |
158
+
159
+ Модели через границу фич не ходят никогда: UI-закон «модель читается только внутри своей фичи» держится именно на
160
+ этом.
161
+
162
+ ## 3. Модель и её kernel
163
+
164
+ ### 3.1 Когда что создаётся
165
+
166
+ | Шаг | Что происходит |
167
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
168
+ | 1 | `own.model(Decl, deps?, factory)` при определении только записывает описатель; фабрика не выполняется |
169
+ | 2 | при открытии экземпляра, внутри `prepare`, описатели обходятся в порядке объявления: `model` получает свежий kernel с id `<feature>.model.<n>` |
170
+ | 3 | kernel это контроллер готовности (он же ограждение модели), реестр таймеров на общем `AbortSignal`, списки исполнителей и disposer-ов |
171
+ | 4 | фабрика выполняется **синхронно**; каждый `ctx.call`, `ctx.calls(source, keys, { lane? })` (те же правила выбора, что у `calls` в `own`: метод `(input, signal) => Output` источника становится `Call`, D143), `ctx.effect`, `ctx.resource`, `ctx.stream`, `ctx.event`, `ctx.scope`, `ctx.lane`, `ctx.state` создаёт узел немедленно с id `<model>.<kind>.<n>`; `ctx.cleanup(disposer)` регистрирует уборку модели (D107) |
172
+ | 5 | возвращённая запись замораживается и кладётся под ref описателя |
173
+ | 6 | fence модели: готовность снята, таймеры закрыты, затем abort и закрытие ячеек; drain: retire исполнителей, затем все disposer-ы модели в обратном порядке регистрации, отказы репортятся; регистрация после фенса это `TypeError` |
174
+
175
+ Описатель зависимостей — запись подлинных импортов и call refs текущей фичи. Материализация один раз разрешает
176
+ её в readonly-запись перед фабрикой; optional-импорты остаются lookup-readable. Форма без зависимостей —
177
+ `model(Decl, factory)`. Refs моделей не образуют граф зависимостей. Порт из модели один раз выбирает вызов после
178
+ создания модели и ограничивает фенсом экземпляра-провайдера даже переданный напрямую вызов (D169).
179
+
180
+ Kernel получает владельца отката до входа в пользовательский код, поэтому узлы фабрики дренируются, даже если
181
+ она после их создания бросила. То же правило держат per-mount UI-модели. Фенс синхронно закрывает каждое
182
+ владеемое состояние и отвергает последующие конструкторы узлов; поздний вызов, таймер или подписка не могут
183
+ выйти за это время жизни (D170).
184
+
185
+ ### 3.2 `state` и `update`
186
+
187
+ `ctx.state(initial)` возвращает `OwnedState<Value>`, это `Readable` с брендом владения. `ctx.update(state, value)`
188
+ принимает только его: производный `Readable` не компилируется, а `OwnedState` соседней модели отвергается
189
+ в рантайме по приватной записи владельца. Запись после
190
+ ограждения не бросает у писателя, а репортится `FeatureError` с кодом `retired`. Равные по `Object.is` записи молчат.
191
+
192
+ ### 3.3 Вызов: `ctx.call({ run, lane?, once?, singleFlight?, policy? })`
193
+
194
+ `ctx.call(options)` создаёт команду; `context.invoke(target, input)` в callback `run` исполняет существующую.
195
+ Поле исполнения называется `invoke` во всех контекстах, которые его предоставляют, включая доверенные шаги
196
+ attachment; фабричный `call` и sugar выбора методов `calls` сохраняют имена (D243).
197
+
198
+ Выбранный метод хоста может не иметь параметров, принимать один input либо input и `AbortSignal`, включая
199
+ необязательные input/signal (D244). Runtime вызывает каждый метод с `(input, signal)` и исходным receiver.
200
+ Число параметров функции не проверяется; единственный параметр всегда input. Физическая работа, игнорирующая
201
+ отмену, остаётся частью drain, даже если вызывающий уже завершился отменой.
202
+
203
+ Политика `queue` по умолчанию сериализует вызовы через lane, `latest` сериализует так же, но держит одно место
204
+ ожидания, `parallel` запускает сразу; `parallel` с `lane` запрещён. На каждый вызов:
205
+
206
+ 1. если родительский или собственный сигнал уже отменён, возвращается отменённый вызов;
207
+ 2. закрытый исполнитель или ограждённый экземпляр дают отмену;
208
+ 3. попадание в кэш `once` возвращает уже решённый результат;
209
+ 4. попадание по ключу `singleFlight` присоединяется к живому исполнению;
210
+ 5. иначе новое исполнение: `parallel` стартует немедленно, `queue` и `latest` берут аренду lane в FIFO, а если lane
211
+ уже на вершине стека вызывающего, переиспользуют её без ожидания;
212
+ 6. `latest` сначала освобождает своё место ожидания: invocation того же вызова, который всё ещё стоит на lane
213
+ последним, отменяется до того, как этот запросит аренду, поэтому ожидающий вход один, а порядок lane не меняется
214
+ (D185).
215
+
216
+ Вложенный `context.invoke(target, input)` наследует время жизни авторитета и стек lane, поэтому отмена родителя
217
+ отменяет ребёнка, а lane не образует цикла: запрос lane, лежащей ниже вершины стека, это ошибка цикла, как и
218
+ присоединение к собственному исполнению.
219
+
220
+ ### 3.4 `effect` и `timers`
221
+
222
+ `ctx.effect({ from, run, when?, onDispose? })` подписывается на источник и сразу выполняется для начального
223
+ значения. Внутри один сериализованный цикл: новое значение отменяет текущее исполнение, ждёт его disposer, проверяет
224
+ `when`, создаёт новый `AbortController` и реестр таймеров и запускает `run`. Промежуточные значения теряются:
225
+ живёт только последнее. `run` возвращает `void` или disposer.
226
+
227
+ `ctx.timers.delay(ms, run)` и `ctx.timers.interval(ms, run)` регистрируют отмену в реестре модели; после закрытия
228
+ реестра оба бросают.
229
+
230
+ ### 3.5 UI-модели вклада
231
+
232
+ Вклад в слот может объявить `models: [model(Decl, (ctx, props) => …)]`: планы, которые frame вклада инстанцирует на
233
+ каждое монтирование. `props` это `Readable` пропсов слота именно этого монтирования, kernel такой модели живёт от
234
+ монтирования до размонтирования. Закон: экземпляры создаются в коммите и никогда в рендере (D188). Первый проход
235
+ вклада не рендерит ничего, `useLayoutEffect` с ключом по идентичности вклада собирает монтирование из тех пропсов,
236
+ которые закоммитились, и публикует его, а React сбрасывает этот дополнительный синхронный рендер до paint; идентичность
237
+ вклада владеет монтированием до реальной замены или размонтирования. Брошенный рендер — двойной вызов StrictMode, прерванный concurrent-проход,
238
+ поддерево, приостановленное ленивым соседом, — не создаёт ничего, поэтому нет ни подметания, ни усыновления, ни
239
+ пересборки, а layout-эффект ребёнка всегда видит живую модель. Входящие пропсы слота доходят до живого
240
+ монтирования своим layout-эффектом, поэтому прерванный рендер по-прежнему не может писать в модель.
241
+ Выдача моделей гибридная (D158): модели `own` фичи монтирование выдаёт неявно любому компоненту своего поддерева, а per-mount модель вклада объявляет через `requiresModels([X])` тот компонент или хук того же модуля, который читает её `useModel(X)`. Объявление принадлежит каждому читателю, а не только месту вклада (D117).
242
+
243
+ Повтор эффектов StrictMode и скрытие/раскрытие Suspense используют тот же закоммиченный набор моделей и сохраняют
244
+ состояние. Реальная замена или размонтирование освобождает его один раз; при размонтировании скрытого поддерева
245
+ это делает микрозадача, потому что React уже отключил layout-эффекты. Insertion effects отмечают retire без
246
+ уведомления Readable (D209).
247
+
248
+ Смонтированная модель читает пропсы из состояния монтирования. Прямые пропсы и необязательный props-адаптер
249
+ читают тот же снимок; входящие пропсы слота публикуются в layout до paint. В insertion effects состояние не
250
+ обновляется. Retry спроса хранит идентичность источника, поэтому незавершённый retry заменённого источника
251
+ не блокирует и не перезаписывает retry нового (D170).
252
+
253
+ ## 4. Реактивность
254
+
255
+ ### 4.1 Контракт и источники
256
+
257
+ ```ts
258
+ interface Readable<T> {
259
+ getSnapshot(): T;
260
+ subscribe(listener: () => void): () => void;
261
+ }
262
+ ```
263
+
264
+ Слушателю ничего не передают, он перечитывает. Ячейка состояния (`createState`, с D142 только на internal; автор получает её как `ctx.state` модели) хранит значение и множество слушателей; `set` и `update`
265
+ молчат при равенстве по `Object.is`; после `close()` чтение и запись дают `ReadableError` с кодом `closed`. Закрытие распространяется: `close()` это одно уведомление зависимым узлам, после которого активные `derive`, `computed`, `collection` и `selectByKey` отвечают `closed` вместо последнего значения и дальше молчат; неподписанный производный узел получает тот же отказ при следующем чтении (D146).
266
+
267
+ ### 4.2 `derive` и `computed`
268
+
269
+ Оба это узлы одного графа с моделью «инвалидация толчком, пересчёт вытягиванием»: у узла свежесть `clean`, `check`,
270
+ `dirty` и монотонная ревизия. Неподписанный узел пересчитывается по требованию при `getSnapshot()`, подписки вверх не
271
+ нужны. Узел становится активным, когда у него есть слушатели или зависимые, и деактивируется с задержкой в одну
272
+ микрозадачу, чтобы переподписка в том же тике не сбрасывала аренду вверх.
273
+
274
+ `computed({ read: get => … })` на каждом пересчёте строит карту зависимостей заново по вызовам `get` и запоминает их
275
+ ревизии; в состоянии `check` тело не выполняется, если ни одна ревизия не сдвинулась. Повторный вход в вычисление это
276
+ ошибка «цикл». Смена `error → value` считается изменением даже при равном значении.
277
+
278
+ ### 4.3 Один scheduler
279
+
280
+ Глитчей нет структурно: единственный scheduler перед публикацией стабилизирует все ожидающие узлы в топологическом
281
+ порядке, и только потом зовёт слушателей. Инвалидация и обход итеративные, цепочка в тысячу узлов не переполняет стек. Чтение активного чистого узла
282
+ возвращает осевшее значение без обхода графа, O(1) вместо O(графа), и это закреплено бюджетом `coreData.settledRead`
283
+ (D130). Активация транзакционна: если узел не смог активироваться, уже активированные узлы откатываются в обратном
284
+ порядке, а отказы диспозеров при откате агрегируются с исходной причиной, так что часть графа не остаётся активной
285
+ без владельца подписки (D134).
286
+
287
+ Когда слушатели узнают об изменении:
288
+
289
+ | Источник | Когда |
290
+ | -------------------------------------------------- | ------------------------------------------------------------------- |
291
+ | прямые слушатели `State.set` | синхронно, внутри транзакции записи |
292
+ | `derive` и `computed` ниже по графу от этой записи | синхронно в конце `set()`: транзакция закрывается и сливает очередь |
293
+ | внешний источник без открытой транзакции | открывает свою транзакцию, то же самое |
294
+ | инвалидация без транзакции | следующая микрозадача |
295
+ | вложенные записи | одна публикация на внешней границе транзакции |
296
+
297
+ ### 4.4 Коллекции
298
+
299
+ `collection({ from, key, project })` держит кэш проекций по объекту элемента, стабильные lookup-и по ключу и слушателей
300
+ по ключу: `selectByKey(source, key)` пробуждается только когда меняется его ключ. Дубликаты ключей это ошибка.
301
+ Неиспользуемые селекторы вычищаются через микрозадачу.
302
+
303
+ ### 4.5 Хуки
304
+
305
+ | Хук | Механика |
306
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
307
+ | `useReadable`, `useSelector` | `useSyncExternalStore` с кэшем выбранного значения; один и тот же getter для клиента и сервера |
308
+ | `useResource` | `useSyncExternalStore` по снапшоту плюс `retain()` в эффекте: монтирование и есть аренда |
309
+ | `useModel` | ближайший frame монтирования вклада; вне frame `ContributionError` `missing`, неактивное монтирование `inactive`, чужая модель `missing` |
310
+ | `useCommand` | состояние на потребителя, без слушателя на invoker: `run` возвращает промис исхода, `inFlight` на время вызова, `result` последний исход `ok` и переживает следующий запуск, `lastError` ошибка последнего `failed`, очищается только успехом и не сбрасывается стартом повтора (D116); отмена не меняет ни того, ни другого |
311
+
312
+ `useCommand` держит статус отдельно для хука и invoker и ничего не планирует: каждый `run` доходит до вызова, и
313
+ объявленная политика вызова решает, встанет ли он в очередь, заменит ожидающий вход или пойдёт параллельно (D203).
314
+ У каждого запроса свои callbacks и abort-подписка, уже отменённый signal отвергается, не доходя до вызова, и `run`
315
+ размонтированного потребителя отвергается так же. `inFlight` истинен, пока не завершился хоть один начатый им прогон.
316
+
317
+ ## 5. Ресурсы, потоки, события
318
+
319
+ И фича, и модель выбирают target через `(source) => Readable<T | null | undefined>`; возвращённый объект
320
+ проверяется до начала работы. Скалярный snapshot контракту не соответствует. `retention` необязателен в обоих
321
+ контекстах, а `scoped({ capacity })` фиксирует LRU-вытеснение (D169).
322
+
323
+ ### 5.1 `resource(from, target, { key, load, retention, retry })`
324
+
325
+ | Шаг | Что происходит |
326
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
327
+ | 1 | retention `scoped({ capacity })` активирует контроллер сразу, ресурс тёплый без наблюдателя; retention `observer` — литерал `{ kind: 'observer' }` и значение по умолчанию — активирует по первой аренде (D160) |
328
+ | 2 | активация подписывает на `target` и читает его; `null` цель закрывает выбор |
329
+ | 3 | смена ключа: тот же ключ обновляет цель без перезагрузки; новый ключ ограждает исполнение, поднимает эпоху ключа и берёт из LRU-кэша, если есть, иначе публикует `opening` (без данных) или `refreshing` (с устаревшими данными) и грузит |
330
+ | 4 | `load(target, { signal, source })`; результат принимается только если исполнение, эпоха ключа и активность не сменились |
331
+ | 5 | ошибка: повтор по `retry: { attempts, delayMs }`, затем `status: 'error'` с `retryable: true` и отчёт в `reporter` |
332
+ | 6 | последняя аренда снята при `observer`: деактивация через микрозадачу с защитой ревизией |
333
+ | 7 | закрытие в drain: ограждение, отписка, очистка кэша, `idle`, ожидание незавершённых операций |
334
+
335
+ `refresh()` перезагружает тот же ключ мимо кэша, сохраняя данные; `retry()` только из `error`; `invalidate()` чистит
336
+ кэш и перезагружает без сохранения данных.
337
+
338
+ ### 5.2 `stream(from, target, { connect, consume, key, backpressure })`
339
+
340
+ Та же машина ключей и эпох плюс слой допуска из одного слота: единственная политика `latest()` держит только новейшее ожидающее значение (D160).
341
+ Каждое значение сначала кэшируется и публикуется как `ready`, затем передаётся в авторский `consume`. При смене ключа
342
+ новое подключение ждёт завершения disposer-а старого: соединения не перекрываются. `backpressure` обязателен.
343
+
344
+ ### 5.3 `event(from, subscribe, { run, backpressure })`
345
+
346
+ Очередь на один слот перед последовательным `run`, и опция говорит, кому принадлежит слот. Без `backpressure` он
347
+ принадлежит payload, который занял его первым: пока `run` занят и один payload ждёт, новый `emit` отбрасывается и
348
+ репортится записью отказа `queue-capacity` — это drop-newest, форма `exhaustMap` и `takeLeading`. С
349
+ `backpressure: latest()` новый payload заменяет ожидающий, и ничего не репортится — это склейка, форма `conflate`;
350
+ в отличие от `switchMap` она никогда не прерывает уже идущий `run` (D247). Очередь без потерь это ни та, ни другая
351
+ политика, и она остаётся открытым вопросом (D183). `run` выполняется последовательно, обязан вернуть `undefined`,
352
+ ошибки после отмены глотаются. Отмена владельца закрывает очередь и отбрасывает то, что в ней ждёт, а закрытие ждёт
353
+ disposer и слив очереди.
354
+
355
+ ## 6. Вклады и слоты
356
+
357
+ ### 6.1 Публикация
358
+
359
+ | Шаг | Что происходит |
360
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
361
+ | 1 | при определении `provides` даёт непрозрачные объявления `{ target, value, priority, when }`; значение всегда нормализовано в фабрику экземпляра |
362
+ | 2 | каждому вкладу назначается id `<feature>.<key>`, один ключ на объявление |
363
+ | 3 | компилятор перечисляет вклады по id |
364
+ | 4 | в `prepare` регистрируется участник retire, чей fence снимает публикацию; запоминается авторитет: модели экземпляра |
365
+ | 5 | **после `ready`** фабрики выполняются с контекстом экземпляра, значение помечается владельцем, `when` резолвится: предикат становится одним computed-readable этого экземпляра |
366
+ | 6 | все затронутые цели обновляются **в одной транзакции**, слушатели зовутся после |
367
+
368
+ Порядок записей: `priority` по возрастанию, при равенстве по id. `when` (`Readable<boolean>` либо предикат
369
+ экземпляра, который рантайм понижает в такой readable): запись с `false` не
370
+ входит в `entries`, поэтому `Slot`, `fold`, `select` и проверки пустоты её не видят; переключение публикует одной
371
+ транзакцией; уникальность id проверяется по всем опубликованным, так что переключение не может упасть на проверке. Публикация
372
+ атомарна: подписка на `when` и его чтение идут в preflight до любой мутации целей, освобождение наблюдателя ушедшей
373
+ записи идёт после фиксации всех целей, а читает публикация только `when` своих записей, поэтому снятие не читает их
374
+ вовсе (D225). Отказ откатывает добавленных наблюдателей и оставляет цели нетронутыми, `when`, подавший сигнал во
375
+ время проекции, заставляет затронутые цели спроецироваться заново, а снятие публикации, не дошедшей до `published`,
376
+ убирает то, что успело лечь (D124).
377
+
378
+ ### 6.2 Рендер вклада
379
+
380
+ `Slot` читает `entries` цели через `useReadable`, при пустом списке даёт `null`, иначе рендерит `Component` каждой
381
+ записи с пропсами слота, оборачивая в frame авторитета фичи-контрибьютора: внутри вклада `useModel` достаёт модели
382
+ своей фичи и ничего чужого. Вклад без моделей не платит за frame. Вклад это `{ Component, props?, models? }`:
383
+ `props` переводит пропсы слота в собственные пропсы компонента, `models` даёт per-mount UI-модели, а компонент с
384
+ `requiresModels([...])` объявляет, какие из них ему нужны, и это сверяется типом на границе вклада. Монтирование мемоизировано: публикация или
385
+ снятие одного вклада не перерисовывает остальные монтирования той же цели (D130).
386
+
387
+ ### 6.3 `fold` у pipe
388
+
389
+ `fold(value, meta, read?)` проходит обработчики по порядку; с переданным читателем внутри `computed` в зависимости
390
+ попадают и список записей, и всё, что прочитали обработчики через свой `{ read }`, поэтому результат пересчитывается
391
+ при смене их источников, а не только при перерегистрации вкладов. Без читателя чтение нетрекаемое.
392
+
393
+ Обработчик объявляется дескриптором, `pipe(target, { fold })`, и рантайм связывает с ним один контекст вычисления в
394
+ момент публикации вклада: `exports`, `imports` и материализованный `own` разрешаются один раз, от вызова к вызову
395
+ меняется только читатель, поэтому свёртка по многим обработчикам не аллоцирует контекст на обработчик (D223).
396
+ Обработчик запускает только свёртка: объявление, предзагрузка и публикация его не вызывают.
397
+
398
+ ### 6.4 Реестр
399
+
400
+ `defineRegistry` это цель поверх `collection`: `byId`, `list`, `select(key)` с `{ kind: 'found' | 'missing' }`.
401
+ Дубликат ключа это ошибка.
402
+
403
+ ## 7. Ошибки
404
+
405
+ Один класс на субъект, состояние в поле `code`.
406
+
407
+ | Класс | Коды | Кто бросает |
408
+ | ---------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
409
+ | `FeatureError` | `retired`, `not-ready`, `quarantined`, `cleanup-failed` | ограждение экземпляра, неудачное открытие, settle retire |
410
+ | `CallError` | `cancelled`, `closed`, `publication-rejected`, `unavailable` | kernel вызовов; отмена это только `cancelled` и `closed` (D138) |
411
+ | `ReadableError` | `closed` | чтение и запись закрытой ячейки |
412
+ | `DeclarationError` | `invalid-id` | `declarationId` |
413
+ | `ApplicationError` | `closed` | приложение закрыто до готовности |
414
+ | `ContributionError` | `binding-invalid`, `duplicate`, `inactive`, `missing` | монтирование вклада: хук вне frame, неактивный frame, дубль модели, неверная привязка модели или команды |
415
+ | `CancellationError` | без кода, только бренд отмены | авторская отмена, которую `isCancellation` распознаёт |
416
+ | `FeatureBoundaryError` | `missing` | `useFeatureRetry` вне error-поддерева boundary |
417
+
418
+ Отмена это свойство ошибки, а не класса: предикат `isCancellation` (internal, автору не нужен — отмену читают `useCommand` и `resource`) смотрит на бренд, который несут `CancellationError`, `CallError` с кодами `cancelled` и `closed`, `FeatureError` с кодом `retired` и `ContributionError` с кодом `inactive`. `unavailable` и `publication-rejected` это ответы продукта: `useCommand` показывает их исходом `failed` с ошибкой в `lastError`, `resource` показывает их ошибкой (D138). `useCommand` и `resource` не показывают отмену как
419
+ ошибку пользователя.
420
+
421
+ Путь ошибки к приложению: `reporter` из `defineApplication` передаётся в каждый `openFeature`, оттуда в kernel-ы
422
+ моделей, таймеры и контроллеры; единая воронка `reportRuntimeFailure` при отсутствии или падении `reporter` уходит в
423
+ глобальный `reportError` или в отложенный `throw`. Отказы группы условий в работе уходят в `reporter`; удерживаются и бросаются из `close()` только те, что случились уже после начала закрытия.
424
+
425
+ ## 8. Границы и хост
426
+
427
+ В таблице повседневные входы; полные списки экспортов — в spec.md §3.
428
+
429
+ | Вход | Кому | Что там |
430
+ | ---------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
431
+ | `@opetope/core` | автор фичи | `defineModel`, `definePort`, `definePipe`, `defineRegistry`, `declarationId`, `derive`, `computed`, `externalReadable`, `collection`, `selectByKey`, ошибки, типы `Readable`, `Lookup`, `Call`, `Port`, `ModelContext`, `OwnedState`, `Resource` |
432
+ | `@opetope/runtime` | автор фичи и хост | `defineFeature`, `defineHostContract`, `onDemand`, `optional`, `openFeature`, `defineCondition`, `defineApplication`, `openApplication`, `bind`, `scoped`, `latest` |
433
+ | `@opetope/react` | автор компонентов | `useModel`, `useReadable`, `useSelector`, `useCommand`, `useResource`, `defineSlot`, `defineSwitchSlot`, `Slot`, `requiresModels` |
434
+ | `@opetope/react/integration` | хост | `FeatureBoundary`, `useFeature`, `useFeatureRetry`, `FeatureBoundaryError` |
435
+ | `@opetope/react/testing` | тесты | `renderSlot`, `command`, `createScenario`, `ScenarioTimeoutError` |
436
+ | `@opetope/*/internal` | реализация фреймворка и интеграция хоста | kernel, компилятор, контроллеры, `createState`, `isCancellation`, порт наблюдения `createInspectionSession` и порт управления `createControlSession`; авторам недоступно (D142) |
437
+
438
+ Поле `exports` в `package.json` открывает точные входы без wildcard. Авторы фич используют безопасные входы;
439
+ интеграция хоста и тесты — свои выделенные входы. Правила импортов приложения сохраняют это разделение ролей.
440
+
441
+ Хост с управлением по спросу показывает UI фичи так: `FeatureBoundary` берёт аренду на экземпляр через `useFeature`, открывает его при
442
+ необходимости, показывает `fallback` пока открывается и `error` при отказе, а внутри рендерит `<Slot target props />`,
443
+ куда фича вложила свой UI. Размонтирование boundary отпускает аренду; последняя отпущенная аренда ведёт к retire.
444
+
445
+ ### 8.1 Наблюдение
446
+
447
+ Порт наблюдения (D122) живёт на `@opetope/runtime/internal`: `createInspectionSession(execution, { ringCapacity })`, где
448
+ `execution` это результат `openApplication`. Связь держит `WeakMap`, глобального хука и публичного значения нет, сессия
449
+ на приложение одна. Пока сессия не открыта, приложение ничего не записывает; `close()` снимает наблюдателя, не трогая
450
+ приложение. Потребитель получает ровно четыре метода, `getSnapshot`, `readSince`, `subscribe` и `close`, и не может
451
+ управлять графом, который читает. Протокол `@opetope/devtools` рантайм реализует, а не импортирует; соответствие
452
+ держит гейт `ci:inspection`.
453
+
454
+ Оба порта остаются на `/internal`, потому что вызывает их пакет `@opetope/devtools`, а не приложение. Хост
455
+ подключает панель через `installDevtools({ execution, … })` и ради ничего другого на этот вход не заходит (D248).
456
+
457
+ ### 8.2 Управление
458
+
459
+ Порт записи это вторая, отдельная фабрика на том же входе: `createControlSession(execution)` (D176). Держать сессию
460
+ наблюдения не значит иметь право писать, поэтому у них свои реестры и своя authority. Единственный управляемый
461
+ субъект — условие и через него его группа: `suspend(conditionId)` ставит debug-override, принудительно читающий
462
+ условие как `false`, `resume(conditionId)` его снимает, и группа снова следует собственному источнику. У фичи без
463
+ `when` условия нет, поэтому адресовать её нечем — devtools не становится источником времени жизни, которого в плане
464
+ нет.
465
+
466
+ Override оборачивает собственный `Readable` условия, поэтому приостановленная группа гаснет обычной транзакцией: тот
467
+ же планировщик, тот же атомарный кадр и закон зависимостей
468
+ [devtools.md §5.3](devtools.ru.md) — приостановка провайдера уводит
469
+ жёстких зависимых их собственной семантикой, а приостановка потребителя провайдера не трогает. Кадр, который
470
+ публикует переход, называет команду управления своей причиной, поэтому «что я сделал» и «что случилось» читаются
471
+ одной лентой, а снимок группы показывает override рядом с продуктовой правдой: `desired.source` — что говорит
472
+ условие, `desired.effective` — по чему действует рантайм, `desired.override` — `force-inactive`, пока override стоит.
473
+
474
+ `retryCleanup(instanceId)` повторяет уборку ровно карантинного фронтира через существующий retry-контракт; экземпляр
475
+ без карантина отвечает `not-controllable`. Отказ это данные, а не исключение:
476
+ `{ kind: 'rejected', reason: 'not-controllable' | 'stale' | 'unknown-target' }`, а применённая команда отвечает
477
+ номером кадра, в котором виден эффект. `close()` снимает все override сессии, поэтому закрытая панель не оставляет
478
+ приложение в отлаженном состоянии, а сам override живёт только в текущем execution и не переживает reload и HMR.
479
+
480
+ ## 9. Как это проверять
481
+
482
+ - Экземпляр фичи в тесте: `openFeature(feature, { imports, reporter })`, затем `ready`, затем `close()`; фейки
483
+ контрактов хоста это обычные объекты.
484
+ - Компонент вклада: `renderSlot(target, { props, models })` монтирует вклад с фикстурой per-mount моделей и даёт
485
+ `updateProps`; `command(run)` минтит подлинный `Call` для фикстуры.
486
+ - Граф приложения: `defineApplication` в тесте манифеста ловит отсутствие провайдера, цикл и нарушение включения
487
+ наборов условий до старта.
488
+ - Бюджеты и гейты: `ci:public-surface` считает публичные слова и запрещает kernel- и retired-слова на безопасных
489
+ входах; `ci:perf-memory` меряет удержанную память, слушателей и линейность стоимости операций, включая `coreData.settledRead` и аллокации записи без слушателей (`unobservedWrite`, медиана из трёх прогонов по 100k операций); `ci:type-stress` меряет стоимость типов, латентность языковой службы печатается только информационно, а инстанцирования, память и диагностики проверяются как обязательные бюджеты (D224); `ci:browser-floor` держит синтаксис и API уровня
490
+ Chrome 82; `ci:inspection` проверяет контракт порта наблюдения; `ci:size-limit` держит пять бюджетов, которые видит автор (core `author primitives consumer` 1.1 и `data layer consumer` 4.9 кБ, runtime `public feature consumer` 32 и `application graph consumer` 48 кБ, `@opetope/react` 18 кБ).
491
+ - Проверка авторского кода: каждый компонент и хук модуля, который читает per-mount модель вклада через
492
+ `useModel(X)`, объявляет `requiresModels([X])`; owner-модели читаются без объявления (D117, D158). Импорты используют
493
+ входы пакетов, разрешённые для своей роли.
494
+ - Проверка сборки приложения: хост-потребитель проверяет бюджеты своего бандла и сохранение задуманных dynamic
495
+ boundaries после сборки; size-фикстуры пакетов не измеряют приложение целиком.