ink-cartridge 3.6.1

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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/dist/binary-storage/BinaryStorage.d.ts +308 -0
  4. package/dist/binary-storage/BinaryStorage.js +548 -0
  5. package/dist/binary-storage/StreamingReader.d.ts +53 -0
  6. package/dist/binary-storage/StreamingReader.js +530 -0
  7. package/dist/binary-storage/index.d.ts +91 -0
  8. package/dist/binary-storage/index.js +91 -0
  9. package/dist/binary-storage/types.d.ts +400 -0
  10. package/dist/binary-storage/types.js +15 -0
  11. package/dist/cli/index.d.ts +2 -0
  12. package/dist/cli/index.js +214 -0
  13. package/dist/cli/initTheme.d.ts +14 -0
  14. package/dist/cli/initTheme.js +124 -0
  15. package/dist/cli/makeLanguageType.d.ts +46 -0
  16. package/dist/cli/makeLanguageType.js +264 -0
  17. package/dist/cli/makeThemeType.d.ts +43 -0
  18. package/dist/cli/makeThemeType.js +222 -0
  19. package/dist/components/badge/Badge.d.ts +7 -0
  20. package/dist/components/badge/Badge.js +9 -0
  21. package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
  22. package/dist/components/dialog/ConfirmDialog.js +57 -0
  23. package/dist/components/dialog/types.d.ts +20 -0
  24. package/dist/components/dialog/types.js +1 -0
  25. package/dist/components/divider/Divider.d.ts +8 -0
  26. package/dist/components/divider/Divider.js +10 -0
  27. package/dist/components/fold/Fold.d.ts +3 -0
  28. package/dist/components/fold/Fold.js +44 -0
  29. package/dist/components/fold/types.d.ts +28 -0
  30. package/dist/components/fold/types.js +1 -0
  31. package/dist/components/form/Field.d.ts +30 -0
  32. package/dist/components/form/Field.js +59 -0
  33. package/dist/components/form/Form.d.ts +32 -0
  34. package/dist/components/form/Form.js +163 -0
  35. package/dist/components/form/context.d.ts +14 -0
  36. package/dist/components/form/context.js +20 -0
  37. package/dist/components/form/types.d.ts +115 -0
  38. package/dist/components/form/types.js +1 -0
  39. package/dist/components/key-hint/KeyHint.d.ts +10 -0
  40. package/dist/components/key-hint/KeyHint.js +12 -0
  41. package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
  42. package/dist/components/multi-select/MultiSelectInput.js +287 -0
  43. package/dist/components/multi-select/types.d.ts +100 -0
  44. package/dist/components/multi-select/types.js +1 -0
  45. package/dist/components/number-input/NumberInput.d.ts +11 -0
  46. package/dist/components/number-input/NumberInput.js +44 -0
  47. package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
  48. package/dist/components/progress-bar/ProgressBar.js +9 -0
  49. package/dist/components/search-input/SearchInput.d.ts +10 -0
  50. package/dist/components/search-input/SearchInput.js +15 -0
  51. package/dist/components/select/SelectInput.d.ts +100 -0
  52. package/dist/components/select/SelectInput.js +147 -0
  53. package/dist/components/select/types.d.ts +63 -0
  54. package/dist/components/select/types.js +1 -0
  55. package/dist/components/spinner/Spinner.d.ts +12 -0
  56. package/dist/components/spinner/Spinner.js +24 -0
  57. package/dist/components/tabs/Tabs.d.ts +3 -0
  58. package/dist/components/tabs/Tabs.js +57 -0
  59. package/dist/components/tabs/types.d.ts +32 -0
  60. package/dist/components/tabs/types.js +1 -0
  61. package/dist/components/text/TextInput.d.ts +39 -0
  62. package/dist/components/text/TextInput.js +233 -0
  63. package/dist/components/text/types.d.ts +62 -0
  64. package/dist/components/text/types.js +1 -0
  65. package/dist/dev/dev-screen.d.ts +36 -0
  66. package/dist/dev/dev-screen.js +142 -0
  67. package/dist/dev/entrance.d.ts +51 -0
  68. package/dist/dev/entrance.js +67 -0
  69. package/dist/dev/globalKey-display.d.ts +2 -0
  70. package/dist/dev/globalKey-display.js +8 -0
  71. package/dist/dev/types.d.ts +9 -0
  72. package/dist/dev/types.js +1 -0
  73. package/dist/index.d.ts +46 -0
  74. package/dist/index.js +50 -0
  75. package/dist/keyboard/check-global-key.d.ts +18 -0
  76. package/dist/keyboard/check-global-key.js +52 -0
  77. package/dist/keyboard/context.d.ts +398 -0
  78. package/dist/keyboard/context.js +9 -0
  79. package/dist/keyboard/global-key-processor/index.d.ts +16 -0
  80. package/dist/keyboard/global-key-processor/index.js +75 -0
  81. package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
  82. package/dist/keyboard/global-sequence-processor/index.js +165 -0
  83. package/dist/keyboard/hook.d.ts +51 -0
  84. package/dist/keyboard/hook.js +105 -0
  85. package/dist/keyboard/index.d.ts +5 -0
  86. package/dist/keyboard/index.js +3 -0
  87. package/dist/keyboard/keyNormalizer.d.ts +37 -0
  88. package/dist/keyboard/keyNormalizer.js +119 -0
  89. package/dist/keyboard/layer-handler.d.ts +57 -0
  90. package/dist/keyboard/layer-handler.js +274 -0
  91. package/dist/keyboard/modal-processor/index.d.ts +16 -0
  92. package/dist/keyboard/modal-processor/index.js +97 -0
  93. package/dist/keyboard/overlay-processor/index.d.ts +16 -0
  94. package/dist/keyboard/overlay-processor/index.js +30 -0
  95. package/dist/keyboard/pipeline/chain.d.ts +10 -0
  96. package/dist/keyboard/pipeline/chain.js +45 -0
  97. package/dist/keyboard/pipeline/context.d.ts +34 -0
  98. package/dist/keyboard/pipeline/context.js +42 -0
  99. package/dist/keyboard/pipeline/index.d.ts +3 -0
  100. package/dist/keyboard/pipeline/index.js +2 -0
  101. package/dist/keyboard/provider.d.ts +32 -0
  102. package/dist/keyboard/provider.js +985 -0
  103. package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
  104. package/dist/keyboard/screen-stack-processor/index.js +32 -0
  105. package/dist/keyboard/types.d.ts +632 -0
  106. package/dist/keyboard/types.js +1 -0
  107. package/dist/language/context.d.ts +7 -0
  108. package/dist/language/context.js +7 -0
  109. package/dist/language/hook.d.ts +19 -0
  110. package/dist/language/hook.js +26 -0
  111. package/dist/language/index.d.ts +3 -0
  112. package/dist/language/index.js +2 -0
  113. package/dist/language/provider.d.ts +30 -0
  114. package/dist/language/provider.js +155 -0
  115. package/dist/language/types.d.ts +124 -0
  116. package/dist/language/types.js +1 -0
  117. package/dist/screen/ModalContext.d.ts +11 -0
  118. package/dist/screen/ModalContext.js +12 -0
  119. package/dist/screen/OverlayContext.d.ts +9 -0
  120. package/dist/screen/OverlayContext.js +10 -0
  121. package/dist/screen/context.d.ts +61 -0
  122. package/dist/screen/context.js +8 -0
  123. package/dist/screen/current-screen.d.ts +16 -0
  124. package/dist/screen/current-screen.js +38 -0
  125. package/dist/screen/hook.d.ts +13 -0
  126. package/dist/screen/hook.js +20 -0
  127. package/dist/screen/index.d.ts +7 -0
  128. package/dist/screen/index.js +5 -0
  129. package/dist/screen/provider.d.ts +112 -0
  130. package/dist/screen/provider.js +577 -0
  131. package/dist/screen/registry.d.ts +27 -0
  132. package/dist/screen/registry.js +71 -0
  133. package/dist/screen/types.d.ts +227 -0
  134. package/dist/screen/types.js +1 -0
  135. package/dist/storage/index.d.ts +45 -0
  136. package/dist/storage/index.js +46 -0
  137. package/dist/storage/storage.d.ts +139 -0
  138. package/dist/storage/storage.js +236 -0
  139. package/dist/storage/types.d.ts +145 -0
  140. package/dist/storage/types.js +1 -0
  141. package/dist/theme/context.d.ts +7 -0
  142. package/dist/theme/context.js +7 -0
  143. package/dist/theme/hook.d.ts +11 -0
  144. package/dist/theme/hook.js +18 -0
  145. package/dist/theme/index.d.ts +4 -0
  146. package/dist/theme/index.js +2 -0
  147. package/dist/theme/provider.d.ts +18 -0
  148. package/dist/theme/provider.js +308 -0
  149. package/dist/theme/types.d.ts +53 -0
  150. package/dist/theme/types.js +1 -0
  151. package/package.json +71 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BAIGAO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # ink-cartridge
