@far-world-labs/verblets 0.1.1 → 0.1.3

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 (300) hide show
  1. package/.cursor/launch.json +30 -0
  2. package/.cursor/settings.json +20 -0
  3. package/.github/workflows/branch-protection.yml +22 -0
  4. package/.github/workflows/ci.yml +120 -0
  5. package/.prettierrc +6 -0
  6. package/.release-it.json +4 -1
  7. package/.vscode/launch.json +31 -0
  8. package/AGENTS.md +220 -0
  9. package/DEVELOPING.md +105 -0
  10. package/README.md +254 -0
  11. package/eslint.config.js +80 -0
  12. package/package.json +29 -17
  13. package/scripts/generate-test/index.js +29 -3
  14. package/scripts/runner/index.js +26 -0
  15. package/scripts/simple-editor/index.js +29 -18
  16. package/scripts/summarize-files/index.js +28 -4
  17. package/src/chains/README.md +30 -0
  18. package/src/chains/anonymize/README.md +21 -0
  19. package/src/chains/anonymize/index.examples.js +75 -0
  20. package/src/chains/anonymize/index.js +121 -0
  21. package/src/chains/anonymize/index.spec.js +78 -0
  22. package/src/chains/bulk-central-tendency/index.examples.js +138 -0
  23. package/src/chains/bulk-central-tendency/index.js +91 -0
  24. package/src/chains/bulk-filter/README.md +21 -0
  25. package/src/chains/bulk-filter/index.examples.js +22 -0
  26. package/src/chains/bulk-filter/index.js +58 -0
  27. package/src/chains/bulk-filter/index.spec.js +38 -0
  28. package/src/chains/bulk-find/README.md +16 -0
  29. package/src/chains/bulk-find/index.examples.js +20 -0
  30. package/src/chains/bulk-find/index.js +30 -0
  31. package/src/chains/bulk-find/index.spec.js +26 -0
  32. package/src/chains/bulk-group/README.md +23 -0
  33. package/src/chains/bulk-group/index.examples.js +18 -0
  34. package/src/chains/bulk-group/index.js +34 -0
  35. package/src/chains/bulk-group/index.spec.js +41 -0
  36. package/src/chains/bulk-map/README.md +43 -0
  37. package/src/chains/bulk-map/index.examples.js +17 -0
  38. package/src/chains/bulk-map/index.js +86 -0
  39. package/src/chains/bulk-map/index.spec.js +44 -0
  40. package/src/chains/bulk-reduce/README.md +12 -0
  41. package/src/chains/bulk-reduce/index.examples.js +15 -0
  42. package/src/chains/bulk-reduce/index.js +13 -0
  43. package/src/chains/bulk-reduce/index.spec.js +25 -0
  44. package/src/chains/bulk-score/README.md +16 -0
  45. package/src/chains/bulk-score/bulk-score-result.json +18 -0
  46. package/src/chains/bulk-score/index.examples.js +22 -0
  47. package/src/chains/bulk-score/index.js +133 -0
  48. package/src/chains/bulk-score/index.spec.js +30 -0
  49. package/src/chains/category-samples/README.md +61 -0
  50. package/src/chains/category-samples/index.examples.js +103 -0
  51. package/src/chains/category-samples/index.js +134 -0
  52. package/src/chains/collect-terms/README.md +12 -0
  53. package/src/chains/collect-terms/index.examples.js +16 -0
  54. package/src/chains/collect-terms/index.js +44 -0
  55. package/src/chains/collect-terms/index.spec.js +25 -0
  56. package/src/chains/date/README.md +12 -0
  57. package/src/chains/date/index.examples.js +47 -0
  58. package/src/chains/date/index.js +74 -0
  59. package/src/chains/date/index.spec.js +62 -0
  60. package/src/chains/disambiguate/README.md +22 -0
  61. package/src/chains/disambiguate/disambiguate-meanings-result.json +16 -0
  62. package/src/chains/disambiguate/index.examples.js +18 -0
  63. package/src/chains/disambiguate/index.js +92 -0
  64. package/src/chains/disambiguate/index.spec.js +25 -0
  65. package/src/chains/dismantle/README.md +67 -0
  66. package/src/chains/dismantle/dismantle.examples.js +27 -0
  67. package/src/chains/dismantle/index.js +6 -17
  68. package/src/chains/dismantle/index.spec.js +1 -2
  69. package/src/chains/expect/README.md +171 -0
  70. package/src/chains/expect/index.examples.js +146 -0
  71. package/src/chains/expect/index.js +173 -0
  72. package/src/chains/expect/index.spec.js +324 -0
  73. package/src/chains/filter-ambiguous/README.md +11 -0
  74. package/src/chains/filter-ambiguous/index.examples.js +20 -0
  75. package/src/chains/filter-ambiguous/index.js +49 -0
  76. package/src/chains/filter-ambiguous/index.spec.js +31 -0
  77. package/src/chains/glossary/README.md +19 -0
  78. package/src/chains/glossary/index.examples.js +386 -0
  79. package/src/chains/glossary/index.js +75 -0
  80. package/src/chains/glossary/index.spec.js +19 -0
  81. package/src/chains/intersections/README.md +152 -0
  82. package/src/chains/intersections/index.examples.js +279 -0
  83. package/src/chains/intersections/index.js +366 -0
  84. package/src/chains/intersections/intersection-result.json +38 -0
  85. package/src/chains/list/index.examples.js +12 -16
  86. package/src/chains/list/index.js +106 -53
  87. package/src/chains/list/index.spec.js +8 -9
  88. package/src/chains/list/list-result.json +16 -0
  89. package/src/chains/llm-logger/README.md +208 -0
  90. package/src/chains/llm-logger/index.js +205 -0
  91. package/src/chains/llm-logger/index.spec.js +330 -0
  92. package/src/chains/questions/index.examples.js +2 -1
  93. package/src/chains/questions/index.js +14 -15
  94. package/src/chains/scan-js/index.js +6 -9
  95. package/src/chains/set-interval/README.md +81 -0
  96. package/src/chains/set-interval/index.examples.js +36 -0
  97. package/src/chains/set-interval/index.js +131 -0
  98. package/src/chains/set-interval/index.spec.js +70 -0
  99. package/src/chains/socratic/README.md +17 -0
  100. package/src/chains/socratic/index.js +64 -0
  101. package/src/chains/socratic/index.spec.js +24 -0
  102. package/src/chains/sort/index.examples.js +3 -7
  103. package/src/chains/sort/index.js +65 -15
  104. package/src/chains/sort/index.spec.js +5 -8
  105. package/src/chains/sort/sort-result.json +16 -0
  106. package/src/chains/summary-map/README.md +9 -1
  107. package/src/chains/summary-map/index.examples.js +9 -2
  108. package/src/chains/summary-map/index.js +43 -25
  109. package/src/chains/summary-map/index.spec.js +78 -3
  110. package/src/chains/test/index.js +9 -13
  111. package/src/chains/test-advice/index.js +4 -5
  112. package/src/chains/themes/README.md +20 -0
  113. package/src/chains/themes/index.examples.js +17 -0
  114. package/src/chains/themes/index.js +28 -0
  115. package/src/chains/themes/index.spec.js +19 -0
  116. package/src/chains/veiled-variants/index.examples.js +18 -0
  117. package/src/chains/veiled-variants/index.js +107 -0
  118. package/src/chains/veiled-variants/index.spec.js +40 -0
  119. package/src/constants/common.js +0 -2
  120. package/src/constants/models.js +172 -0
  121. package/src/index.js +178 -18
  122. package/src/json-schemas/README.md +13 -0
  123. package/src/json-schemas/index.js +8 -14
  124. package/src/json-schemas/schema-dot-org-photograph.json +11 -5
  125. package/src/json-schemas/schema-dot-org-place.json +78 -5
  126. package/src/lib/README.md +26 -0
  127. package/src/lib/bulk-filter/README.md +22 -0
  128. package/src/lib/bulk-filter/index.examples.js +27 -0
  129. package/src/lib/bulk-filter/index.js +63 -0
  130. package/src/lib/bulk-filter/index.spec.js +38 -0
  131. package/src/lib/bulk-find/README.md +18 -0
  132. package/src/lib/bulk-find/index.examples.js +19 -0
  133. package/src/lib/bulk-find/index.js +30 -0
  134. package/src/lib/bulk-find/index.spec.js +41 -0
  135. package/src/lib/chatgpt/index.js +63 -43
  136. package/src/lib/combinations/index.js +30 -0
  137. package/src/lib/combinations/index.spec.js +23 -0
  138. package/src/lib/functional/index.js +28 -0
  139. package/src/lib/logger-service/index.js +32 -0
  140. package/src/lib/parse-js-parts/index.js +9 -21
  141. package/src/lib/parse-llm-list/README.md +39 -0
  142. package/src/lib/parse-llm-list/index.js +54 -0
  143. package/src/lib/parse-llm-list/index.spec.js +59 -0
  144. package/src/lib/path-aliases/index.js +1 -3
  145. package/src/lib/path-aliases/index.spec.js +2 -8
  146. package/src/lib/pave/index.js +4 -4
  147. package/src/lib/pave/index.spec.js +6 -3
  148. package/src/lib/prompt-cache/index.js +14 -10
  149. package/src/lib/retry/index.js +11 -8
  150. package/src/lib/ring-buffer/README.md +460 -0
  151. package/src/lib/ring-buffer/index.js +1074 -0
  152. package/src/lib/search-best-first/city-walk.spec.js +37 -0
  153. package/src/lib/search-best-first/index.js +42 -11
  154. package/src/lib/search-best-first/index.spec.js +35 -0
  155. package/src/lib/search-js-files/index.js +44 -47
  156. package/src/lib/search-js-files/scan-file.js +10 -21
  157. package/src/lib/shorten-text/index.js +2 -7
  158. package/src/lib/shorten-text/index.spec.js +3 -3
  159. package/src/lib/strip-response/index.js +2 -7
  160. package/src/lib/template-replace/index.js +23 -0
  161. package/src/lib/template-replace/index.spec.js +60 -0
  162. package/src/lib/to-date/index.js +11 -0
  163. package/src/lib/to-number/index.js +1 -1
  164. package/src/lib/transcribe/index.js +26 -9
  165. package/src/prompts/README.md +3 -1
  166. package/src/prompts/as-object-with-schema.js +3 -8
  167. package/src/prompts/as-schema-org-text.js +10 -2
  168. package/src/prompts/code-features.js +1 -5
  169. package/src/prompts/constants.js +27 -27
  170. package/src/prompts/generate-collection.js +1 -1
  171. package/src/prompts/intent.js +16 -22
  172. package/src/prompts/select-from-threshold.js +1 -2
  173. package/src/prompts/sort.js +4 -8
  174. package/src/prompts/style.js +4 -7
  175. package/src/prompts/wrap-list.js +1 -4
  176. package/src/services/llm-model/global-overrides.spec.js +432 -0
  177. package/src/services/llm-model/index.js +234 -40
  178. package/src/services/llm-model/model.js +2 -2
  179. package/src/services/llm-model/negotiate.spec.js +447 -0
  180. package/src/services/redis/index.js +70 -7
  181. package/src/test/setup.js +20 -0
  182. package/src/verblets/README.md +26 -0
  183. package/src/verblets/auto/index.examples.js +12 -9
  184. package/src/verblets/auto/index.js +10 -10
  185. package/src/verblets/auto/index.spec.js +4 -6
  186. package/src/verblets/bool/README.md +36 -0
  187. package/src/verblets/bool/index.examples.js +53 -1
  188. package/src/verblets/bool/index.js +6 -9
  189. package/src/verblets/bool/index.spec.js +1 -3
  190. package/src/verblets/central-tendency/README.md +166 -0
  191. package/src/verblets/central-tendency/central-tendency-result.json +24 -0
  192. package/src/verblets/central-tendency/index.examples.js +196 -0
  193. package/src/verblets/central-tendency/index.js +171 -0
  194. package/src/verblets/central-tendency/index.spec.js +148 -0
  195. package/src/verblets/enum/index.examples.js +1 -4
  196. package/src/verblets/enum/index.js +7 -4
  197. package/src/verblets/expect/README.md +64 -0
  198. package/src/verblets/expect/index.examples.js +109 -0
  199. package/src/verblets/expect/index.js +75 -0
  200. package/src/verblets/expect/index.spec.js +127 -0
  201. package/src/verblets/intent/index.examples.js +95 -7
  202. package/src/verblets/intent/index.js +56 -68
  203. package/src/verblets/intersection/README.md +16 -0
  204. package/src/verblets/intersection/index.examples.js +89 -0
  205. package/src/verblets/intersection/index.js +84 -0
  206. package/src/verblets/intersection/index.spec.js +60 -0
  207. package/src/verblets/intersection/intersection-result.json +16 -0
  208. package/src/verblets/list-expand/README.md +10 -0
  209. package/src/verblets/list-expand/index.examples.js +14 -0
  210. package/src/verblets/list-expand/index.js +104 -0
  211. package/src/verblets/list-expand/index.spec.js +18 -0
  212. package/src/verblets/list-expand/list-expand-result.json +16 -0
  213. package/src/verblets/list-filter/README.md +22 -0
  214. package/src/verblets/list-filter/index.examples.js +26 -0
  215. package/src/verblets/list-filter/index.js +18 -0
  216. package/src/verblets/list-filter/index.spec.js +19 -0
  217. package/src/verblets/list-find/README.md +11 -0
  218. package/src/verblets/list-find/index.examples.js +15 -0
  219. package/src/verblets/list-find/index.js +17 -0
  220. package/src/verblets/list-find/index.spec.js +19 -0
  221. package/src/verblets/list-group/README.md +16 -0
  222. package/src/verblets/list-group/index.examples.js +16 -0
  223. package/src/verblets/list-group/index.js +112 -0
  224. package/src/verblets/list-group/index.spec.js +35 -0
  225. package/src/verblets/list-group/list-group-result.json +16 -0
  226. package/src/verblets/list-map/README.md +11 -0
  227. package/src/verblets/list-map/index.examples.js +15 -0
  228. package/src/verblets/list-map/index.js +26 -0
  229. package/src/verblets/list-map/index.spec.js +17 -0
  230. package/src/verblets/list-reduce/README.md +10 -0
  231. package/src/verblets/list-reduce/index.examples.js +14 -0
  232. package/src/verblets/list-reduce/index.js +21 -0
  233. package/src/verblets/list-reduce/index.spec.js +27 -0
  234. package/src/verblets/list-reduce/index.spec.jsx +27 -0
  235. package/src/verblets/name/README.md +15 -0
  236. package/src/verblets/name/index.examples.js +28 -0
  237. package/src/verblets/name/index.js +19 -0
  238. package/src/verblets/name/index.spec.js +33 -0
  239. package/src/verblets/name-similar-to/README.md +26 -0
  240. package/src/verblets/name-similar-to/index.examples.js +18 -0
  241. package/src/verblets/name-similar-to/index.js +20 -0
  242. package/src/verblets/name-similar-to/index.spec.js +13 -0
  243. package/src/verblets/number/index.examples.js +173 -7
  244. package/src/verblets/number/index.js +5 -2
  245. package/src/verblets/number/index.spec.js +1 -3
  246. package/src/verblets/number-with-units/index.examples.js +5 -1
  247. package/src/verblets/number-with-units/index.js +74 -9
  248. package/src/verblets/number-with-units/number-with-units-result.json +23 -0
  249. package/src/verblets/schema-org/index.examples.js +2 -7
  250. package/src/verblets/schema-org/index.js +32 -3
  251. package/src/verblets/sentiment/README.md +10 -0
  252. package/src/verblets/sentiment/index.examples.js +20 -0
  253. package/src/verblets/sentiment/index.js +9 -0
  254. package/src/verblets/sentiment/index.spec.js +20 -0
  255. package/src/verblets/to-object/index.js +10 -15
  256. package/src/verblets/to-object/index.spec.js +1 -4
  257. package/.eslintrc.json +0 -42
  258. package/docs/README.md +0 -41
  259. package/docs/babel.config.js +0 -3
  260. package/docs/blog/2019-05-28-first-blog-post.md +0 -12
  261. package/docs/blog/2019-05-29-long-blog-post.md +0 -44
  262. package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -20
  263. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  264. package/docs/blog/2021-08-26-welcome/index.md +0 -25
  265. package/docs/blog/authors.yml +0 -17
  266. package/docs/docs/api/bool.md +0 -74
  267. package/docs/docs/api/search.md +0 -51
  268. package/docs/docs/intro.md +0 -47
  269. package/docs/docs/tutorial-basics/_category_.json +0 -8
  270. package/docs/docs/tutorial-basics/congratulations.md +0 -23
  271. package/docs/docs/tutorial-basics/create-a-blog-post.md +0 -34
  272. package/docs/docs/tutorial-basics/create-a-document.md +0 -57
  273. package/docs/docs/tutorial-basics/create-a-page.md +0 -43
  274. package/docs/docs/tutorial-basics/deploy-your-site.md +0 -31
  275. package/docs/docs/tutorial-basics/markdown-features.mdx +0 -152
  276. package/docs/docs/tutorial-extras/_category_.json +0 -7
  277. package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
  278. package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
  279. package/docs/docs/tutorial-extras/manage-docs-versions.md +0 -55
  280. package/docs/docs/tutorial-extras/translate-your-site.md +0 -88
  281. package/docs/docusaurus.config.js +0 -120
  282. package/docs/package.json +0 -44
  283. package/docs/sidebars.js +0 -31
  284. package/docs/src/components/HomepageFeatures/index.js +0 -61
  285. package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  286. package/docs/src/css/custom.css +0 -30
  287. package/docs/src/pages/index.js +0 -43
  288. package/docs/src/pages/index.module.css +0 -23
  289. package/docs/src/pages/markdown-page.md +0 -7
  290. package/docs/static/.nojekyll +0 -0
  291. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  292. package/docs/static/img/docusaurus.png +0 -0
  293. package/docs/static/img/favicon.ico +0 -0
  294. package/docs/static/img/logo.svg +0 -1
  295. package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  296. package/docs/static/img/undraw_docusaurus_react.svg +0 -170
  297. package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  298. package/src/constants/openai.js +0 -65
  299. /package/{.vite.config.examples.js → .vitest.config.examples.js} +0 -0
  300. /package/{.vite.config.js → .vitest.config.js} +0 -0
