@videojs/spf 10.0.0-beta.2 → 10.0.0-beta.21

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 (264) hide show
  1. package/README.md +16 -40
  2. package/dist/default/core/abr/bandwidth-estimator.js +95 -0
  3. package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
  4. package/dist/default/core/abr/ewma.js +71 -0
  5. package/dist/default/core/abr/ewma.js.map +1 -0
  6. package/dist/default/core/abr/quality-selection.js +53 -0
  7. package/dist/default/core/abr/quality-selection.js.map +1 -0
  8. package/dist/default/core/buffer/back-buffer.js +46 -0
  9. package/dist/default/core/buffer/back-buffer.js.map +1 -0
  10. package/dist/default/core/buffer/forward-buffer.js +73 -0
  11. package/dist/default/core/buffer/forward-buffer.js.map +1 -0
  12. package/dist/default/core/create-machine-actor.js +91 -0
  13. package/dist/default/core/create-machine-actor.js.map +1 -0
  14. package/dist/default/core/create-machine-reactor.js +83 -0
  15. package/dist/default/core/create-machine-reactor.js.map +1 -0
  16. package/dist/default/core/create-transition-actor.js +46 -0
  17. package/dist/default/core/create-transition-actor.js.map +1 -0
  18. package/dist/default/core/features/calculate-presentation-duration.js +53 -0
  19. package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
  20. package/dist/default/core/features/quality-switching.js +69 -0
  21. package/dist/default/core/features/quality-switching.js.map +1 -0
  22. package/dist/default/core/features/resolve-presentation.js +73 -0
  23. package/dist/default/core/features/resolve-presentation.js.map +1 -0
  24. package/dist/default/core/features/resolve-track.js +66 -0
  25. package/dist/default/core/features/resolve-track.js.map +1 -0
  26. package/dist/default/core/features/select-tracks.js +130 -0
  27. package/dist/default/core/features/select-tracks.js.map +1 -0
  28. package/dist/default/core/features/sync-preload-attribute.js +26 -0
  29. package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
  30. package/dist/default/core/hls/parse-attributes.js +134 -0
  31. package/dist/default/core/hls/parse-attributes.js.map +1 -0
  32. package/dist/default/core/hls/parse-media-playlist.js +82 -0
  33. package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
  34. package/dist/default/core/hls/parse-multivariant.js +214 -0
  35. package/dist/default/core/hls/parse-multivariant.js.map +1 -0
  36. package/dist/default/core/hls/resolve-url.js +11 -0
  37. package/dist/default/core/hls/resolve-url.js.map +1 -0
  38. package/dist/default/core/machine.js +26 -0
  39. package/dist/default/core/machine.js.map +1 -0
  40. package/dist/default/core/signals/effect.js +41 -0
  41. package/dist/default/core/signals/effect.js.map +1 -0
  42. package/dist/default/core/signals/primitives.js +32 -0
  43. package/dist/default/core/signals/primitives.js.map +1 -0
  44. package/dist/default/core/task.js +191 -0
  45. package/dist/default/core/task.js.map +1 -0
  46. package/dist/default/core/types/index.js +15 -0
  47. package/dist/default/core/types/index.js.map +1 -0
  48. package/dist/default/core/utils/generate-id.js +21 -0
  49. package/dist/default/core/utils/generate-id.js.map +1 -0
  50. package/dist/default/core/utils/track-selection.js +39 -0
  51. package/dist/default/core/utils/track-selection.js.map +1 -0
  52. package/dist/default/dom/features/end-of-stream.js +145 -0
  53. package/dist/default/dom/features/end-of-stream.js.map +1 -0
  54. package/dist/default/dom/features/load-segments.js +192 -0
  55. package/dist/default/dom/features/load-segments.js.map +1 -0
  56. package/dist/default/dom/features/load-text-track-cues.js +104 -0
  57. package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
  58. package/dist/default/dom/features/segment-loader-actor.js +243 -0
  59. package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
  60. package/dist/default/dom/features/setup-mediasource.js +48 -0
  61. package/dist/default/dom/features/setup-mediasource.js.map +1 -0
  62. package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
  63. package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
  64. package/dist/default/dom/features/sync-text-tracks.js +95 -0
  65. package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
  66. package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
  67. package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
  68. package/dist/default/dom/features/text-tracks-actor.js +42 -0
  69. package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
  70. package/dist/default/dom/features/track-current-time.js +49 -0
  71. package/dist/default/dom/features/track-current-time.js.map +1 -0
  72. package/dist/default/dom/features/track-playback-initiated.js +66 -0
  73. package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
  74. package/dist/default/dom/features/track-playback-rate.js +32 -0
  75. package/dist/default/dom/features/track-playback-rate.js.map +1 -0
  76. package/dist/default/dom/features/update-duration.js +78 -0
  77. package/dist/default/dom/features/update-duration.js.map +1 -0
  78. package/dist/default/dom/media/append-segment.js +60 -0
  79. package/dist/default/dom/media/append-segment.js.map +1 -0
  80. package/dist/default/dom/media/buffer-flusher.js +55 -0
  81. package/dist/default/dom/media/buffer-flusher.js.map +1 -0
  82. package/dist/default/dom/media/mediasource-setup.js +146 -0
  83. package/dist/default/dom/media/mediasource-setup.js.map +1 -0
  84. package/dist/default/dom/media/source-buffer-actor.js +137 -0
  85. package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
  86. package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
  87. package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
  88. package/dist/default/dom/network/fetch.js +54 -0
  89. package/dist/default/dom/network/fetch.js.map +1 -0
  90. package/dist/default/dom/playback-engine/adapter.js +100 -0
  91. package/dist/default/dom/playback-engine/adapter.js.map +1 -0
  92. package/dist/default/dom/playback-engine/engine.js +142 -0
  93. package/dist/default/dom/playback-engine/engine.js.map +1 -0
  94. package/dist/default/dom/playback-engine.js +4 -0
  95. package/dist/default/dom/text/parse-vtt-segment.js +56 -0
  96. package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
  97. package/dist/default/dom.js +10 -0
  98. package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
  99. package/dist/default/index.js.map +1 -0
  100. package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
  101. package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
  102. package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
  103. package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
  104. package/dist/dev/core/abr/ewma.js +71 -0
  105. package/dist/dev/core/abr/ewma.js.map +1 -0
  106. package/dist/dev/core/abr/quality-selection.js +53 -0
  107. package/dist/dev/core/abr/quality-selection.js.map +1 -0
  108. package/dist/dev/core/actor.d.ts +27 -0
  109. package/dist/dev/core/actor.d.ts.map +1 -0
  110. package/dist/dev/core/buffer/back-buffer.js +46 -0
  111. package/dist/dev/core/buffer/back-buffer.js.map +1 -0
  112. package/dist/dev/core/buffer/forward-buffer.js +73 -0
  113. package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
  114. package/dist/dev/core/create-machine-actor.d.ts +12 -0
  115. package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
  116. package/dist/dev/core/create-machine-actor.js +91 -0
  117. package/dist/dev/core/create-machine-actor.js.map +1 -0
  118. package/dist/dev/core/create-machine-reactor.d.ts +8 -0
  119. package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
  120. package/dist/dev/core/create-machine-reactor.js +83 -0
  121. package/dist/dev/core/create-machine-reactor.js.map +1 -0
  122. package/dist/dev/core/create-transition-actor.d.ts +23 -0
  123. package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
  124. package/dist/dev/core/create-transition-actor.js +46 -0
  125. package/dist/dev/core/create-transition-actor.js.map +1 -0
  126. package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
  127. package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
  128. package/dist/dev/core/features/quality-switching.js +69 -0
  129. package/dist/dev/core/features/quality-switching.js.map +1 -0
  130. package/dist/dev/core/features/resolve-presentation.js +73 -0
  131. package/dist/dev/core/features/resolve-presentation.js.map +1 -0
  132. package/dist/dev/core/features/resolve-track.js +66 -0
  133. package/dist/dev/core/features/resolve-track.js.map +1 -0
  134. package/dist/dev/core/features/select-tracks.js +130 -0
  135. package/dist/dev/core/features/select-tracks.js.map +1 -0
  136. package/dist/dev/core/features/sync-preload-attribute.js +26 -0
  137. package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
  138. package/dist/dev/core/hls/parse-attributes.js +134 -0
  139. package/dist/dev/core/hls/parse-attributes.js.map +1 -0
  140. package/dist/dev/core/hls/parse-media-playlist.js +82 -0
  141. package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
  142. package/dist/dev/core/hls/parse-multivariant.js +214 -0
  143. package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
  144. package/dist/dev/core/hls/resolve-url.js +11 -0
  145. package/dist/dev/core/hls/resolve-url.js.map +1 -0
  146. package/dist/dev/core/machine.d.ts +21 -0
  147. package/dist/dev/core/machine.d.ts.map +1 -0
  148. package/dist/dev/core/machine.js +26 -0
  149. package/dist/dev/core/machine.js.map +1 -0
  150. package/dist/dev/core/signals/effect.d.ts +15 -0
  151. package/dist/dev/core/signals/effect.d.ts.map +1 -0
  152. package/dist/dev/core/signals/effect.js +41 -0
  153. package/dist/dev/core/signals/effect.js.map +1 -0
  154. package/dist/dev/core/signals/primitives.d.ts +10 -0
  155. package/dist/dev/core/signals/primitives.d.ts.map +1 -0
  156. package/dist/dev/core/signals/primitives.js +32 -0
  157. package/dist/dev/core/signals/primitives.js.map +1 -0
  158. package/dist/dev/core/task.js +191 -0
  159. package/dist/dev/core/task.js.map +1 -0
  160. package/dist/dev/core/types/index.d.ts +166 -0
  161. package/dist/dev/core/types/index.d.ts.map +1 -0
  162. package/dist/dev/core/types/index.js +15 -0
  163. package/dist/dev/core/types/index.js.map +1 -0
  164. package/dist/dev/core/utils/generate-id.js +21 -0
  165. package/dist/dev/core/utils/generate-id.js.map +1 -0
  166. package/dist/dev/core/utils/track-selection.d.ts +16 -0
  167. package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
  168. package/dist/dev/core/utils/track-selection.js +39 -0
  169. package/dist/dev/core/utils/track-selection.js.map +1 -0
  170. package/dist/dev/dom/features/end-of-stream.js +145 -0
  171. package/dist/dev/dom/features/end-of-stream.js.map +1 -0
  172. package/dist/dev/dom/features/load-segments.d.ts +72 -0
  173. package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
  174. package/dist/dev/dom/features/load-segments.js +192 -0
  175. package/dist/dev/dom/features/load-segments.js.map +1 -0
  176. package/dist/dev/dom/features/load-text-track-cues.d.ts +85 -0
  177. package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
  178. package/dist/dev/dom/features/load-text-track-cues.js +104 -0
  179. package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
  180. package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
  181. package/dist/dev/dom/features/segment-loader-actor.js +243 -0
  182. package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
  183. package/dist/dev/dom/features/setup-mediasource.js +48 -0
  184. package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
  185. package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
  186. package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
  187. package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
  188. package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
  189. package/dist/dev/dom/features/sync-text-tracks.js +95 -0
  190. package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
  191. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
  192. package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
  193. package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
  194. package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
  195. package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
  196. package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
  197. package/dist/dev/dom/features/text-tracks-actor.js +42 -0
  198. package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
  199. package/dist/dev/dom/features/track-current-time.d.ts +40 -0
  200. package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
  201. package/dist/dev/dom/features/track-current-time.js +49 -0
  202. package/dist/dev/dom/features/track-current-time.js.map +1 -0
  203. package/dist/dev/dom/features/track-playback-initiated.d.ts +46 -0
  204. package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
  205. package/dist/dev/dom/features/track-playback-initiated.js +66 -0
  206. package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
  207. package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
  208. package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
  209. package/dist/dev/dom/features/track-playback-rate.js +32 -0
  210. package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
  211. package/dist/dev/dom/features/update-duration.js +78 -0
  212. package/dist/dev/dom/features/update-duration.js.map +1 -0
  213. package/dist/dev/dom/media/append-segment.d.ts +24 -0
  214. package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
  215. package/dist/dev/dom/media/append-segment.js +60 -0
  216. package/dist/dev/dom/media/append-segment.js.map +1 -0
  217. package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
  218. package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
  219. package/dist/dev/dom/media/buffer-flusher.js +55 -0
  220. package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
  221. package/dist/dev/dom/media/mediasource-setup.js +146 -0
  222. package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
  223. package/dist/dev/dom/media/source-buffer-actor.d.ts +62 -0
  224. package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
  225. package/dist/dev/dom/media/source-buffer-actor.js +137 -0
  226. package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
  227. package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
  228. package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
  229. package/dist/dev/dom/network/fetch.js +54 -0
  230. package/dist/dev/dom/network/fetch.js.map +1 -0
  231. package/dist/dev/dom/playback-engine/adapter.d.ts +40 -0
  232. package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
  233. package/dist/dev/dom/playback-engine/adapter.js +100 -0
  234. package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
  235. package/dist/dev/dom/playback-engine/engine.d.ts +124 -0
  236. package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
  237. package/dist/dev/dom/playback-engine/engine.js +142 -0
  238. package/dist/dev/dom/playback-engine/engine.js.map +1 -0
  239. package/dist/dev/dom/playback-engine.d.ts +4 -0
  240. package/dist/dev/dom/playback-engine.js +4 -0
  241. package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
  242. package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
  243. package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
  244. package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
  245. package/dist/dev/dom.d.ts +10 -0
  246. package/dist/dev/dom.js +10 -0
  247. package/dist/dev/index.d.ts.map +1 -0
  248. package/dist/dev/index.js +14 -0
  249. package/dist/dev/index.js.map +1 -0
  250. package/package.json +26 -24
  251. package/dist/adapter-CMw-rvbk.js +0 -3512
  252. package/dist/adapter-CMw-rvbk.js.map +0 -1
  253. package/dist/adapter-l8iJN_GI.d.ts +0 -689
  254. package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
  255. package/dist/dom/index.d.ts +0 -243
  256. package/dist/dom/index.d.ts.map +0 -1
  257. package/dist/dom/index.js +0 -38
  258. package/dist/dom/index.js.map +0 -1
  259. package/dist/dom/playback-engine/index.d.ts +0 -2
  260. package/dist/dom/playback-engine/index.js +0 -4
  261. package/dist/index.d.ts.map +0 -1
  262. package/dist/index.js +0 -3
  263. package/dist/src-DRsS_H80.js.map +0 -1
  264. /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
