@gyral/core 0.3.0 → 0.3.2

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 (386) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/NOTICE +19 -0
  3. package/README.md +36 -8
  4. package/dist/call.d.ts +32 -0
  5. package/dist/call.js +63 -0
  6. package/dist/call.js.map +1 -0
  7. package/dist/children.d.ts +25 -3
  8. package/dist/children.js +18 -1
  9. package/dist/children.js.map +1 -1
  10. package/dist/cli/gyral-types.d.ts +2 -0
  11. package/dist/cli/gyral-types.js +23 -0
  12. package/dist/cli/gyral-types.js.map +1 -0
  13. package/dist/command.d.ts +54 -12
  14. package/dist/command.js +25 -11
  15. package/dist/command.js.map +1 -1
  16. package/dist/compiler/ast.d.ts +2 -0
  17. package/dist/compiler/ast.js +5 -1
  18. package/dist/compiler/ast.js.map +1 -1
  19. package/dist/compiler/compile.d.ts +5 -2
  20. package/dist/compiler/compile.js +36 -17
  21. package/dist/compiler/compile.js.map +1 -1
  22. package/dist/compiler/component-entries.d.ts +7 -0
  23. package/dist/compiler/component-entries.js +42 -0
  24. package/dist/compiler/component-entries.js.map +1 -0
  25. package/dist/compiler/components.d.ts +4 -0
  26. package/dist/compiler/components.js +190 -0
  27. package/dist/compiler/components.js.map +1 -0
  28. package/dist/compiler/facts.d.ts +31 -0
  29. package/dist/compiler/facts.js +285 -0
  30. package/dist/compiler/facts.js.map +1 -0
  31. package/dist/compiler/features.d.ts +34 -0
  32. package/dist/compiler/features.js +158 -0
  33. package/dist/compiler/features.js.map +1 -0
  34. package/dist/compiler/hooks.d.ts +4 -0
  35. package/dist/compiler/hooks.js +144 -89
  36. package/dist/compiler/hooks.js.map +1 -1
  37. package/dist/compiler/loader-source.d.ts +14 -0
  38. package/dist/compiler/loader-source.js +86 -0
  39. package/dist/compiler/loader-source.js.map +1 -0
  40. package/dist/compiler/locate.d.ts +14 -0
  41. package/dist/compiler/locate.js +90 -0
  42. package/dist/compiler/locate.js.map +1 -0
  43. package/dist/compiler/parse5-check.d.ts +1 -1
  44. package/dist/compiler/parse5-check.js +8 -3
  45. package/dist/compiler/parse5-check.js.map +1 -1
  46. package/dist/compiler/prop-schemas.d.ts +4 -0
  47. package/dist/compiler/prop-schemas.js +108 -0
  48. package/dist/compiler/prop-schemas.js.map +1 -0
  49. package/dist/compiler/scan.d.ts +13 -3
  50. package/dist/compiler/scan.js +21 -13
  51. package/dist/compiler/scan.js.map +1 -1
  52. package/dist/compiler/scope.d.ts +8 -0
  53. package/dist/compiler/scope.js +116 -0
  54. package/dist/compiler/scope.js.map +1 -0
  55. package/dist/compiler/tag-map.d.ts +41 -0
  56. package/dist/compiler/tag-map.js +126 -0
  57. package/dist/compiler/tag-map.js.map +1 -0
  58. package/dist/components-ready.d.ts +7 -0
  59. package/dist/components-ready.js +19 -0
  60. package/dist/components-ready.js.map +1 -0
  61. package/dist/define.d.ts +12 -4
  62. package/dist/define.js +8 -12
  63. package/dist/define.js.map +1 -1
  64. package/dist/devtools-events.d.ts +8 -0
  65. package/dist/devtools-events.js.map +1 -1
  66. package/dist/devtools-off.d.ts +1 -0
  67. package/dist/devtools-off.js +1 -0
  68. package/dist/devtools-off.js.map +1 -1
  69. package/dist/devtools.d.ts +3 -0
  70. package/dist/devtools.js +14 -0
  71. package/dist/devtools.js.map +1 -1
  72. package/dist/drivers-scope.js.map +1 -1
  73. package/dist/element-internals.d.ts +1 -0
  74. package/dist/element-internals.js +11 -0
  75. package/dist/element-internals.js.map +1 -0
  76. package/dist/element-types.d.ts +7 -4
  77. package/dist/element-types.js.map +1 -1
  78. package/dist/element.js +80 -27
  79. package/dist/element.js.map +1 -1
  80. package/dist/error-view.d.ts +10 -0
  81. package/dist/error-view.js +30 -0
  82. package/dist/error-view.js.map +1 -0
  83. package/dist/errors.d.ts +21 -0
  84. package/dist/errors.js +47 -0
  85. package/dist/errors.js.map +1 -0
  86. package/dist/errors.json +289 -0
  87. package/dist/eslint/imports.d.ts +2 -0
  88. package/dist/eslint/imports.js +2 -2
  89. package/dist/eslint/imports.js.map +1 -1
  90. package/dist/eslint/index.d.ts +2 -1
  91. package/dist/eslint/index.js +11 -3
  92. package/dist/eslint/index.js.map +1 -1
  93. package/dist/eslint/template-rule.d.ts +1 -0
  94. package/dist/eslint/template-rule.js +15 -5
  95. package/dist/eslint/template-rule.js.map +1 -1
  96. package/dist/eslint/unused-intent-rule.d.ts +2 -0
  97. package/dist/eslint/unused-intent-rule.js +186 -0
  98. package/dist/eslint/unused-intent-rule.js.map +1 -0
  99. package/dist/features.d.ts +2 -2
  100. package/dist/features.js.map +1 -1
  101. package/dist/focus.d.ts +6 -0
  102. package/dist/focus.js +32 -2
  103. package/dist/focus.js.map +1 -1
  104. package/dist/form-associated.d.ts +46 -0
  105. package/dist/form-associated.js +77 -0
  106. package/dist/form-associated.js.map +1 -0
  107. package/dist/forms.d.ts +42 -30
  108. package/dist/forms.js +85 -41
  109. package/dist/forms.js.map +1 -1
  110. package/dist/frame-lane.d.ts +11 -0
  111. package/dist/frame-lane.js +28 -0
  112. package/dist/frame-lane.js.map +1 -0
  113. package/dist/head.d.ts +61 -0
  114. package/dist/head.js +85 -0
  115. package/dist/head.js.map +1 -0
  116. package/dist/host-model.d.ts +34 -5
  117. package/dist/host-model.js +146 -23
  118. package/dist/host-model.js.map +1 -1
  119. package/dist/hydration-client.js +2 -2
  120. package/dist/hydration-client.js.map +1 -1
  121. package/dist/hydration-loader.d.ts +13 -0
  122. package/dist/hydration-loader.js +58 -0
  123. package/dist/hydration-loader.js.map +1 -0
  124. package/dist/hydration-off.d.ts +8 -0
  125. package/dist/hydration-off.js +25 -0
  126. package/dist/hydration-off.js.map +1 -0
  127. package/dist/hydration.d.ts +0 -12
  128. package/dist/hydration.js +3 -52
  129. package/dist/hydration.js.map +1 -1
  130. package/dist/index.d.ts +21 -24
  131. package/dist/index.js +10 -19
  132. package/dist/index.js.map +1 -1
  133. package/dist/indexeddb-types.d.ts +97 -0
  134. package/dist/indexeddb-types.js +2 -0
  135. package/dist/indexeddb-types.js.map +1 -0
  136. package/dist/indexeddb.d.ts +5 -0
  137. package/dist/indexeddb.js +118 -0
  138. package/dist/indexeddb.js.map +1 -0
  139. package/dist/intent.d.ts +42 -20
  140. package/dist/intent.js +145 -107
  141. package/dist/intent.js.map +1 -1
  142. package/dist/internal/indexeddb-open.d.ts +8 -0
  143. package/dist/internal/indexeddb-open.js +96 -0
  144. package/dist/internal/indexeddb-open.js.map +1 -0
  145. package/dist/internal/indexeddb-transaction.d.ts +4 -0
  146. package/dist/internal/indexeddb-transaction.js +134 -0
  147. package/dist/internal/indexeddb-transaction.js.map +1 -0
  148. package/dist/internal/interpreter.d.ts +12 -4
  149. package/dist/internal/interpreter.js +36 -48
  150. package/dist/internal/interpreter.js.map +1 -1
  151. package/dist/internal/token.d.ts +1 -0
  152. package/dist/internal/token.js +7 -0
  153. package/dist/internal/token.js.map +1 -0
  154. package/dist/internal.d.ts +20 -0
  155. package/dist/internal.js +22 -0
  156. package/dist/internal.js.map +1 -0
  157. package/dist/invoker-fallback-used.d.ts +4 -0
  158. package/dist/invoker-fallback-used.js +10 -0
  159. package/dist/invoker-fallback-used.js.map +1 -0
  160. package/dist/invoker-fallback.d.ts +4 -0
  161. package/dist/invoker-fallback.js +15 -0
  162. package/dist/invoker-fallback.js.map +1 -0
  163. package/dist/light-dom.d.ts +1 -1
  164. package/dist/light-dom.js.map +1 -1
  165. package/dist/prop.d.ts +30 -7
  166. package/dist/prop.js +19 -0
  167. package/dist/prop.js.map +1 -1
  168. package/dist/props.d.ts +8 -3
  169. package/dist/props.js +22 -15
  170. package/dist/props.js.map +1 -1
  171. package/dist/retry.d.ts +15 -0
  172. package/dist/retry.js +48 -0
  173. package/dist/retry.js.map +1 -0
  174. package/dist/scheduler.d.ts +7 -3
  175. package/dist/scheduler.js +38 -56
  176. package/dist/scheduler.js.map +1 -1
  177. package/dist/script-json.d.ts +2 -0
  178. package/dist/script-json.js +13 -0
  179. package/dist/script-json.js.map +1 -0
  180. package/dist/server-component.d.ts +1 -1
  181. package/dist/server-component.js +45 -14
  182. package/dist/server-component.js.map +1 -1
  183. package/dist/server.d.ts +9 -3
  184. package/dist/server.js +12 -3
  185. package/dist/server.js.map +1 -1
  186. package/dist/settled.d.ts +4 -3
  187. package/dist/settled.js +48 -26
  188. package/dist/settled.js.map +1 -1
  189. package/dist/spec-features-used.d.ts +21 -0
  190. package/dist/spec-features-used.js +50 -0
  191. package/dist/spec-features-used.js.map +1 -0
  192. package/dist/spec-features.d.ts +28 -0
  193. package/dist/spec-features.js +42 -0
  194. package/dist/spec-features.js.map +1 -0
  195. package/dist/states.d.ts +3 -5
  196. package/dist/states.js +22 -8
  197. package/dist/states.js.map +1 -1
  198. package/dist/storage.d.ts +69 -0
  199. package/dist/storage.js +158 -0
  200. package/dist/storage.js.map +1 -0
  201. package/dist/store-binding.js +2 -2
  202. package/dist/store-binding.js.map +1 -1
  203. package/dist/store-scope.d.ts +2 -4
  204. package/dist/store-scope.js +6 -16
  205. package/dist/store-scope.js.map +1 -1
  206. package/dist/store.d.ts +2 -6
  207. package/dist/store.js +36 -8
  208. package/dist/store.js.map +1 -1
  209. package/dist/subscription.d.ts +35 -0
  210. package/dist/subscription.js +74 -0
  211. package/dist/subscription.js.map +1 -0
  212. package/dist/subscriptions.d.ts +26 -0
  213. package/dist/subscriptions.js +98 -0
  214. package/dist/subscriptions.js.map +1 -0
  215. package/dist/transitions.d.ts +4 -5
  216. package/dist/transitions.js +10 -4
  217. package/dist/transitions.js.map +1 -1
  218. package/dist/types.d.ts +141 -32
  219. package/dist/types.js.map +1 -1
  220. package/dist/use-dialogs.d.ts +2 -0
  221. package/dist/use-dialogs.js +9 -0
  222. package/dist/use-dialogs.js.map +1 -0
  223. package/dist/use-disposal.d.ts +2 -0
  224. package/dist/use-disposal.js +9 -0
  225. package/dist/use-disposal.js.map +1 -0
  226. package/dist/use-error-view.d.ts +2 -0
  227. package/dist/use-error-view.js +9 -0
  228. package/dist/use-error-view.js.map +1 -0
  229. package/dist/use-errored.d.ts +2 -0
  230. package/dist/use-errored.js +9 -0
  231. package/dist/use-errored.js.map +1 -0
  232. package/dist/use-form-associated.d.ts +2 -0
  233. package/dist/use-form-associated.js +7 -0
  234. package/dist/use-form-associated.js.map +1 -0
  235. package/dist/use-frame-lane.d.ts +2 -0
  236. package/dist/use-frame-lane.js +10 -0
  237. package/dist/use-frame-lane.js.map +1 -0
  238. package/dist/use-invokers.d.ts +2 -0
  239. package/dist/use-invokers.js +10 -0
  240. package/dist/use-invokers.js.map +1 -0
  241. package/dist/use-states.d.ts +2 -0
  242. package/dist/use-states.js +10 -0
  243. package/dist/use-states.js.map +1 -0
  244. package/dist/use-subscriptions.d.ts +2 -0
  245. package/dist/use-subscriptions.js +10 -0
  246. package/dist/use-subscriptions.js.map +1 -0
  247. package/dist/use-transitions.d.ts +2 -0
  248. package/dist/use-transitions.js +10 -0
  249. package/dist/use-transitions.js.map +1 -0
  250. package/dist/view/attributes.d.ts +5 -0
  251. package/dist/view/attributes.js +5 -0
  252. package/dist/view/attributes.js.map +1 -1
  253. package/dist/view/compiled.d.ts +1 -1
  254. package/dist/view/compiled.js +1 -1
  255. package/dist/view/compiled.js.map +1 -1
  256. package/dist/view/index.d.ts +9 -4
  257. package/dist/view/index.js +9 -3
  258. package/dist/view/index.js.map +1 -1
  259. package/dist/view/loc.d.ts +9 -0
  260. package/dist/view/loc.js +50 -0
  261. package/dist/view/loc.js.map +1 -0
  262. package/dist/view/message.d.ts +17 -0
  263. package/dist/view/message.js +40 -0
  264. package/dist/view/message.js.map +1 -0
  265. package/dist/view/messages.d.ts +43 -0
  266. package/dist/view/messages.js +84 -0
  267. package/dist/view/messages.js.map +1 -0
  268. package/dist/view/normalize/check.d.ts +5 -2
  269. package/dist/view/normalize/check.js +6 -3
  270. package/dist/view/normalize/check.js.map +1 -1
  271. package/dist/view/normalize/emit.d.ts +2 -0
  272. package/dist/view/normalize/emit.js +40 -11
  273. package/dist/view/normalize/emit.js.map +1 -1
  274. package/dist/view/normalize/errors.js +1 -1
  275. package/dist/view/normalize/errors.js.map +1 -1
  276. package/dist/view/normalize/id.d.ts +5 -2
  277. package/dist/view/normalize/id.js +9 -2
  278. package/dist/view/normalize/id.js.map +1 -1
  279. package/dist/view/normalize/normalize.d.ts +7 -4
  280. package/dist/view/normalize/normalize.js +15 -9
  281. package/dist/view/normalize/normalize.js.map +1 -1
  282. package/dist/view/normalize/rules.d.ts +9 -0
  283. package/dist/view/normalize/rules.js +35 -5
  284. package/dist/view/normalize/rules.js.map +1 -1
  285. package/dist/view/normalize/svg.d.ts +14 -0
  286. package/dist/view/normalize/svg.js +40 -0
  287. package/dist/view/normalize/svg.js.map +1 -0
  288. package/dist/view/normalize/tokens.d.ts +5 -1
  289. package/dist/view/normalize/tokens.js.map +1 -1
  290. package/dist/view/normalize/tree.d.ts +8 -2
  291. package/dist/view/normalize/tree.js +58 -13
  292. package/dist/view/normalize/tree.js.map +1 -1
  293. package/dist/view/normalize/types.d.ts +24 -4
  294. package/dist/view/normalize/types.js +4 -1
  295. package/dist/view/normalize/types.js.map +1 -1
  296. package/dist/view/normalize/whitespace.js +4 -3
  297. package/dist/view/normalize/whitespace.js.map +1 -1
  298. package/dist/view/prepare-stub.js +2 -2
  299. package/dist/view/prepare-stub.js.map +1 -1
  300. package/dist/view/prepare.d.ts +5 -2
  301. package/dist/view/prepare.js +9 -3
  302. package/dist/view/prepare.js.map +1 -1
  303. package/dist/view/registry.d.ts +15 -0
  304. package/dist/view/registry.js +10 -2
  305. package/dist/view/registry.js.map +1 -1
  306. package/dist/view/render/adopt-attr.js +7 -1
  307. package/dist/view/render/adopt-attr.js.map +1 -1
  308. package/dist/view/render/adopt.js +21 -4
  309. package/dist/view/render/adopt.js.map +1 -1
  310. package/dist/view/render/attr-parts.d.ts +12 -1
  311. package/dist/view/render/attr-parts.js +38 -5
  312. package/dist/view/render/attr-parts.js.map +1 -1
  313. package/dist/view/render/child-part.js +5 -2
  314. package/dist/view/render/child-part.js.map +1 -1
  315. package/dist/view/render/dispose.d.ts +3 -0
  316. package/dist/view/render/dispose.js +76 -0
  317. package/dist/view/render/dispose.js.map +1 -0
  318. package/dist/view/render/hook-part.d.ts +3 -1
  319. package/dist/view/render/hook-part.js +21 -5
  320. package/dist/view/render/hook-part.js.map +1 -1
  321. package/dist/view/render/hook-run.d.ts +2 -0
  322. package/dist/view/render/hook-run.js +72 -0
  323. package/dist/view/render/hook-run.js.map +1 -0
  324. package/dist/view/render/hooks.d.ts +74 -6
  325. package/dist/view/render/hooks.js +79 -17
  326. package/dist/view/render/hooks.js.map +1 -1
  327. package/dist/view/render/hydrate.js +2 -2
  328. package/dist/view/render/hydrate.js.map +1 -1
  329. package/dist/view/render/keyed.d.ts +6 -0
  330. package/dist/view/render/keyed.js +15 -0
  331. package/dist/view/render/keyed.js.map +1 -0
  332. package/dist/view/render/list.d.ts +1 -1
  333. package/dist/view/render/list.js.map +1 -1
  334. package/dist/view/render/mismatch.js +5 -3
  335. package/dist/view/render/mismatch.js.map +1 -1
  336. package/dist/view/render/modal.d.ts +3 -0
  337. package/dist/view/render/modal.js +39 -0
  338. package/dist/view/render/modal.js.map +1 -0
  339. package/dist/view/render/raw.d.ts +4 -1
  340. package/dist/view/render/raw.js +6 -2
  341. package/dist/view/render/raw.js.map +1 -1
  342. package/dist/view/render/render.d.ts +4 -1
  343. package/dist/view/render/render.js +17 -12
  344. package/dist/view/render/render.js.map +1 -1
  345. package/dist/view/render/toggles.d.ts +6 -0
  346. package/dist/view/render/toggles.js +6 -0
  347. package/dist/view/render/toggles.js.map +1 -0
  348. package/dist/view/render/values.d.ts +1 -1
  349. package/dist/view/render/values.js.map +1 -1
  350. package/dist/view/render/warn.d.ts +14 -0
  351. package/dist/view/render/warn.js +45 -0
  352. package/dist/view/render/warn.js.map +1 -1
  353. package/dist/view/server/component.d.ts +6 -3
  354. package/dist/view/server/component.js +29 -7
  355. package/dist/view/server/component.js.map +1 -1
  356. package/dist/view/server/index.d.ts +20 -0
  357. package/dist/view/server/index.js +12 -4
  358. package/dist/view/server/index.js.map +1 -1
  359. package/dist/view/server/tags.d.ts +13 -2
  360. package/dist/view/server/tags.js +13 -2
  361. package/dist/view/server/tags.js.map +1 -1
  362. package/dist/view/server/values.d.ts +2 -0
  363. package/dist/view/server/values.js +8 -0
  364. package/dist/view/server/values.js.map +1 -1
  365. package/dist/view/server/writer.d.ts +9 -4
  366. package/dist/view/server/writer.js +33 -18
  367. package/dist/view/server/writer.js.map +1 -1
  368. package/dist/view/template-element.d.ts +7 -0
  369. package/dist/view/template-element.js +25 -5
  370. package/dist/view/template-element.js.map +1 -1
  371. package/dist/view/template.d.ts +9 -2
  372. package/dist/view/template.js +61 -8
  373. package/dist/view/template.js.map +1 -1
  374. package/dist/view/trusted-html.d.ts +6 -0
  375. package/dist/view/trusted-html.js +34 -0
  376. package/dist/view/trusted-html.js.map +1 -0
  377. package/dist/vite.d.ts +38 -31
  378. package/dist/vite.js +206 -31
  379. package/dist/vite.js.map +1 -1
  380. package/package.json +34 -3
  381. package/dist/hooks/labelled-by.d.ts +0 -13
  382. package/dist/hooks/labelled-by.js +0 -51
  383. package/dist/hooks/labelled-by.js.map +0 -1
  384. package/dist/random.d.ts +0 -13
  385. package/dist/random.js +0 -19
  386. package/dist/random.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # @gyral/core