@@ -0,0 +1,1074 @@
1
+ /**
2
+ * Ring Buffer - Memory-efficient circular buffer for data ingestion and processing
3
+ *
4
+ * A generic ring buffer (circular buffer) that automatically evicts oldest entries when full.
5
+ * Designed for high-throughput data ingestion, lane-based processing, and batch operations
6
+ * where memory efficiency and fast access patterns are critical.
7
+ *
8
+ * Features:
9
+ * - Automatic memory management with configurable size limits
10
+ * - Multiple cursor support for concurrent processing
11
+ * - Lane-based filtering and processing with independent flush loops
12
+ * - Batch operations optimized for various workflows
13
+ * - Slice operations with ID-based and index-based access
14
+ * - Statistics and analysis helpers
15
+ * - Iterator support for streaming operations
16
+ * - Generic data support (not limited to logs)
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} RingBufferEntry
21
+ * @property {number} id - Unique incrementing identifier
22
+ * @property {Date} timestamp - When entry was added
23
+ * @property {any} data - The actual data payload
24
+ * @property {Map<string, any>} [meta] - Optional metadata
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} DataEntry
29
+ * @property {number} id - Unique incrementing identifier
30
+ * @property {Date} ts - When entry was created
31
+ * @property {any} raw - The raw data payload
32
+ * @property {Object} variables - Extracted/computed variables
33
+ * @property {Object} context - Context information (file, line, etc.)
34
+ * @property {string[]} tags - Classification tags/biomarkers
35
+ * @property {Map<string, any>} meta - Additional metadata
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} ProcessingLane
40
+ * @property {string} id - Lane identifier
41
+ * @property {Function} [filter] - Filter function (entry) => boolean
42
+ * @property {Function} writer - Output writer function (lines: string[]) => void
43
+ * @property {Function} [batchHandler] - Batch processing function
44
+ * @property {RingBufferEntry[]} buffer - Pending entries for this lane
45
+ * @property {boolean} flushActive - Whether flush loop is active
46
+ * @property {string} [cursorName] - Associated cursor name for tracking
47
+ * @property {Object} [config] - Lane-specific configuration
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} RingBufferCursor
52
+ * @property {string} name - Cursor identifier
53
+ * @property {number} position - Current position (entry ID)
54
+ * @property {Date} lastMoved - When cursor was last updated
55
+ * @property {Map<string, any>} meta - Cursor-specific metadata
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} RingBufferStats
60
+ * @property {number} size - Current number of entries
61
+ * @property {number} capacity - Maximum capacity
62
+ * @property {number} totalAdded - Total entries added (including evicted)
63
+ * @property {number} totalEvicted - Total entries evicted
64
+ * @property {number} oldestId - ID of oldest entry in buffer
65
+ * @property {number} newestId - ID of newest entry in buffer
66
+ * @property {Date} oldestTimestamp - Timestamp of oldest entry
67
+ * @property {Date} newestTimestamp - Timestamp of newest entry
68
+ * @property {number} cursors - Number of active cursors
69
+ * @property {number} lanes - Number of active processing lanes
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} StableBatch
74
+ * @property {string} batchId - Unique identifier for the batch
75
+ * @property {number} batchIndex - Index of the batch in the sequence
76
+ * @property {number} startId - Starting entry ID (inclusive)
77
+ * @property {number} endId - Ending entry ID (exclusive)
78
+ * @property {number} size - Number of entries in the batch
79
+ * @property {Date} timestamp - When the batch definition was created
80
+ */
81
+
82
+ /**
83
+ * @typedef {Object} StableBatchResult
84
+ * @property {string} batchId - Unique identifier for the batch
85
+ * @property {StableBatch} batchDef - The batch definition
86
+ * @property {any} result - Result from processing the batch
87
+ * @property {boolean} skipped - Whether the batch was skipped
88
+ * @property {number} entriesProcessed - Number of entries processed
89
+ * @property {number} attempts - Number of processing attempts
90
+ */
91
+
92
+ /**
93
+ * @typedef {Object} BatchCursorResult
94
+ * @property {boolean} done - Whether iteration is complete
95
+ * @property {RingBufferEntry[]} entries - Entries in the current batch
96
+ * @property {StableBatch|null} batchDef - Batch definition (null if done)
97
+ * @property {boolean} [hasMore] - Whether more batches are available
98
+ */
99
+
100
+ /**
101
+ * @typedef {Object} BatchCursor
102
+ * @property {string} cursorName - Name of the cursor
103
+ * @property {number} batchSize - Size of each batch
104
+ * @property {string} batchIdPrefix - Prefix for batch IDs
105
+ * @property {Function} next - Get next batch: (moveCursor?: boolean) => BatchCursorResult
106
+ * @property {Function} reset - Reset cursor: (position?: number) => void
107
+ * @property {Function} getStatus - Get cursor status: () => Object
108
+ */
109
+
110
+ /**
111
+ * @typedef {Object} BatchHandler
112
+ * @property {Function} process - Process batch: (params: {head: number, cursor: number, entries: RingBufferEntry[], batch: RingBufferEntry[]}) => Promise<string[][]> | undefined
113
+ */
114
+
115
+ export default class RingBuffer {
116
+ constructor(maxSize = 10000) {
117
+ this.maxSize = maxSize;
118
+ this.entries = [];
119
+ this.nextId = 1;
120
+ this.totalAdded = 0;
121
+ this.totalEvicted = 0;
122
+ this.cursors = new Map();
123
+ this.lanes = new Map(); // Processing lanes
124
+ }
125
+
126
+ /**
127
+ * Add entry to buffer
128
+ * @param {any} data - Data to store
129
+ * @param {Map<string, any>} [meta] - Entry metadata
130
+ * @returns {RingBufferEntry} The created entry
131
+ */
132
+ push(data, meta = new Map()) {
133
+ const entry = {
134
+ id: this.nextId++,
135
+ ts: new Date(),
136
+ data,
137
+ meta: new Map(meta),
138
+ };
139
+
140
+ // Handle eviction if buffer is full
141
+ if (this.entries.length >= this.maxSize) {
142
+ const evicted = this.entries.shift();
143
+ this.totalEvicted++;
144
+
145
+ // Update cursors that point to evicted entries
146
+ for (const cursor of this.cursors.values()) {
147
+ if (cursor.position <= evicted.id) {
148
+ cursor.position = evicted.id + 1;
149
+ }
150
+ }
151
+ }
152
+
153
+ this.entries.push(entry);
154
+ this.totalAdded++;
155
+
156
+ // Process through lanes
157
+ this._processLanes(entry);
158
+
159
+ return entry;
160
+ }
161
+
162
+ /**
163
+ * Ingest structured data entry (PEC-01 compatible)
164
+ * @param {string} raw - Raw log data
165
+ * @param {Object} [options] - Structured data options
166
+ * @param {Object} [options.variables] - Variable data
167
+ * @param {Object} [options.context] - Context information
168
+ * @param {string[]} [options.tags] - Tags array
169
+ * @param {Map} [options.meta] - Additional metadata
170
+ * @returns {DataEntry} The created structured entry
171
+ */
172
+ ingest(raw, options = {}) {
173
+ const entry = {
174
+ id: this.nextId++,
175
+ ts: new Date(),
176
+ raw,
177
+ variables: options.variables || {},
178
+ context: options.context || { filePath: '', line: 0 },
179
+ tags: options.tags || [],
180
+ meta: options.meta || new Map(),
181
+ };
182
+
183
+ // Handle eviction if buffer is full
184
+ if (this.entries.length >= this.maxSize) {
185
+ const evicted = this.entries.shift();
186
+ this.totalEvicted++;
187
+
188
+ // Update cursors that point to evicted entries
189
+ for (const cursor of this.cursors.values()) {
190
+ if (cursor.position <= evicted.id) {
191
+ cursor.position = evicted.id + 1;
192
+ }
193
+ }
194
+ }
195
+
196
+ this.entries.push(entry);
197
+ this.totalAdded++;
198
+
199
+ // Process through lanes
200
+ this._processLanes(entry);
201
+
202
+ return entry;
203
+ }
204
+
205
+ /**
206
+ * Register a processing lane
207
+ * @param {ProcessingLane} lane - Lane configuration
208
+ * @returns {ProcessingLane} The registered lane
209
+ */
210
+ addLane(lane) {
211
+ const laneConfig = {
212
+ id: lane.id,
213
+ filter: lane.filter,
214
+ writer: lane.writer,
215
+ batchHandler: lane.batchHandler,
216
+ buffer: [],
217
+ flushActive: false,
218
+ cursorName: lane.cursorName || `lane-${lane.id}`,
219
+ config: lane.config || {},
220
+ ...lane,
221
+ };
222
+
223
+ this.lanes.set(lane.id, laneConfig);
224
+
225
+ // Create cursor for this lane if specified
226
+ if (laneConfig.cursorName) {
227
+ this.setCursor(laneConfig.cursorName, this.nextId - 1);
228
+ }
229
+
230
+ return laneConfig;
231
+ }
232
+
233
+ /**
234
+ * Remove a processing lane
235
+ * @param {string} laneId - Lane identifier
236
+ * @returns {boolean} True if lane was removed
237
+ */
238
+ removeLane(laneId) {
239
+ const lane = this.lanes.get(laneId);
240
+ if (lane && lane.cursorName) {
241
+ this.removeCursor(lane.cursorName);
242
+ }
243
+ return this.lanes.delete(laneId);
244
+ }
245
+
246
+ /**
247
+ * Get processing lane by ID
248
+ * @param {string} laneId - Lane identifier
249
+ * @returns {ProcessingLane|undefined} The lane configuration
250
+ */
251
+ getLane(laneId) {
252
+ return this.lanes.get(laneId);
253
+ }
254
+
255
+ /**
256
+ * Get all processing lanes
257
+ * @returns {ProcessingLane[]} Array of all lanes
258
+ */
259
+ getAllLanes() {
260
+ return Array.from(this.lanes.values());
261
+ }
262
+
263
+ /**
264
+ * Process entry through all registered lanes
265
+ * @private
266
+ * @param {RingBufferEntry|DataEntry} entry - Entry to process
267
+ */
268
+ _processLanes(entry) {
269
+ for (const lane of this.lanes.values()) {
270
+ // Apply filter if specified
271
+ if (!lane.filter || lane.filter(entry)) {
272
+ lane.buffer.push(entry);
273
+
274
+ // Check if we should trigger flush
275
+ const batchSize = lane.config.batchSize || 10;
276
+ if (lane.buffer.length >= batchSize && !lane.flushActive) {
277
+ // Use setTimeout to avoid blocking
278
+ setTimeout(() => this._flushLane(lane), 0);
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Flush a processing lane
286
+ * @private
287
+ * @param {ProcessingLane} lane - Lane to flush
288
+ */
289
+ async _flushLane(lane) {
290
+ if (lane.flushActive) return;
291
+
292
+ lane.flushActive = true;
293
+
294
+ try {
295
+ while (lane.buffer.length > 0) {
296
+ const batchSize = lane.config.batchSize || 10;
297
+ const batch = lane.buffer.slice(0, Math.min(batchSize, lane.buffer.length));
298
+
299
+ if (batch.length === 0) break;
300
+
301
+ const cursor = batch[batch.length - 1].id;
302
+ let result = batch; // Default to the batch itself
303
+
304
+ // Use batch handler if available
305
+ if (lane.batchHandler) {
306
+ const params = {
307
+ head: this.nextId - 1,
308
+ cursor,
309
+ entries: this.entries,
310
+ batch,
311
+ };
312
+
313
+ try {
314
+ result = await lane.batchHandler.process(params);
315
+ } catch (error) {
316
+ console.error(`Lane ${lane.id} batch handler error:`, error);
317
+ // Skip this batch on error
318
+ lane.buffer.splice(0, batch.length);
319
+ continue;
320
+ }
321
+ }
322
+
323
+ // Write results
324
+ if (lane.writer && result) {
325
+ const lines = Array.isArray(result) ? result.flat() : [String(result)];
326
+ if (lines.length > 0) {
327
+ try {
328
+ await lane.writer(lines);
329
+ } catch (error) {
330
+ console.error(`Lane ${lane.id} writer error:`, error);
331
+ }
332
+ }
333
+ }
334
+
335
+ // Remove processed entries from buffer
336
+ lane.buffer.splice(0, batch.length);
337
+
338
+ // Update cursor if specified
339
+ if (lane.cursorName) {
340
+ this.moveCursor(lane.cursorName, cursor);
341
+ }
342
+
343
+ // If batch was smaller than requested size, we're done
344
+ if (batch.length < batchSize) {
345
+ break;
346
+ }
347
+ }
348
+ } finally {
349
+ lane.flushActive = false;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Manually trigger flush for a specific lane
355
+ * @param {string} laneId - Lane identifier
356
+ * @returns {Promise<void>}
357
+ */
358
+ async flushLane(laneId) {
359
+ const lane = this.lanes.get(laneId);
360
+ if (lane) {
361
+ await this._flushLane(lane);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Manually trigger flush for all lanes
367
+ * @returns {Promise<void>}
368
+ */
369
+ async flushAllLanes() {
370
+ const flushPromises = Array.from(this.lanes.values()).map((lane) => this._flushLane(lane));
371
+ await Promise.all(flushPromises);
372
+ }
373
+
374
+ /**
375
+ * Add multiple entries in batch
376
+ * @param {any[]} dataArray - Array of data to add
377
+ * @param {Map<string, any>} [sharedMeta] - Metadata to apply to all entries
378
+ * @returns {RingBufferEntry[]} Array of created entries
379
+ */
380
+ pushBatch(dataArray, sharedMeta = new Map()) {
381
+ const entries = [];
382
+ for (const data of dataArray) {
383
+ entries.push(this.push(data, new Map(sharedMeta)));
384
+ }
385
+ return entries;
386
+ }
387
+
388
+ /**
389
+ * Get all entries in buffer (oldest → newest)
390
+ * @returns {RingBufferEntry[]} All entries
391
+ */
392
+ all() {
393
+ return [...this.entries];
394
+ }
395
+
396
+ /**
397
+ * Get entries within ID range
398
+ * @param {number} startId - Starting ID (inclusive)
399
+ * @param {number} [endId] - Ending ID (exclusive)
400
+ * @returns {RingBufferEntry[]} Filtered entries
401
+ */
402
+ slice(startId, endId) {
403
+ return this.entries.filter((entry) => {
404
+ return entry.id >= startId && (endId === undefined || entry.id < endId);
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Get entries within index range (like Array.slice)
410
+ * @param {number} [start=0] - Start index
411
+ * @param {number} [end] - End index
412
+ * @returns {RingBufferEntry[]} Sliced entries
413
+ */
414
+ sliceByIndex(start = 0, end) {
415
+ return this.entries.slice(start, end);
416
+ }
417
+
418
+ /**
419
+ * Get entries within time range
420
+ * @param {Date} startTime - Start time (inclusive)
421
+ * @param {Date} [endTime] - End time (exclusive)
422
+ * @returns {RingBufferEntry[]} Filtered entries
423
+ */
424
+ sliceByTime(startTime, endTime) {
425
+ return this.entries.filter((entry) => {
426
+ const timestamp = entry.timestamp || entry.ts;
427
+ return timestamp >= startTime && (endTime === undefined || timestamp < endTime);
428
+ });
429
+ }
430
+
431
+ /**
432
+ * Get the most recent N entries
433
+ * @param {number} count - Number of entries to retrieve
434
+ * @returns {RingBufferEntry[]} Most recent entries (oldest → newest)
435
+ */
436
+ tail(count) {
437
+ return this.entries.slice(-count);
438
+ }
439
+
440
+ /**
441
+ * Get the oldest N entries
442
+ * @param {number} count - Number of entries to retrieve
443
+ * @returns {RingBufferEntry[]} Oldest entries
444
+ */
445
+ head(count) {
446
+ return this.entries.slice(0, count);
447
+ }
448
+
449
+ /**
450
+ * Find entries matching a predicate
451
+ * @param {Function} predicate - Function to test entries (entry) => boolean
452
+ * @returns {RingBufferEntry[]} Matching entries
453
+ */
454
+ filter(predicate) {
455
+ return this.entries.filter(predicate);
456
+ }
457
+
458
+ /**
459
+ * Find first entry matching predicate
460
+ * @param {Function} predicate - Function to test entries
461
+ * @returns {RingBufferEntry|undefined} First matching entry
462
+ */
463
+ find(predicate) {
464
+ return this.entries.find(predicate);
465
+ }
466
+
467
+ /**
468
+ * Transform entries using a mapping function
469
+ * @param {Function} mapper - Function to transform entries (entry) => any
470
+ * @returns {any[]} Transformed results
471
+ */
472
+ map(mapper) {
473
+ return this.entries.map(mapper);
474
+ }
475
+
476
+ /**
477
+ * Reduce entries to a single value
478
+ * @param {Function} reducer - Reducer function (acc, entry, index) => any
479
+ * @param {any} initialValue - Initial accumulator value
480
+ * @returns {any} Reduced value
481
+ */
482
+ reduce(reducer, initialValue) {
483
+ return this.entries.reduce(reducer, initialValue);
484
+ }
485
+
486
+ /**
487
+ * Create or update a cursor for tracking position
488
+ * @param {string} name - Cursor name
489
+ * @param {number} [position] - Position to set (defaults to current newest)
490
+ * @returns {RingBufferCursor} The cursor
491
+ */
492
+ setCursor(name, position) {
493
+ const currentPosition =
494
+ position !== undefined
495
+ ? position
496
+ : this.entries.length > 0
497
+ ? this.entries[this.entries.length - 1].id
498
+ : 0;
499
+
500
+ const cursor = {
501
+ name,
502
+ position: currentPosition,
503
+ lastMoved: new Date(),
504
+ meta: new Map(),
505
+ };
506
+
507
+ this.cursors.set(name, cursor);
508
+ return cursor;
509
+ }
510
+
511
+ /**
512
+ * Get cursor by name
513
+ * @param {string} name - Cursor name
514
+ * @returns {RingBufferCursor|undefined} The cursor
515
+ */
516
+ getCursor(name) {
517
+ return this.cursors.get(name);
518
+ }
519
+
520
+ /**
521
+ * Move cursor to new position
522
+ * @param {string} name - Cursor name
523
+ * @param {number} position - New position
524
+ * @returns {RingBufferCursor|undefined} Updated cursor
525
+ */
526
+ moveCursor(name, position) {
527
+ const cursor = this.cursors.get(name);
528
+ if (cursor) {
529
+ cursor.position = position;
530
+ cursor.lastMoved = new Date();
531
+ return cursor;
532
+ }
533
+ return undefined;
534
+ }
535
+
536
+ /**
537
+ * Get entries since cursor position
538
+ * @param {string} cursorName - Cursor name
539
+ * @param {boolean} [moveCursor=true] - Whether to move cursor to end
540
+ * @returns {RingBufferEntry[]} Entries since cursor
541
+ */
542
+ getSinceCursor(cursorName, moveCursor = true) {
543
+ const cursor = this.cursors.get(cursorName);
544
+ if (!cursor) {
545
+ return [];
546
+ }
547
+
548
+ const entries = this.entries.filter((entry) => entry.id > cursor.position);
549
+
550
+ if (moveCursor && entries.length > 0) {
551
+ cursor.position = entries[entries.length - 1].id;
552
+ cursor.lastMoved = new Date();
553
+ }
554
+
555
+ return entries;
556
+ }
557
+
558
+ /**
559
+ * Process entries in batches with a callback
560
+ * @param {number} batchSize - Size of each batch
561
+ * @param {Function} processor - Function to process each batch (batch, batchIndex) => Promise|any
562
+ * @param {Object} [options] - Processing options
563
+ * @param {number} [options.startIndex=0] - Index to start from
564
+ * @param {number} [options.endIndex] - Index to end at
565
+ * @param {boolean} [options.parallel=false] - Process batches in parallel
566
+ * @returns {Promise<any[]>} Results from each batch
567
+ */
568
+ async processBatches(batchSize, processor, options = {}) {
569
+ const { startIndex = 0, endIndex = this.entries.length, parallel = false } = options;
570
+ const results = [];
571
+ const batches = [];
572
+
573
+ // Create batches
574
+ for (let i = startIndex; i < endIndex; i += batchSize) {
575
+ const batch = this.entries.slice(i, Math.min(i + batchSize, endIndex));
576
+ batches.push({ batch, batchIndex: Math.floor(i / batchSize) });
577
+ }
578
+
579
+ if (parallel) {
580
+ // Process all batches in parallel
581
+ const promises = batches.map(({ batch, batchIndex }) => processor(batch, batchIndex));
582
+ results.push(...(await Promise.all(promises)));
583
+ } else {
584
+ // Process batches sequentially
585
+ for (const { batch, batchIndex } of batches) {
586
+ const result = await processor(batch, batchIndex);
587
+ results.push(result);
588
+ }
589
+ }
590
+
591
+ return results;
592
+ }
593
+
594
+ /**
595
+ * Create stable batch definitions based on entry IDs (not indices)
596
+ * These batches remain consistent even if the ring buffer window changes
597
+ * @param {number} batchSize - Size of each batch
598
+ * @param {Object} [options] - Batch creation options
599
+ * @param {number} [options.startId] - Starting entry ID (defaults to oldest)
600
+ * @param {number} [options.endId] - Ending entry ID (defaults to newest)
601
+ * @param {string} [options.batchIdPrefix='batch'] - Prefix for batch IDs
602
+ * @returns {StableBatch[]} Array of stable batch definitions
603
+ */
604
+ createStableBatches(batchSize, options = {}) {
605
+ const {
606
+ startId = this.entries.length > 0 ? this.entries[0].id : 0,
607
+ endId = this.entries.length > 0 ? this.entries[this.entries.length - 1].id + 1 : 0,
608
+ batchIdPrefix = 'batch',
609
+ } = options;
610
+
611
+ const batches = [];
612
+ let currentId = startId;
613
+ let batchIndex = 0;
614
+
615
+ while (currentId < endId) {
616
+ const batchEndId = Math.min(currentId + batchSize, endId);
617
+ batches.push({
618
+ batchId: `${batchIdPrefix}_${batchIndex}`,
619
+ batchIndex,
620
+ startId: currentId,
621
+ endId: batchEndId,
622
+ size: batchEndId - currentId,
623
+ timestamp: new Date(),
624
+ });
625
+
626
+ currentId = batchEndId;
627
+ batchIndex++;
628
+ }
629
+
630
+ return batches;
631
+ }
632
+
633
+ /**
634
+ * Get entries for a stable batch definition
635
+ * @param {StableBatch} batchDef - Stable batch definition
636
+ * @returns {RingBufferEntry[]} Entries in the batch (may be empty if evicted)
637
+ */
638
+ getBatchEntries(batchDef) {
639
+ return this.slice(batchDef.startId, batchDef.endId);
640
+ }
641
+
642
+ /**
643
+ * Process stable batches with retry support and cursor tracking
644
+ * @param {number} batchSize - Size of each batch
645
+ * @param {Function} processor - Function to process each batch (entries, batchDef, context) => Promise|any
646
+ * @param {Object} [options] - Processing options
647
+ * @param {string} [options.cursorName] - Cursor name for tracking progress
648
+ * @param {number} [options.startId] - Starting entry ID
649
+ * @param {number} [options.endId] - Ending entry ID
650
+ * @param {boolean} [options.parallel=false] - Process batches in parallel
651
+ * @param {number} [options.maxRetries=3] - Maximum retry attempts per batch
652
+ * @param {number} [options.retryDelay] - Function to calculate retry delay (attempt) => ms
653
+ * @param {Function} [options.onBatchStart] - Callback when batch starts (batchDef) => void
654
+ * @param {Function} [options.onBatchComplete] - Callback when batch completes (batchDef, result) => void
655
+ * @param {Function} [options.onBatchError] - Callback when batch fails (batchDef, error, attempt) => void
656
+ * @param {boolean} [options.skipMissingEntries=false] - Skip batches with no available entries
657
+ * @returns {Promise<StableBatchResult[]>} Results from each batch
658
+ */
659
+ async processStableBatches(batchSize, processor, options = {}) {
660
+ const {
661
+ startId = this.entries.length > 0 ? this.entries[0].id : 1,
662
+ endId = this.nextId,
663
+ cursorName,
664
+ parallel = false,
665
+ maxRetries = 0,
666
+ retryDelay = 100,
667
+ skipMissingEntries = false,
668
+ batchIdPrefix = 'batch',
669
+ } = options;
670
+
671
+ // Create stable batch definitions
672
+ const batches = this.createStableBatches(batchSize, {
673
+ startId,
674
+ endId,
675
+ batchIdPrefix,
676
+ });
677
+
678
+ // Filter batches based on cursor position if provided
679
+ let batchesToProcess = batches;
680
+ if (cursorName) {
681
+ let cursor = this.getCursor(cursorName);
682
+ if (!cursor) {
683
+ // Create cursor if it doesn't exist
684
+ cursor = this.setCursor(cursorName, startId - 1);
685
+ }
686
+ batchesToProcess = batches.filter((batch) => batch.startId > cursor.position);
687
+ }
688
+
689
+ const processBatchWithRetry = async (batchDef) => {
690
+ let attempts = 0;
691
+ let lastError;
692
+
693
+ // maxRetries means: 1 initial attempt + maxRetries additional attempts
694
+ const maxAttempts = maxRetries + 1;
695
+
696
+ while (attempts < maxAttempts) {
697
+ attempts++;
698
+ try {
699
+ const entries = this.getBatchEntries(batchDef);
700
+
701
+ // Skip if no entries and skipMissingEntries is true
702
+ if (entries.length === 0 && skipMissingEntries) {
703
+ return {
704
+ batchId: batchDef.batchId,
705
+ batchDef,
706
+ result: null,
707
+ skipped: true,
708
+ entriesProcessed: 0,
709
+ attempts,
710
+ };
711
+ }
712
+
713
+ const result = await processor(entries, batchDef, {
714
+ head: this.nextId - 1,
715
+ cursor: batchDef.endId - 1,
716
+ entries: this.entries,
717
+ });
718
+
719
+ // Update cursor if provided
720
+ if (cursorName) {
721
+ this.moveCursor(cursorName, batchDef.endId - 1);
722
+ }
723
+
724
+ return {
725
+ batchId: batchDef.batchId,
726
+ batchDef,
727
+ result,
728
+ skipped: false,
729
+ entriesProcessed: entries.length,
730
+ attempts,
731
+ };
732
+ } catch (error) {
733
+ lastError = error;
734
+ // Only delay if we have more attempts left
735
+ if (attempts < maxAttempts) {
736
+ await new Promise((resolve) => setTimeout(resolve, retryDelay * attempts));
737
+ }
738
+ }
739
+ }
740
+
741
+ // If we get here, all retries failed
742
+ return {
743
+ batchId: batchDef.batchId,
744
+ batchDef,
745
+ result: null,
746
+ skipped: false,
747
+ entriesProcessed: 0,
748
+ attempts,
749
+ error: lastError,
750
+ };
751
+ };
752
+
753
+ // Process batches
754
+ if (parallel) {
755
+ return await Promise.all(batchesToProcess.map(processBatchWithRetry));
756
+ } else {
757
+ const results = [];
758
+ for (const batchDef of batchesToProcess) {
759
+ results.push(await processBatchWithRetry(batchDef));
760
+ }
761
+ return results;
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Create a batch cursor for iterating over stable batches
767
+ * @param {string} cursorName - Name for the batch cursor
768
+ * @param {number} batchSize - Size of each batch
769
+ * @param {Object} [options] - Cursor options
770
+ * @param {number} [options.startId] - Starting entry ID
771
+ * @param {string} [options.batchIdPrefix] - Prefix for batch IDs
772
+ * @returns {BatchCursor} Batch cursor for iteration
773
+ */
774
+ createBatchCursor(cursorName, batchSize, options = {}) {
775
+ const { startId, batchIdPrefix = 'batch' } = options;
776
+
777
+ // Set initial cursor position
778
+ const initialPosition =
779
+ startId !== undefined ? startId : this.entries.length > 0 ? this.entries[0].id : 0;
780
+
781
+ this.setCursor(cursorName, initialPosition);
782
+
783
+ return {
784
+ cursorName,
785
+ batchSize,
786
+ batchIdPrefix,
787
+
788
+ /**
789
+ * Get the next batch of entries
790
+ * @param {boolean} [moveCursor=true] - Whether to advance the cursor
791
+ * @returns {BatchCursorResult} Next batch result
792
+ */
793
+ next: (moveCursor = true) => {
794
+ const cursor = this.getCursor(cursorName);
795
+ if (!cursor) {
796
+ return { done: true, entries: [], batchDef: null };
797
+ }
798
+
799
+ const startId = cursor.position;
800
+ const endId = startId + batchSize;
801
+ const entries = this.slice(startId, endId);
802
+
803
+ if (entries.length === 0) {
804
+ return { done: true, entries: [], batchDef: null };
805
+ }
806
+
807
+ const batchDef = {
808
+ batchId: `${batchIdPrefix}_${Math.floor(startId / batchSize)}`,
809
+ startId,
810
+ endId: startId + entries.length,
811
+ size: entries.length,
812
+ timestamp: new Date(),
813
+ };
814
+
815
+ if (moveCursor) {
816
+ this.moveCursor(cursorName, batchDef.endId);
817
+ }
818
+
819
+ return {
820
+ done: false,
821
+ entries,
822
+ batchDef,
823
+ hasMore: this.entries.some((entry) => entry.id >= batchDef.endId),
824
+ };
825
+ },
826
+
827
+ /**
828
+ * Reset cursor to beginning or specific position
829
+ * @param {number} [position] - Position to reset to
830
+ */
831
+ reset: (position) => {
832
+ const resetPos =
833
+ position !== undefined ? position : this.entries.length > 0 ? this.entries[0].id : 0;
834
+ this.moveCursor(cursorName, resetPos);
835
+ },
836
+
837
+ /**
838
+ * Get cursor status
839
+ * @returns {Object} Cursor status information
840
+ */
841
+ getStatus: () => {
842
+ const cursor = this.getCursor(cursorName);
843
+ const stats = this.getStats();
844
+ return {
845
+ cursorName,
846
+ position: cursor?.position || 0,
847
+ lastMoved: cursor?.lastMoved,
848
+ batchSize,
849
+ remainingEntries: Math.max(0, stats.newestId - (cursor?.position || 0)),
850
+ bufferSize: stats.size,
851
+ };
852
+ },
853
+ };
854
+ }
855
+
856
+ /**
857
+ * Create multiple synchronized batch cursors that iterate over the same batches
858
+ * Useful for parallel processing where different workers need identical batch boundaries
859
+ * @param {string[]} cursorNames - Names for the batch cursors
860
+ * @param {number} batchSize - Size of each batch
861
+ * @param {Object} [options] - Cursor options
862
+ * @returns {Object} Map of cursor names to BatchCursor objects
863
+ */
864
+ createSynchronizedBatchCursors(cursorNames, batchSize, options = {}) {
865
+ const cursors = {};
866
+
867
+ // Create stable batch definitions first
868
+ const batchDefs = this.createStableBatches(batchSize, options);
869
+
870
+ // Create cursors with identical starting positions
871
+ const startPosition =
872
+ options.startId !== undefined
873
+ ? options.startId
874
+ : this.entries.length > 0
875
+ ? this.entries[0].id
876
+ : 0;
877
+
878
+ for (const cursorName of cursorNames) {
879
+ this.setCursor(cursorName, startPosition);
880
+
881
+ cursors[cursorName] = {
882
+ cursorName,
883
+ batchSize,
884
+ batchDefs,
885
+ currentBatchIndex: 0,
886
+
887
+ /**
888
+ * Get the next batch using stable batch definitions
889
+ * @param {boolean} [moveCursor=true] - Whether to advance the cursor
890
+ * @returns {BatchCursorResult} Next batch result
891
+ */
892
+ next: (moveCursor = true) => {
893
+ const cursor = this.getCursor(cursorName);
894
+ if (!cursor || cursors[cursorName].currentBatchIndex >= batchDefs.length) {
895
+ return { done: true, entries: [], batchDef: null };
896
+ }
897
+
898
+ const batchDef = batchDefs[cursors[cursorName].currentBatchIndex];
899
+ const entries = this.getBatchEntries(batchDef);
900
+
901
+ if (moveCursor) {
902
+ this.moveCursor(cursorName, batchDef.endId - 1);
903
+ cursors[cursorName].currentBatchIndex++;
904
+ }
905
+
906
+ return {
907
+ done: false,
908
+ entries,
909
+ batchDef,
910
+ hasMore: cursors[cursorName].currentBatchIndex < batchDefs.length - 1,
911
+ };
912
+ },
913
+
914
+ /**
915
+ * Reset cursor to beginning or specific batch
916
+ * @param {number} [batchIndex=0] - Batch index to reset to
917
+ */
918
+ reset: (batchIndex = 0) => {
919
+ cursors[cursorName].currentBatchIndex = Math.max(
920
+ 0,
921
+ Math.min(batchIndex, batchDefs.length - 1)
922
+ );
923
+ const batchDef = batchDefs[cursors[cursorName].currentBatchIndex];
924
+ if (batchDef) {
925
+ this.moveCursor(cursorName, batchDef.startId);
926
+ }
927
+ },
928
+
929
+ /**
930
+ * Get cursor status
931
+ * @returns {Object} Cursor status information
932
+ */
933
+ getStatus: () => {
934
+ const cursor = this.getCursor(cursorName);
935
+ return {
936
+ cursorName,
937
+ position: cursor?.position || 0,
938
+ lastMoved: cursor?.lastMoved,
939
+ batchSize,
940
+ currentBatchIndex: cursors[cursorName].currentBatchIndex,
941
+ totalBatches: batchDefs.length,
942
+ remainingBatches: batchDefs.length - cursors[cursorName].currentBatchIndex,
943
+ };
944
+ },
945
+ };
946
+ }
947
+
948
+ return cursors;
949
+ }
950
+
951
+ /**
952
+ * Get buffer statistics
953
+ * @returns {RingBufferStats} Current statistics
954
+ */
955
+ getStats() {
956
+ const size = this.entries.length;
957
+ const oldest = size > 0 ? this.entries[0] : null;
958
+ const newest = size > 0 ? this.entries[size - 1] : null;
959
+
960
+ return {
961
+ size,
962
+ capacity: this.maxSize,
963
+ totalAdded: this.totalAdded,
964
+ totalEvicted: this.totalEvicted,
965
+ oldestId: oldest?.id || 0,
966
+ newestId: newest?.id || 0,
967
+ oldestTimestamp: oldest?.timestamp || oldest?.ts || null,
968
+ newestTimestamp: newest?.timestamp || newest?.ts || null,
969
+ cursors: this.cursors.size,
970
+ lanes: this.lanes.size,
971
+ };
972
+ }
973
+
974
+ /**
975
+ * Clear all entries and reset counters
976
+ * @param {boolean} [keepCursors=false] - Whether to keep cursor positions
977
+ * @param {boolean} [keepLanes=false] - Whether to keep processing lanes
978
+ */
979
+ clear(keepCursors = false, keepLanes = false) {
980
+ this.entries = [];
981
+ this.totalAdded = 0;
982
+ this.totalEvicted = 0;
983
+
984
+ if (!keepCursors) {
985
+ this.cursors.clear();
986
+ }
987
+
988
+ if (!keepLanes) {
989
+ this.lanes.clear();
990
+ } else {
991
+ // Clear lane buffers but keep lane configurations
992
+ for (const lane of this.lanes.values()) {
993
+ lane.buffer = [];
994
+ lane.flushActive = false;
995
+ }
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Remove cursor
1001
+ * @param {string} name - Cursor name to remove
1002
+ * @returns {boolean} True if cursor was removed
1003
+ */
1004
+ removeCursor(name) {
1005
+ return this.cursors.delete(name);
1006
+ }
1007
+
1008
+ /**
1009
+ * Get all cursor names
1010
+ * @returns {string[]} Array of cursor names
1011
+ */
1012
+ getCursorNames() {
1013
+ return Array.from(this.cursors.keys());
1014
+ }
1015
+
1016
+ /**
1017
+ * Check if buffer is full
1018
+ * @returns {boolean} True if at capacity
1019
+ */
1020
+ isFull() {
1021
+ return this.entries.length >= this.maxSize;
1022
+ }
1023
+
1024
+ /**
1025
+ * Check if buffer is empty
1026
+ * @returns {boolean} True if empty
1027
+ */
1028
+ isEmpty() {
1029
+ return this.entries.length === 0;
1030
+ }
1031
+
1032
+ /**
1033
+ * Get current size
1034
+ * @returns {number} Number of entries
1035
+ */
1036
+ size() {
1037
+ return this.entries.length;
1038
+ }
1039
+
1040
+ /**
1041
+ * Get capacity
1042
+ * @returns {number} Maximum capacity
1043
+ */
1044
+ capacity() {
1045
+ return this.maxSize;
1046
+ }
1047
+
1048
+ /**
1049
+ * Iterator support - allows for...of loops
1050
+ * @returns {Iterator<RingBufferEntry>} Iterator over entries
1051
+ */
1052
+ *[Symbol.iterator]() {
1053
+ for (const entry of this.entries) {
1054
+ yield entry;
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Create a new ring buffer from this one with filtered entries
1060
+ * @param {Function} predicate - Filter function
1061
+ * @param {number} [maxSize] - Size of new buffer (defaults to same)
1062
+ * @returns {RingBuffer} New filtered ring buffer
1063
+ */
1064
+ createFiltered(predicate, maxSize = this.maxSize) {
1065
+ const newBuffer = new RingBuffer(maxSize);
1066
+ const filtered = this.entries.filter(predicate);
1067
+
1068
+ for (const entry of filtered) {
1069
+ newBuffer.push(entry.data, entry.meta);
1070
+ }
1071
+
1072
+ return newBuffer;
1073
+ }
1074
+ }