@@ -0,0 +1,71 @@
1
+ //#region src/core/abr/ewma.ts
2
+ /**
3
+ * Exponentially Weighted Moving Average (EWMA)
4
+ *
5
+ * Pure functional implementation of EWMA calculations.
6
+ * Based on Shaka Player's EWMA algorithm.
7
+ */
8
+ /**
9
+ * Calculate alpha (decay factor) from half-life.
10
+ *
11
+ * Alpha determines how quickly old data "expires":
12
+ * - alpha close to 1 = slow decay (long memory)
13
+ * - alpha close to 0 = fast decay (short memory)
14
+ *
15
+ * @param halfLife - The quantity of prior samples (by weight) that make up
16
+ * half of the new estimate. Must be positive.
17
+ * @returns Alpha value between 0 and 1
18
+ *
19
+ * @example
20
+ * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life
21
+ */
22
+ function calculateAlpha(halfLife) {
23
+ return Math.exp(Math.log(.5) / halfLife);
24
+ }
25
+ /**
26
+ * Calculate exponentially weighted moving average.
27
+ *
28
+ * Updates an estimate by blending a new value with the previous estimate,
29
+ * weighted by the sample duration. Longer samples have more influence.
30
+ *
31
+ * @param prevEstimate - Previous EWMA estimate
32
+ * @param value - New sample value to incorporate
33
+ * @param weight - Sample weight (typically duration in seconds)
34
+ * @param halfLife - Half-life for decay (typically 2-5 seconds)
35
+ * @returns Updated EWMA estimate
36
+ *
37
+ * @example
38
+ * let estimate = 0;
39
+ * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample
40
+ * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample
41
+ */
42
+ function calculateEwma(prevEstimate, value, weight, halfLife) {
43
+ const adjAlpha = calculateAlpha(halfLife) ** weight;
44
+ return value * (1 - adjAlpha) + adjAlpha * prevEstimate;
45
+ }
46
+ /**
47
+ * Apply zero-factor correction to EWMA estimate.
48
+ *
49
+ * The zero-factor correction compensates for bias when starting from zero.
50
+ * Without this correction, early estimates would be artificially low.
51
+ *
52
+ * As totalWeight increases, the correction factor approaches 1, meaning
53
+ * the estimate becomes more reliable and needs less correction.
54
+ *
55
+ * @param estimate - Raw EWMA estimate (uncorrected)
56
+ * @param totalWeight - Accumulated weight from all samples
57
+ * @param halfLife - Half-life used in EWMA calculation
58
+ * @returns Corrected estimate, or 0 if totalWeight is 0
59
+ *
60
+ * @example
61
+ * const raw = calculateEwma(0, 1_000_000, 1, 2);
62
+ * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000
63
+ */
64
+ function applyZeroFactor(estimate, totalWeight, halfLife) {
65
+ if (totalWeight === 0) return 0;
66
+ return estimate / (1 - calculateAlpha(halfLife) ** totalWeight);
67
+ }
68
+ //#endregion
69
+ export { applyZeroFactor, calculateEwma };
70
+
71
+ //# sourceMappingURL=ewma.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ewma.js","names":[],"sources":["../../../../src/core/abr/ewma.ts"],"sourcesContent":["/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,UAA0B;AACvD,QAAO,KAAK,IAAI,KAAK,IAAI,GAAI,GAAG,SAAS;;;;;;;;;;;;;;;;;;;AAoB3C,SAAgB,cAAc,cAAsB,OAAe,QAAgB,UAA0B;CAE3G,MAAM,WADQ,eAAe,SAAS,IACZ;AAC1B,QAAO,SAAS,IAAI,YAAY,WAAW;;;;;;;;;;;;;;;;;;;;AAqB7C,SAAgB,gBAAgB,UAAkB,aAAqB,UAA0B;AAC/F,KAAI,gBAAgB,EAClB,QAAO;AAKT,QAAO,YADY,IADL,eAAe,SAAS,IACN"}
@@ -0,0 +1,53 @@
1
+ //#region src/core/abr/quality-selection.ts
2
+ /**
3
+ * Default quality selection configuration.
4
+ * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
5
+ */
6
+ const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
7
+ /**
8
+ * Select the best video track based on current bandwidth estimate.
9
+ *
10
+ * Selects the highest quality track where bandwidth is sufficient with safety margin:
11
+ * - currentBandwidth >= track.bandwidth / safetyMargin
12
+ * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
13
+ * - At same bandwidth, prefers higher resolution
14
+ *
15
+ * @param tracks - Available video tracks (can be unsorted)
16
+ * @param currentBandwidth - Current bandwidth estimate in bits per second
17
+ * @param config - Optional quality selection configuration
18
+ * @returns Selected track, or undefined if no tracks available
19
+ *
20
+ * @example
21
+ * const tracks = [
22
+ * { id: '360p', bandwidth: 500_000, ... },
23
+ * { id: '720p', bandwidth: 2_000_000, ... },
24
+ * { id: '1080p', bandwidth: 4_000_000, ... },
25
+ * ];
26
+ *
27
+ * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
28
+ * const selected = selectQuality(tracks, 2_500_000);
29
+ */
30
+ function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
31
+ if (tracks.length === 0) return;
32
+ const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
33
+ let chosen;
34
+ for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
35
+ if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
36
+ }
37
+ return chosen ?? sortedTracks[0];
38
+ }
39
+ /**
40
+ * Check if track A has higher resolution than track B.
41
+ * Compares by total pixel count (width × height).
42
+ *
43
+ * @param trackA - First track to compare
44
+ * @param trackB - Second track to compare
45
+ * @returns True if trackA has more pixels than trackB
46
+ */
47
+ function hasHigherResolution(trackA, trackB) {
48
+ return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
49
+ }
50
+ //#endregion
51
+ export { selectQuality };
52
+
53
+ //# sourceMappingURL=quality-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality-selection.js","names":[],"sources":["../../../../src/core/abr/quality-selection.ts"],"sourcesContent":["/**\n * Quality Selection Algorithm\n *\n * Selects optimal video track based on bandwidth estimate with safety margin.\n * Stateless selection - picks highest quality that fits bandwidth.\n *\n * Key concepts:\n * - **Safety margin** (0.85): Pick track where bandwidth >= track.bandwidth / 0.85\n * - This ensures 15% headroom to avoid buffering\n * - At same bandwidth, prefer higher resolution\n */\n\nimport type { PartiallyResolvedVideoTrack } from '../types';\n\n/**\n * Quality selection configuration.\n */\nexport interface QualityConfig {\n /**\n * Safety margin (0-1).\n * To select a track, need: currentBandwidth >= track.bandwidth / safetyMargin.\n * Default 0.85 means track must use ≤85% of available bandwidth (15% headroom).\n */\n safetyMargin: number;\n}\n\n/**\n * Default quality selection configuration.\n * Values match Shaka Player upgrade threshold (0.85 = 15% headroom).\n */\nexport const DEFAULT_QUALITY_CONFIG: QualityConfig = {\n safetyMargin: 0.85,\n};\n\n/**\n * Select the best video track based on current bandwidth estimate.\n *\n * Selects the highest quality track where bandwidth is sufficient with safety margin:\n * - currentBandwidth >= track.bandwidth / safetyMargin\n * - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)\n * - At same bandwidth, prefers higher resolution\n *\n * @param tracks - Available video tracks (can be unsorted)\n * @param currentBandwidth - Current bandwidth estimate in bits per second\n * @param config - Optional quality selection configuration\n * @returns Selected track, or undefined if no tracks available\n *\n * @example\n * const tracks = [\n * { id: '360p', bandwidth: 500_000, ... },\n * { id: '720p', bandwidth: 2_000_000, ... },\n * { id: '1080p', bandwidth: 4_000_000, ... },\n * ];\n *\n * // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)\n * const selected = selectQuality(tracks, 2_500_000);\n */\nexport function selectQuality(\n tracks: PartiallyResolvedVideoTrack[],\n currentBandwidth: number,\n config: QualityConfig = DEFAULT_QUALITY_CONFIG\n): PartiallyResolvedVideoTrack | undefined {\n if (tracks.length === 0) {\n return undefined;\n }\n\n // Sort tracks by bandwidth (lowest first)\n const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);\n\n // Start with no selection\n let chosen: PartiallyResolvedVideoTrack | undefined;\n\n for (const track of sortedTracks) {\n // Check if we have enough bandwidth for this track with safety margin\n // Required bandwidth = track.bandwidth / safetyMargin\n const requiredBandwidth = track.bandwidth / config.safetyMargin;\n\n if (currentBandwidth >= requiredBandwidth) {\n // We can support this track - prefer it if better than current choice\n if (\n !chosen ||\n track.bandwidth > chosen.bandwidth ||\n (track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen))\n ) {\n chosen = track;\n }\n }\n }\n\n // If no track fits with safety margin, fall back to lowest quality\n return chosen ?? sortedTracks[0];\n}\n\n/**\n * Check if track A has higher resolution than track B.\n * Compares by total pixel count (width × height).\n *\n * @param trackA - First track to compare\n * @param trackB - Second track to compare\n * @returns True if trackA has more pixels than trackB\n */\nfunction hasHigherResolution(trackA: PartiallyResolvedVideoTrack, trackB: PartiallyResolvedVideoTrack): boolean {\n const pixelsA = (trackA.width ?? 0) * (trackA.height ?? 0);\n const pixelsB = (trackB.width ?? 0) * (trackB.height ?? 0);\n return pixelsA > pixelsB;\n}\n"],"mappings":";;;;;AA8BA,MAAa,yBAAwC,EACnD,cAAc,KACf;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,SAAgB,cACd,QACA,kBACA,SAAwB,wBACiB;AACzC,KAAI,OAAO,WAAW,EACpB;CAIF,MAAM,eAAe,OAAO,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;CAG7E,IAAI;AAEJ,MAAK,MAAM,SAAS,aAKlB,KAAI,oBAFsB,MAAM,YAAY,OAAO;MAK/C,CAAC,UACD,MAAM,YAAY,OAAO,aACxB,MAAM,cAAc,OAAO,aAAa,oBAAoB,OAAO,OAAO,CAE3E,UAAS;;AAMf,QAAO,UAAU,aAAa;;;;;;;;;;AAWhC,SAAS,oBAAoB,QAAqC,QAA8C;AAG9G,SAFiB,OAAO,SAAS,MAAM,OAAO,UAAU,MACvC,OAAO,SAAS,MAAM,OAAO,UAAU"}
@@ -0,0 +1,27 @@
1
+ import { Machine, MachineSnapshot } from "./machine.js";
2
+
3
+ //#region src/core/actor.d.ts
4
+ /**
5
+ * Complete actor snapshot: finite state + non-finite context.
6
+ * Extends `MachineSnapshot` with context — the non-finite data managed by the actor.
7
+ */
8
+ interface ActorSnapshot<State extends string, Context extends object> extends MachineSnapshot<State> {
9
+ context: Context;
10
+ }
11
+ /** Generic actor interface: owns its snapshot as a reactive signal. */
12
+ interface SignalActor<State extends string, Context extends object> extends Machine<ActorSnapshot<State, Context>> {}
13
+ /**
14
+ * A message-driven actor with no reactive snapshot.
15
+ *
16
+ * Use for actors that coordinate async work but have no state that external
17
+ * consumers need to observe. Analogous to XState's `fromCallback`.
18
+ */
19
+ interface CallbackActor<Message extends {
20
+ type: string;
21
+ }> {
22
+ send(message: Message): void;
23
+ destroy(): void;
24
+ }
25
+ //#endregion
26
+ export { ActorSnapshot, CallbackActor, SignalActor };
27
+ //# sourceMappingURL=actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.d.ts","names":[],"sources":["../../../src/core/actor.ts"],"mappings":";;;;;;;UAeiB,aAAA,uDAAoE,eAAA,CAAgB,KAAA;EACnG,OAAA,EAAS,OAAA;AAAA;;UAIM,WAAA,uDACP,OAAA,CAAQ,aAAA,CAAc,KAAA,EAAO,OAAA;AADvC;;;;;;AAAA,UASiB,aAAA;EAAgC,IAAA;AAAA;EAC/C,IAAA,CAAK,OAAA,EAAS,OAAA;EACd,OAAA;AAAA"}
@@ -0,0 +1,46 @@
1
+ //#region src/core/buffer/back-buffer.ts
2
+ /**
3
+ * Default back buffer configuration.
4
+ */
5
+ const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
6
+ /**
7
+ * Calculate back buffer flush point.
8
+ *
9
+ * Determines where to flush old segments from the back buffer.
10
+ * Keeps a fixed number of segments behind the current playback position.
11
+ *
12
+ * Algorithm:
13
+ * 1. Find segments before currentTime
14
+ * 2. Count back N segments (keepSegments)
15
+ * 3. Return startTime of segment N+1 back (flush everything before this)
16
+ *
17
+ * @param segments - Available segments (should be sorted by startTime)
18
+ * @param currentTime - Current playback position in seconds
19
+ * @param config - Optional back buffer configuration
20
+ * @returns Time in seconds to flush up to (flush range: [0, flushEnd))
21
+ *
22
+ * @example
23
+ * const segments = [
24
+ * { startTime: 0, duration: 6, ... },
25
+ * { startTime: 6, duration: 6, ... },
26
+ * { startTime: 12, duration: 6, ... },
27
+ * { startTime: 18, duration: 6, ... },
28
+ * ];
29
+ *
30
+ * // Playing at 18s, keep 2 segments
31
+ * const flushEnd = calculateBackBufferFlushPoint(segments, 18);
32
+ * // Returns 6 (flush [0, 6), keep [6-18))
33
+ */
34
+ function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
35
+ if (segments.length === 0) return 0;
36
+ const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
37
+ if (segmentsBefore.length === 0) return 0;
38
+ const segmentsToFlush = segmentsBefore.length - config.keepSegments;
39
+ if (segmentsToFlush <= 0) return 0;
40
+ if (segmentsToFlush >= segmentsBefore.length) return currentTime;
41
+ return segmentsBefore[segmentsToFlush].startTime;
42
+ }
43
+ //#endregion
44
+ export { calculateBackBufferFlushPoint };
45
+
46
+ //# sourceMappingURL=back-buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-buffer.js","names":[],"sources":["../../../../src/core/buffer/back-buffer.ts"],"sourcesContent":["/**\n * Back Buffer Strategy (Simple)\n *\n * Calculates flush points for back buffer management.\n * V1 uses simple \"keep N segments\" strategy.\n */\n\nimport type { Segment } from '../types';\n\n/**\n * Back buffer configuration.\n */\nexport interface BackBufferConfig {\n /**\n * Number of segments to keep behind current playback position.\n * Default: 2 segments.\n */\n keepSegments: number;\n}\n\n/**\n * Default back buffer configuration.\n */\nexport const DEFAULT_BACK_BUFFER_CONFIG: BackBufferConfig = {\n keepSegments: 2,\n};\n\n/**\n * Calculate back buffer flush point.\n *\n * Determines where to flush old segments from the back buffer.\n * Keeps a fixed number of segments behind the current playback position.\n *\n * Algorithm:\n * 1. Find segments before currentTime\n * 2. Count back N segments (keepSegments)\n * 3. Return startTime of segment N+1 back (flush everything before this)\n *\n * @param segments - Available segments (should be sorted by startTime)\n * @param currentTime - Current playback position in seconds\n * @param config - Optional back buffer configuration\n * @returns Time in seconds to flush up to (flush range: [0, flushEnd))\n *\n * @example\n * const segments = [\n * { startTime: 0, duration: 6, ... },\n * { startTime: 6, duration: 6, ... },\n * { startTime: 12, duration: 6, ... },\n * { startTime: 18, duration: 6, ... },\n * ];\n *\n * // Playing at 18s, keep 2 segments\n * const flushEnd = calculateBackBufferFlushPoint(segments, 18);\n * // Returns 6 (flush [0, 6), keep [6-18))\n */\nexport function calculateBackBufferFlushPoint(\n segments: Segment[],\n currentTime: number,\n config: BackBufferConfig = DEFAULT_BACK_BUFFER_CONFIG\n): number {\n if (segments.length === 0) {\n return 0;\n }\n\n // Find all segments before current time (not including current segment)\n const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);\n\n // If no segments before current time, nothing to flush\n if (segmentsBefore.length === 0) {\n return 0;\n }\n\n // Calculate how many segments to flush\n // Keep last N segments, flush the rest\n const segmentsToFlush = segmentsBefore.length - config.keepSegments;\n\n // If we don't have enough segments to flush, keep everything\n if (segmentsToFlush <= 0) {\n return 0;\n }\n\n // If we want to flush all segments, return currentTime\n // (flush everything before current playback position)\n if (segmentsToFlush >= segmentsBefore.length) {\n return currentTime;\n }\n\n // Return the startTime of the first segment we want to keep\n // Everything before this will be flushed\n return segmentsBefore[segmentsToFlush]!.startTime;\n}\n"],"mappings":";;;;AAuBA,MAAa,6BAA+C,EAC1D,cAAc,GACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BD,SAAgB,8BACd,UACA,aACA,SAA2B,4BACnB;AACR,KAAI,SAAS,WAAW,EACtB,QAAO;CAIT,MAAM,iBAAiB,SAAS,QAAQ,QAAQ,IAAI,YAAY,YAAY;AAG5E,KAAI,eAAe,WAAW,EAC5B,QAAO;CAKT,MAAM,kBAAkB,eAAe,SAAS,OAAO;AAGvD,KAAI,mBAAmB,EACrB,QAAO;AAKT,KAAI,mBAAmB,eAAe,OACpC,QAAO;AAKT,QAAO,eAAe,iBAAkB"}
@@ -0,0 +1,73 @@
1
+ //#region src/core/buffer/forward-buffer.ts
2
+ /**
3
+ * Default forward buffer configuration.
4
+ */
5
+ const DEFAULT_FORWARD_BUFFER_CONFIG = { bufferDuration: 30 };
6
+ /**
7
+ * Get segments that need to be loaded for forward buffer.
8
+ *
9
+ * Determines which segments to load to maintain target buffer duration.
10
+ * Handles discontiguous buffering (gaps after seeks).
11
+ *
12
+ * Algorithm:
13
+ * 1. Calculate target time: currentTime + bufferDuration
14
+ * 2. Find all segments in range [currentTime, targetTime)
15
+ * 3. Filter out segments already buffered at that time position
16
+ * 4. Return segments to load (fills gaps + extends to target)
17
+ *
18
+ * @param segments - All available segments from playlist
19
+ * @param bufferedSegments - Segments already buffered (ordered by startTime)
20
+ * @param currentTime - Current playback position in seconds
21
+ * @param config - Optional forward buffer configuration
22
+ * @returns Array of segments to load (empty if buffer is sufficient)
23
+ *
24
+ * @example
25
+ * // After seek: buffered [0-12, 18-30], playing at 7s
26
+ * const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });
27
+ * // Returns [seg-12, seg-30] (fills gap, extends to target 31s)
28
+ */
29
+ /**
30
+ * Calculate the start time from which to flush forward buffer content.
31
+ *
32
+ * Content that starts at or beyond `currentTime + bufferDuration` is no
33
+ * longer needed for the current playback position and should be removed
34
+ * from the SourceBuffer. This prevents unbounded accumulation of scattered
35
+ * SourceBuffer content after seeks, which can cause QuotaExceededError on
36
+ * long-form content.
37
+ *
38
+ * Returns `Infinity` when nothing needs flushing (no buffered segments
39
+ * exist beyond the threshold).
40
+ *
41
+ * @param bufferedSegments - Segments currently tracked in the buffer model
42
+ * @param currentTime - Current playback position in seconds
43
+ * @param config - Optional forward buffer configuration
44
+ * @returns Start time to flush from (flush range: [flushStart, Infinity)),
45
+ * or Infinity if no flush is needed
46
+ *
47
+ * @example
48
+ * // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30
49
+ * const flushStart = calculateForwardFlushPoint(segments, 0);
50
+ * // Returns 30 — flush [30, Infinity), keep [0, 30)
51
+ */
52
+ function calculateForwardFlushPoint(bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
53
+ if (bufferedSegments.length === 0) return Infinity;
54
+ const threshold = currentTime + config.bufferDuration;
55
+ const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);
56
+ if (beyond.length === 0) return Infinity;
57
+ return Math.min(...beyond.map((seg) => seg.startTime));
58
+ }
59
+ function getSegmentsToLoad(segments, bufferedSegments, currentTime, config = DEFAULT_FORWARD_BUFFER_CONFIG) {
60
+ if (segments.length === 0) return [];
61
+ const targetTime = currentTime + config.bufferDuration;
62
+ const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));
63
+ return segments.filter((seg) => {
64
+ const segmentEnd = seg.startTime + seg.duration;
65
+ const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;
66
+ const isNotBuffered = !bufferedStartTimes.has(seg.startTime);
67
+ return isInRange && isNotBuffered;
68
+ });
69
+ }
70
+ //#endregion
71
+ export { DEFAULT_FORWARD_BUFFER_CONFIG, calculateForwardFlushPoint, getSegmentsToLoad };
72
+
73
+ //# sourceMappingURL=forward-buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forward-buffer.js","names":[],"sources":["../../../../src/core/buffer/forward-buffer.ts"],"sourcesContent":["/**\n * Forward Buffer Strategy (Simple)\n *\n * Determines which segments to load for forward buffer management.\n * V1 uses simple fixed-duration strategy (buffer N seconds ahead).\n */\n\nimport type { Segment } from '../types';\n\n/**\n * Forward buffer configuration.\n */\nexport interface ForwardBufferConfig {\n /**\n * Duration in seconds to buffer ahead of current playback position.\n * Default: 30 seconds.\n */\n bufferDuration: number;\n}\n\n/**\n * Default forward buffer configuration.\n */\nexport const DEFAULT_FORWARD_BUFFER_CONFIG: ForwardBufferConfig = {\n bufferDuration: 30,\n};\n\n/**\n * Get segments that need to be loaded for forward buffer.\n *\n * Determines which segments to load to maintain target buffer duration.\n * Handles discontiguous buffering (gaps after seeks).\n *\n * Algorithm:\n * 1. Calculate target time: currentTime + bufferDuration\n * 2. Find all segments in range [currentTime, targetTime)\n * 3. Filter out segments already buffered at that time position\n * 4. Return segments to load (fills gaps + extends to target)\n *\n * @param segments - All available segments from playlist\n * @param bufferedSegments - Segments already buffered (ordered by startTime)\n * @param currentTime - Current playback position in seconds\n * @param config - Optional forward buffer configuration\n * @returns Array of segments to load (empty if buffer is sufficient)\n *\n * @example\n * // After seek: buffered [0-12, 18-30], playing at 7s\n * const toLoad = getSegmentsToLoad(segments, buffered, 7, { bufferDuration: 24 });\n * // Returns [seg-12, seg-30] (fills gap, extends to target 31s)\n */\n/**\n * Calculate the start time from which to flush forward buffer content.\n *\n * Content that starts at or beyond `currentTime + bufferDuration` is no\n * longer needed for the current playback position and should be removed\n * from the SourceBuffer. This prevents unbounded accumulation of scattered\n * SourceBuffer content after seeks, which can cause QuotaExceededError on\n * long-form content.\n *\n * Returns `Infinity` when nothing needs flushing (no buffered segments\n * exist beyond the threshold).\n *\n * @param bufferedSegments - Segments currently tracked in the buffer model\n * @param currentTime - Current playback position in seconds\n * @param config - Optional forward buffer configuration\n * @returns Start time to flush from (flush range: [flushStart, Infinity)),\n * or Infinity if no flush is needed\n *\n * @example\n * // Playing at 0s, buffered [0,6,12,18,24,30,36], bufferDuration=30\n * const flushStart = calculateForwardFlushPoint(segments, 0);\n * // Returns 30 — flush [30, Infinity), keep [0, 30)\n */\nexport function calculateForwardFlushPoint(\n bufferedSegments: readonly Segment[],\n currentTime: number,\n config: ForwardBufferConfig = DEFAULT_FORWARD_BUFFER_CONFIG\n): number {\n if (bufferedSegments.length === 0) return Infinity;\n\n const threshold = currentTime + config.bufferDuration;\n\n // Find segments that start at or beyond the threshold\n const beyond = bufferedSegments.filter((seg) => seg.startTime >= threshold);\n\n if (beyond.length === 0) return Infinity;\n\n // Flush from the earliest such segment onward\n return Math.min(...beyond.map((seg) => seg.startTime));\n}\n\nexport function getSegmentsToLoad(\n segments: readonly Segment[],\n bufferedSegments: readonly Pick<Segment, 'startTime' | 'duration'>[],\n currentTime: number,\n config: ForwardBufferConfig = DEFAULT_FORWARD_BUFFER_CONFIG\n): Segment[] {\n if (segments.length === 0) {\n return [];\n }\n\n // Calculate target buffer end time\n const targetTime = currentTime + config.bufferDuration;\n\n // Create set of buffered segment start times for fast lookup\n // V1 simple: if ANY segment is buffered at a given time, don't load for that time\n // V2 (future): would compare by startTime + bitrate/track for quality switching\n const bufferedStartTimes = new Set(bufferedSegments.map((seg) => seg.startTime));\n\n // Find segments to load:\n // - Overlaps buffer window [currentTime, targetTime)\n // - Not already buffered at that time position\n const toLoad = segments.filter((seg) => {\n // Segment must overlap the buffer window\n const segmentEnd = seg.startTime + seg.duration;\n const isInRange = seg.startTime < targetTime && segmentEnd > currentTime;\n\n // Must not have a segment buffered at this time position\n const isNotBuffered = !bufferedStartTimes.has(seg.startTime);\n\n return isInRange && isNotBuffered;\n });\n\n return toLoad;\n}\n"],"mappings":";;;;AAuBA,MAAa,gCAAqD,EAChE,gBAAgB,IACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDD,SAAgB,2BACd,kBACA,aACA,SAA8B,+BACtB;AACR,KAAI,iBAAiB,WAAW,EAAG,QAAO;CAE1C,MAAM,YAAY,cAAc,OAAO;CAGvC,MAAM,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,aAAa,UAAU;AAE3E,KAAI,OAAO,WAAW,EAAG,QAAO;AAGhC,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,QAAQ,IAAI,UAAU,CAAC;;AAGxD,SAAgB,kBACd,UACA,kBACA,aACA,SAA8B,+BACnB;AACX,KAAI,SAAS,WAAW,EACtB,QAAO,EAAE;CAIX,MAAM,aAAa,cAAc,OAAO;CAKxC,MAAM,qBAAqB,IAAI,IAAI,iBAAiB,KAAK,QAAQ,IAAI,UAAU,CAAC;AAgBhF,QAXe,SAAS,QAAQ,QAAQ;EAEtC,MAAM,aAAa,IAAI,YAAY,IAAI;EACvC,MAAM,YAAY,IAAI,YAAY,cAAc,aAAa;EAG7D,MAAM,gBAAgB,CAAC,mBAAmB,IAAI,IAAI,UAAU;AAE5D,SAAO,aAAa;GACpB"}
@@ -0,0 +1,12 @@
1
+ import { SignalActor } from "./actor.js";
2
+
3
+ //#region src/core/create-machine-actor.d.ts
4
+ /** Live actor instance returned by `createMachineActor`. */
5
+ interface MessageActor<State extends string, Context extends object, Message extends {
6
+ type: string;
7
+ }> extends SignalActor<State, Context> {
8
+ send(message: Message): void;
9
+ }
10
+ //#endregion
11
+ export { MessageActor };
12
+ //# sourceMappingURL=create-machine-actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-machine-actor.d.ts","names":[],"sources":["../../../src/core/create-machine-actor.ts"],"mappings":";;;;UA2GiB,YAAA;EAA6E,IAAA;AAAA,WACpF,WAAA,CAAY,KAAA,EAAO,OAAA;EAC3B,IAAA,CAAK,OAAA,EAAS,OAAA;AAAA"}
@@ -0,0 +1,91 @@
1
+ import { untrack, update } from "./signals/primitives.js";
2
+ import { createMachineCore } from "./machine.js";
3
+ //#region src/core/create-machine-actor.ts
4
+ /**
5
+ * Creates a message-driven actor from a declarative definition.
6
+ *
7
+ * The actor owns a reactive snapshot signal (state + context), an optional
8
+ * runner, and dispatches incoming messages to per-state handlers. `'destroyed'`
9
+ * is always the implicit terminal state — `destroy()` transitions there
10
+ * unconditionally and all subsequent `send()` calls are no-ops.
11
+ *
12
+ * When a state declares `onSettled`, the framework calls `runner.whenSettled()`
13
+ * after the handler returns. The runner owns the generation-token logic — if
14
+ * new tasks are scheduled before the current batch settles, the callback is
15
+ * automatically superseded.
16
+ *
17
+ * @example
18
+ * const actor = createMachineActor({
19
+ * runner: () => new SerialRunner(),
20
+ * initial: 'idle',
21
+ * context: {},
22
+ * states: {
23
+ * idle: {
24
+ * on: {
25
+ * load: (msg, { transition, runner }) => {
26
+ * segments.forEach(s => runner.schedule(new Task(...)));
27
+ * transition('loading');
28
+ * }
29
+ * }
30
+ * },
31
+ * loading: {
32
+ * onSettled: 'idle',
33
+ * on: {
34
+ * load: (msg, { runner }) => {
35
+ * runner.abortAll();
36
+ * segments.forEach(s => runner.schedule(new Task(...)));
37
+ * }
38
+ * }
39
+ * }
40
+ * }
41
+ * });
42
+ */
43
+ function createMachineActor(def) {
44
+ const runner = def.runner?.();
45
+ const { snapshotSignal, getState, transition } = createMachineCore({
46
+ value: def.initial,
47
+ context: def.context
48
+ });
49
+ const getContext = () => untrack(() => snapshotSignal.get().context);
50
+ const setContext = (context) => {
51
+ update(snapshotSignal, { context });
52
+ };
53
+ return {
54
+ get snapshot() {
55
+ return snapshotSignal;
56
+ },
57
+ send(message) {
58
+ const state = getState();
59
+ if (state === "destroyed") return;
60
+ const handler = def.states[state]?.on?.[message.type];
61
+ if (!handler) return;
62
+ handler(message, {
63
+ context: getContext(),
64
+ getContext,
65
+ transition: (to) => transition(to),
66
+ setContext,
67
+ ...runner ? { runner } : {}
68
+ });
69
+ const newState = getState();
70
+ if (newState !== "destroyed") {
71
+ const newStateDef = def.states[newState];
72
+ if (newStateDef?.onSettled && runner) {
73
+ const targetState = newStateDef.onSettled;
74
+ runner.whenSettled(() => {
75
+ if (getState() !== newState) return;
76
+ transition(targetState);
77
+ });
78
+ }
79
+ }
80
+ },
81
+ destroy() {
82
+ if (getState() === "destroyed") return;
83
+ runner?.destroy();
84
+ transition("destroyed");
85
+ }
86
+ };
87
+ }
88
+ //#endregion
89
+ export { createMachineActor };
90
+
91
+ //# sourceMappingURL=create-machine-actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-machine-actor.js","names":[],"sources":["../../../src/core/create-machine-actor.ts"],"sourcesContent":["import type { ActorSnapshot, SignalActor } from './actor';\nimport { createMachineCore } from './machine';\nimport { untrack, update } from './signals/primitives';\nimport type { TaskLike } from './task';\n\n// =============================================================================\n// Runner interfaces\n// =============================================================================\n\n/**\n * Minimal interface for any runner that can be used with createMachineActor.\n */\nexport interface RunnerLike {\n schedule<Value = void, Err = unknown>(task: TaskLike<Value, Err>): Promise<Value>;\n abortAll(): void;\n destroy(): void;\n whenSettled(callback: () => void): void;\n}\n\n// =============================================================================\n// Definition types\n// =============================================================================\n\n/**\n * Context passed to message handlers.\n * `runner` is present and typed as the exact runner instance only when the\n * definition includes a runner factory.\n */\nexport type HandlerContext<\n UserState extends string,\n Context extends object,\n RunnerFactory extends (() => RunnerLike) | undefined,\n> = {\n transition: (to: UserState) => void;\n /** Context snapshot captured at dispatch time. Stale after any `setContext` call. */\n context: Context;\n /**\n * Live untracked read of the current context. Use in async task closures that\n * execute after the handler returns — e.g. `getCtx: getContext` passed to tasks\n * scheduled on the runner, so each task reads the context committed by the\n * previous task rather than the stale snapshot from dispatch time.\n */\n getContext: () => Context;\n setContext: (next: Context) => void;\n} & (RunnerFactory extends () => infer R ? { runner: R } : object);\n\n/**\n * Definition for a single user-defined state.\n */\nexport type ActorStateDefinition<\n UserState extends string,\n Context extends object,\n Message extends { type: string },\n RunnerFactory extends (() => RunnerLike) | undefined,\n> = {\n /**\n * When the actor's runner settles while in this state, automatically\n * transition to this state. The framework owns the generation-token logic —\n * re-registering after each `runner.schedule()` call so that\n * `abortAll()` + reschedule correctly supersedes stale callbacks.\n */\n onSettled?: UserState;\n /** Message handlers active in this state. Messages with no handler are silently dropped. */\n on?: {\n [M in Message as M['type']]?: (\n message: Extract<Message, { type: M['type'] }>,\n ctx: HandlerContext<UserState, Context, RunnerFactory>\n ) => void;\n };\n};\n\n/**\n * Full actor definition passed to `createMachineActor`.\n *\n * `UserState` is the set of domain-meaningful states. `'destroyed'` is always\n * added by the framework as the implicit terminal state — do not include it here.\n */\nexport type ActorDefinition<\n UserState extends string,\n Context extends object,\n Message extends { type: string },\n RunnerFactory extends (() => RunnerLike) | undefined = undefined,\n> = {\n /**\n * Runner factory — called once at `createMachineActor()` time.\n * The runner lives for the full actor lifetime and is destroyed with it.\n *\n * @example\n * runner: () => new SerialRunner()\n */\n runner?: RunnerFactory;\n /** Initial state. */\n initial: UserState;\n /** Initial context. */\n context: Context;\n /**\n * Per-state definitions. States with no definition silently drop all messages.\n * All user-defined states must appear as keys in the `UserState` union.\n */\n states: Partial<Record<UserState, ActorStateDefinition<UserState, Context, Message, RunnerFactory>>>;\n};\n\n// =============================================================================\n// Live actor interface\n// =============================================================================\n\n/** Live actor instance returned by `createMachineActor`. */\nexport interface MessageActor<State extends string, Context extends object, Message extends { type: string }>\n extends SignalActor<State, Context> {\n send(message: Message): void;\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a message-driven actor from a declarative definition.\n *\n * The actor owns a reactive snapshot signal (state + context), an optional\n * runner, and dispatches incoming messages to per-state handlers. `'destroyed'`\n * is always the implicit terminal state — `destroy()` transitions there\n * unconditionally and all subsequent `send()` calls are no-ops.\n *\n * When a state declares `onSettled`, the framework calls `runner.whenSettled()`\n * after the handler returns. The runner owns the generation-token logic — if\n * new tasks are scheduled before the current batch settles, the callback is\n * automatically superseded.\n *\n * @example\n * const actor = createMachineActor({\n * runner: () => new SerialRunner(),\n * initial: 'idle',\n * context: {},\n * states: {\n * idle: {\n * on: {\n * load: (msg, { transition, runner }) => {\n * segments.forEach(s => runner.schedule(new Task(...)));\n * transition('loading');\n * }\n * }\n * },\n * loading: {\n * onSettled: 'idle',\n * on: {\n * load: (msg, { runner }) => {\n * runner.abortAll();\n * segments.forEach(s => runner.schedule(new Task(...)));\n * }\n * }\n * }\n * }\n * });\n */\nexport function createMachineActor<\n UserState extends string,\n Context extends object,\n Message extends { type: string },\n RunnerFactory extends (() => RunnerLike) | undefined = undefined,\n>(\n def: ActorDefinition<UserState, Context, Message, RunnerFactory>\n): MessageActor<UserState | 'destroyed', Context, Message> {\n type FullState = UserState | 'destroyed';\n\n const runner = def.runner?.() as RunnerLike | undefined;\n const { snapshotSignal, getState, transition } = createMachineCore<FullState, ActorSnapshot<FullState, Context>>({\n value: def.initial as FullState,\n context: def.context,\n });\n\n const getContext = (): Context => untrack(() => snapshotSignal.get().context);\n\n const setContext = (context: Context): void => {\n update(snapshotSignal, { context });\n };\n\n return {\n get snapshot() {\n return snapshotSignal;\n },\n\n send(message: Message): void {\n const state = getState();\n if (state === 'destroyed') return;\n const stateDef = def.states[state as UserState];\n const handler = stateDef?.on?.[message.type as keyof typeof stateDef.on] as\n | ((msg: Message, ctx: HandlerContext<UserState, Context, RunnerFactory>) => void)\n | undefined;\n if (!handler) return;\n handler(message, {\n context: getContext(),\n getContext,\n transition: (to: UserState) => transition(to as FullState),\n setContext,\n ...(runner ? { runner } : {}),\n } as HandlerContext<UserState, Context, RunnerFactory>);\n // Register onSettled after the handler so we read the post-transition state.\n const newState = getState();\n if (newState !== 'destroyed') {\n const newStateDef = def.states[newState as UserState];\n if (newStateDef?.onSettled && runner) {\n const targetState = newStateDef.onSettled as FullState;\n runner.whenSettled(() => {\n if (getState() !== newState) return;\n transition(targetState);\n });\n }\n }\n },\n\n destroy(): void {\n if (getState() === 'destroyed') return;\n runner?.destroy();\n transition('destroyed');\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2JA,SAAgB,mBAMd,KACyD;CAGzD,MAAM,SAAS,IAAI,UAAU;CAC7B,MAAM,EAAE,gBAAgB,UAAU,eAAe,kBAAgE;EAC/G,OAAO,IAAI;EACX,SAAS,IAAI;EACd,CAAC;CAEF,MAAM,mBAA4B,cAAc,eAAe,KAAK,CAAC,QAAQ;CAE7E,MAAM,cAAc,YAA2B;AAC7C,SAAO,gBAAgB,EAAE,SAAS,CAAC;;AAGrC,QAAO;EACL,IAAI,WAAW;AACb,UAAO;;EAGT,KAAK,SAAwB;GAC3B,MAAM,QAAQ,UAAU;AACxB,OAAI,UAAU,YAAa;GAE3B,MAAM,UADW,IAAI,OAAO,QACF,KAAK,QAAQ;AAGvC,OAAI,CAAC,QAAS;AACd,WAAQ,SAAS;IACf,SAAS,YAAY;IACrB;IACA,aAAa,OAAkB,WAAW,GAAgB;IAC1D;IACA,GAAI,SAAS,EAAE,QAAQ,GAAG,EAAE;IAC7B,CAAsD;GAEvD,MAAM,WAAW,UAAU;AAC3B,OAAI,aAAa,aAAa;IAC5B,MAAM,cAAc,IAAI,OAAO;AAC/B,QAAI,aAAa,aAAa,QAAQ;KACpC,MAAM,cAAc,YAAY;AAChC,YAAO,kBAAkB;AACvB,UAAI,UAAU,KAAK,SAAU;AAC7B,iBAAW,YAAY;OACvB;;;;EAKR,UAAgB;AACd,OAAI,UAAU,KAAK,YAAa;AAChC,WAAQ,SAAS;AACjB,cAAW,YAAY;;EAE1B"}
@@ -0,0 +1,8 @@
1
+ import { Machine, MachineSnapshot } from "./machine.js";
2
+
3
+ //#region src/core/create-machine-reactor.d.ts
4
+ /** Live reactor instance returned by `createMachineReactor`. */
5
+ type Reactor<State extends string> = Machine<MachineSnapshot<State>>;
6
+ //#endregion
7
+ export { Reactor };
8
+ //# sourceMappingURL=create-machine-reactor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-machine-reactor.d.ts","names":[],"sources":["../../../src/core/create-machine-reactor.ts"],"mappings":";;;;KAyEY,OAAA,yBAAgC,OAAA,CAAQ,eAAA,CAAgB,KAAA"}
@@ -0,0 +1,83 @@
1
+ import { effect } from "./signals/effect.js";
2
+ import { untrack } from "./signals/primitives.js";
3
+ import { createMachineCore } from "./machine.js";
4
+ //#region src/core/create-machine-reactor.ts
5
+ const toArray = (x) => x === void 0 ? [] : Array.isArray(x) ? x : [x];
6
+ /**
7
+ * Creates a reactive Reactor from a declarative definition.
8
+ *
9
+ * A Reactor is driven by subscriptions to external signals rather than
10
+ * imperative messages. Each state holds an array of effect functions —
11
+ * every element becomes one independent `effect()` call gated on that state,
12
+ * with its own dependency tracking and cleanup lifecycle.
13
+ *
14
+ * `'destroying'` and `'destroyed'` are always implicit terminal states.
15
+ * `destroy()` transitions through both in sequence: `'destroying'` first (for
16
+ * potential async teardown in a future extension), then immediately `'destroyed'`
17
+ * for the synchronous base case. Active effect cleanups fire via disposal.
18
+ *
19
+ * @example
20
+ * const reactor = createMachineReactor({
21
+ * initial: 'waiting',
22
+ * monitor: () => srcSignal.get() ? 'active' : 'waiting',
23
+ * states: {
24
+ * active: {
25
+ * // entry: runs once on state entry; fn body is automatically untracked.
26
+ * entry: () => listen(el, 'play', handler),
27
+ * // effects: re-runs whenever tracked signals change.
28
+ * effects: () => { currentTimeSignal.get(); return cleanup; },
29
+ * },
30
+ * waiting: {},
31
+ * }
32
+ * });
33
+ */
34
+ function createMachineReactor(def) {
35
+ const { snapshotSignal, getState, transition } = createMachineCore({ value: def.initial });
36
+ const effectDisposals = [];
37
+ const wrapResult = (result) => {
38
+ if (!result) return void 0;
39
+ if (typeof result === "function") return result;
40
+ return () => result.abort();
41
+ };
42
+ const untracked = (baseCall) => () => untrack(baseCall);
43
+ const isTerminal = (snapshot) => snapshot.value === "destroying" || snapshot.value === "destroyed";
44
+ const descriptors = [...toArray(def.monitor).map((fn) => ({
45
+ fn: () => {
46
+ const target = fn();
47
+ if (target !== getState()) transition(target);
48
+ },
49
+ shouldSkip: isTerminal
50
+ })), ...Object.entries(def.states).flatMap(([state, stateDef]) => {
51
+ const isNotState = (snapshot) => snapshot.value !== state;
52
+ return [...toArray(stateDef.entry).map((fn) => ({
53
+ fn,
54
+ shouldSkip: isNotState,
55
+ toFnCall: untracked
56
+ })), ...toArray(stateDef.effects).map((fn) => ({
57
+ fn,
58
+ shouldSkip: isNotState
59
+ }))];
60
+ })];
61
+ const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }) => effect(() => {
62
+ if (shouldSkip(snapshotSignal.get())) return;
63
+ const baseCall = () => fn();
64
+ return wrapResult(toFnCall(baseCall)());
65
+ });
66
+ effectDisposals.push(...descriptors.map(toEffect));
67
+ return {
68
+ get snapshot() {
69
+ return snapshotSignal;
70
+ },
71
+ destroy() {
72
+ const state = getState();
73
+ if (state === "destroying" || state === "destroyed") return;
74
+ transition("destroying");
75
+ transition("destroyed");
76
+ for (const dispose of effectDisposals) dispose();
77
+ }
78
+ };
79
+ }
80
+ //#endregion
81
+ export { createMachineReactor };
82
+
83
+ //# sourceMappingURL=create-machine-reactor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-machine-reactor.js","names":[],"sources":["../../../src/core/create-machine-reactor.ts"],"sourcesContent":["import type { Machine, MachineSnapshot } from './machine';\nimport { createMachineCore } from './machine';\nimport { effect } from './signals/effect';\nimport { untrack } from './signals/primitives';\n\n// =============================================================================\n// Definition types\n// =============================================================================\n\n/**\n * A reactive state-deriving function used in the `monitor` field.\n *\n * Returns the target state the reactor should be in. Any signals read inside\n * the fn body create reactive dependencies — the framework re-evaluates it when\n * those signals change and automatically calls `transition()` when the returned\n * state differs from the current one.\n */\nexport type ReactorDeriveFn<State extends string> = () => State;\n\n/**\n * An effect function used in reactor `entry` and `effects` blocks.\n *\n * May return a cleanup function that runs before each re-evaluation and on\n * state exit (including destroy).\n */\nexport type ReactorEffectFn = () => (() => void) | { abort(): void } | void;\n\n/**\n * Per-state effect grouping for a single reactor state.\n *\n * - `entry` effects run once on state entry. The fn body is automatically\n * untracked — no `untrack()` calls are needed inside. Use this for\n * one-time setup: reading current values, attaching event listeners, etc.\n * - `effects` run on state entry and re-run whenever a signal read inside\n * the fn body changes. Use `untrack()` for reads you do not want to track.\n * Use this for work that must stay in sync with reactive state.\n *\n * Both are optional; pass `{}` for states with no effects.\n */\nexport type ReactorStateDefinition = {\n entry?: ReactorEffectFn | ReactorEffectFn[];\n effects?: ReactorEffectFn | ReactorEffectFn[];\n};\n\n/**\n * Full reactor definition passed to `createMachineReactor`.\n *\n * `State` is the set of domain-meaningful states. `'destroying'` and\n * `'destroyed'` are always added by the framework as implicit terminal states —\n * do not include them here.\n */\nexport type ReactorDefinition<State extends string> = {\n /** Initial state. */\n initial: State;\n /**\n * Reactive state derivation. Registered before per-state effects — the\n * ordering guarantee ensures transitions fired here take effect before\n * per-state effects re-evaluate in the same flush.\n */\n monitor?: ReactorDeriveFn<State> | ReactorDeriveFn<State>[];\n /**\n * Per-state effect groupings. Every valid state must be declared — pass `{}`\n * for states with no effects. `entry` and `effects` each become independent\n * `effect()` calls gated on that state, with their own cleanup lifecycles.\n */\n states: Record<State, ReactorStateDefinition>;\n};\n\n// =============================================================================\n// Live reactor interface\n// =============================================================================\n\n/** Live reactor instance returned by `createMachineReactor`. */\nexport type Reactor<State extends string> = Machine<MachineSnapshot<State>>;\n\n// =============================================================================\n// Implementation helpers\n// =============================================================================\n\nconst toArray = <T>(x: T | T[] | undefined): T[] => (x === undefined ? [] : Array.isArray(x) ? x : [x]);\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a reactive Reactor from a declarative definition.\n *\n * A Reactor is driven by subscriptions to external signals rather than\n * imperative messages. Each state holds an array of effect functions —\n * every element becomes one independent `effect()` call gated on that state,\n * with its own dependency tracking and cleanup lifecycle.\n *\n * `'destroying'` and `'destroyed'` are always implicit terminal states.\n * `destroy()` transitions through both in sequence: `'destroying'` first (for\n * potential async teardown in a future extension), then immediately `'destroyed'`\n * for the synchronous base case. Active effect cleanups fire via disposal.\n *\n * @example\n * const reactor = createMachineReactor({\n * initial: 'waiting',\n * monitor: () => srcSignal.get() ? 'active' : 'waiting',\n * states: {\n * active: {\n * // entry: runs once on state entry; fn body is automatically untracked.\n * entry: () => listen(el, 'play', handler),\n * // effects: re-runs whenever tracked signals change.\n * effects: () => { currentTimeSignal.get(); return cleanup; },\n * },\n * waiting: {},\n * }\n * });\n */\nexport function createMachineReactor<State extends string>(\n def: ReactorDefinition<State>\n): Reactor<State | 'destroying' | 'destroyed'> {\n type FullState = State | 'destroying' | 'destroyed';\n\n const { snapshotSignal, getState, transition } = createMachineCore<FullState, MachineSnapshot<FullState>>({\n value: def.initial as FullState,\n });\n\n const effectDisposals: Array<() => void> = [];\n\n const wrapResult = (result: ReturnType<ReactorEffectFn>) => {\n if (!result) return undefined;\n if (typeof result === 'function') return result;\n return () => result.abort();\n };\n\n type EffectCall = () => ReturnType<ReactorEffectFn>;\n\n type EffectDescriptor = {\n fn: ReactorEffectFn;\n shouldSkip: (snapshot: { value: FullState }) => boolean;\n toFnCall?: (baseCall: EffectCall) => EffectCall;\n };\n\n const untracked: EffectDescriptor['toFnCall'] = (baseCall) => () => untrack(baseCall);\n\n const isTerminal = (snapshot: { value: FullState }) =>\n snapshot.value === 'destroying' || snapshot.value === 'destroyed';\n\n // `monitor` descriptors are built first — the ordering guarantee ensures\n // transitions they trigger take effect before per-state effects re-evaluate\n // in the same flush. See the comment on effect registration order in the\n // previous implementation for full details.\n const descriptors: EffectDescriptor[] = [\n ...toArray(def.monitor).map((fn) => ({\n fn: () => {\n const target = fn();\n if (target !== (getState() as State)) transition(target as FullState);\n },\n shouldSkip: isTerminal,\n })),\n ...(Object.entries(def.states) as Array<[State, ReactorStateDefinition]>).flatMap(([state, stateDef]) => {\n const isNotState = (snapshot: { value: FullState }) => snapshot.value !== state;\n return [\n ...toArray(stateDef.entry).map((fn) => ({ fn, shouldSkip: isNotState, toFnCall: untracked })),\n ...toArray(stateDef.effects).map((fn) => ({ fn, shouldSkip: isNotState })),\n ];\n }),\n ];\n\n const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }: EffectDescriptor) =>\n effect(() => {\n const snapshot = snapshotSignal.get();\n if (shouldSkip(snapshot)) return;\n const baseCall = () => fn();\n return wrapResult(toFnCall(baseCall)());\n });\n\n effectDisposals.push(...descriptors.map(toEffect));\n\n return {\n get snapshot() {\n return snapshotSignal;\n },\n\n destroy(): void {\n const state = getState();\n if (state === 'destroying' || state === 'destroyed') return;\n // Two-step teardown: transition through 'destroying' first to leave room\n // for async teardown in a future extension, then immediately 'destroyed'\n // for the synchronous base case. Active effect cleanups fire via disposal.\n transition('destroying');\n transition('destroyed');\n for (const dispose of effectDisposals) dispose();\n },\n };\n}\n"],"mappings":";;;;AA+EA,MAAM,WAAc,MAAiC,MAAM,KAAA,IAAY,EAAE,GAAG,MAAM,QAAQ,EAAE,GAAG,IAAI,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCtG,SAAgB,qBACd,KAC6C;CAG7C,MAAM,EAAE,gBAAgB,UAAU,eAAe,kBAAyD,EACxG,OAAO,IAAI,SACZ,CAAC;CAEF,MAAM,kBAAqC,EAAE;CAE7C,MAAM,cAAc,WAAwC;AAC1D,MAAI,CAAC,OAAQ,QAAO,KAAA;AACpB,MAAI,OAAO,WAAW,WAAY,QAAO;AACzC,eAAa,OAAO,OAAO;;CAW7B,MAAM,aAA2C,mBAAmB,QAAQ,SAAS;CAErF,MAAM,cAAc,aAClB,SAAS,UAAU,gBAAgB,SAAS,UAAU;CAMxD,MAAM,cAAkC,CACtC,GAAG,QAAQ,IAAI,QAAQ,CAAC,KAAK,QAAQ;EACnC,UAAU;GACR,MAAM,SAAS,IAAI;AACnB,OAAI,WAAY,UAAU,CAAY,YAAW,OAAoB;;EAEvE,YAAY;EACb,EAAE,EACH,GAAI,OAAO,QAAQ,IAAI,OAAO,CAA4C,SAAS,CAAC,OAAO,cAAc;EACvG,MAAM,cAAc,aAAmC,SAAS,UAAU;AAC1E,SAAO,CACL,GAAG,QAAQ,SAAS,MAAM,CAAC,KAAK,QAAQ;GAAE;GAAI,YAAY;GAAY,UAAU;GAAW,EAAE,EAC7F,GAAG,QAAQ,SAAS,QAAQ,CAAC,KAAK,QAAQ;GAAE;GAAI,YAAY;GAAY,EAAE,CAC3E;GACD,CACH;CAED,MAAM,YAAY,EAAE,IAAI,YAAY,YAAY,aAAa,eAC3D,aAAa;AAEX,MAAI,WADa,eAAe,KAAK,CACb,CAAE;EAC1B,MAAM,iBAAiB,IAAI;AAC3B,SAAO,WAAW,SAAS,SAAS,EAAE,CAAC;GACvC;AAEJ,iBAAgB,KAAK,GAAG,YAAY,IAAI,SAAS,CAAC;AAElD,QAAO;EACL,IAAI,WAAW;AACb,UAAO;;EAGT,UAAgB;GACd,MAAM,QAAQ,UAAU;AACxB,OAAI,UAAU,gBAAgB,UAAU,YAAa;AAIrD,cAAW,aAAa;AACxB,cAAW,YAAY;AACvB,QAAK,MAAM,WAAW,gBAAiB,UAAS;;EAEnD"}
@@ -0,0 +1,23 @@
1
+ import { Machine } from "./machine.js";
2
+ import { ActorSnapshot } from "./actor.js";
3
+
4
+ //#region src/core/create-transition-actor.d.ts
5
+ /**
6
+ * A reducer-shaped actor: `(context, message) => context`.
7
+ *
8
+ * No finite states — the snapshot carries `value: 'active' | 'destroyed'`
9
+ * as a universal lifecycle marker rather than domain state. The interesting
10
+ * state is entirely in the context, which is reactive via `snapshot`.
11
+ *
12
+ * Use this when the actor has context that needs to be reactive but no
13
+ * meaningful state machine (e.g., a message-driven model with DOM side
14
+ * effects). For actors that need per-state behavior, use `createMachineActor`.
15
+ */
16
+ interface TransitionActor<Context extends object, Message extends {
17
+ type: string;
18
+ }> extends Machine<ActorSnapshot<'active' | 'destroyed', Context>> {
19
+ send(message: Message): void;
20
+ }
21
+ //#endregion
22
+ export { TransitionActor };
23
+ //# sourceMappingURL=create-transition-actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-transition-actor.d.ts","names":[],"sources":["../../../src/core/create-transition-actor.ts"],"mappings":";;;;;;AAoBA;;;;;;;;;UAAiB,eAAA;EAA0D,IAAA;AAAA,WACjE,OAAA,CAAQ,aAAA,yBAAsC,OAAA;EACtD,IAAA,CAAK,OAAA,EAAS,OAAA;AAAA"}
@@ -0,0 +1,46 @@
1
+ import { untrack, update } from "./signals/primitives.js";
2
+ import { createMachineCore } from "./machine.js";
3
+ //#region src/core/create-transition-actor.ts
4
+ /**
5
+ * Creates a reducer-shaped actor from an initial context and a reducer function.
6
+ *
7
+ * The reducer receives the current context and a message and returns the next
8
+ * context. Returning the same reference (by identity) skips the signal update —
9
+ * so early-returning `context` unchanged is both the no-op and the optimization.
10
+ *
11
+ * Side effects (e.g. DOM mutations) may be performed inside the reducer.
12
+ * They run synchronously before the signal is updated.
13
+ *
14
+ * @example
15
+ * const actor = createTransitionActor(
16
+ * { count: 0 },
17
+ * (context, message: { type: 'increment' }) => ({ count: context.count + 1 })
18
+ * );
19
+ */
20
+ function createTransitionActor(initialContext, reducer) {
21
+ const { snapshotSignal, getState, transition } = createMachineCore({
22
+ value: "active",
23
+ context: initialContext
24
+ });
25
+ const getContext = () => untrack(() => snapshotSignal.get().context);
26
+ const setContext = (context) => update(snapshotSignal, { context });
27
+ return {
28
+ get snapshot() {
29
+ return snapshotSignal;
30
+ },
31
+ send(message) {
32
+ if (getState() === "destroyed") return;
33
+ const context = getContext();
34
+ const newContext = reducer(context, message);
35
+ if (newContext !== context) setContext(newContext);
36
+ },
37
+ destroy() {
38
+ if (getState() === "destroyed") return;
39
+ transition("destroyed");
40
+ }
41
+ };
42
+ }
43
+ //#endregion
44
+ export { createTransitionActor };
45
+
46
+ //# sourceMappingURL=create-transition-actor.js.map