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,530 @@
1
+ import * as fs from 'node:fs';
2
+ const TAG_SIZE = 1;
3
+ const NUM_SIZE = 8;
4
+ const BOOL_SIZE = 1;
5
+ const LEN_SIZE = 4;
6
+ /**
7
+ * Externally-resolvable Promise.
8
+ *
9
+ * A regular `new Promise(resolve => ...)` only gives you the resolve
10
+ * callback inside the constructor. Deferred stores both callbacks as
11
+ * properties so they can be called from stream event handlers or
12
+ * lifecycle methods (onData, onEnd, destroy, fail) that fire
13
+ * asynchronously and independently of the consumer's await point.
14
+ */
15
+ class Deferred {
16
+ promise;
17
+ resolve;
18
+ reject;
19
+ constructor() {
20
+ this.promise = new Promise((res, rej) => {
21
+ this.resolve = res;
22
+ this.reject = rej;
23
+ });
24
+ }
25
+ }
26
+ /**
27
+ * State machine for incremental chunk parsing.
28
+ *
29
+ * The Node.js ReadStream delivers data in chunks (default 64 KiB).
30
+ * A single chunk may split a value at any byte boundary — e.g. a
31
+ * 9-byte number could arrive as 4 bytes in one chunk and 5 in the
32
+ * next. The state machine tracks exactly which field is being
33
+ * decoded so parsing can pause mid-value and resume when more data
34
+ * arrives, without buffering the entire file.
35
+ *
36
+ * Transitions:
37
+ * NeedTag ────► NeedNum / NeedBool / NeedLen / ──► NeedTag (null)
38
+ * NeedLen ────► NeedVar
39
+ * NeedNum / NeedBool / NeedVar ────► NeedTag (value complete)
40
+ */
41
+ var ParseState;
42
+ (function (ParseState) {
43
+ ParseState[ParseState["NeedTag"] = 0] = "NeedTag";
44
+ ParseState[ParseState["NeedNum"] = 1] = "NeedNum";
45
+ ParseState[ParseState["NeedBool"] = 2] = "NeedBool";
46
+ ParseState[ParseState["NeedLen"] = 3] = "NeedLen";
47
+ ParseState[ParseState["NeedVar"] = 4] = "NeedVar";
48
+ })(ParseState || (ParseState = {}));
49
+ /**
50
+ * Error thrown when the binary file is structurally invalid.
51
+ *
52
+ * Three conditions trigger this:
53
+ * 1. An unknown type tag byte (value outside 0x01–0x06).
54
+ * 2. The file ends mid-value (truncated — fewer bytes remaining than
55
+ * the length prefix declares).
56
+ * 3. Invalid JSON inside an Object or Array payload.
57
+ *
58
+ * The `offset` property gives the byte position in the file where
59
+ * corruption was first detected, aiding debugging.
60
+ */
61
+ export class StreamCorruptError extends Error {
62
+ offset;
63
+ constructor(message, offset) {
64
+ super(`[Ink-Cartridge] StreamingReader: ${message} at byte ${offset}.`);
65
+ this.offset = offset;
66
+ this.name = 'StreamCorruptError';
67
+ }
68
+ }
69
+ /**
70
+ * Sequential binary value reader backed by a Node.js ReadStream.
71
+ *
72
+ * Unlike {@link BinaryStorage} (which loads the entire file into one
73
+ * Buffer), this keeps only a small working buffer (~64 KB above the
74
+ * largest unparsed value) by:
75
+ *
76
+ * 1. A **state machine** (`ParseState`) that tracks which field of
77
+ * the current value is being decoded, so parsing can pause mid-byte
78
+ * and resume when the next chunk arrives.
79
+ * 2. **Buffer trimming** (`byteTrimming` + `advanceToNeedTag`) that
80
+ * discards already-consumed bytes once they exceed 64 KB, keeping
81
+ * memory bounded regardless of file size.
82
+ * 3. **Backpressure** (`maxQueueSize`) that pauses the underlying
83
+ * ReadStream when the parsed-value queue grows too large, and
84
+ * resumes it when consumers drain the queue.
85
+ *
86
+ * ## Lifecycle
87
+ *
88
+ * The stream starts in **paused** mode. It only resumes when there
89
+ * is consumer demand — a `readBatch()` call with insufficient queued
90
+ * values, or an active async-iterator `next()`. This guarantees that
91
+ * the file descriptor is not reading ahead of what the consumer can
92
+ * process, eliminating tail latency from excessive buffering.
93
+ *
94
+ * ## Error isolation
95
+ *
96
+ * Two separate flags track terminal states:
97
+ * - `fatalError` — set by corrupt/truncated/I/O errors. All subsequent
98
+ * calls throw the error. The reader is dead.
99
+ * - `destroyed` — set by user-initiated `destroy()`. Subsequent calls
100
+ * return empty results silently. Idempotent and safe.
101
+ *
102
+ * This distinction matters: a corrupt file must keep throwing so the
103
+ * user knows data is lost, while a graceful shutdown should not spam
104
+ * errors.
105
+ */
106
+ class StreamingReader {
107
+ stream;
108
+ buf = Buffer.alloc(0);
109
+ offset = 0;
110
+ state = ParseState.NeedTag;
111
+ /** The type tag of the value currently being parsed (for NeedVar dispatch). */
112
+ currentTag = 6 /* TypeTag.Null */;
113
+ pendingLen = 0;
114
+ queue = [];
115
+ maxQueueSize;
116
+ streamEnded = false;
117
+ destroyed = false;
118
+ /** Set when a corruption / I/O error occurs. User destroy() does NOT set this. */
119
+ fatalError = null;
120
+ /**
121
+ * Accumulated bytes discarded from the front of the internal buffer
122
+ * via `advanceToNeedTag` trimming. Added to `this.offset` when
123
+ * computing absolute file offsets for error messages so that even
124
+ * after trimming 100 MB of processed data, a corrupt tag at
125
+ * file offset 100,000,100 is reported correctly.
126
+ */
127
+ byteTrimming = 0;
128
+ /**
129
+ * Absolute file offset of the most recently read type tag.
130
+ * Used in `StreamCorruptError` to pinpoint where corruption was
131
+ * first detected, after accounting for buffer trimming.
132
+ */
133
+ lastTagOffset = 0;
134
+ /** Waiters for readBatch(count) calls that haven't been satisfied yet. */
135
+ batchWaiters = [];
136
+ /**
137
+ * Waiters for async-iterator `next()` calls.
138
+ * Each entry represents one `for await` iteration waiting for a
139
+ * single value to become available.
140
+ */
141
+ iterWaiters = [];
142
+ constructor(filePath, options) {
143
+ this.maxQueueSize = options?.maxQueueSize ?? 1000;
144
+ this.stream = fs.createReadStream(filePath, {
145
+ highWaterMark: options?.highWaterMark ?? 65536,
146
+ });
147
+ this.stream.on('data', (chunk) => this.onData(chunk));
148
+ this.stream.on('end', () => this.onEnd());
149
+ this.stream.on('error', (err) => this.onStreamError(err));
150
+ this.stream.on('close', () => {
151
+ // If the stream closes without 'end' or 'error' (possible on destroy),
152
+ // make sure streamEnded is true so waiters don't hang.
153
+ if (!this.streamEnded && !this.fatalError && !this.destroyed) {
154
+ this.streamEnded = true;
155
+ this.settleWaiters();
156
+ }
157
+ });
158
+ // Start paused; resume only when there is demand.
159
+ this.stream.pause();
160
+ }
161
+ // ── Public API ──────────────────────────────────────────
162
+ async readBatch(count) {
163
+ if (this.fatalError)
164
+ throw this.fatalError;
165
+ if (this.destroyed)
166
+ return [];
167
+ if (count <= 0)
168
+ return [];
169
+ this.parseLoop();
170
+ if (this.fatalError)
171
+ throw this.fatalError;
172
+ if (this.queue.length > 0) {
173
+ const result = this.drainQueue(count);
174
+ if (result.length > 0)
175
+ return result;
176
+ }
177
+ if (this.streamEnded)
178
+ return [];
179
+ const waiter = { deferred: new Deferred(), count };
180
+ this.batchWaiters.push(waiter);
181
+ this.maybeResumeStream();
182
+ return waiter.deferred.promise;
183
+ }
184
+ [Symbol.asyncIterator]() {
185
+ const reader = this;
186
+ return {
187
+ async next() {
188
+ if (reader.fatalError)
189
+ throw reader.fatalError;
190
+ if (reader.destroyed)
191
+ return { done: true, value: undefined };
192
+ reader.parseLoop();
193
+ if (reader.fatalError)
194
+ throw reader.fatalError;
195
+ if (reader.queue.length > 0) {
196
+ const value = reader.queue.shift();
197
+ reader.maybeResumeIfLow();
198
+ return { done: false, value };
199
+ }
200
+ if (reader.streamEnded) {
201
+ return { done: true, value: undefined };
202
+ }
203
+ const d = new Deferred();
204
+ reader.iterWaiters.push(d);
205
+ reader.maybeResumeStream();
206
+ return d.promise;
207
+ },
208
+ };
209
+ }
210
+ destroy() {
211
+ if (this.destroyed)
212
+ return;
213
+ this.destroyed = true;
214
+ this.stream.destroy();
215
+ this.queue.length = 0;
216
+ for (const w of this.batchWaiters)
217
+ w.deferred.resolve([]);
218
+ this.batchWaiters.length = 0;
219
+ for (const w of this.iterWaiters)
220
+ w.resolve({ done: true, value: undefined });
221
+ this.iterWaiters.length = 0;
222
+ }
223
+ // ── Stream event handlers ────────────────────────────────
224
+ onData(chunk) {
225
+ if (this.destroyed || this.fatalError)
226
+ return;
227
+ this.buf = Buffer.concat([this.buf, chunk]);
228
+ this.parseLoop();
229
+ this.settleWaiters();
230
+ // Pause the underlying read stream if the value queue is full.
231
+ // This is the core of backpressure: the consumer processes values
232
+ // at its own pace; we don't let the OS buffer exceed maxQueueSize.
233
+ if (this.queue.length >= this.maxQueueSize && !this.fatalError) {
234
+ this.stream.pause();
235
+ }
236
+ }
237
+ onEnd() {
238
+ if (this.destroyed || this.fatalError)
239
+ return;
240
+ this.streamEnded = true;
241
+ // If there are leftover unprocessed bytes, the file is truncated.
242
+ if (this.offset < this.buf.length) {
243
+ this.fail(new StreamCorruptError('Incomplete data — file truncated mid-value', this.byteTrimming + this.lastTagOffset));
244
+ return;
245
+ }
246
+ this.settleWaiters();
247
+ }
248
+ onStreamError(err) {
249
+ if (this.destroyed || this.fatalError)
250
+ return;
251
+ this.streamEnded = true;
252
+ this.fail(err);
253
+ }
254
+ // ── Parse state machine ──────────────────────────────────
255
+ parseLoop() {
256
+ // Loop: keep parsing values as long as there are enough bytes
257
+ // in the internal buffer. `return` = need more data (chunk
258
+ // boundary hit). `continue` = finished one value, parse next.
259
+ while (true) {
260
+ switch (this.state) {
261
+ // ── NeedTag ──────────────────────────────────
262
+ // Read 1 byte, validate it's a known TypeTag (0x01–0x06),
263
+ // then transition to the appropriate payload state.
264
+ case ParseState.NeedTag: {
265
+ if (this.offset + TAG_SIZE > this.buf.length)
266
+ return;
267
+ const tag = this.buf.readUInt8(this.offset);
268
+ this.lastTagOffset = this.byteTrimming + this.offset;
269
+ this.offset += TAG_SIZE;
270
+ if (tag < 1 /* TypeTag.Number */ || tag > 6 /* TypeTag.Null */) {
271
+ this.fail(new StreamCorruptError(`Unknown type tag 0x${tag.toString(16)}`, this.lastTagOffset));
272
+ return;
273
+ }
274
+ this.currentTag = tag;
275
+ switch (tag) {
276
+ case 1 /* TypeTag.Number */:
277
+ this.state = ParseState.NeedNum;
278
+ break;
279
+ case 3 /* TypeTag.Boolean */:
280
+ this.state = ParseState.NeedBool;
281
+ break;
282
+ case 6 /* TypeTag.Null */:
283
+ this.queue.push(null);
284
+ this.advanceToNeedTag();
285
+ continue;
286
+ default:
287
+ // String, Object, Array — all length-prefixed.
288
+ this.state = ParseState.NeedLen;
289
+ break;
290
+ }
291
+ break;
292
+ }
293
+ // ── NeedNum ──────────────────────────────────
294
+ // Expect 8 bytes of IEEE 754 float64 (little-endian).
295
+ case ParseState.NeedNum: {
296
+ if (this.offset + NUM_SIZE > this.buf.length)
297
+ return;
298
+ const val = this.buf.readDoubleLE(this.offset);
299
+ this.offset += NUM_SIZE;
300
+ this.queue.push(val);
301
+ this.advanceToNeedTag();
302
+ continue;
303
+ }
304
+ // ── NeedBool ─────────────────────────────────
305
+ // Expect 1 byte: 0x00 = false, any non-zero = true.
306
+ case ParseState.NeedBool: {
307
+ if (this.offset + BOOL_SIZE > this.buf.length)
308
+ return;
309
+ const val = this.buf.readUInt8(this.offset) !== 0;
310
+ this.offset += BOOL_SIZE;
311
+ this.queue.push(val);
312
+ this.advanceToNeedTag();
313
+ continue;
314
+ }
315
+ // ── NeedLen ──────────────────────────────────
316
+ // Expect 4 bytes of uint32 LE length prefix (for strings,
317
+ // objects, arrays). Read the length then transition to
318
+ // NeedVar to wait for the variable-length content.
319
+ case ParseState.NeedLen: {
320
+ if (this.offset + LEN_SIZE > this.buf.length)
321
+ return;
322
+ this.pendingLen = this.buf.readUInt32LE(this.offset);
323
+ this.offset += LEN_SIZE;
324
+ this.state = ParseState.NeedVar;
325
+ break;
326
+ }
327
+ // ── NeedVar ──────────────────────────────────
328
+ // Expect `pendingLen` bytes of content. Once read:
329
+ // - TypeTag.String → push as UTF-8 string
330
+ // - TypeTag.Object / TypeTag.Array → JSON-parse and push
331
+ case ParseState.NeedVar: {
332
+ if (this.offset + this.pendingLen > this.buf.length)
333
+ return;
334
+ const raw = this.buf.toString('utf-8', this.offset, this.offset + this.pendingLen);
335
+ this.offset += this.pendingLen;
336
+ if (this.currentTag === 2 /* TypeTag.String */) {
337
+ this.queue.push(raw);
338
+ }
339
+ else {
340
+ // Object or Array — JSON payload.
341
+ let val;
342
+ try {
343
+ val = JSON.parse(raw);
344
+ }
345
+ catch {
346
+ this.fail(new StreamCorruptError('Invalid JSON in variable-length payload', this.lastTagOffset));
347
+ return;
348
+ }
349
+ this.queue.push(val);
350
+ }
351
+ this.advanceToNeedTag();
352
+ continue;
353
+ }
354
+ default:
355
+ return;
356
+ }
357
+ }
358
+ }
359
+ advanceToNeedTag() {
360
+ this.state = ParseState.NeedTag;
361
+ // Periodically discard already-consumed bytes from the front of
362
+ // the internal buffer so memory doesn't grow unboundedly with
363
+ // file size. The threshold (65536 = 64 KB) means we waste at
364
+ // most 64 KB beyond the data currently being parsed, regardless
365
+ // of whether the file is 1 MB or 100 GB.
366
+ //
367
+ // We use `this.buf.subarray()` (zero-copy view) instead of
368
+ // `Buffer.from()` (copy) to keep trimming O(1). The original
369
+ // backing ArrayBuffer is retained until all views are GC'd, but
370
+ // since we're trimming in ~64 KB increments, the retained window
371
+ // is negligible compared to the total file size.
372
+ if (this.offset > 65536) {
373
+ this.byteTrimming += this.offset;
374
+ this.buf = this.buf.subarray(this.offset);
375
+ this.offset = 0;
376
+ }
377
+ }
378
+ // ── Waiter management ────────────────────────────────────
379
+ /**
380
+ * Resolve as many batch and iterator waiters as possible.
381
+ *
382
+ * Called after every `onData` (new values may have been parsed) and
383
+ * on `onEnd` / `onStreamError`. For batch waiters, if the queue has
384
+ * at least `count` items we drain immediately. For iterator waiters
385
+ * we deliver one value at a time.
386
+ *
387
+ * If a fatal error occurred, all waiters are rejected so the
388
+ * consumer sees the error at its await point.
389
+ */
390
+ settleWaiters() {
391
+ // Batch waiters.
392
+ let i = 0;
393
+ while (i < this.batchWaiters.length) {
394
+ const w = this.batchWaiters[i];
395
+ if (this.fatalError) {
396
+ w.deferred.reject(this.fatalError);
397
+ this.batchWaiters.splice(i, 1);
398
+ continue;
399
+ }
400
+ if (this.queue.length >= w.count || (this.streamEnded && this.queue.length > 0)) {
401
+ w.deferred.resolve(this.drainQueue(w.count));
402
+ this.batchWaiters.splice(i, 1);
403
+ }
404
+ else if (this.streamEnded && this.queue.length === 0) {
405
+ w.deferred.resolve([]);
406
+ this.batchWaiters.splice(i, 1);
407
+ }
408
+ else {
409
+ i++;
410
+ }
411
+ }
412
+ // Iterator waiters.
413
+ i = 0;
414
+ while (i < this.iterWaiters.length) {
415
+ const w = this.iterWaiters[i];
416
+ if (this.fatalError) {
417
+ w.reject(this.fatalError);
418
+ this.iterWaiters.splice(i, 1);
419
+ continue;
420
+ }
421
+ if (this.queue.length > 0) {
422
+ const value = this.queue.shift();
423
+ w.resolve({ done: false, value });
424
+ this.iterWaiters.splice(i, 1);
425
+ this.maybeResumeIfLow();
426
+ }
427
+ else if (this.streamEnded) {
428
+ w.resolve({ done: true, value: undefined });
429
+ this.iterWaiters.splice(i, 1);
430
+ }
431
+ else {
432
+ i++;
433
+ }
434
+ }
435
+ }
436
+ /**
437
+ * Extract up to `count` values from the queue and hand them to the
438
+ * consumer. Resumes the underlying stream if the queue drops below
439
+ * maxQueueSize (backpressure release).
440
+ */
441
+ drainQueue(count) {
442
+ const take = Math.min(count, this.queue.length);
443
+ const result = this.queue.splice(0, take);
444
+ this.maybeResumeIfLow();
445
+ return result;
446
+ }
447
+ /**
448
+ * Transition to the fatal-error state.
449
+ *
450
+ * - Sets `fatalError` so all future calls throw.
451
+ * - Destroys the underlying ReadStream (file descriptor is closed).
452
+ * - Rejects all pending waiters so no consumer hangs.
453
+ *
454
+ * The caller guarantees that the error is non-recoverable — corrupt
455
+ * data, I/O failure, or truncated file. User-initiated `destroy()`
456
+ * does NOT call this method; it sets `destroyed` instead.
457
+ */
458
+ fail(err) {
459
+ this.fatalError = err;
460
+ this.stream.destroy();
461
+ for (const w of this.batchWaiters)
462
+ w.deferred.reject(err);
463
+ this.batchWaiters.length = 0;
464
+ for (const w of this.iterWaiters)
465
+ w.reject(err);
466
+ this.iterWaiters.length = 0;
467
+ }
468
+ /**
469
+ * Resume the underlying stream if it is currently paused and the
470
+ * reader is still active. Used when a consumer calls readBatch or
471
+ * iterator next() and there aren't enough queued values yet.
472
+ */
473
+ maybeResumeStream() {
474
+ if (!this.streamEnded && !this.destroyed && !this.fatalError && this.stream.isPaused()) {
475
+ this.stream.resume();
476
+ }
477
+ }
478
+ /**
479
+ * Resume the underlying stream if the queue has drained below the
480
+ * maxQueueSize threshold. This is the backpressure release — called
481
+ * after each value is consumed (drainQueue or iterator shift).
482
+ */
483
+ maybeResumeIfLow() {
484
+ if (!this.streamEnded &&
485
+ !this.destroyed &&
486
+ !this.fatalError &&
487
+ this.queue.length < this.maxQueueSize &&
488
+ this.stream.isPaused()) {
489
+ this.stream.resume();
490
+ }
491
+ }
492
+ }
493
+ /**
494
+ * Create a streaming binary reader for a file.
495
+ *
496
+ * Unlike {@link createBinaryStorage} (which loads the entire file into
497
+ * memory), this reads values sequentially from disk via a Node.js
498
+ * `ReadStream`. Values are parsed one at a time and returned in
499
+ * complete batches — the user never sees partial / truncated data.
500
+ *
501
+ * @param filePath — Absolute or relative path to the binary file.
502
+ * @param options — Optional tuning knobs (queue size, chunk size).
503
+ * @returns A reader with `readBatch()`, async iteration, and `destroy()`.
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * import { createStreamingReader } from 'ink-cartridge';
508
+ *
509
+ * const reader = createStreamingReader('large.bin');
510
+ *
511
+ * let batch;
512
+ * while ((batch = await reader.readBatch(1000)).length > 0) {
513
+ * for (const value of batch) {
514
+ * console.log(value);
515
+ * }
516
+ * }
517
+ *
518
+ * // Or: async iteration
519
+ * for await (const value of reader) {
520
+ * console.log(value);
521
+ * }
522
+ * ```
523
+ *
524
+ * @throws `ENOENT` if the file does not exist.
525
+ * @throws `StreamCorruptError` if the file is corrupt or truncated.
526
+ */
527
+ export function createStreamingReader(filePath, options) {
528
+ fs.accessSync(filePath, fs.constants.R_OK);
529
+ return new StreamingReader(filePath, options);
530
+ }
@@ -0,0 +1,91 @@
1
+ import type { BinaryStorageOptions, BinaryStorageAPI } from './types.js';
2
+ /**
3
+ * Create a sequential binary storage station.
4
+ *
5
+ * Unlike {@link createStorage} (which is a key-value store backed by
6
+ * JSON), this creates a FIFO binary stream ideal for ordered data
7
+ * where keys are unnecessary overhead:
8
+ *
9
+ * - **Game replay recording** — timestamp, action, position per frame
10
+ * - **Sensor data logging** — time series of numeric readings
11
+ * - **Chat / event logs** — append-only sequential messages
12
+ * - **Checkpoint chains** — ordered snapshots of application state
13
+ *
14
+ * ## How it works
15
+ *
16
+ * Each value written to the stream is encoded with a 1-byte type tag
17
+ * followed by its payload. Reading consumes values in the same order.
18
+ * This guarantees type safety at the individual value level — if you
19
+ * call `read.num()` on a position that contains a string, you get an
20
+ * immediate error with the exact byte offset.
21
+ *
22
+ * The read and write cursors are independent, so you can write a
23
+ * sequence, `resetRead()`, and replay it immediately — all from
24
+ * memory, no disk I/O needed.
25
+ *
26
+ * ## Binary format
27
+ *
28
+ * ```
29
+ * [1 byte TypeTag] [payload...]
30
+ *
31
+ * Number: [0x01] [8 bytes float64 LE]
32
+ * String: [0x02] [4 bytes uint32 LE length] [UTF-8 bytes]
33
+ * Boolean: [0x03] [1 byte: 0x00=false, 0x01=true]
34
+ * Object: [0x04] [4 bytes uint32 LE length] [JSON UTF-8 bytes]
35
+ * Array: [0x05] [4 bytes uint32 LE length] [JSON UTF-8 bytes]
36
+ * Null: [0x06]
37
+ * ```
38
+ *
39
+ * @param options Optional directory, file name, and flush behaviour.
40
+ * @returns A typed binary storage API with positional read/write
41
+ * cursors and full seek/truncate support.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { createBinaryStorage } from 'ink-cartridge';
46
+ *
47
+ * const bin = createBinaryStorage({ file: 'replay.bin' });
48
+ *
49
+ * // ── Recording phase ──
50
+ * await bin.write.num(Date.now()); // timestamp
51
+ * await bin.write.str('attack'); // action
52
+ * await bin.write.num(120); // damage
53
+ * await bin.write.b(true); // critical hit
54
+ *
55
+ * // ── Playback phase ──
56
+ * bin.resetRead();
57
+ * while (bin.tellRead() < bin.tellWrite()) {
58
+ * const ts = await bin.read.num();
59
+ * const action = await bin.read.str();
60
+ * const damage = await bin.read.num();
61
+ * const crit = await bin.read.b();
62
+ * console.log(`${ts}: ${action} for ${damage} (crit=${crit})`);
63
+ * }
64
+ *
65
+ * // ── Bookmarking ──
66
+ * bin.resetRead();
67
+ * await bin.read.num(); // skip timestamp
68
+ * const pos = bin.tellRead();
69
+ * // ... later ...
70
+ * bin.seekRead(pos); // re-read from action
71
+ * ```
72
+ *
73
+ * @throws Never throws during construction. Read errors (type
74
+ * mismatch, end of stream) are thrown as Promise rejections.
75
+ * Write errors from the filesystem propagate as standard
76
+ * Node.js exceptions.
77
+ */
78
+ export declare function createBinaryStorage(options?: BinaryStorageOptions): BinaryStorageAPI;
79
+ export type { BinaryStorageOptions, BinaryStorageAPI };
80
+ /**
81
+ * Low-level exports for advanced use cases.
82
+ *
83
+ * `TypeTag` is the numeric enum of type markers (0x01–0x06).
84
+ * `TAG_NAMES` maps each tag to its human-readable name.
85
+ *
86
+ * These are useful if you need to inspect or manipulate binary
87
+ * storage files outside the normal read/write API.
88
+ */
89
+ export { TypeTag, TAG_NAMES } from './types.js';
90
+ export { createStreamingReader, StreamCorruptError } from './StreamingReader.js';
91
+ export type { StreamingReaderOptions, StreamingReaderAPI } from './types.js';