@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.
- package/.cursor/launch.json +30 -0
- package/.cursor/settings.json +20 -0
- package/.github/workflows/branch-protection.yml +22 -0
- package/.github/workflows/ci.yml +120 -0
- package/.prettierrc +6 -0
- package/.release-it.json +4 -1
- package/.vscode/launch.json +31 -0
- package/AGENTS.md +220 -0
- package/DEVELOPING.md +105 -0
- package/README.md +254 -0
- package/eslint.config.js +80 -0
- package/package.json +29 -17
- package/scripts/generate-test/index.js +29 -3
- package/scripts/runner/index.js +26 -0
- package/scripts/simple-editor/index.js +29 -18
- package/scripts/summarize-files/index.js +28 -4
- package/src/chains/README.md +30 -0
- package/src/chains/anonymize/README.md +21 -0
- package/src/chains/anonymize/index.examples.js +75 -0
- package/src/chains/anonymize/index.js +121 -0
- package/src/chains/anonymize/index.spec.js +78 -0
- package/src/chains/bulk-central-tendency/index.examples.js +138 -0
- package/src/chains/bulk-central-tendency/index.js +91 -0
- package/src/chains/bulk-filter/README.md +21 -0
- package/src/chains/bulk-filter/index.examples.js +22 -0
- package/src/chains/bulk-filter/index.js +58 -0
- package/src/chains/bulk-filter/index.spec.js +38 -0
- package/src/chains/bulk-find/README.md +16 -0
- package/src/chains/bulk-find/index.examples.js +20 -0
- package/src/chains/bulk-find/index.js +30 -0
- package/src/chains/bulk-find/index.spec.js +26 -0
- package/src/chains/bulk-group/README.md +23 -0
- package/src/chains/bulk-group/index.examples.js +18 -0
- package/src/chains/bulk-group/index.js +34 -0
- package/src/chains/bulk-group/index.spec.js +41 -0
- package/src/chains/bulk-map/README.md +43 -0
- package/src/chains/bulk-map/index.examples.js +17 -0
- package/src/chains/bulk-map/index.js +86 -0
- package/src/chains/bulk-map/index.spec.js +44 -0
- package/src/chains/bulk-reduce/README.md +12 -0
- package/src/chains/bulk-reduce/index.examples.js +15 -0
- package/src/chains/bulk-reduce/index.js +13 -0
- package/src/chains/bulk-reduce/index.spec.js +25 -0
- package/src/chains/bulk-score/README.md +16 -0
- package/src/chains/bulk-score/bulk-score-result.json +18 -0
- package/src/chains/bulk-score/index.examples.js +22 -0
- package/src/chains/bulk-score/index.js +133 -0
- package/src/chains/bulk-score/index.spec.js +30 -0
- package/src/chains/category-samples/README.md +61 -0
- package/src/chains/category-samples/index.examples.js +103 -0
- package/src/chains/category-samples/index.js +134 -0
- package/src/chains/collect-terms/README.md +12 -0
- package/src/chains/collect-terms/index.examples.js +16 -0
- package/src/chains/collect-terms/index.js +44 -0
- package/src/chains/collect-terms/index.spec.js +25 -0
- package/src/chains/date/README.md +12 -0
- package/src/chains/date/index.examples.js +47 -0
- package/src/chains/date/index.js +74 -0
- package/src/chains/date/index.spec.js +62 -0
- package/src/chains/disambiguate/README.md +22 -0
- package/src/chains/disambiguate/disambiguate-meanings-result.json +16 -0
- package/src/chains/disambiguate/index.examples.js +18 -0
- package/src/chains/disambiguate/index.js +92 -0
- package/src/chains/disambiguate/index.spec.js +25 -0
- package/src/chains/dismantle/README.md +67 -0
- package/src/chains/dismantle/dismantle.examples.js +27 -0
- package/src/chains/dismantle/index.js +6 -17
- package/src/chains/dismantle/index.spec.js +1 -2
- package/src/chains/expect/README.md +171 -0
- package/src/chains/expect/index.examples.js +146 -0
- package/src/chains/expect/index.js +173 -0
- package/src/chains/expect/index.spec.js +324 -0
- package/src/chains/filter-ambiguous/README.md +11 -0
- package/src/chains/filter-ambiguous/index.examples.js +20 -0
- package/src/chains/filter-ambiguous/index.js +49 -0
- package/src/chains/filter-ambiguous/index.spec.js +31 -0
- package/src/chains/glossary/README.md +19 -0
- package/src/chains/glossary/index.examples.js +386 -0
- package/src/chains/glossary/index.js +75 -0
- package/src/chains/glossary/index.spec.js +19 -0
- package/src/chains/intersections/README.md +152 -0
- package/src/chains/intersections/index.examples.js +279 -0
- package/src/chains/intersections/index.js +366 -0
- package/src/chains/intersections/intersection-result.json +38 -0
- package/src/chains/list/index.examples.js +12 -16
- package/src/chains/list/index.js +106 -53
- package/src/chains/list/index.spec.js +8 -9
- package/src/chains/list/list-result.json +16 -0
- package/src/chains/llm-logger/README.md +208 -0
- package/src/chains/llm-logger/index.js +205 -0
- package/src/chains/llm-logger/index.spec.js +330 -0
- package/src/chains/questions/index.examples.js +2 -1
- package/src/chains/questions/index.js +14 -15
- package/src/chains/scan-js/index.js +6 -9
- package/src/chains/set-interval/README.md +81 -0
- package/src/chains/set-interval/index.examples.js +36 -0
- package/src/chains/set-interval/index.js +131 -0
- package/src/chains/set-interval/index.spec.js +70 -0
- package/src/chains/socratic/README.md +17 -0
- package/src/chains/socratic/index.js +64 -0
- package/src/chains/socratic/index.spec.js +24 -0
- package/src/chains/sort/index.examples.js +3 -7
- package/src/chains/sort/index.js +65 -15
- package/src/chains/sort/index.spec.js +5 -8
- package/src/chains/sort/sort-result.json +16 -0
- package/src/chains/summary-map/README.md +9 -1
- package/src/chains/summary-map/index.examples.js +9 -2
- package/src/chains/summary-map/index.js +43 -25
- package/src/chains/summary-map/index.spec.js +78 -3
- package/src/chains/test/index.js +9 -13
- package/src/chains/test-advice/index.js +4 -5
- package/src/chains/themes/README.md +20 -0
- package/src/chains/themes/index.examples.js +17 -0
- package/src/chains/themes/index.js +28 -0
- package/src/chains/themes/index.spec.js +19 -0
- package/src/chains/veiled-variants/index.examples.js +18 -0
- package/src/chains/veiled-variants/index.js +107 -0
- package/src/chains/veiled-variants/index.spec.js +40 -0
- package/src/constants/common.js +0 -2
- package/src/constants/models.js +172 -0
- package/src/index.js +178 -18
- package/src/json-schemas/README.md +13 -0
- package/src/json-schemas/index.js +8 -14
- package/src/json-schemas/schema-dot-org-photograph.json +11 -5
- package/src/json-schemas/schema-dot-org-place.json +78 -5
- package/src/lib/README.md +26 -0
- package/src/lib/bulk-filter/README.md +22 -0
- package/src/lib/bulk-filter/index.examples.js +27 -0
- package/src/lib/bulk-filter/index.js +63 -0
- package/src/lib/bulk-filter/index.spec.js +38 -0
- package/src/lib/bulk-find/README.md +18 -0
- package/src/lib/bulk-find/index.examples.js +19 -0
- package/src/lib/bulk-find/index.js +30 -0
- package/src/lib/bulk-find/index.spec.js +41 -0
- package/src/lib/chatgpt/index.js +63 -43
- package/src/lib/combinations/index.js +30 -0
- package/src/lib/combinations/index.spec.js +23 -0
- package/src/lib/functional/index.js +28 -0
- package/src/lib/logger-service/index.js +32 -0
- package/src/lib/parse-js-parts/index.js +9 -21
- package/src/lib/parse-llm-list/README.md +39 -0
- package/src/lib/parse-llm-list/index.js +54 -0
- package/src/lib/parse-llm-list/index.spec.js +59 -0
- package/src/lib/path-aliases/index.js +1 -3
- package/src/lib/path-aliases/index.spec.js +2 -8
- package/src/lib/pave/index.js +4 -4
- package/src/lib/pave/index.spec.js +6 -3
- package/src/lib/prompt-cache/index.js +14 -10
- package/src/lib/retry/index.js +11 -8
- package/src/lib/ring-buffer/README.md +460 -0
- package/src/lib/ring-buffer/index.js +1074 -0
- package/src/lib/search-best-first/city-walk.spec.js +37 -0
- package/src/lib/search-best-first/index.js +42 -11
- package/src/lib/search-best-first/index.spec.js +35 -0
- package/src/lib/search-js-files/index.js +44 -47
- package/src/lib/search-js-files/scan-file.js +10 -21
- package/src/lib/shorten-text/index.js +2 -7
- package/src/lib/shorten-text/index.spec.js +3 -3
- package/src/lib/strip-response/index.js +2 -7
- package/src/lib/template-replace/index.js +23 -0
- package/src/lib/template-replace/index.spec.js +60 -0
- package/src/lib/to-date/index.js +11 -0
- package/src/lib/to-number/index.js +1 -1
- package/src/lib/transcribe/index.js +26 -9
- package/src/prompts/README.md +3 -1
- package/src/prompts/as-object-with-schema.js +3 -8
- package/src/prompts/as-schema-org-text.js +10 -2
- package/src/prompts/code-features.js +1 -5
- package/src/prompts/constants.js +27 -27
- package/src/prompts/generate-collection.js +1 -1
- package/src/prompts/intent.js +16 -22
- package/src/prompts/select-from-threshold.js +1 -2
- package/src/prompts/sort.js +4 -8
- package/src/prompts/style.js +4 -7
- package/src/prompts/wrap-list.js +1 -4
- package/src/services/llm-model/global-overrides.spec.js +432 -0
- package/src/services/llm-model/index.js +234 -40
- package/src/services/llm-model/model.js +2 -2
- package/src/services/llm-model/negotiate.spec.js +447 -0
- package/src/services/redis/index.js +70 -7
- package/src/test/setup.js +20 -0
- package/src/verblets/README.md +26 -0
- package/src/verblets/auto/index.examples.js +12 -9
- package/src/verblets/auto/index.js +10 -10
- package/src/verblets/auto/index.spec.js +4 -6
- package/src/verblets/bool/README.md +36 -0
- package/src/verblets/bool/index.examples.js +53 -1
- package/src/verblets/bool/index.js +6 -9
- package/src/verblets/bool/index.spec.js +1 -3
- package/src/verblets/central-tendency/README.md +166 -0
- package/src/verblets/central-tendency/central-tendency-result.json +24 -0
- package/src/verblets/central-tendency/index.examples.js +196 -0
- package/src/verblets/central-tendency/index.js +171 -0
- package/src/verblets/central-tendency/index.spec.js +148 -0
- package/src/verblets/enum/index.examples.js +1 -4
- package/src/verblets/enum/index.js +7 -4
- package/src/verblets/expect/README.md +64 -0
- package/src/verblets/expect/index.examples.js +109 -0
- package/src/verblets/expect/index.js +75 -0
- package/src/verblets/expect/index.spec.js +127 -0
- package/src/verblets/intent/index.examples.js +95 -7
- package/src/verblets/intent/index.js +56 -68
- package/src/verblets/intersection/README.md +16 -0
- package/src/verblets/intersection/index.examples.js +89 -0
- package/src/verblets/intersection/index.js +84 -0
- package/src/verblets/intersection/index.spec.js +60 -0
- package/src/verblets/intersection/intersection-result.json +16 -0
- package/src/verblets/list-expand/README.md +10 -0
- package/src/verblets/list-expand/index.examples.js +14 -0
- package/src/verblets/list-expand/index.js +104 -0
- package/src/verblets/list-expand/index.spec.js +18 -0
- package/src/verblets/list-expand/list-expand-result.json +16 -0
- package/src/verblets/list-filter/README.md +22 -0
- package/src/verblets/list-filter/index.examples.js +26 -0
- package/src/verblets/list-filter/index.js +18 -0
- package/src/verblets/list-filter/index.spec.js +19 -0
- package/src/verblets/list-find/README.md +11 -0
- package/src/verblets/list-find/index.examples.js +15 -0
- package/src/verblets/list-find/index.js +17 -0
- package/src/verblets/list-find/index.spec.js +19 -0
- package/src/verblets/list-group/README.md +16 -0
- package/src/verblets/list-group/index.examples.js +16 -0
- package/src/verblets/list-group/index.js +112 -0
- package/src/verblets/list-group/index.spec.js +35 -0
- package/src/verblets/list-group/list-group-result.json +16 -0
- package/src/verblets/list-map/README.md +11 -0
- package/src/verblets/list-map/index.examples.js +15 -0
- package/src/verblets/list-map/index.js +26 -0
- package/src/verblets/list-map/index.spec.js +17 -0
- package/src/verblets/list-reduce/README.md +10 -0
- package/src/verblets/list-reduce/index.examples.js +14 -0
- package/src/verblets/list-reduce/index.js +21 -0
- package/src/verblets/list-reduce/index.spec.js +27 -0
- package/src/verblets/list-reduce/index.spec.jsx +27 -0
- package/src/verblets/name/README.md +15 -0
- package/src/verblets/name/index.examples.js +28 -0
- package/src/verblets/name/index.js +19 -0
- package/src/verblets/name/index.spec.js +33 -0
- package/src/verblets/name-similar-to/README.md +26 -0
- package/src/verblets/name-similar-to/index.examples.js +18 -0
- package/src/verblets/name-similar-to/index.js +20 -0
- package/src/verblets/name-similar-to/index.spec.js +13 -0
- package/src/verblets/number/index.examples.js +173 -7
- package/src/verblets/number/index.js +5 -2
- package/src/verblets/number/index.spec.js +1 -3
- package/src/verblets/number-with-units/index.examples.js +5 -1
- package/src/verblets/number-with-units/index.js +74 -9
- package/src/verblets/number-with-units/number-with-units-result.json +23 -0
- package/src/verblets/schema-org/index.examples.js +2 -7
- package/src/verblets/schema-org/index.js +32 -3
- package/src/verblets/sentiment/README.md +10 -0
- package/src/verblets/sentiment/index.examples.js +20 -0
- package/src/verblets/sentiment/index.js +9 -0
- package/src/verblets/sentiment/index.spec.js +20 -0
- package/src/verblets/to-object/index.js +10 -15
- package/src/verblets/to-object/index.spec.js +1 -4
- package/.eslintrc.json +0 -42
- package/docs/README.md +0 -41
- package/docs/babel.config.js +0 -3
- package/docs/blog/2019-05-28-first-blog-post.md +0 -12
- package/docs/blog/2019-05-29-long-blog-post.md +0 -44
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -20
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.md +0 -25
- package/docs/blog/authors.yml +0 -17
- package/docs/docs/api/bool.md +0 -74
- package/docs/docs/api/search.md +0 -51
- package/docs/docs/intro.md +0 -47
- package/docs/docs/tutorial-basics/_category_.json +0 -8
- package/docs/docs/tutorial-basics/congratulations.md +0 -23
- package/docs/docs/tutorial-basics/create-a-blog-post.md +0 -34
- package/docs/docs/tutorial-basics/create-a-document.md +0 -57
- package/docs/docs/tutorial-basics/create-a-page.md +0 -43
- package/docs/docs/tutorial-basics/deploy-your-site.md +0 -31
- package/docs/docs/tutorial-basics/markdown-features.mdx +0 -152
- package/docs/docs/tutorial-extras/_category_.json +0 -7
- package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
- package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
- package/docs/docs/tutorial-extras/manage-docs-versions.md +0 -55
- package/docs/docs/tutorial-extras/translate-your-site.md +0 -88
- package/docs/docusaurus.config.js +0 -120
- package/docs/package.json +0 -44
- package/docs/sidebars.js +0 -31
- package/docs/src/components/HomepageFeatures/index.js +0 -61
- package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- package/docs/src/css/custom.css +0 -30
- package/docs/src/pages/index.js +0 -43
- package/docs/src/pages/index.module.css +0 -23
- package/docs/src/pages/markdown-page.md +0 -7
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.svg +0 -1
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/src/constants/openai.js +0 -65
- /package/{.vite.config.examples.js → .vitest.config.examples.js} +0 -0
- /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
|
+
}
|