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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/binary-storage/BinaryStorage.d.ts +308 -0
- package/dist/binary-storage/BinaryStorage.js +548 -0
- package/dist/binary-storage/StreamingReader.d.ts +53 -0
- package/dist/binary-storage/StreamingReader.js +530 -0
- package/dist/binary-storage/index.d.ts +91 -0
- package/dist/binary-storage/index.js +91 -0
- package/dist/binary-storage/types.d.ts +400 -0
- package/dist/binary-storage/types.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +214 -0
- package/dist/cli/initTheme.d.ts +14 -0
- package/dist/cli/initTheme.js +124 -0
- package/dist/cli/makeLanguageType.d.ts +46 -0
- package/dist/cli/makeLanguageType.js +264 -0
- package/dist/cli/makeThemeType.d.ts +43 -0
- package/dist/cli/makeThemeType.js +222 -0
- package/dist/components/badge/Badge.d.ts +7 -0
- package/dist/components/badge/Badge.js +9 -0
- package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
- package/dist/components/dialog/ConfirmDialog.js +57 -0
- package/dist/components/dialog/types.d.ts +20 -0
- package/dist/components/dialog/types.js +1 -0
- package/dist/components/divider/Divider.d.ts +8 -0
- package/dist/components/divider/Divider.js +10 -0
- package/dist/components/fold/Fold.d.ts +3 -0
- package/dist/components/fold/Fold.js +44 -0
- package/dist/components/fold/types.d.ts +28 -0
- package/dist/components/fold/types.js +1 -0
- package/dist/components/form/Field.d.ts +30 -0
- package/dist/components/form/Field.js +59 -0
- package/dist/components/form/Form.d.ts +32 -0
- package/dist/components/form/Form.js +163 -0
- package/dist/components/form/context.d.ts +14 -0
- package/dist/components/form/context.js +20 -0
- package/dist/components/form/types.d.ts +115 -0
- package/dist/components/form/types.js +1 -0
- package/dist/components/key-hint/KeyHint.d.ts +10 -0
- package/dist/components/key-hint/KeyHint.js +12 -0
- package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
- package/dist/components/multi-select/MultiSelectInput.js +287 -0
- package/dist/components/multi-select/types.d.ts +100 -0
- package/dist/components/multi-select/types.js +1 -0
- package/dist/components/number-input/NumberInput.d.ts +11 -0
- package/dist/components/number-input/NumberInput.js +44 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
- package/dist/components/progress-bar/ProgressBar.js +9 -0
- package/dist/components/search-input/SearchInput.d.ts +10 -0
- package/dist/components/search-input/SearchInput.js +15 -0
- package/dist/components/select/SelectInput.d.ts +100 -0
- package/dist/components/select/SelectInput.js +147 -0
- package/dist/components/select/types.d.ts +63 -0
- package/dist/components/select/types.js +1 -0
- package/dist/components/spinner/Spinner.d.ts +12 -0
- package/dist/components/spinner/Spinner.js +24 -0
- package/dist/components/tabs/Tabs.d.ts +3 -0
- package/dist/components/tabs/Tabs.js +57 -0
- package/dist/components/tabs/types.d.ts +32 -0
- package/dist/components/tabs/types.js +1 -0
- package/dist/components/text/TextInput.d.ts +39 -0
- package/dist/components/text/TextInput.js +233 -0
- package/dist/components/text/types.d.ts +62 -0
- package/dist/components/text/types.js +1 -0
- package/dist/dev/dev-screen.d.ts +36 -0
- package/dist/dev/dev-screen.js +142 -0
- package/dist/dev/entrance.d.ts +51 -0
- package/dist/dev/entrance.js +67 -0
- package/dist/dev/globalKey-display.d.ts +2 -0
- package/dist/dev/globalKey-display.js +8 -0
- package/dist/dev/types.d.ts +9 -0
- package/dist/dev/types.js +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +50 -0
- package/dist/keyboard/check-global-key.d.ts +18 -0
- package/dist/keyboard/check-global-key.js +52 -0
- package/dist/keyboard/context.d.ts +398 -0
- package/dist/keyboard/context.js +9 -0
- package/dist/keyboard/global-key-processor/index.d.ts +16 -0
- package/dist/keyboard/global-key-processor/index.js +75 -0
- package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
- package/dist/keyboard/global-sequence-processor/index.js +165 -0
- package/dist/keyboard/hook.d.ts +51 -0
- package/dist/keyboard/hook.js +105 -0
- package/dist/keyboard/index.d.ts +5 -0
- package/dist/keyboard/index.js +3 -0
- package/dist/keyboard/keyNormalizer.d.ts +37 -0
- package/dist/keyboard/keyNormalizer.js +119 -0
- package/dist/keyboard/layer-handler.d.ts +57 -0
- package/dist/keyboard/layer-handler.js +274 -0
- package/dist/keyboard/modal-processor/index.d.ts +16 -0
- package/dist/keyboard/modal-processor/index.js +97 -0
- package/dist/keyboard/overlay-processor/index.d.ts +16 -0
- package/dist/keyboard/overlay-processor/index.js +30 -0
- package/dist/keyboard/pipeline/chain.d.ts +10 -0
- package/dist/keyboard/pipeline/chain.js +45 -0
- package/dist/keyboard/pipeline/context.d.ts +34 -0
- package/dist/keyboard/pipeline/context.js +42 -0
- package/dist/keyboard/pipeline/index.d.ts +3 -0
- package/dist/keyboard/pipeline/index.js +2 -0
- package/dist/keyboard/provider.d.ts +32 -0
- package/dist/keyboard/provider.js +985 -0
- package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
- package/dist/keyboard/screen-stack-processor/index.js +32 -0
- package/dist/keyboard/types.d.ts +632 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/language/context.d.ts +7 -0
- package/dist/language/context.js +7 -0
- package/dist/language/hook.d.ts +19 -0
- package/dist/language/hook.js +26 -0
- package/dist/language/index.d.ts +3 -0
- package/dist/language/index.js +2 -0
- package/dist/language/provider.d.ts +30 -0
- package/dist/language/provider.js +155 -0
- package/dist/language/types.d.ts +124 -0
- package/dist/language/types.js +1 -0
- package/dist/screen/ModalContext.d.ts +11 -0
- package/dist/screen/ModalContext.js +12 -0
- package/dist/screen/OverlayContext.d.ts +9 -0
- package/dist/screen/OverlayContext.js +10 -0
- package/dist/screen/context.d.ts +61 -0
- package/dist/screen/context.js +8 -0
- package/dist/screen/current-screen.d.ts +16 -0
- package/dist/screen/current-screen.js +38 -0
- package/dist/screen/hook.d.ts +13 -0
- package/dist/screen/hook.js +20 -0
- package/dist/screen/index.d.ts +7 -0
- package/dist/screen/index.js +5 -0
- package/dist/screen/provider.d.ts +112 -0
- package/dist/screen/provider.js +577 -0
- package/dist/screen/registry.d.ts +27 -0
- package/dist/screen/registry.js +71 -0
- package/dist/screen/types.d.ts +227 -0
- package/dist/screen/types.js +1 -0
- package/dist/storage/index.d.ts +45 -0
- package/dist/storage/index.js +46 -0
- package/dist/storage/storage.d.ts +139 -0
- package/dist/storage/storage.js +236 -0
- package/dist/storage/types.d.ts +145 -0
- package/dist/storage/types.js +1 -0
- package/dist/theme/context.d.ts +7 -0
- package/dist/theme/context.js +7 -0
- package/dist/theme/hook.d.ts +11 -0
- package/dist/theme/hook.js +18 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/provider.d.ts +18 -0
- package/dist/theme/provider.js +308 -0
- package/dist/theme/types.d.ts +53 -0
- package/dist/theme/types.js +1 -0
- 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
|
+
[](https://github.com/BAIGAOa/ink-trc/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/ink-cartridge)
|
|
7
|
+
[](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
|
+
}
|