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
@@ -0,0 +1,548 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { TAG_NAMES } from './types.js';
4
+ /**
5
+ * Size constants for the binary encoding format.
6
+ *
7
+ * Every value on disk has this layout:
8
+ *
9
+ * ```
10
+ * [1 byte: TypeTag] [N bytes: payload]
11
+ * ```
12
+ *
13
+ * The payload size depends on the type:
14
+ * - Number: 8 bytes (IEEE 754 float64, little-endian)
15
+ * - Boolean: 1 byte (0x00 or 0x01)
16
+ * - String: 4 bytes (uint32 LE length) + UTF-8 content
17
+ * - Object: same as String (JSON-serialised)
18
+ * - Array: same as String (JSON-serialised)
19
+ * - Null: 0 bytes (just the tag)
20
+ */
21
+ const TAG_SIZE = 1;
22
+ const NUM_SIZE = 8;
23
+ const BOOL_SIZE = 1;
24
+ const LEN_SIZE = 4;
25
+ /**
26
+ * Default allocation increment for the internal Buffer.
27
+ *
28
+ * When the buffer needs to grow, we allocate at least 4 KiB
29
+ * extra to amortise the cost of `Buffer.alloc()` + `copy()`
30
+ * across many small writes. A single `write.num()` call needs
31
+ * only 9 bytes, but allocating 9 bytes at a time would be slow.
32
+ */
33
+ const GROW_STEP = 4096;
34
+ /**
35
+ * Sequential binary key-value store backed by a single file.
36
+ *
37
+ * ## Architecture
38
+ *
39
+ * The entire file is loaded into a single in-memory `Buffer` on
40
+ * construction (lazy — only if the file already exists). Two
41
+ * cursors track the current position:
42
+ *
43
+ * - `readPos` — where the next `read.*()` will start reading.
44
+ * Starts at 0. Advanced each time a value is consumed.
45
+ *
46
+ * - `writePos` — where the next `write.*()` will append data.
47
+ * Starts at the file size (or 0 for a new file). Advanced
48
+ * each time a value is written.
49
+ *
50
+ * Because both cursors point into the same Buffer, you can write
51
+ * values, then immediately read them back by resetting the read
52
+ * cursor — no disk round-trip needed.
53
+ *
54
+ * ## Write path
55
+ *
56
+ * 1. `write.num(v)` calls `enqueue(() => appendNum(v))`.
57
+ * 2. `enqueue` chains the task onto the internal `pending` promise
58
+ * so writes are strictly ordered.
59
+ * 3. `appendNum` calls `growBuf()` to ensure capacity, writes
60
+ * `[TypeTag.Number][8-byte float64 LE]` at `writePos`, then
61
+ * advances `writePos`.
62
+ * 4. If `flush: true` (default), `forceFlush()` writes the buffer
63
+ * to disk atomically (temp file → rename).
64
+ *
65
+ * ## Read path
66
+ *
67
+ * 1. `read.num()` calls `readTyped(TypeTag.Number, ...)`.
68
+ * 2. `readTyped` calls `readTag()` which checks `readPos < writePos`
69
+ * (end-of-stream guard) and returns the byte at `readPos`.
70
+ * 3. If the tag matches `TypeTag.Number`, `readPos` advances past
71
+ * the tag and the payload reader (`readNumPayload`) is invoked.
72
+ * 4. `readNumPayload` reads 8 bytes at `readPos` as float64 LE,
73
+ * advances `readPos`, and returns the number.
74
+ *
75
+ * ## Concurrency model
76
+ *
77
+ * Writes are serialised through a promise chain (`this.pending`).
78
+ * The chain uses `.then(task, task)` — the second argument means
79
+ * that even if one write task rejects, subsequent writes still run.
80
+ * Reads do NOT go through the queue because they are synchronous
81
+ * operations on the in-memory buffer (the `async` wrapper is for
82
+ * API consistency only).
83
+ *
84
+ * ## Atomicity guarantee
85
+ *
86
+ * Flush uses the same temp-file-then-rename pattern as the JSON
87
+ * {@link Storage}: write to `file.bin.tmp`, then `fs.renameSync`.
88
+ * On POSIX systems, `rename` is atomic — the on-disk file is
89
+ * always either the complete old version or the complete new
90
+ * version. A crash mid-write leaves the original file intact.
91
+ *
92
+ * ## Buffer growth strategy
93
+ *
94
+ * {@link growBuf} doubles the buffer capacity when it runs out
95
+ * of space, with a minimum increment of {@link GROW_STEP} (4 KiB).
96
+ * This means writing 10,000 small values allocates ~20 times
97
+ * instead of 10,000 times — a classic amortised-O(1) approach.
98
+ */
99
+ export class BinaryStorage {
100
+ /** Resolved configuration with defaults applied. */
101
+ options;
102
+ /** Absolute path to the backing file. */
103
+ filePath;
104
+ /**
105
+ * In-memory copy of the file. Growable — always
106
+ * `buffer.length >= writePos`. Only the first `writePos`
107
+ * bytes are meaningful; the rest is pre-allocated capacity.
108
+ */
109
+ buffer;
110
+ /** Next byte to read from. Always `0 <= readPos <= writePos`. */
111
+ readPos = 0;
112
+ /** Next byte to write to. Equal to total valid data size. */
113
+ writePos;
114
+ /**
115
+ * Promise chain for serialising writes. Each write task
116
+ * is attached via `.then(task, task)` so the chain never
117
+ * breaks even if a task rejects.
118
+ */
119
+ pending = Promise.resolve();
120
+ /**
121
+ * @param options Optional configuration. Instances should be
122
+ * created via {@link createBinaryStorage}.
123
+ */
124
+ constructor(options) {
125
+ this.options = {
126
+ dir: options?.dir ?? './data',
127
+ file: options?.file ?? 'storage.bin',
128
+ flush: options?.flush ?? true,
129
+ };
130
+ this.filePath = path.resolve(this.options.dir, this.options.file);
131
+ this.ensureDir();
132
+ // Load existing file into memory, or start with an empty buffer.
133
+ this.buffer = this.loadFile();
134
+ this.writePos = this.buffer.length;
135
+ }
136
+ /**
137
+ * Typed write methods.
138
+ *
139
+ * Each method enqueues the write task so that concurrent
140
+ * callers never interleave. The task:
141
+ * 1. Ensures buffer capacity via `growBuf`.
142
+ * 2. Writes the type tag + payload at `writePos`.
143
+ * 3. Advances `writePos`.
144
+ * 4. Flushes if `options.flush` is true.
145
+ */
146
+ write = {
147
+ num: (value) => this.enqueue(() => this.appendNum(value)),
148
+ str: (value) => this.enqueue(() => this.appendStr(value)),
149
+ b: (value) => this.enqueue(() => this.appendBool(value)),
150
+ obj: (v) => this.enqueue(() => this.appendObj(v)),
151
+ arr: (v) => this.enqueue(() => this.appendArr(v)),
152
+ any: (value) => this.enqueue(() => this.appendAny(value)),
153
+ null: () => this.enqueue(() => this.appendNull()),
154
+ /** Force-persist regardless of the `flush` option. */
155
+ flush: () => this.enqueue(async () => { await this.forceFlush(); }),
156
+ };
157
+ /**
158
+ * Typed read methods.
159
+ *
160
+ * Each method delegates to `readTyped()` which validates the
161
+ * type tag, then calls the type-specific payload reader.
162
+ * Wrapped in `async` so that errors thrown synchronously
163
+ * (e.g. type mismatch) become Promise rejections.
164
+ */
165
+ read = {
166
+ num: async () => this.readTyped(1 /* TypeTag.Number */, () => this.readNumPayload()),
167
+ str: async () => this.readTyped(2 /* TypeTag.String */, () => this.readStrPayload()),
168
+ b: async () => this.readTyped(3 /* TypeTag.Boolean */, () => this.readBoolPayload()),
169
+ obj: async () => this.readTyped(4 /* TypeTag.Object */, () => this.readJSONPayload()),
170
+ arr: async () => this.readTyped(5 /* TypeTag.Array */, () => this.readJSONPayload()),
171
+ any: async () => this.readAnyPayload(),
172
+ };
173
+ tellRead() { return this.readPos; }
174
+ tellWrite() { return this.writePos; }
175
+ seekRead(pos) {
176
+ if (pos < 0 || pos > this.writePos) {
177
+ throw new Error(`[Ink-Cartridge] BinaryStorage: seekRead(${pos}) out of range ` +
178
+ `(0..${this.writePos}).`);
179
+ }
180
+ this.readPos = pos;
181
+ }
182
+ /**
183
+ * Truncate the stream at `pos` and persist.
184
+ *
185
+ * Because this mutates the buffer, it goes through the write
186
+ * queue to avoid racing with in-flight writes. If the read
187
+ * cursor would end up past the new write position, it is
188
+ * clamped back.
189
+ */
190
+ async seekWrite(pos) {
191
+ if (pos < 0) {
192
+ throw new Error(`[Ink-Cartridge] BinaryStorage: seekWrite(${pos}) must be >= 0.`);
193
+ }
194
+ await this.enqueue(async () => {
195
+ // If the truncation point is within the current buffer,
196
+ // slice away everything beyond it. Using Buffer.from()
197
+ // creates an independent copy so the old buffer can be GC'd.
198
+ if (pos < this.buffer.length) {
199
+ this.buffer = Buffer.from(this.buffer.subarray(0, pos));
200
+ }
201
+ this.writePos = pos;
202
+ if (this.readPos > this.writePos) {
203
+ this.readPos = this.writePos;
204
+ }
205
+ await this.flush();
206
+ });
207
+ }
208
+ resetRead() { this.seekRead(0); }
209
+ /** Convenience: discard everything after the read cursor. */
210
+ async truncate() {
211
+ await this.seekWrite(this.readPos);
212
+ }
213
+ // ═══════════════════════════════════════════════════════════
214
+ // Internal helpers — not part of the public API
215
+ // ═══════════════════════════════════════════════════════════
216
+ ensureDir() {
217
+ fs.mkdirSync(this.options.dir, { recursive: true });
218
+ }
219
+ /**
220
+ * Read the file into a Buffer. Returns an empty buffer if the
221
+ * file does not exist (first use) or can't be read.
222
+ */
223
+ loadFile() {
224
+ try {
225
+ return fs.readFileSync(this.filePath);
226
+ }
227
+ catch {
228
+ return Buffer.alloc(0);
229
+ }
230
+ }
231
+ /**
232
+ * Serialise access so concurrent callers never interleave.
233
+ *
234
+ * Each task is chained onto `this.pending` using
235
+ * `.then(task, task)`. The second `task` argument is critical:
236
+ * it means that if a previous task rejects, the next task
237
+ * still executes. Without it, a single failed write would
238
+ * permanently break the chain and all subsequent writes
239
+ * would hang.
240
+ */
241
+ enqueue(task) {
242
+ this.pending = this.pending.then(task, task);
243
+ return this.pending;
244
+ }
245
+ /**
246
+ * Conditional flush — only persists if `options.flush` is true.
247
+ *
248
+ * Called automatically after every write when `flush: true`.
249
+ * When `flush: false`, writes are buffered in memory until
250
+ * {@link forceFlush} is called (via `write.flush()`).
251
+ */
252
+ async flush() {
253
+ if (!this.options.flush)
254
+ return;
255
+ await this.forceFlush();
256
+ }
257
+ /**
258
+ * Unconditional flush — always writes to disk.
259
+ *
260
+ * Only the first `writePos` bytes are written; the rest of the
261
+ * buffer is pre-allocated capacity. Uses atomic rename to
262
+ * prevent corruption from partial writes or crashes.
263
+ */
264
+ async forceFlush() {
265
+ const tmpPath = this.filePath + '.tmp';
266
+ fs.writeFileSync(tmpPath, this.buffer.subarray(0, this.writePos));
267
+ fs.renameSync(tmpPath, this.filePath);
268
+ }
269
+ // ── write payload helpers ──────────────────────────────────
270
+ /**
271
+ * Ensure the internal Buffer has at least `needed` free bytes
272
+ * after `writePos`.
273
+ *
274
+ * Growth strategy: allocate `max(needed, GROW_STEP)` extra bytes,
275
+ * copy the old buffer into the new one. This is amortised O(1)
276
+ * per byte written — the classic dynamic-array approach.
277
+ */
278
+ growBuf(needed) {
279
+ if (this.writePos + needed <= this.buffer.length)
280
+ return;
281
+ const extra = Math.max(needed, GROW_STEP);
282
+ const grown = Buffer.alloc(this.buffer.length + extra);
283
+ this.buffer.copy(grown);
284
+ this.buffer = grown;
285
+ }
286
+ /**
287
+ * Encode a number: `[0x01][8 bytes float64 LE]`.
288
+ *
289
+ * Uses `writeDoubleLE` which writes IEEE 754 binary64 format.
290
+ * This preserves all JS numbers faithfully, including special
291
+ * values like `Infinity`, `-Infinity`, and `NaN`.
292
+ */
293
+ async appendNum(value) {
294
+ this.growBuf(TAG_SIZE + NUM_SIZE);
295
+ this.buffer.writeUInt8(1 /* TypeTag.Number */, this.writePos);
296
+ this.buffer.writeDoubleLE(value, this.writePos + TAG_SIZE);
297
+ this.writePos += TAG_SIZE + NUM_SIZE;
298
+ await this.flush();
299
+ }
300
+ /**
301
+ * Encode a string: `[0x02][4 bytes uint32 LE length][UTF-8 bytes]`.
302
+ *
303
+ * The length is the byte count of the UTF-8 encoding, not the
304
+ * character count. This ensures multi-byte characters (emoji,
305
+ * CJK) are handled correctly.
306
+ */
307
+ async appendStr(value) {
308
+ const encoded = Buffer.from(value, 'utf-8');
309
+ this.growBuf(TAG_SIZE + LEN_SIZE + encoded.length);
310
+ let pos = this.writePos;
311
+ this.buffer.writeUInt8(2 /* TypeTag.String */, pos);
312
+ pos += TAG_SIZE;
313
+ this.buffer.writeUInt32LE(encoded.length, pos);
314
+ pos += LEN_SIZE;
315
+ encoded.copy(this.buffer, pos);
316
+ this.writePos = pos + encoded.length;
317
+ await this.flush();
318
+ }
319
+ /**
320
+ * Encode a boolean: `[0x03][1 byte: 0x00 or 0x01]`.
321
+ */
322
+ async appendBool(value) {
323
+ this.growBuf(TAG_SIZE + BOOL_SIZE);
324
+ this.buffer.writeUInt8(3 /* TypeTag.Boolean */, this.writePos);
325
+ this.buffer.writeUInt8(value ? 1 : 0, this.writePos + TAG_SIZE);
326
+ this.writePos += TAG_SIZE + BOOL_SIZE;
327
+ await this.flush();
328
+ }
329
+ /**
330
+ * Encode an object as JSON: `[0x04][4 bytes length][JSON UTF-8]`.
331
+ *
332
+ * Objects and arrays use the same wire format (length-prefixed
333
+ * UTF-8 string) but different type tags so that `read.obj()`
334
+ * and `read.arr()` can validate the expected shape.
335
+ */
336
+ async appendObj(value) {
337
+ const json = JSON.stringify(value);
338
+ const encoded = Buffer.from(json, 'utf-8');
339
+ this.growBuf(TAG_SIZE + LEN_SIZE + encoded.length);
340
+ let pos = this.writePos;
341
+ this.buffer.writeUInt8(4 /* TypeTag.Object */, pos);
342
+ pos += TAG_SIZE;
343
+ this.buffer.writeUInt32LE(encoded.length, pos);
344
+ pos += LEN_SIZE;
345
+ encoded.copy(this.buffer, pos);
346
+ this.writePos = pos + encoded.length;
347
+ await this.flush();
348
+ }
349
+ /**
350
+ * Encode an array as JSON: `[0x05][4 bytes length][JSON UTF-8]`.
351
+ */
352
+ async appendArr(value) {
353
+ const json = JSON.stringify(value);
354
+ const encoded = Buffer.from(json, 'utf-8');
355
+ this.growBuf(TAG_SIZE + LEN_SIZE + encoded.length);
356
+ let pos = this.writePos;
357
+ this.buffer.writeUInt8(5 /* TypeTag.Array */, pos);
358
+ pos += TAG_SIZE;
359
+ this.buffer.writeUInt32LE(encoded.length, pos);
360
+ pos += LEN_SIZE;
361
+ encoded.copy(this.buffer, pos);
362
+ this.writePos = pos + encoded.length;
363
+ await this.flush();
364
+ }
365
+ /**
366
+ * Auto-detect the type of `value` and delegate to the
367
+ * appropriate typed append method.
368
+ *
369
+ * Detection order:
370
+ * 1. `typeof` for number/string/boolean → typed tags
371
+ * 2. `null` / `undefined` → TypeTag.Null
372
+ * 3. `Array.isArray` → TypeTag.Array
373
+ * 4. `typeof === 'object'` → TypeTag.Object
374
+ * 5. Fallback → TypeTag.Null (shouldn't happen, but safe)
375
+ */
376
+ async appendAny(value) {
377
+ if (typeof value === 'number') {
378
+ await this.appendNum(value);
379
+ }
380
+ else if (typeof value === 'string') {
381
+ await this.appendStr(value);
382
+ }
383
+ else if (typeof value === 'boolean') {
384
+ await this.appendBool(value);
385
+ }
386
+ else if (value === null || value === undefined) {
387
+ await this.appendNull();
388
+ }
389
+ else if (Array.isArray(value)) {
390
+ await this.appendArr(value);
391
+ }
392
+ else if (typeof value === 'object') {
393
+ await this.appendObj(value);
394
+ }
395
+ else {
396
+ await this.appendNull();
397
+ }
398
+ }
399
+ /**
400
+ * Encode null: `[0x06]` — just the tag, no payload.
401
+ */
402
+ async appendNull() {
403
+ this.growBuf(TAG_SIZE);
404
+ this.buffer.writeUInt8(6 /* TypeTag.Null */, this.writePos);
405
+ this.writePos += TAG_SIZE;
406
+ await this.flush();
407
+ }
408
+ // ── read helpers ───────────────────────────────────────────
409
+ /**
410
+ * Peek at the type tag byte at `readPos`.
411
+ *
412
+ * This is the end-of-stream guard for all typed read methods.
413
+ * It checks against `writePos` (not `buffer.length`) because
414
+ * the buffer may have pre-allocated capacity beyond the valid
415
+ * data region.
416
+ *
417
+ * @returns The tag byte (0x01–0x06).
418
+ * @throws If `readPos >= writePos` (no more values to read).
419
+ */
420
+ readTag() {
421
+ if (this.readPos >= this.writePos) {
422
+ throw new Error(`[Ink-Cartridge] BinaryStorage: end of stream at byte ${this.readPos} — ` +
423
+ `no more values to read.`);
424
+ }
425
+ return this.buffer.readUInt8(this.readPos);
426
+ }
427
+ /**
428
+ * Read a uint32 LE length prefix from `readPos` and advance.
429
+ *
430
+ * @returns The length value.
431
+ * @throws If there aren't enough bytes for the length prefix.
432
+ */
433
+ readUInt32LE() {
434
+ if (this.readPos + LEN_SIZE > this.writePos) {
435
+ throw new Error(`[Ink-Cartridge] BinaryStorage: unexpected end of stream at byte ${this.readPos} ` +
436
+ `while reading length prefix.`);
437
+ }
438
+ const v = this.buffer.readUInt32LE(this.readPos);
439
+ this.readPos += LEN_SIZE;
440
+ return v;
441
+ }
442
+ /**
443
+ * Core typed-read routine.
444
+ *
445
+ * 1. Peek at the tag byte.
446
+ * 2. If it doesn't match `expected`, throw a descriptive error
447
+ * with the byte position and human-readable type names.
448
+ * 3. Otherwise advance past the tag and invoke the payload
449
+ * reader callback.
450
+ *
451
+ * @typeParam T — The return type of the payload reader.
452
+ * @param expected — The TypeTag we expect to find.
453
+ * @param readPayload — Callback that reads the payload bytes
454
+ * and returns the decoded value.
455
+ */
456
+ readTyped(expected, readPayload) {
457
+ const tag = this.readTag();
458
+ if (tag !== expected) {
459
+ const expectedName = TAG_NAMES[expected];
460
+ const actualName = TAG_NAMES[tag] ?? `unknown(0x${tag.toString(16)})`;
461
+ throw new Error(`[Ink-Cartridge] BinaryStorage: type mismatch at byte ${this.readPos}. ` +
462
+ `Expected ${expectedName}, got ${actualName}.`);
463
+ }
464
+ this.readPos += TAG_SIZE;
465
+ return readPayload();
466
+ }
467
+ /**
468
+ * Read 8 bytes as a float64 LE number.
469
+ *
470
+ * Uses `readDoubleLE` which decodes IEEE 754 binary64.
471
+ * `Infinity`, `-Infinity`, and `NaN` round-trip correctly.
472
+ */
473
+ readNumPayload() {
474
+ if (this.readPos + NUM_SIZE > this.writePos) {
475
+ throw new Error(`[Ink-Cartridge] BinaryStorage: unexpected end of stream at byte ${this.readPos} ` +
476
+ `while reading number (need ${NUM_SIZE} bytes).`);
477
+ }
478
+ const val = this.buffer.readDoubleLE(this.readPos);
479
+ this.readPos += NUM_SIZE;
480
+ return val;
481
+ }
482
+ /**
483
+ * Read a length-prefixed UTF-8 string.
484
+ *
485
+ * 1. Read 4-byte uint32 LE → byte length.
486
+ * 2. Read that many bytes as UTF-8.
487
+ */
488
+ readStrPayload() {
489
+ const len = this.readUInt32LE();
490
+ if (this.readPos + len > this.writePos) {
491
+ throw new Error(`[Ink-Cartridge] BinaryStorage: unexpected end of stream at byte ${this.readPos}. ` +
492
+ `Expected ${len} bytes of string data, only ${this.writePos - this.readPos} available.`);
493
+ }
494
+ const val = this.buffer.toString('utf-8', this.readPos, this.readPos + len);
495
+ this.readPos += len;
496
+ return val;
497
+ }
498
+ /**
499
+ * Read a single byte as a boolean.
500
+ *
501
+ * Any non-zero byte is treated as `true`.
502
+ */
503
+ readBoolPayload() {
504
+ if (this.readPos + BOOL_SIZE > this.writePos) {
505
+ throw new Error(`[Ink-Cartridge] BinaryStorage: unexpected end of stream at byte ${this.readPos} ` +
506
+ `while reading boolean.`);
507
+ }
508
+ const val = this.buffer.readUInt8(this.readPos) !== 0;
509
+ this.readPos += BOOL_SIZE;
510
+ return val;
511
+ }
512
+ /**
513
+ * Read a string payload and parse it as JSON.
514
+ *
515
+ * Shared by `read.obj()` and `read.arr()` — the type tag
516
+ * distinguishes the two, but the on-wire format is identical
517
+ * (JSON string).
518
+ */
519
+ readJSONPayload() {
520
+ const raw = this.readStrPayload();
521
+ return JSON.parse(raw);
522
+ }
523
+ /**
524
+ * Read the next value regardless of type.
525
+ *
526
+ * Reads the tag byte, then dispatches to the appropriate
527
+ * payload reader. This is the universal read method — it
528
+ * accepts any type and returns the decoded value directly.
529
+ *
530
+ * @throws If the tag byte is unknown (file corruption or
531
+ * newer format version).
532
+ */
533
+ readAnyPayload() {
534
+ const tag = this.readTag();
535
+ this.readPos += TAG_SIZE;
536
+ switch (tag) {
537
+ case 1 /* TypeTag.Number */: return this.readNumPayload();
538
+ case 2 /* TypeTag.String */: return this.readStrPayload();
539
+ case 3 /* TypeTag.Boolean */: return this.readBoolPayload();
540
+ case 4 /* TypeTag.Object */: return this.readJSONPayload();
541
+ case 5 /* TypeTag.Array */: return this.readJSONPayload();
542
+ case 6 /* TypeTag.Null */: return null;
543
+ default:
544
+ throw new Error(`[Ink-Cartridge] BinaryStorage: unknown type tag 0x${tag.toString(16)} ` +
545
+ `at byte ${this.readPos - TAG_SIZE}. File may be corrupt.`);
546
+ }
547
+ }
548
+ }
@@ -0,0 +1,53 @@
1
+ import type { StreamingReaderOptions, StreamingReaderAPI } from './types.js';
2
+ /**
3
+ * Error thrown when the binary file is structurally invalid.
4
+ *
5
+ * Three conditions trigger this:
6
+ * 1. An unknown type tag byte (value outside 0x01–0x06).
7
+ * 2. The file ends mid-value (truncated — fewer bytes remaining than
8
+ * the length prefix declares).
9
+ * 3. Invalid JSON inside an Object or Array payload.
10
+ *
11
+ * The `offset` property gives the byte position in the file where
12
+ * corruption was first detected, aiding debugging.
13
+ */
14
+ export declare class StreamCorruptError extends Error {
15
+ readonly offset: number;
16
+ constructor(message: string, offset: number);
17
+ }
18
+ /**
19
+ * Create a streaming binary reader for a file.
20
+ *
21
+ * Unlike {@link createBinaryStorage} (which loads the entire file into
22
+ * memory), this reads values sequentially from disk via a Node.js
23
+ * `ReadStream`. Values are parsed one at a time and returned in
24
+ * complete batches — the user never sees partial / truncated data.
25
+ *
26
+ * @param filePath — Absolute or relative path to the binary file.
27
+ * @param options — Optional tuning knobs (queue size, chunk size).
28
+ * @returns A reader with `readBatch()`, async iteration, and `destroy()`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { createStreamingReader } from 'ink-cartridge';
33
+ *
34
+ * const reader = createStreamingReader('large.bin');
35
+ *
36
+ * let batch;
37
+ * while ((batch = await reader.readBatch(1000)).length > 0) {
38
+ * for (const value of batch) {
39
+ * console.log(value);
40
+ * }
41
+ * }
42
+ *
43
+ * // Or: async iteration
44
+ * for await (const value of reader) {
45
+ * console.log(value);
46
+ * }
47
+ * ```
48
+ *
49
+ * @throws `ENOENT` if the file does not exist.
50
+ * @throws `StreamCorruptError` if the file is corrupt or truncated.
51
+ */
52
+ export declare function createStreamingReader(filePath: string, options?: StreamingReaderOptions): StreamingReaderAPI;
53
+ export type { StreamingReaderOptions, StreamingReaderAPI };