2
+
3
+ > Ready-to-use Ink components and tools for building terminal UI applications.
4
+
5
+ [![CI](https://github.com/BAIGAOa/ink-cartridge/actions/workflows/ci.yml/badge.svg)](https://github.com/BAIGAOa/ink-trc/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/ink-cartridge.svg)](https://www.npmjs.com/package/ink-cartridge)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+
10
+ ---
11
+
12
+ ## Getting Started
13
+
14
+ ### Quick Start (scaffold a new project)
15
+
16
+ ```bash
17
+ npx ink-cartridge init my-tui
18
+ cd my-tui
19
+ npm start
20
+ ```
21
+
22
+ ### Install in existing project
23
+
24
+ ```bash
25
+ npm install ink-cartridge
26
+ ```
27
+
28
+ ### Requirements
29
+
30
+ | Dependency | Minimum Version |
31
+ | ---------- | --------------- |
32
+ | Node.js | 22 |
33
+ | ink | 5 |
34
+ | react | 18 |
35
+
36
+ ---
37
+
38
+ ## Design Philosophy
39
+
40
+ ink-cartridge aims to make **screen management** and **keyboard event handling** in Ink applications composable, maintainable, and type-safe.
41
+
42
+ ### Screen as Component
43
+
44
+ In ink-cartridge, **every React component is a "screen"**. Register them into a **screen tree** via `registerComponent`, then navigate the tree with `skip` / `back` / `gotoScreen`. This design makes screen navigation predictable and eliminates the chaos of hand-written conditional rendering (`if-else` / `switch`).
45
+
46
+ ### Layered Keyboard Events
47
+
48
+ No more global `useInput` cluttered with `if-else` chains. ink-cartridge's keyboard system maintains **per-screen-layer** key bindings. Events bubble from **top to bottom** through the stack, with four key mechanisms:
49
+
50
+ - **Sequence (`boundSequence`)** — Multi-key chords (e.g. `gg`, `dd`, `cw`) with timeout and exclusive/non-exclusive modes. Sequences take priority over ordinary bindings.
51
+ - **Blocked Key (`blockedKey`)** — Let a key pass through the current layer to be handled below
52
+ - **Stop (`stop`)** — Prevent a key from propagating to lower layers. Supports `stopAction` mode to block by shortcut action ID instead of literal key name
53
+ - **Global Key (`globalKeys`)** — Shortcuts independent of the screen stack
54
+
55
+ #### Finer-grained partitioning
56
+
57
+ Within the same level, identical keys are also in competition. To address this, we have a complete **focus system**.
58
+ Each level maintains a set of focus targets, and only one focus is active at any given time within a level. Each focus target has its own bound keyboard operations. Only the activated focus target is eligible to execute them during event dispatching in **useInput**.
59
+
60
+ **For more details, please refer to the API documentation.**
61
+
62
+ ### Shortcut Actions
63
+
64
+ Decouple operation definition from key binding with `defineShortcutAction`. Register named operations once, then reference them by string ID in `boundKeyboard`, `globalKeys`, and `stop`:
65
+
66
+ ```tsx
67
+ defineShortcutAction([
68
+ { actionId: 'quit', action: () => process.exit() },
69
+ ]);
70
+ boundKeyboard(['q'], 'quit');
71
+ globalKeys([{ key: 'escape', operate: 'quit' }]);
72
+ stop(['quit'], { stopAction: true });
73
+ ```
74
+
75
+ ### Sequence Actions
76
+
77
+ Decouple sequence operation definition from key binding with `defineSequenceAction`. Register named sequence operations once, then reference them by string ID in `globalSequence` and `boundSequence`:
78
+
79
+ ```tsx
80
+ defineSequenceAction([
81
+ { sequenceActionId: 'save', action: () => saveFile(), keys: ['ctrl+s'] },
82
+ ]);
83
+
84
+ // Global sequence referencing the action
85
+ globalSequence([{ keys: ['ctrl+s'], operate: 'save' }]);
86
+
87
+ // Screen-level sequence using the action's predefined keys
88
+ boundSequence('save');
89
+
90
+ // Modify an existing action's keys dynamically
91
+ modifySequenceAction('save', ['ctrl+shift+s']);
92
+ ```
93
+
94
+ ### Overlay System
95
+
96
+ `openOverlay()` and `closeOverlay()` provide floating dialogs on top of the screen stack. Combined with the keyboard system, overlays intercept keys before they reach the underlying screen — ideal for confirmation dialogs, modals, and pop-up menus.
97
+
98
+ ### Module-Level Functions
99
+
100
+ Navigation functions (`skip`, `back`, `gotoScreen`, `openOverlay`, `closeOverlay`) work both inside React components (via hooks) and as **module-level imports** in any `.ts` / `.tsx` file. This allows non-UI layers — game engines, state managers, etc. — to trigger screen transitions.
101
+
102
+ ### Type Safety
103
+
104
+ Every API provides full TypeScript type inference. Functions like `skip`, `gotoScreen`, and `openOverlay` automatically infer parameter types from your component's props, catching errors at compile time.
105
+
106
+ ---
107
+
108
+ ## ⚠️ Important: Component Nesting Order
109
+
110
+ `KeyboardProvider` **must** be nested inside `ScenarioManagementProvider`, because it depends on the screen context to obtain the current screen stack.
111
+
112
+ ```tsx
113
+ {/* ❌ Wrong: KeyboardProvider outside screen context */}
114
+ <KeyboardProvider>
115
+ <ScenarioManagementProvider defaultScreen={Menu}>
116
+ ...
117
+ </ScenarioManagementProvider>
118
+ </KeyboardProvider>
119
+
120
+ {/* ✅ Correct: KeyboardProvider inside screen context */}
121
+ <ScenarioManagementProvider defaultScreen={Menu}>
122
+ <KeyboardProvider>
123
+ ...
124
+ </KeyboardProvider>
125
+ </ScenarioManagementProvider>
126
+ ```
127
+
128
+ > The screen system can be used independently without `KeyboardProvider`; but the keyboard system requires the screen context.
129
+
130
+ ---
131
+
132
+ ## Documentation
133
+
134
+ - **[Screen Management System](docs/screen.md)** — `registerComponent`, `ScenarioManagementProvider`, `CurrentScreen`, `useScreenSystem`, `skip` / `back` / `gotoScreen` / `openOverlay` / `closeOverlay`
135
+ - **[Keyboard System](docs/keyboard.md)** — `KeyboardProvider`, `useKeyboard`, `boundKeyboard`, `boundSequence`, `blockedKey`, `stop`, `globalKeys`, `defineShortcutAction`, focus management
136
+ - **[Internationalization](docs/language.md)** — `LanguageProvider`, `useI18n`, `t()` translation with interpolation, language switching, **`ink-cartridge makeLanguageType`** CLI for compile-time type-safe translation keys
137
+ - **[Theme System](docs/theme.md)** — `ThemeProvider`, `useTheme`, There is also a companion type generator and theme profile generator
138
+ - **[Persistence System](docs/storage.md)** — `createStorage`, typed key-value JSON storage with automatic type validation, atomic writes, and zero config
139
+ - **[Binary Storage System](docs/binary-storage.md)** — `createBinaryStorage` for sequential typed binary streams with positional cursors, and `createStreamingReader` for memory-efficient streaming of large files (500 MB+) with backpressure support
140
+
141
+ ---
142
+
143
+ ## Components
144
+
145
+ ### Selection & Input
146
+ - **[SelectInput](src/components/select/README.md)** — Single-select list with focus-aware keyboard navigation
147
+ - **[MultiSelectInput](src/components/multi-select/README.md)** — Multi-select list with checkbox toggling (Space to toggle, Enter to submit)
148
+ - **[TextInput](src/components/text/README.md)** — Text input with cursor, mask, and focus system integration
149
+ - **[SearchInput](src/components/search-input/README.md)** — Search field with 🔍 icon and Esc-to-clear
150
+ - **[NumberInput](src/components/number-input/README.md)** — Numeric stepper with min/max/step and keyboard controls
151
+
152
+ ### Display & Feedback
153
+ - **[Spinner](src/components/spinner/README.md)** — Animated spinner with multiple preset styles
154
+ - **[ProgressBar](src/components/progress-bar/README.md)** — Customizable progress bar with percentage display
155
+ - **[Badge](src/components/badge/README.md)** — Colored label/tag component
156
+ - **[KeyHint](src/components/key-hint/README.md)** — Keyboard shortcut hint bar (`[S] Save`)
157
+
158
+ ### Navigation
159
+ - **[Tabs](src/components/tabs/README.md)** — Tabbed panel with keyboard navigation and focus system integration
160
+ - **[Fold](src/components/fold/README.md)** — Collapsible panel with preview and Space-toggle, integrated with focus system
161
+
162
+ ### Layout
163
+ - **[Divider](src/components/divider/README.md)** — Horizontal separator with optional centered label
164
+
165
+ ### Form
166
+ - **[Form & Field](src/components/form/README.md)** — Context‑based form system with validation, error focus, and Ctrl+Enter submit
167
+
168
+ ### Dialog
169
+ - **[ConfirmDialog](src/components/dialog/README.md)** — Modal confirmation dialog with two buttons, designed for the overlay system
170
+
171
+ ---
172
+
173
+ ## Quick Overview
174
+
175
+ ```tsx
176
+ import React, { useEffect } from 'react';
177
+ import { Box, Text, render } from 'ink';
178
+ import {
179
+ registerComponent,
180
+ ScenarioManagementProvider,
181
+ CurrentScreen,
182
+ useScreenSystem,
183
+ KeyboardProvider,
184
+ useKeyboard,
185
+ ConfirmDialog,
186
+ } from 'ink-cartridge';
187
+
188
+ // ── Register screens ──
189
+ function Menu() {
190
+ const { skip } = useScreenSystem();
191
+ const { boundKeyboard } = useKeyboard();
192
+ useEffect(() => {
193
+ boundKeyboard(['s'], () => skip(Game, { level: 1 }));
194
+ }, []);
195
+ return (
196
+ <Box>
197
+ <Text>Main Menu — Press S to start</Text>
198
+ </Box>
199
+ );
200
+ }
201
+ registerComponent(Menu, {});
202
+
203
+ function Game({ level }: { level: number }) {
204
+ const { back } = useScreenSystem();
205
+ const { boundKeyboard } = useKeyboard();
206
+ useEffect(() => {
207
+ boundKeyboard(['b'], () => back());
208
+ }, []);
209
+ return (
210
+ <Box>
211
+ <Text>Level {level} — Press B to go back</Text>
212
+ </Box>
213
+ );
214
+ }
215
+ registerComponent(Game, { level: 1 }, { parent: Menu });
216
+
217
+ // Register the dialog so it can be used with openOverlay()
218
+ registerComponent(ConfirmDialog, {
219
+ title: '', message: '', onConfirm: () => {}, onCancel: () => {},
220
+ });
221
+
222
+ // ── Wire up ──
223
+ function App() {
224
+ return (
225
+ <KeyboardProvider>
226
+ <CurrentScreen />
227
+ </KeyboardProvider>
228
+ );
229
+ }
230
+
231
+ render(
232
+ <ScenarioManagementProvider defaultScreen={Menu}>
233
+ <App />
234
+ </ScenarioManagementProvider>,
235
+ );
236
+ ```
237
+
238
+ ---
239
+
240
+
241
+ ## Other
242
+
243
+ I admit I thought about some things too quickly at the beginning of the project.
244
+ For example, the method of **blockedKeys**. Why is it called that? I don't know. Maybe I didn't think about it at that time.
245
+ Actually, it should be called **penetration**, but I don't want to change it.
246
+
247
+ ## License
248
+
249
+ [MIT](LICENSE)
@@ -0,0 +1,308 @@
1
+ import type { BinaryStorageOptions, BinaryStorageAPI } from './types.js';
2
+ /**
3
+ * Sequential binary key-value store backed by a single file.
4
+ *
5
+ * ## Architecture
6
+ *
7
+ * The entire file is loaded into a single in-memory `Buffer` on
8
+ * construction (lazy — only if the file already exists). Two
9
+ * cursors track the current position:
10
+ *
11
+ * - `readPos` — where the next `read.*()` will start reading.
12
+ * Starts at 0. Advanced each time a value is consumed.
13
+ *
14
+ * - `writePos` — where the next `write.*()` will append data.
15
+ * Starts at the file size (or 0 for a new file). Advanced
16
+ * each time a value is written.
17
+ *
18
+ * Because both cursors point into the same Buffer, you can write
19
+ * values, then immediately read them back by resetting the read
20
+ * cursor — no disk round-trip needed.
21
+ *
22
+ * ## Write path
23
+ *
24
+ * 1. `write.num(v)` calls `enqueue(() => appendNum(v))`.
25
+ * 2. `enqueue` chains the task onto the internal `pending` promise
26
+ * so writes are strictly ordered.
27
+ * 3. `appendNum` calls `growBuf()` to ensure capacity, writes
28
+ * `[TypeTag.Number][8-byte float64 LE]` at `writePos`, then
29
+ * advances `writePos`.
30
+ * 4. If `flush: true` (default), `forceFlush()` writes the buffer
31
+ * to disk atomically (temp file → rename).
32
+ *
33
+ * ## Read path
34
+ *
35
+ * 1. `read.num()` calls `readTyped(TypeTag.Number, ...)`.
36
+ * 2. `readTyped` calls `readTag()` which checks `readPos < writePos`
37
+ * (end-of-stream guard) and returns the byte at `readPos`.
38
+ * 3. If the tag matches `TypeTag.Number`, `readPos` advances past
39
+ * the tag and the payload reader (`readNumPayload`) is invoked.
40
+ * 4. `readNumPayload` reads 8 bytes at `readPos` as float64 LE,
41
+ * advances `readPos`, and returns the number.
42
+ *
43
+ * ## Concurrency model
44
+ *
45
+ * Writes are serialised through a promise chain (`this.pending`).
46
+ * The chain uses `.then(task, task)` — the second argument means
47
+ * that even if one write task rejects, subsequent writes still run.
48
+ * Reads do NOT go through the queue because they are synchronous
49
+ * operations on the in-memory buffer (the `async` wrapper is for
50
+ * API consistency only).
51
+ *
52
+ * ## Atomicity guarantee
53
+ *
54
+ * Flush uses the same temp-file-then-rename pattern as the JSON
55
+ * {@link Storage}: write to `file.bin.tmp`, then `fs.renameSync`.
56
+ * On POSIX systems, `rename` is atomic — the on-disk file is
57
+ * always either the complete old version or the complete new
58
+ * version. A crash mid-write leaves the original file intact.
59
+ *
60
+ * ## Buffer growth strategy
61
+ *
62
+ * {@link growBuf} doubles the buffer capacity when it runs out
63
+ * of space, with a minimum increment of {@link GROW_STEP} (4 KiB).
64
+ * This means writing 10,000 small values allocates ~20 times
65
+ * instead of 10,000 times — a classic amortised-O(1) approach.
66
+ */
67
+ export declare class BinaryStorage implements BinaryStorageAPI {
68
+ /** Resolved configuration with defaults applied. */
69
+ private options;
70
+ /** Absolute path to the backing file. */
71
+ private filePath;
72
+ /**
73
+ * In-memory copy of the file. Growable — always
74
+ * `buffer.length >= writePos`. Only the first `writePos`
75
+ * bytes are meaningful; the rest is pre-allocated capacity.
76
+ */
77
+ private buffer;
78
+ /** Next byte to read from. Always `0 <= readPos <= writePos`. */
79
+ private readPos;
80
+ /** Next byte to write to. Equal to total valid data size. */
81
+ private writePos;
82
+ /**
83
+ * Promise chain for serialising writes. Each write task
84
+ * is attached via `.then(task, task)` so the chain never
85
+ * breaks even if a task rejects.
86
+ */
87
+ private pending;
88
+ /**
89
+ * @param options Optional configuration. Instances should be
90
+ * created via {@link createBinaryStorage}.
91
+ */
92
+ constructor(options?: BinaryStorageOptions);
93
+ /**
94
+ * Typed write methods.
95
+ *
96
+ * Each method enqueues the write task so that concurrent
97
+ * callers never interleave. The task:
98
+ * 1. Ensures buffer capacity via `growBuf`.
99
+ * 2. Writes the type tag + payload at `writePos`.
100
+ * 3. Advances `writePos`.
101
+ * 4. Flushes if `options.flush` is true.
102
+ */
103
+ write: {
104
+ num: (value: number) => Promise<void>;
105
+ str: (value: string) => Promise<void>;
106
+ b: (value: boolean) => Promise<void>;
107
+ obj: <T extends object>(v: T) => Promise<void>;
108
+ arr: <T>(v: T[]) => Promise<void>;
109
+ any: (value: unknown) => Promise<void>;
110
+ null: () => Promise<void>;
111
+ /** Force-persist regardless of the `flush` option. */
112
+ flush: () => Promise<void>;
113
+ };
114
+ /**
115
+ * Typed read methods.
116
+ *
117
+ * Each method delegates to `readTyped()` which validates the
118
+ * type tag, then calls the type-specific payload reader.
119
+ * Wrapped in `async` so that errors thrown synchronously
120
+ * (e.g. type mismatch) become Promise rejections.
121
+ */
122
+ read: {
123
+ num: () => Promise<number>;
124
+ str: () => Promise<string>;
125
+ b: () => Promise<boolean>;
126
+ obj: <T extends object>() => Promise<T>;
127
+ arr: <T>() => Promise<T[]>;
128
+ any: () => Promise<unknown>;
129
+ };
130
+ tellRead(): number;
131
+ tellWrite(): number;
132
+ seekRead(pos: number): void;
133
+ /**
134
+ * Truncate the stream at `pos` and persist.
135
+ *
136
+ * Because this mutates the buffer, it goes through the write
137
+ * queue to avoid racing with in-flight writes. If the read
138
+ * cursor would end up past the new write position, it is
139
+ * clamped back.
140
+ */
141
+ seekWrite(pos: number): Promise<void>;
142
+ resetRead(): void;
143
+ /** Convenience: discard everything after the read cursor. */
144
+ truncate(): Promise<void>;
145
+ private ensureDir;
146
+ /**
147
+ * Read the file into a Buffer. Returns an empty buffer if the
148
+ * file does not exist (first use) or can't be read.
149
+ */
150
+ private loadFile;
151
+ /**
152
+ * Serialise access so concurrent callers never interleave.
153
+ *
154
+ * Each task is chained onto `this.pending` using
155
+ * `.then(task, task)`. The second `task` argument is critical:
156
+ * it means that if a previous task rejects, the next task
157
+ * still executes. Without it, a single failed write would
158
+ * permanently break the chain and all subsequent writes
159
+ * would hang.
160
+ */
161
+ private enqueue;
162
+ /**
163
+ * Conditional flush — only persists if `options.flush` is true.
164
+ *
165
+ * Called automatically after every write when `flush: true`.
166
+ * When `flush: false`, writes are buffered in memory until
167
+ * {@link forceFlush} is called (via `write.flush()`).
168
+ */
169
+ private flush;
170
+ /**
171
+ * Unconditional flush — always writes to disk.
172
+ *
173
+ * Only the first `writePos` bytes are written; the rest of the
174
+ * buffer is pre-allocated capacity. Uses atomic rename to
175
+ * prevent corruption from partial writes or crashes.
176
+ */
177
+ private forceFlush;
178
+ /**
179
+ * Ensure the internal Buffer has at least `needed` free bytes
180
+ * after `writePos`.
181
+ *
182
+ * Growth strategy: allocate `max(needed, GROW_STEP)` extra bytes,
183
+ * copy the old buffer into the new one. This is amortised O(1)
184
+ * per byte written — the classic dynamic-array approach.
185
+ */
186
+ private growBuf;
187
+ /**
188
+ * Encode a number: `[0x01][8 bytes float64 LE]`.
189
+ *
190
+ * Uses `writeDoubleLE` which writes IEEE 754 binary64 format.
191
+ * This preserves all JS numbers faithfully, including special
192
+ * values like `Infinity`, `-Infinity`, and `NaN`.
193
+ */
194
+ private appendNum;
195
+ /**
196
+ * Encode a string: `[0x02][4 bytes uint32 LE length][UTF-8 bytes]`.
197
+ *
198
+ * The length is the byte count of the UTF-8 encoding, not the
199
+ * character count. This ensures multi-byte characters (emoji,
200
+ * CJK) are handled correctly.
201
+ */
202
+ private appendStr;
203
+ /**
204
+ * Encode a boolean: `[0x03][1 byte: 0x00 or 0x01]`.
205
+ */
206
+ private appendBool;
207
+ /**
208
+ * Encode an object as JSON: `[0x04][4 bytes length][JSON UTF-8]`.
209
+ *
210
+ * Objects and arrays use the same wire format (length-prefixed
211
+ * UTF-8 string) but different type tags so that `read.obj()`
212
+ * and `read.arr()` can validate the expected shape.
213
+ */
214
+ private appendObj;
215
+ /**
216
+ * Encode an array as JSON: `[0x05][4 bytes length][JSON UTF-8]`.
217
+ */
218
+ private appendArr;
219
+ /**
220
+ * Auto-detect the type of `value` and delegate to the
221
+ * appropriate typed append method.
222
+ *
223
+ * Detection order:
224
+ * 1. `typeof` for number/string/boolean → typed tags
225
+ * 2. `null` / `undefined` → TypeTag.Null
226
+ * 3. `Array.isArray` → TypeTag.Array
227
+ * 4. `typeof === 'object'` → TypeTag.Object
228
+ * 5. Fallback → TypeTag.Null (shouldn't happen, but safe)
229
+ */
230
+ private appendAny;
231
+ /**
232
+ * Encode null: `[0x06]` — just the tag, no payload.
233
+ */
234
+ private appendNull;
235
+ /**
236
+ * Peek at the type tag byte at `readPos`.
237
+ *
238
+ * This is the end-of-stream guard for all typed read methods.
239
+ * It checks against `writePos` (not `buffer.length`) because
240
+ * the buffer may have pre-allocated capacity beyond the valid
241
+ * data region.
242
+ *
243
+ * @returns The tag byte (0x01–0x06).
244
+ * @throws If `readPos >= writePos` (no more values to read).
245
+ */
246
+ private readTag;
247
+ /**
248
+ * Read a uint32 LE length prefix from `readPos` and advance.
249
+ *
250
+ * @returns The length value.
251
+ * @throws If there aren't enough bytes for the length prefix.
252
+ */
253
+ private readUInt32LE;
254
+ /**
255
+ * Core typed-read routine.
256
+ *
257
+ * 1. Peek at the tag byte.
258
+ * 2. If it doesn't match `expected`, throw a descriptive error
259
+ * with the byte position and human-readable type names.
260
+ * 3. Otherwise advance past the tag and invoke the payload
261
+ * reader callback.
262
+ *
263
+ * @typeParam T — The return type of the payload reader.
264
+ * @param expected — The TypeTag we expect to find.
265
+ * @param readPayload — Callback that reads the payload bytes
266
+ * and returns the decoded value.
267
+ */
268
+ private readTyped;
269
+ /**
270
+ * Read 8 bytes as a float64 LE number.
271
+ *
272
+ * Uses `readDoubleLE` which decodes IEEE 754 binary64.
273
+ * `Infinity`, `-Infinity`, and `NaN` round-trip correctly.
274
+ */
275
+ private readNumPayload;
276
+ /**
277
+ * Read a length-prefixed UTF-8 string.
278
+ *
279
+ * 1. Read 4-byte uint32 LE → byte length.
280
+ * 2. Read that many bytes as UTF-8.
281
+ */
282
+ private readStrPayload;
283
+ /**
284
+ * Read a single byte as a boolean.
285
+ *
286
+ * Any non-zero byte is treated as `true`.
287
+ */
288
+ private readBoolPayload;
289
+ /**
290
+ * Read a string payload and parse it as JSON.
291
+ *
292
+ * Shared by `read.obj()` and `read.arr()` — the type tag
293
+ * distinguishes the two, but the on-wire format is identical
294
+ * (JSON string).
295
+ */
296
+ private readJSONPayload;
297
+ /**
298
+ * Read the next value regardless of type.
299
+ *
300
+ * Reads the tag byte, then dispatches to the appropriate
301
+ * payload reader. This is the universal read method — it
302
+ * accepts any type and returns the decoded value directly.
303
+ *
304
+ * @throws If the tag byte is unknown (file corruption or
305
+ * newer format version).
306
+ */
307
+ private readAnyPayload;
308
+ }