@textui/core 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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/index.d.ts +12 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +11 -0
  6. package/dist/app/app.d.ts +200 -0
  7. package/dist/app/app.d.ts.map +1 -0
  8. package/dist/app/app.js +980 -0
  9. package/dist/core/animation.d.ts +34 -0
  10. package/dist/core/animation.d.ts.map +1 -0
  11. package/dist/core/animation.js +125 -0
  12. package/dist/core/clipboard.d.ts +29 -0
  13. package/dist/core/clipboard.d.ts.map +1 -0
  14. package/dist/core/clipboard.js +30 -0
  15. package/dist/core/commands.d.ts +42 -0
  16. package/dist/core/commands.d.ts.map +1 -0
  17. package/dist/core/commands.js +146 -0
  18. package/dist/core/components.d.ts +27 -0
  19. package/dist/core/components.d.ts.map +1 -0
  20. package/dist/core/components.js +78 -0
  21. package/dist/core/events.d.ts +23 -0
  22. package/dist/core/events.d.ts.map +1 -0
  23. package/dist/core/events.js +77 -0
  24. package/dist/core/focus.d.ts +69 -0
  25. package/dist/core/focus.d.ts.map +1 -0
  26. package/dist/core/focus.js +336 -0
  27. package/dist/core/i18n.d.ts +27 -0
  28. package/dist/core/i18n.d.ts.map +1 -0
  29. package/dist/core/i18n.js +88 -0
  30. package/dist/core/keybindings.d.ts +53 -0
  31. package/dist/core/keybindings.d.ts.map +1 -0
  32. package/dist/core/keybindings.js +163 -0
  33. package/dist/core/layers.d.ts +28 -0
  34. package/dist/core/layers.d.ts.map +1 -0
  35. package/dist/core/layers.js +84 -0
  36. package/dist/core/manifest.d.ts +22 -0
  37. package/dist/core/manifest.d.ts.map +1 -0
  38. package/dist/core/manifest.js +85 -0
  39. package/dist/core/navigation.d.ts +47 -0
  40. package/dist/core/navigation.d.ts.map +1 -0
  41. package/dist/core/navigation.js +110 -0
  42. package/dist/core/resources.d.ts +115 -0
  43. package/dist/core/resources.d.ts.map +1 -0
  44. package/dist/core/resources.js +321 -0
  45. package/dist/core/services.d.ts +21 -0
  46. package/dist/core/services.d.ts.map +1 -0
  47. package/dist/core/services.js +63 -0
  48. package/dist/core/store.d.ts +60 -0
  49. package/dist/core/store.d.ts.map +1 -0
  50. package/dist/core/store.js +593 -0
  51. package/dist/core/surfaces.d.ts +81 -0
  52. package/dist/core/surfaces.d.ts.map +1 -0
  53. package/dist/core/surfaces.js +237 -0
  54. package/dist/core/syntax.d.ts +49 -0
  55. package/dist/core/syntax.d.ts.map +1 -0
  56. package/dist/core/syntax.js +172 -0
  57. package/dist/core/when.d.ts +15 -0
  58. package/dist/core/when.d.ts.map +1 -0
  59. package/dist/core/when.js +224 -0
  60. package/dist/index.d.ts +49 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +54 -0
  63. package/dist/jsx/factory.d.ts +27 -0
  64. package/dist/jsx/factory.d.ts.map +1 -0
  65. package/dist/jsx/factory.js +102 -0
  66. package/dist/jsx/intrinsics.d.ts +96 -0
  67. package/dist/jsx/intrinsics.d.ts.map +1 -0
  68. package/dist/jsx/intrinsics.js +1 -0
  69. package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
  70. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
  71. package/dist/jsx/jsx-dev-runtime.js +12 -0
  72. package/dist/jsx/jsx-runtime.d.ts +42 -0
  73. package/dist/jsx/jsx-runtime.d.ts.map +1 -0
  74. package/dist/jsx/jsx-runtime.js +16 -0
  75. package/dist/render/buffer.d.ts +77 -0
  76. package/dist/render/buffer.d.ts.map +1 -0
  77. package/dist/render/buffer.js +275 -0
  78. package/dist/render/color.d.ts +35 -0
  79. package/dist/render/color.d.ts.map +1 -0
  80. package/dist/render/color.js +160 -0
  81. package/dist/render/diff.d.ts +37 -0
  82. package/dist/render/diff.d.ts.map +1 -0
  83. package/dist/render/diff.js +66 -0
  84. package/dist/render/layout.d.ts +72 -0
  85. package/dist/render/layout.d.ts.map +1 -0
  86. package/dist/render/layout.js +633 -0
  87. package/dist/render/static.d.ts +63 -0
  88. package/dist/render/static.d.ts.map +1 -0
  89. package/dist/render/static.js +209 -0
  90. package/dist/runtime/bindings.d.ts +46 -0
  91. package/dist/runtime/bindings.d.ts.map +1 -0
  92. package/dist/runtime/bindings.js +116 -0
  93. package/dist/runtime/hooks.d.ts +284 -0
  94. package/dist/runtime/hooks.d.ts.map +1 -0
  95. package/dist/runtime/hooks.js +846 -0
  96. package/dist/runtime/instance.d.ts +106 -0
  97. package/dist/runtime/instance.d.ts.map +1 -0
  98. package/dist/runtime/instance.js +181 -0
  99. package/dist/runtime/paint.d.ts +27 -0
  100. package/dist/runtime/paint.d.ts.map +1 -0
  101. package/dist/runtime/paint.js +567 -0
  102. package/dist/runtime/reconcile.d.ts +23 -0
  103. package/dist/runtime/reconcile.d.ts.map +1 -0
  104. package/dist/runtime/reconcile.js +260 -0
  105. package/dist/runtime/runtime.d.ts +42 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -0
  107. package/dist/runtime/runtime.js +1 -0
  108. package/dist/runtime/style.d.ts +55 -0
  109. package/dist/runtime/style.d.ts.map +1 -0
  110. package/dist/runtime/style.js +143 -0
  111. package/dist/themes/borders.d.ts +10 -0
  112. package/dist/themes/borders.d.ts.map +1 -0
  113. package/dist/themes/borders.js +88 -0
  114. package/dist/themes/builtin.d.ts +31 -0
  115. package/dist/themes/builtin.d.ts.map +1 -0
  116. package/dist/themes/builtin.js +291 -0
  117. package/dist/themes/glyphs.d.ts +15 -0
  118. package/dist/themes/glyphs.d.ts.map +1 -0
  119. package/dist/themes/glyphs.js +97 -0
  120. package/dist/themes/index.d.ts +5 -0
  121. package/dist/themes/index.d.ts.map +1 -0
  122. package/dist/themes/index.js +4 -0
  123. package/dist/themes/registry.d.ts +20 -0
  124. package/dist/themes/registry.d.ts.map +1 -0
  125. package/dist/themes/registry.js +204 -0
  126. package/dist/types/adapter.d.ts +38 -0
  127. package/dist/types/adapter.d.ts.map +1 -0
  128. package/dist/types/adapter.js +1 -0
  129. package/dist/types/animation.d.ts +31 -0
  130. package/dist/types/animation.d.ts.map +1 -0
  131. package/dist/types/animation.js +1 -0
  132. package/dist/types/app.d.ts +148 -0
  133. package/dist/types/app.d.ts.map +1 -0
  134. package/dist/types/app.js +1 -0
  135. package/dist/types/async.d.ts +30 -0
  136. package/dist/types/async.d.ts.map +1 -0
  137. package/dist/types/async.js +1 -0
  138. package/dist/types/capabilities.d.ts +44 -0
  139. package/dist/types/capabilities.d.ts.map +1 -0
  140. package/dist/types/capabilities.js +33 -0
  141. package/dist/types/cells.d.ts +72 -0
  142. package/dist/types/cells.d.ts.map +1 -0
  143. package/dist/types/cells.js +10 -0
  144. package/dist/types/command.d.ts +127 -0
  145. package/dist/types/command.d.ts.map +1 -0
  146. package/dist/types/command.js +1 -0
  147. package/dist/types/component-registry.d.ts +77 -0
  148. package/dist/types/component-registry.d.ts.map +1 -0
  149. package/dist/types/component-registry.js +1 -0
  150. package/dist/types/disposable.d.ts +8 -0
  151. package/dist/types/disposable.d.ts.map +1 -0
  152. package/dist/types/disposable.js +1 -0
  153. package/dist/types/focus.d.ts +72 -0
  154. package/dist/types/focus.d.ts.map +1 -0
  155. package/dist/types/focus.js +1 -0
  156. package/dist/types/geometry.d.ts +28 -0
  157. package/dist/types/geometry.d.ts.map +1 -0
  158. package/dist/types/geometry.js +14 -0
  159. package/dist/types/graph.d.ts +129 -0
  160. package/dist/types/graph.d.ts.map +1 -0
  161. package/dist/types/graph.js +31 -0
  162. package/dist/types/i18n.d.ts +23 -0
  163. package/dist/types/i18n.d.ts.map +1 -0
  164. package/dist/types/i18n.js +1 -0
  165. package/dist/types/index.d.ts +32 -0
  166. package/dist/types/index.d.ts.map +1 -0
  167. package/dist/types/index.js +31 -0
  168. package/dist/types/input.d.ts +65 -0
  169. package/dist/types/input.d.ts.map +1 -0
  170. package/dist/types/input.js +1 -0
  171. package/dist/types/keybinding.d.ts +49 -0
  172. package/dist/types/keybinding.d.ts.map +1 -0
  173. package/dist/types/keybinding.js +1 -0
  174. package/dist/types/layer.d.ts +54 -0
  175. package/dist/types/layer.d.ts.map +1 -0
  176. package/dist/types/layer.js +1 -0
  177. package/dist/types/manifest.d.ts +114 -0
  178. package/dist/types/manifest.d.ts.map +1 -0
  179. package/dist/types/manifest.js +1 -0
  180. package/dist/types/markdown.d.ts +58 -0
  181. package/dist/types/markdown.d.ts.map +1 -0
  182. package/dist/types/markdown.js +1 -0
  183. package/dist/types/navigation.d.ts +40 -0
  184. package/dist/types/navigation.d.ts.map +1 -0
  185. package/dist/types/navigation.js +1 -0
  186. package/dist/types/render.d.ts +72 -0
  187. package/dist/types/render.d.ts.map +1 -0
  188. package/dist/types/render.js +1 -0
  189. package/dist/types/resource.d.ts +170 -0
  190. package/dist/types/resource.d.ts.map +1 -0
  191. package/dist/types/resource.js +1 -0
  192. package/dist/types/services.d.ts +24 -0
  193. package/dist/types/services.d.ts.map +1 -0
  194. package/dist/types/services.js +3 -0
  195. package/dist/types/shell.d.ts +32 -0
  196. package/dist/types/shell.d.ts.map +1 -0
  197. package/dist/types/shell.js +1 -0
  198. package/dist/types/store.d.ts +124 -0
  199. package/dist/types/store.d.ts.map +1 -0
  200. package/dist/types/store.js +1 -0
  201. package/dist/types/stream.d.ts +18 -0
  202. package/dist/types/stream.d.ts.map +1 -0
  203. package/dist/types/stream.js +1 -0
  204. package/dist/types/style.d.ts +166 -0
  205. package/dist/types/style.d.ts.map +1 -0
  206. package/dist/types/style.js +1 -0
  207. package/dist/types/surface.d.ts +97 -0
  208. package/dist/types/surface.d.ts.map +1 -0
  209. package/dist/types/surface.js +1 -0
  210. package/dist/types/syntax.d.ts +68 -0
  211. package/dist/types/syntax.d.ts.map +1 -0
  212. package/dist/types/syntax.js +5 -0
  213. package/dist/types/terminal.d.ts +52 -0
  214. package/dist/types/terminal.d.ts.map +1 -0
  215. package/dist/types/terminal.js +1 -0
  216. package/dist/types/theme.d.ts +117 -0
  217. package/dist/types/theme.d.ts.map +1 -0
  218. package/dist/types/theme.js +1 -0
  219. package/dist/types/when.d.ts +19 -0
  220. package/dist/types/when.d.ts.map +1 -0
  221. package/dist/types/when.js +1 -0
  222. package/dist/ui/primitives.d.ts +56 -0
  223. package/dist/ui/primitives.d.ts.map +1 -0
  224. package/dist/ui/primitives.js +108 -0
  225. package/dist/ui/screen.d.ts +23 -0
  226. package/dist/ui/screen.d.ts.map +1 -0
  227. package/dist/ui/screen.js +18 -0
  228. package/dist/util/disposable.d.ts +6 -0
  229. package/dist/util/disposable.d.ts.map +1 -0
  230. package/dist/util/disposable.js +49 -0
  231. package/dist/util/markdown.d.ts +24 -0
  232. package/dist/util/markdown.d.ts.map +1 -0
  233. package/dist/util/markdown.js +220 -0
  234. package/dist/util/paths.d.ts +47 -0
  235. package/dist/util/paths.d.ts.map +1 -0
  236. package/dist/util/paths.js +134 -0
  237. package/dist/util/stream.d.ts +32 -0
  238. package/dist/util/stream.d.ts.map +1 -0
  239. package/dist/util/stream.js +206 -0
  240. package/dist/util/text.d.ts +65 -0
  241. package/dist/util/text.d.ts.map +1 -0
  242. package/dist/util/text.js +419 -0
  243. package/package.json +70 -0
  244. package/src/adapters/index.ts +11 -0
  245. package/src/app/app.ts +1096 -0
  246. package/src/core/animation.ts +144 -0
  247. package/src/core/clipboard.ts +40 -0
  248. package/src/core/commands.ts +169 -0
  249. package/src/core/components.ts +91 -0
  250. package/src/core/events.ts +95 -0
  251. package/src/core/focus.ts +358 -0
  252. package/src/core/i18n.ts +107 -0
  253. package/src/core/keybindings.ts +184 -0
  254. package/src/core/layers.ts +94 -0
  255. package/src/core/manifest.ts +84 -0
  256. package/src/core/navigation.ts +135 -0
  257. package/src/core/resources.ts +362 -0
  258. package/src/core/services.ts +69 -0
  259. package/src/core/store.ts +640 -0
  260. package/src/core/surfaces.ts +292 -0
  261. package/src/core/syntax.ts +200 -0
  262. package/src/core/when.ts +238 -0
  263. package/src/index.ts +76 -0
  264. package/src/jsx/factory.ts +124 -0
  265. package/src/jsx/intrinsics.ts +99 -0
  266. package/src/jsx/jsx-dev-runtime.ts +23 -0
  267. package/src/jsx/jsx-runtime.ts +76 -0
  268. package/src/render/buffer.ts +318 -0
  269. package/src/render/color.ts +180 -0
  270. package/src/render/diff.ts +99 -0
  271. package/src/render/layout.ts +764 -0
  272. package/src/render/static.ts +290 -0
  273. package/src/runtime/bindings.ts +150 -0
  274. package/src/runtime/hooks.ts +1057 -0
  275. package/src/runtime/instance.ts +284 -0
  276. package/src/runtime/paint.ts +737 -0
  277. package/src/runtime/reconcile.ts +335 -0
  278. package/src/runtime/runtime.ts +47 -0
  279. package/src/runtime/style.ts +205 -0
  280. package/src/themes/borders.ts +95 -0
  281. package/src/themes/builtin.ts +301 -0
  282. package/src/themes/glyphs.ts +101 -0
  283. package/src/themes/index.ts +4 -0
  284. package/src/themes/registry.ts +224 -0
  285. package/src/types/adapter.ts +41 -0
  286. package/src/types/animation.ts +34 -0
  287. package/src/types/app.ts +149 -0
  288. package/src/types/async.ts +35 -0
  289. package/src/types/capabilities.ts +79 -0
  290. package/src/types/cells.ts +79 -0
  291. package/src/types/command.ts +139 -0
  292. package/src/types/component-registry.ts +82 -0
  293. package/src/types/disposable.ts +8 -0
  294. package/src/types/focus.ts +77 -0
  295. package/src/types/geometry.ts +50 -0
  296. package/src/types/graph.ts +174 -0
  297. package/src/types/i18n.ts +25 -0
  298. package/src/types/index.ts +31 -0
  299. package/src/types/input.ts +81 -0
  300. package/src/types/keybinding.ts +51 -0
  301. package/src/types/layer.ts +46 -0
  302. package/src/types/manifest.ts +101 -0
  303. package/src/types/markdown.ts +47 -0
  304. package/src/types/navigation.ts +42 -0
  305. package/src/types/render.ts +86 -0
  306. package/src/types/resource.ts +185 -0
  307. package/src/types/services.ts +28 -0
  308. package/src/types/shell.ts +30 -0
  309. package/src/types/store.ts +148 -0
  310. package/src/types/stream.ts +24 -0
  311. package/src/types/style.ts +205 -0
  312. package/src/types/surface.ts +119 -0
  313. package/src/types/syntax.ts +93 -0
  314. package/src/types/terminal.ts +58 -0
  315. package/src/types/theme.ts +121 -0
  316. package/src/types/when.ts +21 -0
  317. package/src/ui/primitives.ts +118 -0
  318. package/src/ui/screen.ts +41 -0
  319. package/src/util/disposable.ts +49 -0
  320. package/src/util/markdown.ts +225 -0
  321. package/src/util/paths.ts +138 -0
  322. package/src/util/stream.ts +213 -0
  323. package/src/util/text.ts +428 -0