2
2
 
3
+ ## 0.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Export the helper types `Reducer`, `Variant`, `ParseResult` and `StandardSchemaV1` (type-only),
8
+ so reducers and parsers written outside a component spec can be typed without copying
9
+ Gyral's internal definitions.
10
+ - The development template locator no longer adds branches to your coverage reports: call
11
+ sites now go through a virtual helper module instead of an inline `.at?.() ?? html` fallback.
12
+ - The components build metadata (`.vite/gyral-components.json`) gains `chunkModules`, which maps
13
+ each emitted chunk to the root-relative source modules it contains, including workspace
14
+ modules Vite merged into a shared chunk. Offline caches and service workers can use it to
15
+ know which chunk serves which module.
16
+ - Public doc comments no longer point at repository paths or tracker IDs.
17
+
18
+ ## 0.3.1
19
+
20
+ ### Patch Changes
21
+
22
+ This release includes API removals and behavior changes. Read the
23
+ [0.3.0 → 0.3.1 migration guide](https://github.com/gyraljs/gyral/blob/main/docs/references/migrating-0.3.0-to-0.3.1.md) before upgrading, and keep the nine
24
+ Gyral packages on the same version.
25
+
26
+ - Components use `define<State, Msg>()(tag, spec)` with typed intent names, tag-only intents,
27
+ parser state access and typed outputs. A declined intent bubbles to the next matching
28
+ intent; child outputs stop at their direct consuming parent.
29
+ - Declarative `subscriptions` reconcile long-lived commands, with typed `subscription()`
30
+ builders, cancellation and stable command lanes. Drivers resolve through the host/root
31
+ override boundary; retry supports predicates, backoff and jitter.
32
+ - Views gain `svg` templates, `keyed` replacement, disposable `defineHook` hooks, delegated
33
+ shadow focus, `focus(..., { wait: true })` and post-render `call` commands. Hooks run after
34
+ child renders and dispose when their owned elements are removed.
35
+ - Native modal/popover bindings open after DOM insertion and child renders. Conditional
36
+ insertion, keyed replacement, initial opening and hydration follow the same ordering.
37
+ Intents dispatched by native focus complete their renders before focus commands and `settled`.
38
+ - Opt-in `formAssociated` components project native values and validity and receive reset,
39
+ disabled and restoration messages. Native SSR fallback controls can preserve edits made
40
+ before hydration without duplicate form entries.
41
+ - Optional `@gyral/core/storage` provides schema-decoded localStorage operations and change
42
+ subscriptions. Optional `@gyral/core/indexeddb` provides typed records, bounded queries,
43
+ synchronous migrations and atomic transaction batches. Neither adds code to unused apps.
44
+ - Forms validate and refill through their definitions, retain user edits until model values
45
+ change, and redact password/passcode/CSRF fields plus explicitly named secrets. Shared head
46
+ data covers title, metadata, canonical URLs and JSON-LD across server rendering and routing.
47
+ - The Vite preset supports client-only builds, automatic component modules, explicit wrapper
48
+ module mappings and generated element typings. Entry-imported components stay in their
49
+ entry; loader/preload work and spec features are reduced when unused.
50
+ - Unified `GyralError` reporting, `Errored` reducers and error views handle failures in the
51
+ browser and server. Production diagnostics use stable codes linked to the public reference.
52
+ Strict CSP style bindings use CSSOM; seeded hydration and Trusted Types paths are verified.
53
+ - Compiler/runtime/lint template rules agree, including boolean bindings written as `?name`.
54
+ Public exports have a checked inventory; duplicate/internal helper APIs are removed as
55
+ described in the migration guide.
56
+
57
+ The lockstep release also adds optional/catch-all routes in `@gyral/router` and native
58
+ EventSource/WebSocket entries in `@gyral/http`; see those packages' changelogs.
59
+
3
60
  ## 0.3.0
4
61
 
5
62
  ### Minor Changes
package/NOTICE CHANGED
@@ -5,7 +5,26 @@ Gyral is inspired by Cycle.js (https://github.com/cyclejs/cyclejs), released
5
5
  under the MIT License:
6
6
 
7
7
  The MIT License (MIT)
8
+
8
9
  Copyright (c) 2015-2019 The Cycle.js Core Team
9
10
 
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
10
29
  Gyral is a new implementation. Where ideas, APIs or example programs are adapted
11
30
  from Cycle.js, this notice applies.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gyral/core
2
2
 
3
- Model-View-Intent web components on the web platform, with their own view layer. _Inspired by [Cycle.js](https://cycle.js.org)._ Intent parses platform events into typed messages, the model is a set of pure reducers, and the view is a pure template that names intents. Side effects are commands: data that drivers run at the edges.
3
+ Model-View-Intent web components on the web platform, with their own view layer. Intent parses platform events into typed messages, the model is a set of pure reducers, and the view is a pure template that names intents. Side effects are commands: data that drivers run at the edges.
4
4
 
5
5
  ## Install
6
6
 
@@ -15,11 +15,11 @@ import { define, html } from '@gyral/core';
15
15
 
16
16
  type Msg = { _tag: 'Increment' } | { _tag: 'Decrement' };
17
17
 
18
- define<{ count: number }, Msg>('gy-counter', {
18
+ define<{ count: number }, Msg>()('gy-counter', {
19
19
  init: () => ({ count: 0 }),
20
20
  intent: {
21
- Increment: () => ({ _tag: 'Increment' }),
22
- Decrement: () => ({ _tag: 'Decrement' }),
21
+ Increment: true,
22
+ Decrement: true,
23
23
  },
24
24
  update: {
25
25
  Increment: (s) => ({ count: s.count + 1 }),
@@ -35,16 +35,44 @@ define<{ count: number }, Msg>('gy-counter', {
35
35
 
36
36
  `@gyral/core/vite` exports `gyralVitePreset()`, the Vite/Vitest settings every Gyral app needs
37
37
  (the build-time template compiler). `@gyral/core/eslint` is an ESLint plugin with the same
38
- template rules for the editor: `export default [gyral.configs.recommended]`.
38
+ template rules for the editor (`import gyral from '@gyral/core/eslint'`, then
39
+ `gyral.configs.recommended` in your flat config). `@gyral/ssr` renders components and pages
40
+ to HTML on the server. In tests, `await settled()` waits until
41
+ every component has rendered and messages have stopped arriving.
39
42
 
40
43
  ## Documentation
41
44
 
45
+ `@gyral/core/storage` (0.3.1) exports `makeLocalStorageDriver(schema, { prefix?, name?,
46
+ storage?, encode? })`: JSON Read/Write/Remove/Clear commands and cancellable Watch streams.
47
+ Reads validate persisted values with Standard Schema; watches report invalid data and keep
48
+ listening. Driver writes and native other-tab events produce changes. Clear respects the
49
+ prefix namespace. Start initial watches in `subscriptions` so SSR state hydrates first;
50
+ imports and factories do not access browser globals. This optional entry is absent from
51
+ apps that do not import it. See [the storage guide](https://github.com/gyraljs/gyral/blob/main/skills/gyral/references/browser-storage.md).
52
+
53
+ `@gyral/core/indexeddb` (0.3.1) exports `makeIndexedDBDriver(stores, { database, version,
54
+ upgrade?, name?, factory?, encode? })`: typed Read/Put/Delete, bounded Query and atomic
55
+ Transaction batches. Applications own stores, indexes and synchronous migrations. Success
56
+ means the native transaction committed; stored records decode afterward. Blocked upgrades,
57
+ access and decoding failures are typed. Commands close connections and cancel native work;
58
+ an already committed write cannot be undone. This entry is also optional and SSR-safe.
59
+
42
60
  Guides and API reference: **[gyral.dev](https://gyral.dev)**. Source, issues and the
43
61
  consumer setup guide (packages, Vite preset, template compiler):
44
- [github.com/gyraljs/gyral](https://github.com/gyraljs/gyral). Upgrading from 0.2:
45
- [docs/references/migrating-0.2-to-0.3.md](https://github.com/gyraljs/gyral/blob/main/docs/references/migrating-0.2-to-0.3.md).
62
+ [github.com/gyraljs/gyral](https://github.com/gyraljs/gyral). Upgrading:
63
+ [from 0.3.0 to 0.3.1](https://github.com/gyraljs/gyral/blob/main/docs/references/migrating-0.3.0-to-0.3.1.md),
64
+ [from 0.2 to 0.3](https://github.com/gyraljs/gyral/blob/main/docs/references/migrating-0.2-to-0.3.md).
65
+
66
+ > Status: 0.x, on npm. Breaking changes to the
67
+ > [public API](https://github.com/gyraljs/gyral/blob/main/docs/references/public-api-0.3.1.md)
68
+ > come only in a new minor version, with a migration guide.
69
+
70
+ ## Diagnostic codes
46
71
 
47
- > Status: pre-alpha. APIs change between 0.x releases.
72
+ Production builds print codes such as `Gyral G0010 my-cart Increment`. The full table (code,
73
+ group, message, arguments, link) ships with the package as `@gyral/core/errors.json`, for tools
74
+ and docs sites: `import errors from '@gyral/core/errors.json' with { type: 'json' };`. The
75
+ same table, rendered: <https://gyral.dev/errors/>.
48
76
 
49
77
  ## License
50
78
 
package/dist/call.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Command } from './command.js';
2
+ import type { LocalHost } from './features.js';
3
+ /** The input of a `call()` command. */
4
+ export interface CallInput {
5
+ readonly selector: string;
6
+ readonly method: string;
7
+ readonly args: readonly unknown[];
8
+ }
9
+ /** Marker driver: the host runs call commands itself (`local`), after the render. */
10
+ export declare const CALL: {
11
+ readonly name: "@gyral/call";
12
+ readonly run: () => undefined;
13
+ readonly local: (host: LocalHost, input: unknown) => void;
14
+ };
15
+ /**
16
+ * A command that calls `method` with `args` on the first element matching `selector` inside
17
+ * this component (its shadow root, or its own children in light-DOM mode) once the current
18
+ * update has rendered:
19
+ *
20
+ * Start: (s) => [{ ...s, playing: true }, [call('video', 'play')]]
21
+ * Join: (s, m) => [s, [call('room-stage', 'join', m.room)]]
22
+ *
23
+ * `args` must be plain data (structured-cloneable), so the command stays data a test can
24
+ * compare. A missing element or method, or a method that throws or rejects, is reported
25
+ * through the error channel (ADR 0024); the error's cause says which.
26
+ */
27
+ export declare function call(selector: string, method: string, ...args: readonly unknown[]): Command<never>;
28
+ /**
29
+ * Runs a call command against `root`. No element, no such method, or a method that throws or
30
+ * rejects: reported with `host` (the cause says which).
31
+ */
32
+ export declare function runCall(root: ParentNode, host: Element, tag: string, input: CallInput): void;
package/dist/call.js ADDED
@@ -0,0 +1,63 @@
1
+ import { fail } from './errors.js';
2
+ import { afterRender, POST_FOCUS } from './scheduler.js';
3
+ import { DEV, message } from './view/index.js';
4
+ /** Marker driver: the host runs call commands itself (`local`), after the render. */
5
+ export const CALL = {
6
+ name: '@gyral/call',
7
+ run: () => undefined,
8
+ local: (host, input) => {
9
+ afterRender(POST_FOCUS, () => {
10
+ const root = host.root();
11
+ if (host.el.isConnected && root !== undefined)
12
+ runCall(root, host.el, host.tag, input);
13
+ });
14
+ },
15
+ };
16
+ /**
17
+ * A command that calls `method` with `args` on the first element matching `selector` inside
18
+ * this component (its shadow root, or its own children in light-DOM mode) once the current
19
+ * update has rendered:
20
+ *
21
+ * Start: (s) => [{ ...s, playing: true }, [call('video', 'play')]]
22
+ * Join: (s, m) => [s, [call('room-stage', 'join', m.room)]]
23
+ *
24
+ * `args` must be plain data (structured-cloneable), so the command stays data a test can
25
+ * compare. A missing element or method, or a method that throws or rejects, is reported
26
+ * through the error channel (ADR 0024); the error's cause says which.
27
+ */
28
+ export function call(selector, method, ...args) {
29
+ if (DEV) {
30
+ try {
31
+ structuredClone(args);
32
+ }
33
+ catch {
34
+ throw new TypeError(message(86, selector, method));
35
+ }
36
+ }
37
+ const input = { selector, method, args };
38
+ return { driver: CALL, input, onSuccess: () => undefined };
39
+ }
40
+ /**
41
+ * Runs a call command against `root`. No element, no such method, or a method that throws or
42
+ * rejects: reported with `host` (the cause says which).
43
+ */
44
+ export function runCall(root, host, tag, input) {
45
+ const { selector, method, args } = input;
46
+ const failed = (cause) => {
47
+ fail(cause, 'command', message(85, tag, selector, method), { host, tag, msg: method });
48
+ };
49
+ try {
50
+ const target = root.querySelector(selector);
51
+ if (target === null)
52
+ throw new Error(`no element matches ${selector}`);
53
+ const result = target[method].apply(target, [
54
+ ...args,
55
+ ]);
56
+ if (result instanceof Promise)
57
+ result.catch(failed);
58
+ }
59
+ catch (cause) {
60
+ failed(cause);
61
+ }
62
+ }
63
+ //# sourceMappingURL=call.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call.js","sourceRoot":"","sources":["../src/call.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAS/C,qFAAqF;AACrF,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,IAAI,EAAE,aAAa;IACnB,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;IACpB,KAAK,EAAE,CAAC,IAAe,EAAE,KAAc,EAAE,EAAE;QACzC,WAAW,CAAC,UAAU,EAAE,GAAG,EAAE;YAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,IAAI,IAAI,KAAK,SAAS;gBAC3C,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,KAAkB,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC;CACO,CAAC;AAEX;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,IAAI,CAClB,QAAgB,EAChB,MAAc,EACd,GAAG,IAAwB;IAE3B,IAAI,GAAG,EAAE,CAAC;QACR,IAAI,CAAC;YACH,eAAe,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,SAAS,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACpD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,IAAgB,EAAE,IAAa,EAAE,GAAW,EAAE,KAAgB;IACpF,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IACzC,MAAM,MAAM,GAAG,CAAC,KAAc,EAAQ,EAAE;QACtC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC;IACF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAmC,CAAC;QAC9E,IAAI,MAAM,KAAK,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,EAAE,CAAC,CAAC;QACvE,MAAM,MAAM,GAAa,MAAM,CAAC,MAAM,CAAkC,CAAC,KAAK,CAAC,MAAM,EAAE;YACrF,GAAG,IAAI;SACR,CAAC,CAAC;QACH,IAAI,MAAM,YAAY,OAAO;YAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC;AACH,CAAC","sourcesContent":["// Calling a method on an element from the model (gyral-1zd.27, view/05-element.md \"Calling an\n// element's method\"). Telling a child component, a media element or a widget to do something\n// (play, reset, open a room) is a side effect, so the model asks for it with a command, as it\n// does for focus: `call(selector, method, ...args)`. It runs as post-render work against the\n// component's own tree, so the element already exists. Gyral's answer to refs and ports: the\n// request is plain data, so a model test reads it with `callsIn` (@gyral/testing).\nimport type { Command } from './command.js';\nimport { fail } from './errors.js';\nimport type { LocalHost } from './features.js';\nimport { afterRender, POST_FOCUS } from './scheduler.js';\nimport { DEV, message } from './view/index.js';\n\n/** The input of a `call()` command. */\nexport interface CallInput {\n readonly selector: string;\n readonly method: string;\n readonly args: readonly unknown[];\n}\n\n/** Marker driver: the host runs call commands itself (`local`), after the render. */\nexport const CALL = {\n name: '@gyral/call',\n run: () => undefined,\n local: (host: LocalHost, input: unknown) => {\n afterRender(POST_FOCUS, () => {\n const root = host.root();\n if (host.el.isConnected && root !== undefined)\n runCall(root, host.el, host.tag, input as CallInput);\n });\n },\n} as const;\n\n/**\n * A command that calls `method` with `args` on the first element matching `selector` inside\n * this component (its shadow root, or its own children in light-DOM mode) once the current\n * update has rendered:\n *\n * Start: (s) => [{ ...s, playing: true }, [call('video', 'play')]]\n * Join: (s, m) => [s, [call('room-stage', 'join', m.room)]]\n *\n * `args` must be plain data (structured-cloneable), so the command stays data a test can\n * compare. A missing element or method, or a method that throws or rejects, is reported\n * through the error channel (ADR 0024); the error's cause says which.\n */\nexport function call(\n selector: string,\n method: string,\n ...args: readonly unknown[]\n): Command<never> {\n if (DEV) {\n try {\n structuredClone(args);\n } catch {\n throw new TypeError(message(86, selector, method));\n }\n }\n const input: CallInput = { selector, method, args };\n return { driver: CALL, input, onSuccess: () => undefined };\n}\n\n/**\n * Runs a call command against `root`. No element, no such method, or a method that throws or\n * rejects: reported with `host` (the cause says which).\n */\nexport function runCall(root: ParentNode, host: Element, tag: string, input: CallInput): void {\n const { selector, method, args } = input;\n const failed = (cause: unknown): void => {\n fail(cause, 'command', message(85, tag, selector, method), { host, tag, msg: method });\n };\n try {\n const target = root.querySelector(selector) as Record<string, unknown> | null;\n if (target === null) throw new Error(`no element matches ${selector}`);\n const result: unknown = (target[method] as (...a: unknown[]) => unknown).apply(target, [\n ...args,\n ]);\n if (result instanceof Promise) result.catch(failed);\n } catch (cause) {\n failed(cause);\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import type { Command } from './command.js';
2
2
  import type { LocalHost } from './features.js';
3
- import type { IntentParser, Tagged } from './types.js';
3
+ import type { IntentInput, IntentParser, Tagged } from './types.js';
4
4
  /** Marker driver: the host runs emit commands itself (`local`), synchronously ordered. */
5
5
  export declare const EMIT: {
6
6
  readonly name: "@gyral/emit";
@@ -10,8 +10,30 @@ export declare const EMIT: {
10
10
  /**
11
11
  * A command that sends `output` to the parent component. The parent receives it as an
12
12
  * intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.
13
+ * `outputs<Out>()` gives the same function typed by the component's output union.
13
14
  */
14
15
  export declare function emit(output: Tagged & Readonly<Record<string, unknown>>): Command<never>;
16
+ /**
17
+ * `emit`, typed by a component's output union (the 4th `define` type parameter), so an
18
+ * output of the wrong shape fails to compile. A module-level constant, like `intentsOf()`:
19
+ *
20
+ * type Out = { readonly _tag: 'Picked'; readonly id: number };
21
+ * const emit = outputs<Out>();
22
+ * // in update: Pick: (s, m) => [s, [emit({ _tag: 'Picked', id: m.id })]]
23
+ *
24
+ * Costs nothing at run time: it returns `emit` itself.
25
+ */
26
+ export declare function outputs<O extends Tagged>(): (output: O) => Command<never>;
27
+ /**
28
+ * The `gyral-output` event a child dispatches on its host for each output (ADR 0010): `detail`
29
+ * is the output. Bubbles, not composed. A non-Gyral parent listens for `OUTPUT_EVENT`. Only the
30
+ * direct parent's intent on the child host takes it, and once it does the event stops there.
31
+ */
32
+ export type OutputEvent<O extends Tagged = Tagged> = CustomEvent<O>;
33
+ /** The output union of a `define()` class: `OutputsOf<typeof Item>`. */
34
+ export type OutputsOf<C> = C extends {
35
+ readonly outputs?: infer O;
36
+ } ? Exclude<O, undefined> : never;
15
37
  /**
16
38
  * Sends an output to the parent component. A microtask keeps outputs in order and out of the
17
39
  * parent's render pass (settled() waits for it); the event bubbles through the parent's shadow
@@ -33,8 +55,8 @@ export type ChildSource<O extends Tagged, E extends Element> = OutputSource<O, E
33
55
  * Pass `() => Child` when the class isn't defined yet, e.g. a component that contains
34
56
  * itself; annotate the constant's type so TypeScript accepts the self-reference:
35
57
  *
36
- * const Folder: GyralElementClass<State, Msg, Props, Out> = define('x-folder', {
58
+ * const Folder: GyralElementClass<State, Msg, Props, Out> = define()('x-folder', {
37
59
  * intent: { Child: child(() => Folder, (out, el) => …) }, …
38
60
  * });
39
61
  */
40
- export declare function child<O extends Tagged, E extends Element, M>(source: ChildSource<O, E>, toMsg: (output: O, el: E) => M | undefined): IntentParser<M>;
62
+ export declare function child<O extends Tagged, E extends Element, M>(source: ChildSource<O, E>, toMsg: (output: O, el: E) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;
package/dist/children.js CHANGED
@@ -11,10 +11,27 @@ export const EMIT = {
11
11
  /**
12
12
  * A command that sends `output` to the parent component. The parent receives it as an
13
13
  * intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.
14
+ * `outputs<Out>()` gives the same function typed by the component's output union.
14
15
  */
15
16
  export function emit(output) {
16
17
  return { driver: EMIT, input: output, onSuccess: () => undefined };
17
18
  }
19
+ /**
20
+ * `emit`, typed by a component's output union (the 4th `define` type parameter), so an
21
+ * output of the wrong shape fails to compile. A module-level constant, like `intentsOf()`:
22
+ *
23
+ * type Out = { readonly _tag: 'Picked'; readonly id: number };
24
+ * const emit = outputs<Out>();
25
+ * // in update: Pick: (s, m) => [s, [emit({ _tag: 'Picked', id: m.id })]]
26
+ *
27
+ * Costs nothing at run time: it returns `emit` itself.
28
+ */
29
+ // A type-only argument: the caller names the union the result accepts.
30
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
31
+ export function outputs() {
32
+ // Sound: emit only forwards the output; the stricter parameter type is the point.
33
+ return emit;
34
+ }
18
35
  /**
19
36
  * Sends an output to the parent component. A microtask keeps outputs in order and out of the
20
37
  * parent's render pass (settled() waits for it); the event bubbles through the parent's shadow
@@ -38,7 +55,7 @@ const resolveSource = (source) => 'prototype' in source && source.prototype !==
38
55
  * Pass `() => Child` when the class isn't defined yet, e.g. a component that contains
39
56
  * itself; annotate the constant's type so TypeScript accepts the self-reference:
40
57
  *
41
- * const Folder: GyralElementClass<State, Msg, Props, Out> = define('x-folder', {
58
+ * const Folder: GyralElementClass<State, Msg, Props, Out> = define()('x-folder', {
42
59
  * intent: { Child: child(() => Folder, (out, el) => …) }, …
43
60
  * });
44
61
  */
@@ -1 +1 @@
1
- {"version":3,"file":"children.js","sourceRoot":"","sources":["../src/children.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,0FAA0F;AAC1F,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,IAAI,EAAE,aAAa;IACnB,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;IACpB,KAAK,EAAE,CAAC,IAAe,EAAE,MAAe,EAAE,EAAE;QAC1C,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAClC,CAAC;CACO,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,MAAkD;IACrE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AACrE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,MAAe;IAC3D,KAAK,CAAC,GAAG,EAAE;QACT,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO;QAC9B,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAClF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAaD,oEAAoE;AACpE,MAAM,aAAa,GAAG,CACpB,MAAyB,EACL,EAAE,CACtB,WAAW,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;IACrD,CAAC,CAAE,MAA6B;IAChC,CAAC,CAAE,MAAmC,EAAE,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,KAAK,CACnB,MAAyB,EACzB,KAA0C;IAE1C,OAAO,CAAC,KAAkB,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,sDAAsD;QAC1F,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACpF,8EAA8E;QAC9E,OAAO,KAAK,CAAC,KAAK,CAAC,MAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAChD,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Child components: props down, outputs up (docs/design-docs/0010-child-components.md).\nimport type { Command } from './command.js';\nimport type { LocalHost } from './features.js';\nimport { OUTPUT_EVENT } from './intent.js';\nimport { defer } from './scheduler.js';\nimport type { IntentInput, IntentParser, Tagged } from './types.js';\n\n/** Marker driver: the host runs emit commands itself (`local`), synchronously ordered. */\nexport const EMIT = {\n name: '@gyral/emit',\n run: () => undefined,\n local: (host: LocalHost, output: unknown) => {\n dispatchOutput(host.el, output);\n },\n} as const;\n\n/**\n * A command that sends `output` to the parent component. The parent receives it as an\n * intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.\n */\nexport function emit(output: Tagged & Readonly<Record<string, unknown>>): Command<never> {\n return { driver: EMIT, input: output, onSuccess: () => undefined };\n}\n\n/**\n * Sends an output to the parent component. A microtask keeps outputs in order and out of the\n * parent's render pass (settled() waits for it); the event bubbles through the parent's shadow\n * tree only (not composed).\n */\nexport function dispatchOutput(host: Element, output: unknown): void {\n defer(() => {\n if (!host.isConnected) return;\n host.dispatchEvent(\n new CustomEvent(OUTPUT_EVENT, { detail: output, bubbles: true, composed: false }),\n );\n });\n}\n\n/** Anything `child()` can read an output type from: a `define()` class. */\nexport interface OutputSource<O extends Tagged, E extends Element> {\n new (): E;\n /** Type-only marker for the component's output union. Never set at runtime. */\n readonly outputs?: O;\n}\n\n/** A child class, or a function returning it (for recursive or later-defined components). */\nexport type ChildSource<O extends Tagged, E extends Element> =\n OutputSource<O, E> | (() => OutputSource<O, E>);\n\n// Classes have a prototype; arrow functions (the lazy form) do not.\nconst resolveSource = <O extends Tagged, E extends Element>(\n source: ChildSource<O, E>,\n): OutputSource<O, E> =>\n 'prototype' in source && source.prototype !== undefined\n ? (source as OutputSource<O, E>)\n : (source as () => OutputSource<O, E>)();\n\n/**\n * Intent parser for a child component's outputs, typed by the child's output union.\n * `el` is the child element, for reading its props (an item id, for example).\n *\n * Pass `() => Child` when the class isn't defined yet, e.g. a component that contains\n * itself; annotate the constant's type so TypeScript accepts the self-reference:\n *\n * const Folder: GyralElementClass<State, Msg, Props, Out> = define('x-folder', {\n * intent: { Child: child(() => Folder, (out, el) => …) }, …\n * });\n */\nexport function child<O extends Tagged, E extends Element, M>(\n source: ChildSource<O, E>,\n toMsg: (output: O, el: E) => M | undefined,\n): IntentParser<M> {\n return (input: IntentInput) => {\n const type = resolveSource(source); // resolved per event, so the lazy form never hits TDZ\n if (!(input.target instanceof type) || input.detail === undefined) return undefined;\n // Sound: only `emit()` from a component of this class dispatches this detail.\n return toMsg(input.detail as O, input.target);\n };\n}\n"]}
1
+ {"version":3,"file":"children.js","sourceRoot":"","sources":["../src/children.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,0FAA0F;AAC1F,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,IAAI,EAAE,aAAa;IACnB,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;IACpB,KAAK,EAAE,CAAC,IAAe,EAAE,MAAe,EAAE,EAAE;QAC1C,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAClC,CAAC;CACO,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAAC,MAAkD;IACrE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AACrE,CAAC;AAED;;;;;;;;;GASG;AACH,uEAAuE;AACvE,6EAA6E;AAC7E,MAAM,UAAU,OAAO;IACrB,kFAAkF;IAClF,OAAO,IAAgD,CAAC;AAC1D,CAAC;AAYD;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,MAAe;IAC3D,KAAK,CAAC,GAAG,EAAE;QACT,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO;QAC9B,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAClF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAaD,oEAAoE;AACpE,MAAM,aAAa,GAAG,CACpB,MAAyB,EACL,EAAE,CACtB,WAAW,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;IACrD,CAAC,CAAE,MAA6B;IAChC,CAAC,CAAE,MAAmC,EAAE,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,KAAK,CACnB,MAAyB,EACzB,KAA0C;IAE1C,OAAO,CAAC,KAAkB,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,sDAAsD;QAC1F,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACpF,8EAA8E;QAC9E,OAAO,KAAK,CAAC,KAAK,CAAC,MAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAChD,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Child components: props down, outputs up (docs/design-docs/0010-child-components.md).\nimport type { Command } from './command.js';\nimport type { LocalHost } from './features.js';\nimport { OUTPUT_EVENT } from './intent.js';\nimport { defer } from './scheduler.js';\nimport type { IntentInput, IntentParser, Tagged } from './types.js';\n\n/** Marker driver: the host runs emit commands itself (`local`), synchronously ordered. */\nexport const EMIT = {\n name: '@gyral/emit',\n run: () => undefined,\n local: (host: LocalHost, output: unknown) => {\n dispatchOutput(host.el, output);\n },\n} as const;\n\n/**\n * A command that sends `output` to the parent component. The parent receives it as an\n * intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.\n * `outputs<Out>()` gives the same function typed by the component's output union.\n */\nexport function emit(output: Tagged & Readonly<Record<string, unknown>>): Command<never> {\n return { driver: EMIT, input: output, onSuccess: () => undefined };\n}\n\n/**\n * `emit`, typed by a component's output union (the 4th `define` type parameter), so an\n * output of the wrong shape fails to compile. A module-level constant, like `intentsOf()`:\n *\n * type Out = { readonly _tag: 'Picked'; readonly id: number };\n * const emit = outputs<Out>();\n * // in update: Pick: (s, m) => [s, [emit({ _tag: 'Picked', id: m.id })]]\n *\n * Costs nothing at run time: it returns `emit` itself.\n */\n// A type-only argument: the caller names the union the result accepts.\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters\nexport function outputs<O extends Tagged>(): (output: O) => Command<never> {\n // Sound: emit only forwards the output; the stricter parameter type is the point.\n return emit as unknown as (output: O) => Command<never>;\n}\n\n/**\n * The `gyral-output` event a child dispatches on its host for each output (ADR 0010): `detail`\n * is the output. Bubbles, not composed. A non-Gyral parent listens for `OUTPUT_EVENT`. Only the\n * direct parent's intent on the child host takes it, and once it does the event stops there.\n */\nexport type OutputEvent<O extends Tagged = Tagged> = CustomEvent<O>;\n\n/** The output union of a `define()` class: `OutputsOf<typeof Item>`. */\nexport type OutputsOf<C> = C extends { readonly outputs?: infer O } ? Exclude<O, undefined> : never;\n\n/**\n * Sends an output to the parent component. A microtask keeps outputs in order and out of the\n * parent's render pass (settled() waits for it); the event bubbles through the parent's shadow\n * tree only (not composed).\n */\nexport function dispatchOutput(host: Element, output: unknown): void {\n defer(() => {\n if (!host.isConnected) return;\n host.dispatchEvent(\n new CustomEvent(OUTPUT_EVENT, { detail: output, bubbles: true, composed: false }),\n );\n });\n}\n\n/** Anything `child()` can read an output type from: a `define()` class. */\nexport interface OutputSource<O extends Tagged, E extends Element> {\n new (): E;\n /** Type-only marker for the component's output union. Never set at runtime. */\n readonly outputs?: O;\n}\n\n/** A child class, or a function returning it (for recursive or later-defined components). */\nexport type ChildSource<O extends Tagged, E extends Element> =\n OutputSource<O, E> | (() => OutputSource<O, E>);\n\n// Classes have a prototype; arrow functions (the lazy form) do not.\nconst resolveSource = <O extends Tagged, E extends Element>(\n source: ChildSource<O, E>,\n): OutputSource<O, E> =>\n 'prototype' in source && source.prototype !== undefined\n ? (source as OutputSource<O, E>)\n : (source as () => OutputSource<O, E>)();\n\n/**\n * Intent parser for a child component's outputs, typed by the child's output union.\n * `el` is the child element, for reading its props (an item id, for example).\n *\n * Pass `() => Child` when the class isn't defined yet, e.g. a component that contains\n * itself; annotate the constant's type so TypeScript accepts the self-reference:\n *\n * const Folder: GyralElementClass<State, Msg, Props, Out> = define()('x-folder', {\n * intent: { Child: child(() => Folder, (out, el) => …) }, …\n * });\n */\nexport function child<O extends Tagged, E extends Element, M>(\n source: ChildSource<O, E>,\n toMsg: (output: O, el: E) => M | undefined,\n): (input: IntentInput) => ReturnType<IntentParser<M>> {\n return (input: IntentInput) => {\n const type = resolveSource(source); // resolved per event, so the lazy form never hits TDZ\n if (!(input.target instanceof type) || input.detail === undefined) return undefined;\n // Sound: only `emit()` from a component of this class dispatches this detail.\n return toMsg(input.detail as O, input.target);\n };\n}\n"]}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // gyral-types: writes the element typings gyralVitePreset() writes (gyral-1zd.25), for type
3
+ // checks that run without Vite (`gyral-types && tsc --noEmit`).
4
+ // gyral-types [--root <dir>] [--dir <src dir>]... [--out <file>]
5
+ import { resolve } from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import { writeTagMap } from '../compiler/tag-map.js';
8
+ const { values } = parseArgs({
9
+ options: {
10
+ root: { type: 'string', default: '.' },
11
+ dir: { type: 'string', multiple: true },
12
+ out: { type: 'string' },
13
+ },
14
+ });
15
+ const { entries, written } = writeTagMap({
16
+ root: resolve(values.root),
17
+ ...(values.dir === undefined ? {} : { dirs: values.dir }),
18
+ ...(values.out === undefined ? {} : { out: values.out }),
19
+ });
20
+ const where = values.out ?? '.gyral/elements.d.ts';
21
+ console.log(`gyral-types: ${String(entries.length)} element${entries.length === 1 ? '' : 's'} ` +
22
+ `${written ? 'written to' : 'unchanged in'} ${where}`);
23
+ //# sourceMappingURL=gyral-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gyral-types.js","sourceRoot":"","sources":["../../src/cli/gyral-types.ts"],"names":[],"mappings":";AACA,4FAA4F;AAC5F,gEAAgE;AAChE,mEAAmE;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE;QACP,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE;QACtC,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;QACvC,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KACxB;CACF,CAAC,CAAC;AAEH,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC;IACvC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;IAC1B,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IACzD,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;CACzD,CAAC,CAAC;AACH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,IAAI,sBAAsB,CAAC;AACnD,OAAO,CAAC,GAAG,CACT,gBAAgB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;IACjF,GAAG,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,IAAI,KAAK,EAAE,CACxD,CAAC","sourcesContent":["#!/usr/bin/env node\n// gyral-types: writes the element typings gyralVitePreset() writes (gyral-1zd.25), for type\n// checks that run without Vite (`gyral-types && tsc --noEmit`).\n// gyral-types [--root <dir>] [--dir <src dir>]... [--out <file>]\nimport { resolve } from 'node:path';\nimport { parseArgs } from 'node:util';\nimport { writeTagMap } from '../compiler/tag-map.js';\n\nconst { values } = parseArgs({\n options: {\n root: { type: 'string', default: '.' },\n dir: { type: 'string', multiple: true },\n out: { type: 'string' },\n },\n});\n\nconst { entries, written } = writeTagMap({\n root: resolve(values.root),\n ...(values.dir === undefined ? {} : { dirs: values.dir }),\n ...(values.out === undefined ? {} : { out: values.out }),\n});\nconst where = values.out ?? '.gyral/elements.d.ts';\nconsole.log(\n `gyral-types: ${String(entries.length)} element${entries.length === 1 ? '' : 's'} ` +\n `${written ? 'written to' : 'unchanged in'} ${where}`,\n);\n"]}
package/dist/command.d.ts CHANGED
@@ -1,12 +1,20 @@
1
1
  import type { IntentRejected } from './types.js';
2
2
  /** How commands in the same lane interact. See ADR 0006 for the table. */
3
3
  export type Concurrency = 'merge' | 'switch' | 'exhaust' | 'queue';
4
- export interface RetryPolicy {
4
+ /** How `retry(driver, policy)` retries a rejected run (retry.ts). `E`: the driver's error. */
5
+ export interface RetryPolicy<E = unknown> {
5
6
  /** Retries after the first failure. */
6
7
  readonly times: number;
7
8
  /** Delay before each retry (the base delay for exponential backoff). Default 0. */
8
9
  readonly delayMs?: number;
9
10
  readonly backoff?: 'fixed' | 'exponential';
11
+ /** Full jitter: wait a random time between 0 and the computed delay, so clients spread out. */
12
+ readonly jitter?: boolean;
13
+ /**
14
+ * Which failures to retry, given the driver's typed error (its `toError`, else the thrown
15
+ * value). Default: every failure. For `@gyral/http`, `retryableHttpError`.
16
+ */
17
+ readonly retryIf?: (error: E) => boolean;
10
18
  }
11
19
  export interface DriverContext<O = unknown> {
12
20
  /** Aborts when the command is switched away or its component disconnects. */
@@ -24,15 +32,35 @@ export interface Driver<I, O, E = unknown> {
24
32
  readonly run: (input: I, ctx: DriverContext<O>) => O | Promise<O>;
25
33
  /** Default policy for this driver's commands. Default `'merge'`. */
26
34
  readonly concurrency?: Concurrency;
27
- readonly retry?: RetryPolicy;
28
35
  /** Turns a thrown or rejected value into this driver's typed error. */
29
36
  readonly toError?: (cause: unknown) => E;
30
37
  }
31
- /** Any driver, with its input type erased. */
38
+ /**
39
+ * Any driver, with its input type erased. Every `Driver<I, O, E>` (from `defineDriver`,
40
+ * `subscription`, a test fake) is assignable to it as is: no cast.
41
+ */
32
42
  export type AnyDriver = Driver<never, unknown>;
33
- /** Drivers substituted by name (test fakes, configured instances). */
43
+ /**
44
+ * A driver with no implementation of its own (`defineDriver<I, O, E>('session')`): commands built
45
+ * from it are typed by `I`, `O` and `E`, and it runs only when `provideDrivers` or `el.drivers`
46
+ * supplies a driver of that name (ADR 0006 addendum "Driver tokens").
47
+ */
48
+ export interface DriverToken<E = unknown> {
49
+ readonly name: string;
50
+ readonly concurrency?: Concurrency;
51
+ readonly toError?: (cause: unknown) => E;
52
+ }
53
+ /**
54
+ * Drivers substituted by name (test fakes, configured instances): `el.drivers`,
55
+ * `provideDrivers`. Annotate a driver map with this type (or `Record<string, AnyDriver>`), not
56
+ * `Record<string, Driver<unknown, unknown>>`, which rejects drivers with a typed input.
57
+ */
34
58
  export type DriverOverrides = Readonly<Record<string, AnyDriver>>;
35
- /** A side effect described as data, plus pure mappers from its outcome to messages. */
59
+ /**
60
+ * A side effect described as data, plus pure mappers from its outcome to messages. Commands
61
+ * that answer with no message (`focus()`, `call()`, `navigate()`) are `Command<never>`,
62
+ * which fits wherever a `Command<Msg>` is expected: no type argument needed.
63
+ */
36
64
  export interface Command<M> {
37
65
  readonly driver: AnyDriver;
38
66
  readonly input: unknown;
@@ -40,24 +68,38 @@ export interface Command<M> {
40
68
  readonly key?: string;
41
69
  readonly concurrency?: Concurrency;
42
70
  readonly onSuccess: (output: unknown) => M | undefined;
43
- /** If omitted, failures are logged and dropped. */
71
+ /** If omitted, a failure is reported to the error channel (ADR 0024, G0041). */
44
72
  readonly onFailure?: (error: unknown) => M | undefined;
45
73
  }
74
+ /**
75
+ * How a command's outcome becomes a message. Every field is optional: without `onSuccess` the
76
+ * command answers with nothing (`Command<never>`); without `onFailure` a failure is reported to
77
+ * the error channel (ADR 0024).
78
+ */
46
79
  export interface CommandHandlers<O, E, MS, MF = MS> {
47
- readonly onSuccess: (output: O) => MS | undefined;
80
+ readonly onSuccess?: (output: O) => MS | undefined;
48
81
  readonly onFailure?: (error: E) => MF | undefined;
49
82
  readonly key?: string;
50
83
  readonly concurrency?: Concurrency;
51
84
  }
52
85
  /**
53
86
  * What a reducer (or `init`) returns: new state, optionally with commands to run. Commands may
54
- * also answer with the framework message `IntentRejected` (e.g. `submitForm` in @gyral/http
87
+ * also answer with the framework message `IntentRejected` (e.g. `submitForm` in `@gyral/http`
55
88
  * when the server rejects a form), which goes to the optional `IntentRejected` reducer.
56
89
  */
57
90
  export type Next<S, M> = S | readonly [S, ReadonlyArray<Command<M | IntentRejected>>];
58
- /** Identity helper so driver literals infer `I`, `O` and `E`. */
59
- export declare function defineDriver<I, O, E = unknown>(driver: Driver<I, O, E>): Driver<I, O, E>;
60
- /** Builds a command whose mappers are typed by the driver's input, output and error. */
61
- export declare function command<I, O, E, MS, MF = MS>(driver: Driver<I, O, E>, input: I, handlers: CommandHandlers<O, E, MS, MF>): Command<MS | MF>;
91
+ /**
92
+ * Defines a driver. Given an implementation (`run`), it returns it as is, so literals infer
93
+ * `I`, `O` and `E`. Given only a name, it returns a typed **token**: commands built from it are
94
+ * typed, and it runs only when a provider (`provideDrivers`) or `el.drivers` supplies a driver of
95
+ * that name. A command whose token nothing provides fails with G0043, naming the driver.
96
+ */
97
+ export declare function defineDriver<I, O, E = unknown>(spec: Driver<I, O, E> | DriverToken<E> | string): Driver<I, O, E>;
98
+ /**
99
+ * Builds a command whose mappers are typed by the driver's input, output and error. Without
100
+ * handlers (or without `onSuccess`) it answers with no message: `command(log, line)` is a
101
+ * `Command<never>`, which fits any `Command<Msg>`.
102
+ */
103
+ export declare function command<I, O, E, const MS = never, const MF = MS>(driver: Driver<I, O, E>, input: I, handlers?: CommandHandlers<O, E, MS, MF>): Command<MS | MF>;
62
104
  /** Splits a reducer result into state and commands. State is never an array (ADR 0006). */
63
105
  export declare function splitNext<S, M>(next: Next<S, M>): readonly [S, ReadonlyArray<Command<M | IntentRejected>>];
package/dist/command.js CHANGED
@@ -3,21 +3,35 @@
3
3
  // that build no commands don't bundle it.
4
4
  import { features } from './features.js';
5
5
  import { hostInterpreter } from './internal/interpreter.js';
6
- /** Identity helper so driver literals infer `I`, `O` and `E`. */
7
- export function defineDriver(driver) {
8
- return driver;
6
+ import { unprovided } from './internal/token.js';
7
+ /**
8
+ * Defines a driver. Given an implementation (`run`), it returns it as is, so literals infer
9
+ * `I`, `O` and `E`. Given only a name, it returns a typed **token**: commands built from it are
10
+ * typed, and it runs only when a provider (`provideDrivers`) or `el.drivers` supplies a driver of
11
+ * that name. A command whose token nothing provides fails with G0043, naming the driver.
12
+ */
13
+ export function defineDriver(spec) {
14
+ if (typeof spec === 'string')
15
+ return { name: spec, run: unprovided };
16
+ return 'run' in spec ? spec : { ...spec, run: unprovided };
9
17
  }
10
- /** Builds a command whose mappers are typed by the driver's input, output and error. */
11
- // Two message parameters so success and failure may produce different variants.
12
- export function command(driver, input, handlers) {
18
+ const none = () => undefined;
19
+ /**
20
+ * Builds a command whose mappers are typed by the driver's input, output and error. Without
21
+ * handlers (or without `onSuccess`) it answers with no message: `command(log, line)` is a
22
+ * `Command<never>`, which fits any `Command<Msg>`.
23
+ */
24
+ // Two message parameters so success and failure may produce different variants; `const` keeps
25
+ // a mapper's `{ _tag: 'Loaded' }` literal, so no type arguments are needed.
26
+ export function command(driver, input, handlers = {}) {
13
27
  features.commands = hostInterpreter;
14
- const { onFailure, key, concurrency } = handlers;
28
+ const { onSuccess = none, onFailure, key, concurrency } = handlers;
29
+ // Sound: the interpreter only passes this driver's output and toError() result.
15
30
  return {
16
- driver: driver,
31
+ driver,
17
32
  input,
18
- // Sound: the interpreter only passes this driver's output and toError() result.
19
- onSuccess: (output) => handlers.onSuccess(output),
20
- ...(onFailure === undefined ? {} : { onFailure: (error) => onFailure(error) }),
33
+ onSuccess,
34
+ ...(onFailure && { onFailure }),
21
35
  ...(key === undefined ? {} : { key }),
22
36
  ...(concurrency === undefined ? {} : { concurrency }),
23
37
  };
@@ -1 +1 @@
1
- {"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,+FAA+F;AAC/F,0CAA0C;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAoE5D,iEAAiE;AACjE,MAAM,UAAU,YAAY,CAAoB,MAAuB;IACrE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,wFAAwF;AACxF,gFAAgF;AAChF,MAAM,UAAU,OAAO,CACrB,MAAuB,EACvB,KAAQ,EACR,QAAuC;IAEvC,QAAQ,CAAC,QAAQ,GAAG,eAAe,CAAC;IACpC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC;IACjD,OAAO;QACL,MAAM,EAAE,MAAM;QACd,KAAK;QACL,gFAAgF;QAChF,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAW,CAAC;QACtD,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,SAAS,CAAC,KAAU,CAAC,EAAE,CAAC;QAC5F,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;QACrC,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CACvB,IAAgB;IAEhB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,OAAO,IAAgE,CAAC;IAC1E,CAAC;IACD,OAAO,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC","sourcesContent":["// Effects as data (docs/design-docs/0006-effects-and-drivers.md). Plain TypeScript only: the\n// interpreter lives in ./internal/ (ADR 0015); `command()` registers it (features.ts), so apps\n// that build no commands don't bundle it.\nimport { features } from './features.js';\nimport { hostInterpreter } from './internal/interpreter.js';\nimport type { IntentRejected } from './types.js';\n\n/** How commands in the same lane interact. See ADR 0006 for the table. */\nexport type Concurrency = 'merge' | 'switch' | 'exhaust' | 'queue';\n\nexport interface RetryPolicy {\n /** Retries after the first failure. */\n readonly times: number;\n /** Delay before each retry (the base delay for exponential backoff). Default 0. */\n readonly delayMs?: number;\n readonly backoff?: 'fixed' | 'exponential';\n}\n\nexport interface DriverContext<O = unknown> {\n /** Aborts when the command is switched away or its component disconnects. */\n readonly signal: AbortSignal;\n /**\n * Delivers an extra result while the command runs (streaming drivers: routers, timers,\n * sockets). Each value goes through the command's `onSuccess`. Ignored once the command\n * has settled or been aborted. A streaming `run` usually never resolves; abort ends it.\n */\n readonly emit: (output: O) => void;\n}\n\n/** Performs one kind of side effect. A plain object: easy to fake in tests. */\nexport interface Driver<I, O, E = unknown> {\n readonly name: string;\n readonly run: (input: I, ctx: DriverContext<O>) => O | Promise<O>;\n /** Default policy for this driver's commands. Default `'merge'`. */\n readonly concurrency?: Concurrency;\n readonly retry?: RetryPolicy;\n /** Turns a thrown or rejected value into this driver's typed error. */\n readonly toError?: (cause: unknown) => E;\n}\n\n/** Any driver, with its input type erased. */\nexport type AnyDriver = Driver<never, unknown>;\n\n/** Drivers substituted by name (test fakes, configured instances). */\nexport type DriverOverrides = Readonly<Record<string, AnyDriver>>;\n\n/** A side effect described as data, plus pure mappers from its outcome to messages. */\nexport interface Command<M> {\n readonly driver: AnyDriver;\n readonly input: unknown;\n /** Concurrency lane. Default: the driver's name. */\n readonly key?: string;\n readonly concurrency?: Concurrency;\n readonly onSuccess: (output: unknown) => M | undefined;\n /** If omitted, failures are logged and dropped. */\n readonly onFailure?: (error: unknown) => M | undefined;\n}\n\nexport interface CommandHandlers<O, E, MS, MF = MS> {\n readonly onSuccess: (output: O) => MS | undefined;\n readonly onFailure?: (error: E) => MF | undefined;\n readonly key?: string;\n readonly concurrency?: Concurrency;\n}\n\n/**\n * What a reducer (or `init`) returns: new state, optionally with commands to run. Commands may\n * also answer with the framework message `IntentRejected` (e.g. `submitForm` in @gyral/http\n * when the server rejects a form), which goes to the optional `IntentRejected` reducer.\n */\nexport type Next<S, M> = S | readonly [S, ReadonlyArray<Command<M | IntentRejected>>];\n\n/** Identity helper so driver literals infer `I`, `O` and `E`. */\nexport function defineDriver<I, O, E = unknown>(driver: Driver<I, O, E>): Driver<I, O, E> {\n return driver;\n}\n\n/** Builds a command whose mappers are typed by the driver's input, output and error. */\n// Two message parameters so success and failure may produce different variants.\nexport function command<I, O, E, MS, MF = MS>(\n driver: Driver<I, O, E>,\n input: I,\n handlers: CommandHandlers<O, E, MS, MF>,\n): Command<MS | MF> {\n features.commands = hostInterpreter;\n const { onFailure, key, concurrency } = handlers;\n return {\n driver: driver,\n input,\n // Sound: the interpreter only passes this driver's output and toError() result.\n onSuccess: (output) => handlers.onSuccess(output as O),\n ...(onFailure === undefined ? {} : { onFailure: (error: unknown) => onFailure(error as E) }),\n ...(key === undefined ? {} : { key }),\n ...(concurrency === undefined ? {} : { concurrency }),\n };\n}\n\n/** Splits a reducer result into state and commands. State is never an array (ADR 0006). */\nexport function splitNext<S, M>(\n next: Next<S, M>,\n): readonly [S, ReadonlyArray<Command<M | IntentRejected>>] {\n if (Array.isArray(next) && next.length === 2 && Array.isArray(next[1])) {\n return next as readonly [S, ReadonlyArray<Command<M | IntentRejected>>];\n }\n return [next as S, []];\n}\n"]}
1
+ {"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,+FAA+F;AAC/F,0CAA0C;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAsGjD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,IAA+C;IAE/C,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;IACrE,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,IAAI,GAAG,GAAc,EAAE,CAAC,SAAS,CAAC;AAExC;;;;GAIG;AACH,8FAA8F;AAC9F,4EAA4E;AAC5E,MAAM,UAAU,OAAO,CACrB,MAAuB,EACvB,KAAQ,EACR,WAA0C,EAAE;IAE5C,QAAQ,CAAC,QAAQ,GAAG,eAAe,CAAC;IACpC,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC;IACnE,gFAAgF;IAChF,OAAO;QACL,MAAM;QACN,KAAK;QACL,SAAS;QACT,GAAG,CAAC,SAAS,IAAI,EAAE,SAAS,EAAE,CAAC;QAC/B,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;QACrC,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;KAClC,CAAC;AACxB,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CACvB,IAAgB;IAEhB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,OAAO,IAAgE,CAAC;IAC1E,CAAC;IACD,OAAO,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC","sourcesContent":["// Effects as data (docs/design-docs/0006-effects-and-drivers.md). Plain TypeScript only: the\n// interpreter lives in ./internal/ (ADR 0015); `command()` registers it (features.ts), so apps\n// that build no commands don't bundle it.\nimport { features } from './features.js';\nimport { hostInterpreter } from './internal/interpreter.js';\nimport { unprovided } from './internal/token.js';\nimport type { IntentRejected } from './types.js';\n\n/** How commands in the same lane interact. See ADR 0006 for the table. */\nexport type Concurrency = 'merge' | 'switch' | 'exhaust' | 'queue';\n\n/** How `retry(driver, policy)` retries a rejected run (retry.ts). `E`: the driver's error. */\nexport interface RetryPolicy<E = unknown> {\n /** Retries after the first failure. */\n readonly times: number;\n /** Delay before each retry (the base delay for exponential backoff). Default 0. */\n readonly delayMs?: number;\n readonly backoff?: 'fixed' | 'exponential';\n /** Full jitter: wait a random time between 0 and the computed delay, so clients spread out. */\n readonly jitter?: boolean;\n /**\n * Which failures to retry, given the driver's typed error (its `toError`, else the thrown\n * value). Default: every failure. For `@gyral/http`, `retryableHttpError`.\n */\n readonly retryIf?: (error: E) => boolean;\n}\n\nexport interface DriverContext<O = unknown> {\n /** Aborts when the command is switched away or its component disconnects. */\n readonly signal: AbortSignal;\n /**\n * Delivers an extra result while the command runs (streaming drivers: routers, timers,\n * sockets). Each value goes through the command's `onSuccess`. Ignored once the command\n * has settled or been aborted. A streaming `run` usually never resolves; abort ends it.\n */\n readonly emit: (output: O) => void;\n}\n\n/** Performs one kind of side effect. A plain object: easy to fake in tests. */\nexport interface Driver<I, O, E = unknown> {\n readonly name: string;\n readonly run: (input: I, ctx: DriverContext<O>) => O | Promise<O>;\n /** Default policy for this driver's commands. Default `'merge'`. */\n readonly concurrency?: Concurrency;\n /** Turns a thrown or rejected value into this driver's typed error. */\n readonly toError?: (cause: unknown) => E;\n}\n\n/**\n * Any driver, with its input type erased. Every `Driver<I, O, E>` (from `defineDriver`,\n * `subscription`, a test fake) is assignable to it as is: no cast.\n */\nexport type AnyDriver = Driver<never, unknown>;\n\n/**\n * A driver with no implementation of its own (`defineDriver<I, O, E>('session')`): commands built\n * from it are typed by `I`, `O` and `E`, and it runs only when `provideDrivers` or `el.drivers`\n * supplies a driver of that name (ADR 0006 addendum \"Driver tokens\").\n */\nexport interface DriverToken<E = unknown> {\n readonly name: string;\n readonly concurrency?: Concurrency;\n readonly toError?: (cause: unknown) => E;\n}\n\n/**\n * Drivers substituted by name (test fakes, configured instances): `el.drivers`,\n * `provideDrivers`. Annotate a driver map with this type (or `Record<string, AnyDriver>`), not\n * `Record<string, Driver<unknown, unknown>>`, which rejects drivers with a typed input.\n */\nexport type DriverOverrides = Readonly<Record<string, AnyDriver>>;\n\n/**\n * A side effect described as data, plus pure mappers from its outcome to messages. Commands\n * that answer with no message (`focus()`, `call()`, `navigate()`) are `Command<never>`,\n * which fits wherever a `Command<Msg>` is expected: no type argument needed.\n */\nexport interface Command<M> {\n readonly driver: AnyDriver;\n readonly input: unknown;\n /** Concurrency lane. Default: the driver's name. */\n readonly key?: string;\n readonly concurrency?: Concurrency;\n readonly onSuccess: (output: unknown) => M | undefined;\n /** If omitted, a failure is reported to the error channel (ADR 0024, G0041). */\n readonly onFailure?: (error: unknown) => M | undefined;\n}\n\n/**\n * How a command's outcome becomes a message. Every field is optional: without `onSuccess` the\n * command answers with nothing (`Command<never>`); without `onFailure` a failure is reported to\n * the error channel (ADR 0024).\n */\nexport interface CommandHandlers<O, E, MS, MF = MS> {\n readonly onSuccess?: (output: O) => MS | undefined;\n readonly onFailure?: (error: E) => MF | undefined;\n readonly key?: string;\n readonly concurrency?: Concurrency;\n}\n\n/**\n * What a reducer (or `init`) returns: new state, optionally with commands to run. Commands may\n * also answer with the framework message `IntentRejected` (e.g. `submitForm` in `@gyral/http`\n * when the server rejects a form), which goes to the optional `IntentRejected` reducer.\n */\nexport type Next<S, M> = S | readonly [S, ReadonlyArray<Command<M | IntentRejected>>];\n\n/**\n * Defines a driver. Given an implementation (`run`), it returns it as is, so literals infer\n * `I`, `O` and `E`. Given only a name, it returns a typed **token**: commands built from it are\n * typed, and it runs only when a provider (`provideDrivers`) or `el.drivers` supplies a driver of\n * that name. A command whose token nothing provides fails with G0043, naming the driver.\n */\nexport function defineDriver<I, O, E = unknown>(\n spec: Driver<I, O, E> | DriverToken<E> | string,\n): Driver<I, O, E> {\n if (typeof spec === 'string') return { name: spec, run: unprovided };\n return 'run' in spec ? spec : { ...spec, run: unprovided };\n}\n\nconst none = (): undefined => undefined;\n\n/**\n * Builds a command whose mappers are typed by the driver's input, output and error. Without\n * handlers (or without `onSuccess`) it answers with no message: `command(log, line)` is a\n * `Command<never>`, which fits any `Command<Msg>`.\n */\n// Two message parameters so success and failure may produce different variants; `const` keeps\n// a mapper's `{ _tag: 'Loaded' }` literal, so no type arguments are needed.\nexport function command<I, O, E, const MS = never, const MF = MS>(\n driver: Driver<I, O, E>,\n input: I,\n handlers: CommandHandlers<O, E, MS, MF> = {},\n): Command<MS | MF> {\n features.commands = hostInterpreter;\n const { onSuccess = none, onFailure, key, concurrency } = handlers;\n // Sound: the interpreter only passes this driver's output and toError() result.\n return {\n driver,\n input,\n onSuccess,\n ...(onFailure && { onFailure }),\n ...(key === undefined ? {} : { key }),\n ...(concurrency === undefined ? {} : { concurrency }),\n } as Command<MS | MF>;\n}\n\n/** Splits a reducer result into state and commands. State is never an array (ADR 0006). */\nexport function splitNext<S, M>(\n next: Next<S, M>,\n): readonly [S, ReadonlyArray<Command<M | IntentRejected>>] {\n if (Array.isArray(next) && next.length === 2 && Array.isArray(next[1])) {\n return next as readonly [S, ReadonlyArray<Command<M | IntentRejected>>];\n }\n return [next as S, []];\n}\n"]}