@@ -0,0 +1,220 @@
1
+ import { graphemes, stringWidth, wrapText } from './text.js';
2
+ /**
3
+ * Markdown, laid out.
4
+ *
5
+ * Not a parser - a formatter for the subset that appears in READMEs, notes and
6
+ * anything an agent or a service writes for a person: headings, lists, rules,
7
+ * fences, block quotes, and inline emphasis, code and links.
8
+ *
9
+ * Pure, and measured in cells. It takes the glyphs it needs rather than
10
+ * reading a theme, so it can be called from a test, from a viewer that windows
11
+ * the rows, and from a component that draws all of them.
12
+ */
13
+ /**
14
+ * The delimiters, guarded.
15
+ *
16
+ * The guards are the whole difficulty. A single `*` beside a double one is not
17
+ * an opener - `2 * 3 and **half a bold` italicised " 3 and " and ate a
18
+ * character, because the lone asterisk paired with the first half of the
19
+ * unclosed pair. A delimiter with a space just inside it is arithmetic or a
20
+ * footnote, not emphasis. And an underscore needs a word boundary as well, or
21
+ * `snake_case_name` goes italic in the middle.
22
+ */
23
+ const PATTERNS = [
24
+ String.raw `\*\*(?!\s)[^*]+(?<!\s)\*\*`,
25
+ String.raw `(?<![\w_])__(?!\s)[^_]+(?<!\s)__(?![\w_])`,
26
+ String.raw `\x60[^\x60]+\x60`,
27
+ String.raw `(?<!\*)\*(?!\s)[^*]+(?<!\s)\*(?!\*)`,
28
+ String.raw `(?<![\w_])_(?!\s)[^_]+(?<!\s)_(?![\w_])`,
29
+ String.raw `\[[^\]]+\]\([^)]+\)`,
30
+ ];
31
+ /** One capturing group, so `split` keeps the delimited pieces. */
32
+ const INLINE = new RegExp(`(${PATTERNS.join('|')})`);
33
+ /**
34
+ * Emphasis, code and links, as styled runs.
35
+ *
36
+ * Unmatched markers stay literal: a lone asterisk in prose is an asterisk, and
37
+ * a document that reformats itself as the closing one arrives is worse than
38
+ * one that never tried.
39
+ */
40
+ export function inlineRuns(text) {
41
+ const runs = [];
42
+ for (const piece of text.split(INLINE)) {
43
+ if (piece === '')
44
+ continue;
45
+ if (/^\*\*[^*]+\*\*$/.test(piece) || /^__[^_]+__$/.test(piece)) {
46
+ runs.push({ text: piece.slice(2, -2), bold: true });
47
+ }
48
+ else if (/^`[^`]+`$/.test(piece)) {
49
+ runs.push({ text: piece.slice(1, -1), code: true });
50
+ }
51
+ else if (/^\*[^*]+\*$/.test(piece) || /^_[^_]+_$/.test(piece)) {
52
+ runs.push({ text: piece.slice(1, -1), italic: true });
53
+ }
54
+ else {
55
+ const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(piece);
56
+ if (link)
57
+ runs.push({ text: link[1] ?? '', link: link[2] ?? '' });
58
+ else
59
+ runs.push({ text: piece });
60
+ }
61
+ }
62
+ return runs.length > 0 ? runs : [{ text: '' }];
63
+ }
64
+ /**
65
+ * Fold styled runs into lines that fit.
66
+ *
67
+ * `wrapText` wraps a string, and a string cannot carry which half of it was
68
+ * bold - so the wrap happens over runs, on cell widths rather than character
69
+ * counts. A word longer than the whole width is broken rather than allowed to
70
+ * decide the width of the pane it is in.
71
+ */
72
+ export function wrapRuns(runs, width) {
73
+ if (width <= 0)
74
+ return [runs];
75
+ const lines = [];
76
+ let line = [];
77
+ let used = 0;
78
+ const push = (run, text) => {
79
+ const last = line[line.length - 1];
80
+ if (last && sameStyle(last, run))
81
+ last.text += text;
82
+ else
83
+ line.push({ ...run, text });
84
+ used += stringWidth(text);
85
+ };
86
+ for (const run of runs) {
87
+ for (const word of run.text.split(/(\s+)/)) {
88
+ if (word === '')
89
+ continue;
90
+ const w = stringWidth(word);
91
+ if (/^\s+$/.test(word)) {
92
+ // A space at the start of a line is a space nobody typed.
93
+ if (used > 0 && used + w <= width)
94
+ push(run, word);
95
+ continue;
96
+ }
97
+ if (used + w > width && used > 0) {
98
+ lines.push(line);
99
+ line = [];
100
+ used = 0;
101
+ }
102
+ if (w > width) {
103
+ let rest = graphemes(word);
104
+ while (rest.length > 0) {
105
+ const room = Math.max(1, width - used);
106
+ push(run, rest.slice(0, room).join(''));
107
+ rest = rest.slice(room);
108
+ if (rest.length > 0) {
109
+ lines.push(line);
110
+ line = [];
111
+ used = 0;
112
+ }
113
+ }
114
+ continue;
115
+ }
116
+ push(run, word);
117
+ }
118
+ }
119
+ if (line.length > 0)
120
+ lines.push(line);
121
+ return lines.length > 0 ? lines : [[{ text: '' }]];
122
+ }
123
+ function sameStyle(a, b) {
124
+ return !!a.bold === !!b.bold && !!a.italic === !!b.italic
125
+ && !!a.code === !!b.code && a.link === b.link;
126
+ }
127
+ /** Wrapped rows behind a gutter: the marker on the first, its width on the rest. */
128
+ function withPrefix(runs, width, prefix, options = {}) {
129
+ const gutter = stringWidth(prefix);
130
+ const rest = options.continued ?? ' '.repeat(gutter);
131
+ return wrapRuns(runs, width <= 0 ? 0 : Math.max(1, width - gutter - 1)).map((line, index) => ({
132
+ kind: 'text',
133
+ runs: line,
134
+ prefix: index === 0 ? prefix : rest,
135
+ ...(options.prefixFg ? { prefixFg: options.prefixFg } : {}),
136
+ }));
137
+ }
138
+ export function layoutMarkdown(source, options) {
139
+ const { width, bullet = '-', quoteBar = '|', ruled = true } = options;
140
+ const lines = Array.isArray(source) ? source : source.replace(/\r\n/g, '\n').split('\n');
141
+ const rows = [];
142
+ let fence = 0;
143
+ let inFence = false;
144
+ let language = '';
145
+ for (const line of lines) {
146
+ const marker = /^\s*```(\S*)\s*$/.exec(line);
147
+ if (marker) {
148
+ // An open fence is laid out as a fence from the moment it opens. A turn
149
+ // is streamed, so the closing marker may simply not have been said yet -
150
+ // and a block that flickers between prose and code as the words land is
151
+ // worse than one shown open.
152
+ if (!inFence)
153
+ language = marker[1] ?? '';
154
+ if (ruled)
155
+ rows.push({ kind: 'fence', fence, part: inFence ? 'close' : 'open', ...(language ? { language } : {}) });
156
+ if (inFence)
157
+ fence++;
158
+ inFence = !inFence;
159
+ continue;
160
+ }
161
+ if (inFence) {
162
+ rows.push({ kind: 'fence', fence, part: 'code', text: line, ...(language ? { language } : {}) });
163
+ continue;
164
+ }
165
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
166
+ if (heading) {
167
+ const level = heading[1].length;
168
+ for (const runs of wrapRuns(inlineRuns(heading[2] ?? ''), width)) {
169
+ rows.push({ kind: 'heading', runs, level });
170
+ }
171
+ continue;
172
+ }
173
+ if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
174
+ rows.push({ kind: 'rule' });
175
+ continue;
176
+ }
177
+ const item = /^(\s*)[-*+]\s+(.*)$/.exec(line);
178
+ if (item) {
179
+ rows.push(...withPrefix(inlineRuns(item[2] ?? ''), width, `${item[1]}${bullet}`));
180
+ continue;
181
+ }
182
+ const ordered = /^(\s*)(\d+)[.)]\s+(.*)$/.exec(line);
183
+ if (ordered) {
184
+ rows.push(...withPrefix(inlineRuns(ordered[3] ?? ''), width, `${ordered[1]}${ordered[2]}.`));
185
+ continue;
186
+ }
187
+ const quote = /^\s*>\s?(.*)$/.exec(line);
188
+ if (quote) {
189
+ // The bar repeats down every row of the quote: a rule that stopped after
190
+ // the first wrapped row would read as one quoted line and some prose.
191
+ rows.push(...withPrefix(inlineRuns(quote[1] ?? ''), width, quoteBar, {
192
+ continued: quoteBar,
193
+ prefixFg: 'borderSubtle',
194
+ }));
195
+ continue;
196
+ }
197
+ if (line.trim() === '') {
198
+ rows.push({ kind: 'text', runs: [{ text: '' }] });
199
+ continue;
200
+ }
201
+ for (const runs of wrapRuns(inlineRuns(line), width))
202
+ rows.push({ kind: 'text', runs });
203
+ }
204
+ // An unterminated fence still gets its closing rule, so the box reads as a
205
+ // box rather than as something the renderer forgot to finish.
206
+ if (inFence && ruled)
207
+ rows.push({ kind: 'fence', fence, part: 'close' });
208
+ return rows;
209
+ }
210
+ /** Every run's text, for a measurement or a test that does not care how it looks. */
211
+ export function runsToText(runs) {
212
+ return runs.map((run) => run.text).join('');
213
+ }
214
+ /** Wrap plain text the way a row does, for callers that have no runs. */
215
+ export function wrapAt(text, width) {
216
+ if (width <= 0)
217
+ return [text];
218
+ const wrapped = wrapText(text, width);
219
+ return wrapped.length > 0 ? wrapped : [''];
220
+ }
@@ -0,0 +1,47 @@
1
+ import type { BindingPath } from '../types/graph.js';
2
+ /**
3
+ * Store paths are JSON-Pointer-shaped.
4
+ *
5
+ * `$/scope/a/b` absolute; the first segment is the scope
6
+ * `/a/b` relative to the surrounding data context
7
+ * `..` forbidden - escape to the root with `$/` instead, so a
8
+ * node's meaning never depends on where it was pasted
9
+ * `*` wildcard segment; legal in subscriptions, never in writes
10
+ *
11
+ * Internally a path is its segment list. The canonical key is the segments
12
+ * joined by `/` with no sigil, which is what every map here is keyed by.
13
+ */
14
+ export declare class PathError extends Error {
15
+ readonly path: string;
16
+ constructor(message: string, path: string);
17
+ }
18
+ export declare function escapeSegment(seg: string): string;
19
+ export declare function isAbsolute(path: string): path is `$/${string}`;
20
+ export declare function isRelative(path: string): boolean;
21
+ /** Split into segments. Does not resolve relatives - use `resolvePath`. */
22
+ export declare function segments(path: string): string[];
23
+ /**
24
+ * Canonical map key for a path: segments joined, no sigil.
25
+ *
26
+ * The segments are re-escaped on the way out. Everything downstream - the
27
+ * store's own walk, `ancestorKeys`, `matchKey` - splits a key on `/`, so a
28
+ * segment that legitimately contains one (a URI used as a key, a filename with
29
+ * a slash) has to stay escaped or it silently becomes several segments and the
30
+ * value lands somewhere nobody looks.
31
+ */
32
+ export declare function pathKey(path: string): string;
33
+ export declare function joinPath(...parts: (string | number)[]): BindingPath;
34
+ /**
35
+ * Resolve a possibly-relative path against a data context.
36
+ * A relative path with no context is an error a caller should never reach.
37
+ */
38
+ export declare function resolvePath(path: string, dataContext?: string): BindingPath;
39
+ export declare function scopeOf(path: string): string;
40
+ export declare function parentKey(key: string): string | null;
41
+ /** Every ancestor key of `key`, closest first, including `''` for the root. */
42
+ export declare function ancestorKeys(key: string): string[];
43
+ export declare function isDescendantKey(key: string, ancestor: string): boolean;
44
+ export declare function hasWildcard(path: string): boolean;
45
+ /** Match a concrete key against a pattern key that may contain `*`. */
46
+ export declare function matchKey(key: string, pattern: string, subtree?: boolean): boolean;
47
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/util/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;;;;;;;GAWG;AAEH,qBAAa,SAAU,SAAQ,KAAK;IACL,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAtC,OAAO,EAAE,MAAM,EAAW,IAAI,EAAE,MAAM;CAInD;AAMD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,KAAK,MAAM,EAAE,CAE9D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,QAAQ,CAAC,GAAG,KAAK,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,WAAW,CASnE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,CAW3E;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAWlD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGtE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,UAAQ,GAAG,OAAO,CAa/E"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Store paths are JSON-Pointer-shaped.
3
+ *
4
+ * `$/scope/a/b` absolute; the first segment is the scope
5
+ * `/a/b` relative to the surrounding data context
6
+ * `..` forbidden - escape to the root with `$/` instead, so a
7
+ * node's meaning never depends on where it was pasted
8
+ * `*` wildcard segment; legal in subscriptions, never in writes
9
+ *
10
+ * Internally a path is its segment list. The canonical key is the segments
11
+ * joined by `/` with no sigil, which is what every map here is keyed by.
12
+ */
13
+ export class PathError extends Error {
14
+ path;
15
+ constructor(message, path) {
16
+ super(`${message}: ${path}`);
17
+ this.path = path;
18
+ this.name = 'PathError';
19
+ }
20
+ }
21
+ function unescapeSegment(seg) {
22
+ return seg.includes('~') ? seg.replace(/~1/g, '/').replace(/~0/g, '~') : seg;
23
+ }
24
+ export function escapeSegment(seg) {
25
+ return seg.replace(/~/g, '~0').replace(/\//g, '~1');
26
+ }
27
+ export function isAbsolute(path) {
28
+ return path.startsWith('$/');
29
+ }
30
+ export function isRelative(path) {
31
+ return path.startsWith('/');
32
+ }
33
+ /** Split into segments. Does not resolve relatives - use `resolvePath`. */
34
+ export function segments(path) {
35
+ const body = isAbsolute(path) ? path.slice(2) : path.startsWith('/') ? path.slice(1) : path;
36
+ if (body === '')
37
+ return [];
38
+ const parts = body.split('/');
39
+ for (const p of parts) {
40
+ if (p === '..')
41
+ throw new PathError('".." is forbidden in a store path', path);
42
+ }
43
+ return parts.map(unescapeSegment);
44
+ }
45
+ /**
46
+ * Canonical map key for a path: segments joined, no sigil.
47
+ *
48
+ * The segments are re-escaped on the way out. Everything downstream - the
49
+ * store's own walk, `ancestorKeys`, `matchKey` - splits a key on `/`, so a
50
+ * segment that legitimately contains one (a URI used as a key, a filename with
51
+ * a slash) has to stay escaped or it silently becomes several segments and the
52
+ * value lands somewhere nobody looks.
53
+ */
54
+ export function pathKey(path) {
55
+ return segments(path).map(escapeSegment).join('/');
56
+ }
57
+ export function joinPath(...parts) {
58
+ const segs = [];
59
+ for (const p of parts) {
60
+ const s = String(p);
61
+ for (const seg of s.replace(/^\$\//, '').replace(/^\//, '').split('/')) {
62
+ if (seg !== '')
63
+ segs.push(seg);
64
+ }
65
+ }
66
+ return `$/${segs.join('/')}`;
67
+ }
68
+ /**
69
+ * Resolve a possibly-relative path against a data context.
70
+ * A relative path with no context is an error a caller should never reach.
71
+ */
72
+ export function resolvePath(path, dataContext) {
73
+ if (isAbsolute(path))
74
+ return path;
75
+ if (!path.startsWith('/')) {
76
+ throw new PathError('a path must start with "$/" or "/"', path);
77
+ }
78
+ if (!dataContext) {
79
+ throw new PathError('relative path used with no data context', path);
80
+ }
81
+ const base = segments(dataContext);
82
+ const rest = segments(path);
83
+ return `$/${[...base, ...rest].join('/')}`;
84
+ }
85
+ export function scopeOf(path) {
86
+ const segs = segments(path);
87
+ return segs[0] ?? '';
88
+ }
89
+ export function parentKey(key) {
90
+ const i = key.lastIndexOf('/');
91
+ return i === -1 ? (key === '' ? null : '') : key.slice(0, i);
92
+ }
93
+ /** Every ancestor key of `key`, closest first, including `''` for the root. */
94
+ export function ancestorKeys(key) {
95
+ const out = [];
96
+ let cur = key;
97
+ for (;;) {
98
+ const p = parentKey(cur);
99
+ if (p === null)
100
+ break;
101
+ out.push(p);
102
+ cur = p;
103
+ if (p === '')
104
+ break;
105
+ }
106
+ return out;
107
+ }
108
+ export function isDescendantKey(key, ancestor) {
109
+ if (ancestor === '')
110
+ return key !== '';
111
+ return key.startsWith(ancestor + '/');
112
+ }
113
+ export function hasWildcard(path) {
114
+ return path.includes('*');
115
+ }
116
+ /** Match a concrete key against a pattern key that may contain `*`. */
117
+ export function matchKey(key, pattern, subtree = false) {
118
+ if (!pattern.includes('*')) {
119
+ return key === pattern || (subtree && isDescendantKey(key, pattern));
120
+ }
121
+ const p = pattern.split('/');
122
+ const k = key.split('/');
123
+ if (!subtree && p.length !== k.length)
124
+ return false;
125
+ if (subtree && k.length < p.length)
126
+ return false;
127
+ for (let i = 0; i < p.length; i++) {
128
+ if (p[i] === '*')
129
+ continue;
130
+ if (p[i] !== k[i])
131
+ return false;
132
+ }
133
+ return true;
134
+ }
@@ -0,0 +1,32 @@
1
+ import type { Stream, StreamObserver, StreamSource } from '../types/stream.js';
2
+ import type { ReactiveStore } from '../types/store.js';
3
+ import type { BindingPath } from '../types/graph.js';
4
+ /**
5
+ * The smallest useful stream, plus adapters.
6
+ *
7
+ * A LogViewer should not care whether its lines come from a child process, an
8
+ * async iterable or a store path - so everything is adapted to one shape here,
9
+ * and nowhere else has to know the difference.
10
+ */
11
+ export declare function createStream<T>(start: (observer: StreamObserver<T>) => (() => void) | void): Stream<T>;
12
+ export declare function fromAsyncIterable<T>(iterable: AsyncIterable<T>): Stream<T>;
13
+ export declare function fromIterable<T>(iterable: Iterable<T>): Stream<T>;
14
+ /** Emit the value at a store path each time it changes. */
15
+ export declare function fromStorePath<T>(store: ReactiveStore, path: BindingPath): Stream<T>;
16
+ /** Emit each item appended to a store collection, and nothing already there. */
17
+ export declare function fromStoreCollection<T>(store: ReactiveStore, path: BindingPath): Stream<T>;
18
+ /** A source that pushes from the outside. Adapters and tests use this. */
19
+ export interface Subject<T> extends Stream<T> {
20
+ next(value: T): void;
21
+ error(err: unknown): void;
22
+ complete(): void;
23
+ }
24
+ export declare function createSubject<T>(): Subject<T>;
25
+ export declare function toStream<T>(source: StreamSource<T>): Stream<T>;
26
+ export declare function mapStream<T, U>(source: Stream<T>, fn: (value: T) => U): Stream<U>;
27
+ export declare function filterStream<T>(source: Stream<T>, pred: (value: T) => boolean): Stream<T>;
28
+ /** Split incoming chunks on newlines. What a log stream almost always wants. */
29
+ export declare function lines(source: Stream<string>): Stream<string>;
30
+ /** Emit at most one value per `ms`, keeping the latest. */
31
+ export declare function throttleStream<T>(source: Stream<T>, ms: number): Stream<T>;
32
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/util/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD;;;;;;GAMG;AAEH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,KAAK,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAC1D,MAAM,CAAC,CAAC,CAAC,CAgBX;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAgB1E;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAKhE;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAKnF;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,CAWzF;AAED,0EAA0E;AAC1E,MAAM,WAAW,OAAO,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;IAC3C,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CA8B7C;AAMD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAO9D;AAID,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CASjF;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CASzF;AAED,gFAAgF;AAChF,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAkB5D;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAiC1E"}
@@ -0,0 +1,206 @@
1
+ import { toDisposable } from './disposable.js';
2
+ /**
3
+ * The smallest useful stream, plus adapters.
4
+ *
5
+ * A LogViewer should not care whether its lines come from a child process, an
6
+ * async iterable or a store path - so everything is adapted to one shape here,
7
+ * and nowhere else has to know the difference.
8
+ */
9
+ export function createStream(start) {
10
+ return {
11
+ subscribe(observer) {
12
+ let closed = false;
13
+ const guard = {
14
+ next: (v) => { if (!closed)
15
+ observer.next(v); },
16
+ error: (e) => { if (!closed)
17
+ observer.error?.(e); },
18
+ complete: () => { if (!closed)
19
+ observer.complete?.(); },
20
+ };
21
+ const stop = start(guard);
22
+ return toDisposable(() => {
23
+ closed = true;
24
+ stop?.();
25
+ });
26
+ },
27
+ };
28
+ }
29
+ export function fromAsyncIterable(iterable) {
30
+ return createStream((observer) => {
31
+ let cancelled = false;
32
+ void (async () => {
33
+ try {
34
+ for await (const value of iterable) {
35
+ if (cancelled)
36
+ return;
37
+ observer.next(value);
38
+ }
39
+ observer.complete?.();
40
+ }
41
+ catch (err) {
42
+ if (!cancelled)
43
+ observer.error?.(err);
44
+ }
45
+ })();
46
+ return () => { cancelled = true; };
47
+ });
48
+ }
49
+ export function fromIterable(iterable) {
50
+ return createStream((observer) => {
51
+ for (const value of iterable)
52
+ observer.next(value);
53
+ observer.complete?.();
54
+ });
55
+ }
56
+ /** Emit the value at a store path each time it changes. */
57
+ export function fromStorePath(store, path) {
58
+ return createStream((observer) => {
59
+ const sub = store.subscribe(path, (value) => observer.next(value), { immediate: true });
60
+ return () => sub.dispose();
61
+ });
62
+ }
63
+ /** Emit each item appended to a store collection, and nothing already there. */
64
+ export function fromStoreCollection(store, path) {
65
+ return createStream((observer) => {
66
+ let seen = (store.get(path) ?? []).length;
67
+ const sub = store.subscribe(path, (value) => {
68
+ const list = Array.isArray(value) ? value : [];
69
+ if (list.length < seen)
70
+ seen = 0;
71
+ for (let i = seen; i < list.length; i++)
72
+ observer.next(list[i]);
73
+ seen = list.length;
74
+ });
75
+ return () => sub.dispose();
76
+ });
77
+ }
78
+ export function createSubject() {
79
+ const observers = new Set();
80
+ let done = false;
81
+ return {
82
+ subscribe(observer) {
83
+ if (done) {
84
+ observer.complete?.();
85
+ return toDisposable(() => { });
86
+ }
87
+ observers.add(observer);
88
+ return toDisposable(() => observers.delete(observer));
89
+ },
90
+ next(value) {
91
+ if (done)
92
+ return;
93
+ for (const o of [...observers])
94
+ o.next(value);
95
+ },
96
+ error(err) {
97
+ if (done)
98
+ return;
99
+ done = true;
100
+ for (const o of [...observers])
101
+ o.error?.(err);
102
+ observers.clear();
103
+ },
104
+ complete() {
105
+ if (done)
106
+ return;
107
+ done = true;
108
+ for (const o of [...observers])
109
+ o.complete?.();
110
+ observers.clear();
111
+ },
112
+ };
113
+ }
114
+ function isStream(value) {
115
+ return typeof value === 'object' && value !== null && 'subscribe' in value;
116
+ }
117
+ export function toStream(source) {
118
+ if (typeof source === 'function')
119
+ return fromAsyncIterable(source());
120
+ if (isStream(source))
121
+ return source;
122
+ if (Symbol.asyncIterator in source) {
123
+ return fromAsyncIterable(source);
124
+ }
125
+ return fromIterable(source);
126
+ }
127
+ // ------------------------------------------------------------- operators
128
+ export function mapStream(source, fn) {
129
+ return createStream((observer) => {
130
+ const sub = source.subscribe({
131
+ next: (v) => observer.next(fn(v)),
132
+ error: (e) => observer.error?.(e),
133
+ complete: () => observer.complete?.(),
134
+ });
135
+ return () => sub.dispose();
136
+ });
137
+ }
138
+ export function filterStream(source, pred) {
139
+ return createStream((observer) => {
140
+ const sub = source.subscribe({
141
+ next: (v) => { if (pred(v))
142
+ observer.next(v); },
143
+ error: (e) => observer.error?.(e),
144
+ complete: () => observer.complete?.(),
145
+ });
146
+ return () => sub.dispose();
147
+ });
148
+ }
149
+ /** Split incoming chunks on newlines. What a log stream almost always wants. */
150
+ export function lines(source) {
151
+ return createStream((observer) => {
152
+ let carry = '';
153
+ const sub = source.subscribe({
154
+ next(chunk) {
155
+ carry += chunk;
156
+ const parts = carry.split('\n');
157
+ carry = parts.pop() ?? '';
158
+ for (const line of parts)
159
+ observer.next(line);
160
+ },
161
+ error: (e) => observer.error?.(e),
162
+ complete() {
163
+ if (carry !== '')
164
+ observer.next(carry);
165
+ observer.complete?.();
166
+ },
167
+ });
168
+ return () => sub.dispose();
169
+ });
170
+ }
171
+ /** Emit at most one value per `ms`, keeping the latest. */
172
+ export function throttleStream(source, ms) {
173
+ return createStream((observer) => {
174
+ let pending;
175
+ let has = false;
176
+ let timer = null;
177
+ const sub = source.subscribe({
178
+ next(value) {
179
+ pending = value;
180
+ has = true;
181
+ if (timer)
182
+ return;
183
+ timer = setInterval(() => {
184
+ if (has) {
185
+ observer.next(pending);
186
+ has = false;
187
+ return;
188
+ }
189
+ if (timer)
190
+ clearInterval(timer);
191
+ timer = null;
192
+ }, ms);
193
+ timer.unref?.();
194
+ observer.next(pending);
195
+ has = false;
196
+ },
197
+ error: (e) => observer.error?.(e),
198
+ complete: () => observer.complete?.(),
199
+ });
200
+ return () => {
201
+ if (timer)
202
+ clearInterval(timer);
203
+ sub.dispose();
204
+ };
205
+ });
206
+ }