@nhtio/adk 0.1.0-master-f0aa531d

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 (297) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +3 -0
  3. package/batteries/index.d.ts +28 -0
  4. package/batteries/llm/index.d.ts +11 -0
  5. package/batteries/llm/openai_chat_completions/adapter.cjs +916 -0
  6. package/batteries/llm/openai_chat_completions/adapter.cjs.map +1 -0
  7. package/batteries/llm/openai_chat_completions/adapter.d.ts +101 -0
  8. package/batteries/llm/openai_chat_completions/adapter.mjs +914 -0
  9. package/batteries/llm/openai_chat_completions/adapter.mjs.map +1 -0
  10. package/batteries/llm/openai_chat_completions/exceptions.cjs +89 -0
  11. package/batteries/llm/openai_chat_completions/exceptions.cjs.map +1 -0
  12. package/batteries/llm/openai_chat_completions/exceptions.d.ts +97 -0
  13. package/batteries/llm/openai_chat_completions/exceptions.mjs +81 -0
  14. package/batteries/llm/openai_chat_completions/exceptions.mjs.map +1 -0
  15. package/batteries/llm/openai_chat_completions/helpers.cjs +819 -0
  16. package/batteries/llm/openai_chat_completions/helpers.cjs.map +1 -0
  17. package/batteries/llm/openai_chat_completions/helpers.d.ts +233 -0
  18. package/batteries/llm/openai_chat_completions/helpers.mjs +783 -0
  19. package/batteries/llm/openai_chat_completions/helpers.mjs.map +1 -0
  20. package/batteries/llm/openai_chat_completions/index.d.ts +27 -0
  21. package/batteries/llm/openai_chat_completions/types.cjs +1 -0
  22. package/batteries/llm/openai_chat_completions/types.d.ts +524 -0
  23. package/batteries/llm/openai_chat_completions/types.mjs +0 -0
  24. package/batteries/llm/openai_chat_completions/validation.cjs +190 -0
  25. package/batteries/llm/openai_chat_completions/validation.cjs.map +1 -0
  26. package/batteries/llm/openai_chat_completions/validation.d.ts +31 -0
  27. package/batteries/llm/openai_chat_completions/validation.mjs +187 -0
  28. package/batteries/llm/openai_chat_completions/validation.mjs.map +1 -0
  29. package/batteries/llm/openai_chat_completions.cjs +51 -0
  30. package/batteries/llm/openai_chat_completions.mjs +5 -0
  31. package/batteries/llm/webllm_chat_completions/adapter.cjs +658 -0
  32. package/batteries/llm/webllm_chat_completions/adapter.cjs.map +1 -0
  33. package/batteries/llm/webllm_chat_completions/adapter.d.ts +103 -0
  34. package/batteries/llm/webllm_chat_completions/adapter.mjs +656 -0
  35. package/batteries/llm/webllm_chat_completions/adapter.mjs.map +1 -0
  36. package/batteries/llm/webllm_chat_completions/exceptions.cjs +70 -0
  37. package/batteries/llm/webllm_chat_completions/exceptions.cjs.map +1 -0
  38. package/batteries/llm/webllm_chat_completions/exceptions.d.ts +74 -0
  39. package/batteries/llm/webllm_chat_completions/exceptions.mjs +65 -0
  40. package/batteries/llm/webllm_chat_completions/exceptions.mjs.map +1 -0
  41. package/batteries/llm/webllm_chat_completions/helpers.cjs +38 -0
  42. package/batteries/llm/webllm_chat_completions/helpers.d.ts +6 -0
  43. package/batteries/llm/webllm_chat_completions/helpers.mjs +2 -0
  44. package/batteries/llm/webllm_chat_completions/index.d.ts +25 -0
  45. package/batteries/llm/webllm_chat_completions/types.d.ts +31 -0
  46. package/batteries/llm/webllm_chat_completions/validation.cjs +115 -0
  47. package/batteries/llm/webllm_chat_completions/validation.cjs.map +1 -0
  48. package/batteries/llm/webllm_chat_completions/validation.d.ts +8 -0
  49. package/batteries/llm/webllm_chat_completions/validation.mjs +112 -0
  50. package/batteries/llm/webllm_chat_completions/validation.mjs.map +1 -0
  51. package/batteries/llm/webllm_chat_completions.cjs +50 -0
  52. package/batteries/llm/webllm_chat_completions.mjs +6 -0
  53. package/batteries/llm.cjs +63 -0
  54. package/batteries/llm.mjs +10 -0
  55. package/batteries/storage/flydrive/index.d.ts +167 -0
  56. package/batteries/storage/flydrive.cjs +249 -0
  57. package/batteries/storage/flydrive.cjs.map +1 -0
  58. package/batteries/storage/flydrive.mjs +249 -0
  59. package/batteries/storage/flydrive.mjs.map +1 -0
  60. package/batteries/storage/in_memory/index.d.ts +106 -0
  61. package/batteries/storage/in_memory.cjs +121 -0
  62. package/batteries/storage/in_memory.cjs.map +1 -0
  63. package/batteries/storage/in_memory.mjs +119 -0
  64. package/batteries/storage/in_memory.mjs.map +1 -0
  65. package/batteries/storage/index.d.ts +18 -0
  66. package/batteries/storage/opfs/index.d.ts +299 -0
  67. package/batteries/storage/opfs.cjs +368 -0
  68. package/batteries/storage/opfs.cjs.map +1 -0
  69. package/batteries/storage/opfs.mjs +366 -0
  70. package/batteries/storage/opfs.mjs.map +1 -0
  71. package/batteries/storage.cjs +4 -0
  72. package/batteries/storage.mjs +2 -0
  73. package/batteries/tools/color/index.d.ts +37 -0
  74. package/batteries/tools/color.cjs +659 -0
  75. package/batteries/tools/color.cjs.map +1 -0
  76. package/batteries/tools/color.mjs +655 -0
  77. package/batteries/tools/color.mjs.map +1 -0
  78. package/batteries/tools/comparison/index.d.ts +29 -0
  79. package/batteries/tools/comparison.cjs +171 -0
  80. package/batteries/tools/comparison.cjs.map +1 -0
  81. package/batteries/tools/comparison.mjs +168 -0
  82. package/batteries/tools/comparison.mjs.map +1 -0
  83. package/batteries/tools/data_structure/index.d.ts +30 -0
  84. package/batteries/tools/data_structure.cjs +270 -0
  85. package/batteries/tools/data_structure.cjs.map +1 -0
  86. package/batteries/tools/data_structure.mjs +267 -0
  87. package/batteries/tools/data_structure.mjs.map +1 -0
  88. package/batteries/tools/datetime_extended/index.d.ts +51 -0
  89. package/batteries/tools/datetime_extended.cjs +309 -0
  90. package/batteries/tools/datetime_extended.cjs.map +1 -0
  91. package/batteries/tools/datetime_extended.mjs +302 -0
  92. package/batteries/tools/datetime_extended.mjs.map +1 -0
  93. package/batteries/tools/datetime_math/index.d.ts +36 -0
  94. package/batteries/tools/datetime_math.cjs +175 -0
  95. package/batteries/tools/datetime_math.cjs.map +1 -0
  96. package/batteries/tools/datetime_math.mjs +171 -0
  97. package/batteries/tools/datetime_math.mjs.map +1 -0
  98. package/batteries/tools/encoding/index.d.ts +36 -0
  99. package/batteries/tools/encoding.cjs +156 -0
  100. package/batteries/tools/encoding.cjs.map +1 -0
  101. package/batteries/tools/encoding.mjs +152 -0
  102. package/batteries/tools/encoding.mjs.map +1 -0
  103. package/batteries/tools/formatting/index.d.ts +28 -0
  104. package/batteries/tools/formatting.cjs +120 -0
  105. package/batteries/tools/formatting.cjs.map +1 -0
  106. package/batteries/tools/formatting.mjs +117 -0
  107. package/batteries/tools/formatting.mjs.map +1 -0
  108. package/batteries/tools/geo_basics/index.d.ts +33 -0
  109. package/batteries/tools/geo_basics.cjs +136 -0
  110. package/batteries/tools/geo_basics.cjs.map +1 -0
  111. package/batteries/tools/geo_basics.mjs +132 -0
  112. package/batteries/tools/geo_basics.mjs.map +1 -0
  113. package/batteries/tools/index.d.ts +32 -0
  114. package/batteries/tools/math/index.d.ts +37 -0
  115. package/batteries/tools/math.cjs +136 -0
  116. package/batteries/tools/math.cjs.map +1 -0
  117. package/batteries/tools/math.mjs +133 -0
  118. package/batteries/tools/math.mjs.map +1 -0
  119. package/batteries/tools/memory/index.d.ts +73 -0
  120. package/batteries/tools/memory.cjs +193 -0
  121. package/batteries/tools/memory.cjs.map +1 -0
  122. package/batteries/tools/memory.mjs +187 -0
  123. package/batteries/tools/memory.mjs.map +1 -0
  124. package/batteries/tools/parsing/index.d.ts +47 -0
  125. package/batteries/tools/parsing.cjs +191 -0
  126. package/batteries/tools/parsing.cjs.map +1 -0
  127. package/batteries/tools/parsing.mjs +185 -0
  128. package/batteries/tools/parsing.mjs.map +1 -0
  129. package/batteries/tools/retrievables/index.d.ts +81 -0
  130. package/batteries/tools/retrievables.cjs +215 -0
  131. package/batteries/tools/retrievables.cjs.map +1 -0
  132. package/batteries/tools/retrievables.mjs +209 -0
  133. package/batteries/tools/retrievables.mjs.map +1 -0
  134. package/batteries/tools/standing_instructions/index.d.ts +64 -0
  135. package/batteries/tools/standing_instructions.cjs +126 -0
  136. package/batteries/tools/standing_instructions.cjs.map +1 -0
  137. package/batteries/tools/standing_instructions.mjs +121 -0
  138. package/batteries/tools/standing_instructions.mjs.map +1 -0
  139. package/batteries/tools/statistics/index.d.ts +46 -0
  140. package/batteries/tools/statistics.cjs +253 -0
  141. package/batteries/tools/statistics.cjs.map +1 -0
  142. package/batteries/tools/statistics.mjs +248 -0
  143. package/batteries/tools/statistics.mjs.map +1 -0
  144. package/batteries/tools/string_processing/index.d.ts +29 -0
  145. package/batteries/tools/string_processing.cjs +154 -0
  146. package/batteries/tools/string_processing.cjs.map +1 -0
  147. package/batteries/tools/string_processing.mjs +151 -0
  148. package/batteries/tools/string_processing.mjs.map +1 -0
  149. package/batteries/tools/structured_data/index.d.ts +34 -0
  150. package/batteries/tools/structured_data.cjs +189 -0
  151. package/batteries/tools/structured_data.cjs.map +1 -0
  152. package/batteries/tools/structured_data.mjs +185 -0
  153. package/batteries/tools/structured_data.mjs.map +1 -0
  154. package/batteries/tools/text_analysis/index.d.ts +31 -0
  155. package/batteries/tools/text_analysis.cjs +120 -0
  156. package/batteries/tools/text_analysis.cjs.map +1 -0
  157. package/batteries/tools/text_analysis.mjs +117 -0
  158. package/batteries/tools/text_analysis.mjs.map +1 -0
  159. package/batteries/tools/text_comparison/index.d.ts +28 -0
  160. package/batteries/tools/text_comparison.cjs +96 -0
  161. package/batteries/tools/text_comparison.cjs.map +1 -0
  162. package/batteries/tools/text_comparison.mjs +93 -0
  163. package/batteries/tools/text_comparison.mjs.map +1 -0
  164. package/batteries/tools/time/index.d.ts +27 -0
  165. package/batteries/tools/time.cjs +63 -0
  166. package/batteries/tools/time.cjs.map +1 -0
  167. package/batteries/tools/time.mjs +60 -0
  168. package/batteries/tools/time.mjs.map +1 -0
  169. package/batteries/tools/unit_conversion/index.d.ts +19 -0
  170. package/batteries/tools/unit_conversion.cjs +452 -0
  171. package/batteries/tools/unit_conversion.cjs.map +1 -0
  172. package/batteries/tools/unit_conversion.mjs +450 -0
  173. package/batteries/tools/unit_conversion.mjs.map +1 -0
  174. package/batteries/tools.cjs +80 -0
  175. package/batteries/tools.mjs +21 -0
  176. package/batteries.cjs +142 -0
  177. package/batteries.mjs +30 -0
  178. package/chunk-KmRHZBOW.js +35 -0
  179. package/common-DeZaonK1.mjs +208 -0
  180. package/common-DeZaonK1.mjs.map +1 -0
  181. package/common-Od8edUXU.js +232 -0
  182. package/common-Od8edUXU.js.map +1 -0
  183. package/common.cjs +31 -0
  184. package/common.d.ts +108 -0
  185. package/common.mjs +8 -0
  186. package/dispatch_runner-9j6bXHL3.mjs +1609 -0
  187. package/dispatch_runner-9j6bXHL3.mjs.map +1 -0
  188. package/dispatch_runner-CsoH0nld.js +1627 -0
  189. package/dispatch_runner-CsoH0nld.js.map +1 -0
  190. package/dispatch_runner.cjs +3 -0
  191. package/dispatch_runner.d.ts +17 -0
  192. package/dispatch_runner.mjs +2 -0
  193. package/exceptions-D5YrO9Vm.js +280 -0
  194. package/exceptions-D5YrO9Vm.js.map +1 -0
  195. package/exceptions-NrzIHw_R.mjs +244 -0
  196. package/exceptions-NrzIHw_R.mjs.map +1 -0
  197. package/exceptions.cjs +33 -0
  198. package/exceptions.d.ts +52 -0
  199. package/exceptions.mjs +3 -0
  200. package/factories.cjs +4 -0
  201. package/factories.d.ts +39 -0
  202. package/factories.mjs +2 -0
  203. package/forge.cjs +9 -0
  204. package/forge.d.ts +49 -0
  205. package/forge.mjs +5 -0
  206. package/guards.cjs +96 -0
  207. package/guards.cjs.map +1 -0
  208. package/guards.d.ts +83 -0
  209. package/guards.mjs +72 -0
  210. package/guards.mjs.map +1 -0
  211. package/index.cjs +107 -0
  212. package/index.cjs.map +1 -0
  213. package/index.d.ts +18 -0
  214. package/index.mjs +31 -0
  215. package/index.mjs.map +1 -0
  216. package/lib/classes/artifact_tool.d.ts +129 -0
  217. package/lib/classes/base_exception.d.ts +83 -0
  218. package/lib/classes/identity.d.ts +71 -0
  219. package/lib/classes/media.d.ts +326 -0
  220. package/lib/classes/memory.d.ts +72 -0
  221. package/lib/classes/message.d.ts +137 -0
  222. package/lib/classes/registry.d.ts +79 -0
  223. package/lib/classes/retrievable.d.ts +100 -0
  224. package/lib/classes/spooled_artifact.d.ts +296 -0
  225. package/lib/classes/spooled_json_artifact.d.ts +158 -0
  226. package/lib/classes/spooled_markdown_artifact.d.ts +202 -0
  227. package/lib/classes/thought.d.ts +142 -0
  228. package/lib/classes/tokenizable.d.ts +124 -0
  229. package/lib/classes/tool.d.ts +228 -0
  230. package/lib/classes/tool_call.d.ts +190 -0
  231. package/lib/classes/tool_registry.d.ts +159 -0
  232. package/lib/classes/turn_gate.d.ts +109 -0
  233. package/lib/contracts/dispatch_context.d.ts +345 -0
  234. package/lib/contracts/media_reader.d.ts +60 -0
  235. package/lib/contracts/spool_reader.d.ts +80 -0
  236. package/lib/contracts/spooled_artifact_constructor.d.ts +38 -0
  237. package/lib/contracts/turn_runner_config.d.ts +101 -0
  238. package/lib/contracts/turn_runner_context.d.ts +267 -0
  239. package/lib/dispatch_runner.d.ts +98 -0
  240. package/lib/exceptions/runtime.d.ts +370 -0
  241. package/lib/helpers/media_readers.d.ts +39 -0
  242. package/lib/turn_runner.d.ts +144 -0
  243. package/lib/types/dispatch_context.d.ts +233 -0
  244. package/lib/types/dispatch_runner.d.ts +387 -0
  245. package/lib/types/turn_runner.d.ts +322 -0
  246. package/lib/utils/canonical_json.d.ts +18 -0
  247. package/lib/utils/exceptions.d.ts +78 -0
  248. package/lib/utils/guards.d.ts +32 -0
  249. package/lib/utils/validation.d.ts +77 -0
  250. package/package.json +334 -0
  251. package/runtime-BJVkrGQe.js +519 -0
  252. package/runtime-BJVkrGQe.js.map +1 -0
  253. package/runtime-CrEPIFgr.mjs +346 -0
  254. package/runtime-CrEPIFgr.mjs.map +1 -0
  255. package/skills/adk-assembly/SKILL.md +109 -0
  256. package/skills/adk-assembly/references/assembly-contract.md +66 -0
  257. package/skills/adk-assembly/references/executors-tools-pipelines-events.md +113 -0
  258. package/skills/adk-assembly/references/first-integration.md +93 -0
  259. package/skills/adk-assembly/references/storage-and-context.md +102 -0
  260. package/spooled_artifact-C5ZtGxuJ.mjs +544 -0
  261. package/spooled_artifact-C5ZtGxuJ.mjs.map +1 -0
  262. package/spooled_artifact-Cm9Te22K.js +568 -0
  263. package/spooled_artifact-Cm9Te22K.js.map +1 -0
  264. package/spooled_artifact.cjs +7 -0
  265. package/spooled_artifact.d.ts +40 -0
  266. package/spooled_artifact.mjs +3 -0
  267. package/spooled_markdown_artifact-BpUJol0W.mjs +771 -0
  268. package/spooled_markdown_artifact-BpUJol0W.mjs.map +1 -0
  269. package/spooled_markdown_artifact-RRB113sy.js +786 -0
  270. package/spooled_markdown_artifact-RRB113sy.js.map +1 -0
  271. package/thought-CDb457b4.mjs +470 -0
  272. package/thought-CDb457b4.mjs.map +1 -0
  273. package/thought-DuN2PgdO.js +494 -0
  274. package/thought-DuN2PgdO.js.map +1 -0
  275. package/tool-COSeH8I6.js +302 -0
  276. package/tool-COSeH8I6.js.map +1 -0
  277. package/tool-D2WB1EA1.mjs +296 -0
  278. package/tool-D2WB1EA1.mjs.map +1 -0
  279. package/tool_call-BKyyxGaZ.mjs +578 -0
  280. package/tool_call-BKyyxGaZ.mjs.map +1 -0
  281. package/tool_call-DFgzcVcU.js +608 -0
  282. package/tool_call-DFgzcVcU.js.map +1 -0
  283. package/tool_registry-Dkfprsck.js +641 -0
  284. package/tool_registry-Dkfprsck.js.map +1 -0
  285. package/tool_registry-DqLOyGyG.mjs +592 -0
  286. package/tool_registry-DqLOyGyG.mjs.map +1 -0
  287. package/turn_runner-CMm2BHdX.js +615 -0
  288. package/turn_runner-CMm2BHdX.js.map +1 -0
  289. package/turn_runner-y7eyEcJH.mjs +603 -0
  290. package/turn_runner-y7eyEcJH.mjs.map +1 -0
  291. package/turn_runner.cjs +3 -0
  292. package/turn_runner.d.ts +21 -0
  293. package/turn_runner.mjs +2 -0
  294. package/types.cjs +1 -0
  295. package/types.d.ts +56 -0
  296. package/types.mjs +0 -0
  297. package/vite-env.d.ts +23 -0
@@ -0,0 +1,249 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#endregion
3
+ //#region src/batteries/storage/flydrive/index.ts
4
+ var import___vite_browser_external = (/* @__PURE__ */ require("../../chunk-KmRHZBOW.js").__commonJSMin(((exports, module) => {
5
+ module.exports = {};
6
+ })))();
7
+ var DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024;
8
+ var LF = 10;
9
+ var isNonNegativeFiniteNumber = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
10
+ /**
11
+ * Reads a flydrive-backed file as a {@link @nhtio/adk!SpoolReader}.
12
+ *
13
+ * @remarks
14
+ * Constructor is **not** async — but the first method call awaits a private readiness promise
15
+ * that fetches the object's metadata (and in eager mode, its contents). Subsequent calls reuse
16
+ * the cached state. This keeps construction call sites synchronous while still doing real I/O
17
+ * lazily.
18
+ *
19
+ * Implementations of {@link @nhtio/adk!SpoolReader.line}, {@link @nhtio/adk!SpoolReader.byteLength}, and
20
+ * {@link @nhtio/adk!SpoolReader.lineCount} all return promises. The `SpoolReader` contract supports both
21
+ * sync and async return; consumers of `SpooledArtifact` handle either.
22
+ */
23
+ var FlydriveSpoolReader = class {
24
+ #disk;
25
+ #key;
26
+ #threshold;
27
+ #ready;
28
+ constructor(disk, key, opts = {}) {
29
+ this.#disk = disk;
30
+ this.#key = key;
31
+ const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES;
32
+ if (typeof raw !== "number" || Number.isNaN(raw) || raw < 0) throw new TypeError(`FlydriveSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`);
33
+ this.#threshold = raw;
34
+ }
35
+ async line(index) {
36
+ const state = await this.#load();
37
+ if (state.mode === "eager") return state.lines[index];
38
+ if (index < 0 || index >= state.offsets.length - 1) return void 0;
39
+ return this.#readRange(state.offsets[index], state.offsets[index + 1]);
40
+ }
41
+ async byteLength() {
42
+ return (await this.#load()).bytes;
43
+ }
44
+ async lineCount() {
45
+ const state = await this.#load();
46
+ return state.mode === "eager" ? state.lines.length : state.offsets.length - 1;
47
+ }
48
+ /**
49
+ * Returns the full underlying content as a single decoded string, byte-faithful to the source.
50
+ *
51
+ * @remarks
52
+ * In **eager mode** the content is already cached at construction-time load and this method is
53
+ * effectively a property access. In **streaming mode** there is no cache: the file is
54
+ * re-streamed and concatenated on every call. Use {@link @nhtio/adk!SpooledArtifact.asString} judiciously
55
+ * on large streaming-mode artifacts.
56
+ */
57
+ async readAll() {
58
+ const state = await this.#load();
59
+ if (state.mode === "eager") return state.content;
60
+ const stream = await this.#disk.getStream(this.#key);
61
+ const chunks = [];
62
+ let total = 0;
63
+ for await (const chunk of stream) {
64
+ const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
65
+ chunks.push(view);
66
+ total += view.length;
67
+ }
68
+ const concat = new Uint8Array(total);
69
+ let offset = 0;
70
+ for (const view of chunks) {
71
+ concat.set(view, offset);
72
+ offset += view.length;
73
+ }
74
+ return new TextDecoder().decode(concat);
75
+ }
76
+ /**
77
+ * Lazily initialise the reader's mode-specific state. Called by every public method; the
78
+ * promise is cached so the work runs at most once.
79
+ */
80
+ #load() {
81
+ if (!this.#ready) this.#ready = this.#init();
82
+ return this.#ready;
83
+ }
84
+ async #init() {
85
+ const bytes = (await this.#disk.getMetaData(this.#key)).contentLength;
86
+ if (!isNonNegativeFiniteNumber(bytes)) throw new Error(`FlydriveSpoolReader: disk returned a non-finite contentLength (${String(bytes)}) for key "${this.#key}"`);
87
+ if (bytes < this.#threshold) {
88
+ const content = await this.#disk.get(this.#key);
89
+ return {
90
+ mode: "eager",
91
+ lines: content === "" ? [] : content.split("\n"),
92
+ bytes,
93
+ content
94
+ };
95
+ }
96
+ return this.#buildStreamingIndex(bytes);
97
+ }
98
+ async #buildStreamingIndex(bytes) {
99
+ if (bytes === 0) return {
100
+ mode: "streaming",
101
+ offsets: [0],
102
+ bytes
103
+ };
104
+ const stream = await this.#disk.getStream(this.#key);
105
+ const offsets = [0];
106
+ let position = 0;
107
+ let lastByte = -1;
108
+ for await (const chunk of stream) {
109
+ const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
110
+ for (const byte of view) {
111
+ position++;
112
+ if (byte === LF) offsets.push(position);
113
+ lastByte = byte;
114
+ }
115
+ }
116
+ if (lastByte === LF) offsets.push(position);
117
+ else if (offsets[offsets.length - 1] !== position) offsets.push(position);
118
+ return {
119
+ mode: "streaming",
120
+ offsets,
121
+ bytes
122
+ };
123
+ }
124
+ /**
125
+ * Streams the byte range `[start, end)` from the backing disk and returns it as a UTF-8
126
+ * string, stripping a trailing `\n` if present.
127
+ *
128
+ * @remarks
129
+ * flydrive doesn't expose native byte-range reads, so we open a fresh stream and skip until
130
+ * we reach the requested start offset, then collect until we reach `end`. This is O(end)
131
+ * per call — fine for occasional reads but worth profiling if a workload performs many
132
+ * sequential `line()` calls on a large file.
133
+ */
134
+ async #readRange(start, end) {
135
+ if (start === end) return "";
136
+ const stream = await this.#disk.getStream(this.#key);
137
+ const out = [];
138
+ let position = 0;
139
+ for await (const chunk of stream) {
140
+ const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
141
+ if (position >= end) {
142
+ if (stream instanceof import___vite_browser_external.Readable) stream.destroy();
143
+ break;
144
+ }
145
+ if (position + view.length <= start) {
146
+ position += view.length;
147
+ continue;
148
+ }
149
+ const localStart = Math.max(0, start - position);
150
+ const localEnd = Math.min(view.length, end - position);
151
+ for (let i = localStart; i < localEnd; i++) out.push(view[i]);
152
+ position += view.length;
153
+ }
154
+ if (out.length > 0 && out[out.length - 1] === LF) out.pop();
155
+ return new TextDecoder().decode(new Uint8Array(out));
156
+ }
157
+ };
158
+ /**
159
+ * "Give bytes, get a reader" persistence layer over a flydrive {@link Disk}.
160
+ *
161
+ * @remarks
162
+ * `write(callId, bytes)` calls `disk.put(key, bytes)` where `key = keyPrefix + callId`, then
163
+ * returns a fresh {@link FlydriveSpoolReader} pointed at the same key. `read(callId)` returns
164
+ * a reader without re-writing; `delete(callId)` calls `disk.delete(key)`.
165
+ *
166
+ * The store is stateless — it owns no in-memory cache of writes. Multiple `FlydriveSpoolStore`
167
+ * instances sharing the same disk + key prefix see the same data.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * import { Disk } from 'flydrive'
172
+ * import { FSDriver } from 'flydrive/drivers/fs'
173
+ * import { FlydriveSpoolStore } from '@nhtio/adk/batteries/storage/flydrive'
174
+ *
175
+ * const disk = new Disk(new FSDriver({ location: './tmp', visibility: 'public' }))
176
+ * const store = new FlydriveSpoolStore(disk)
177
+ *
178
+ * const bytes = await tool.executor(ctx)(args)
179
+ * const reader = await store.write(callId, bytes)
180
+ * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact
181
+ * const artifact = new Ctor(reader)
182
+ * ```
183
+ */
184
+ var FlydriveSpoolStore = class {
185
+ #disk;
186
+ #prefix;
187
+ #defaultThreshold;
188
+ constructor(disk, opts = {}) {
189
+ this.#disk = disk;
190
+ this.#prefix = opts.keyPrefix ?? "";
191
+ this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES;
192
+ }
193
+ /**
194
+ * Persists `bytes` under `callId` and returns a reader bound to the stored key.
195
+ *
196
+ * @param callId - Identifier used to retrieve the bytes via {@link FlydriveSpoolStore.read}.
197
+ * @param bytes - The bytes to store, as a `string` or `Uint8Array`.
198
+ * @param opts - Per-call override for `streamThresholdBytes`.
199
+ * @returns A {@link FlydriveSpoolReader} over the stored bytes.
200
+ */
201
+ async write(callId, bytes, opts) {
202
+ const key = this.#prefix + callId;
203
+ await this.#disk.put(key, bytes);
204
+ return new FlydriveSpoolReader(this.#disk, key, { streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold });
205
+ }
206
+ /**
207
+ * Returns a reader over the bytes previously written under `callId`.
208
+ *
209
+ * @remarks
210
+ * Returns `undefined` if the underlying key does not exist. Existence is checked via
211
+ * `disk.exists(key)` before the reader is returned, so callers can rely on a defined return
212
+ * value pointing at a real object.
213
+ *
214
+ * @param callId - Identifier supplied to a prior {@link FlydriveSpoolStore.write} call.
215
+ * @param opts - Per-call override for `streamThresholdBytes`.
216
+ * @returns A {@link FlydriveSpoolReader}, or `undefined` if the key is missing.
217
+ */
218
+ async read(callId, opts) {
219
+ const key = this.#prefix + callId;
220
+ if (!await this.#disk.exists(key)) return void 0;
221
+ return new FlydriveSpoolReader(this.#disk, key, { streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold });
222
+ }
223
+ /**
224
+ * Removes the entry under `callId`.
225
+ *
226
+ * @param callId - Identifier whose entry should be removed.
227
+ * @returns `true` if the key existed and was removed; `false` if it didn't exist.
228
+ */
229
+ async delete(callId) {
230
+ const key = this.#prefix + callId;
231
+ if (!await this.#disk.exists(key)) return false;
232
+ await this.#disk.delete(key);
233
+ return true;
234
+ }
235
+ /**
236
+ * Returns the full disk key for a given `callId` (i.e. `keyPrefix + callId`).
237
+ *
238
+ * @remarks
239
+ * Useful for tests or for callers that want to interact with the underlying disk directly.
240
+ */
241
+ keyFor(callId) {
242
+ return this.#prefix + callId;
243
+ }
244
+ };
245
+ //#endregion
246
+ exports.FlydriveSpoolReader = FlydriveSpoolReader;
247
+ exports.FlydriveSpoolStore = FlydriveSpoolStore;
248
+
249
+ //# sourceMappingURL=flydrive.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flydrive.cjs","names":["#disk","#key","#threshold","#load","#readRange","#ready","#init","#buildStreamingIndex","#prefix","#defaultThreshold"],"sources":["../../../__vite-browser-external","../../../src/batteries/storage/flydrive/index.ts"],"sourcesContent":["module.exports = {}","/**\n * Flydrive-backed spooled artifact storage for Node and server runtimes.\n *\n * @module @nhtio/adk/batteries/storage/flydrive\n *\n * @remarks\n * **Requires Node 24+.** `flydrive` uses the `node:stream` `ReadableStream` web API which is\n * only available from Node 24. This battery does not work in the browser or earlier Node versions.\n *\n * Opt-in storage battery backed by [flydrive](https://flydrive.dev). Provides\n * {@link FlydriveSpoolReader} (a {@link @nhtio/adk!SpoolReader} over a flydrive key) and\n * {@link FlydriveSpoolStore} (a `write(callId, bytes) → reader` persistence layer that wraps an\n * existing `Disk`).\n *\n * The reader has two modes selected at construction time based on the size of the underlying\n * object:\n *\n * - **Eager mode** — when the object's `contentLength` is below `streamThresholdBytes` (default\n * 10 MiB), the reader calls `disk.get(key)` once, splits the content on `\\n`, and caches\n * lines + byte count. All subsequent `line() / byteLength() / lineCount()` calls resolve from\n * memory.\n * - **Streaming mode** — when `contentLength` meets or exceeds the threshold, the reader\n * streams the file once via `disk.getStream(key)` to build a line-offset index (`number[]`\n * of byte offsets per line), then serves each `line(i)` request by streaming the byte range\n * `[offsets[i], offsets[i+1])`. Caps RAM at one index + one line buffer regardless of file\n * size.\n *\n * Set `streamThresholdBytes: 0` to force streaming mode; set it to `Infinity` to force eager\n * mode. The default of 10 MiB matches typical tool output sizes — tune it for your workload.\n *\n * The store and reader are pure-flydrive: they don't know about S3, GCS, or filesystem\n * specifically — they delegate to whatever `Disk` you construct.\n */\n\nimport { Disk } from 'flydrive'\nimport { Readable } from 'node:stream'\nimport type { SpoolReader } from '@nhtio/adk/common'\n\nconst DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024 // 10 MiB\n\nconst LF = 0x0a // '\\n'\n\n/**\n * Constructor options for {@link FlydriveSpoolReader}.\n */\nexport interface FlydriveSpoolReaderOptions {\n /**\n * Byte-length threshold that switches between eager and streaming modes.\n *\n * @remarks\n * - Below the threshold → eager (whole-file in memory).\n * - At or above the threshold → streaming (line-offset index + per-line streaming reads).\n *\n * Set to `0` to force streaming mode; set to `Number.POSITIVE_INFINITY` to force eager mode.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\ninterface EagerState {\n mode: 'eager'\n lines: string[]\n bytes: number\n content: string\n}\n\ninterface StreamingState {\n mode: 'streaming'\n /**\n * Byte offsets where each line *starts*. Length equals lineCount + 1; the final entry equals\n * the total byte length. So `offsets[i + 1] - offsets[i]` is the byte length of line `i`\n * including any trailing `\\n`.\n */\n offsets: number[]\n bytes: number\n}\n\ntype ReaderState = EagerState | StreamingState\n\nconst isNonNegativeFiniteNumber = (n: unknown): n is number =>\n typeof n === 'number' && Number.isFinite(n) && n >= 0\n\n/**\n * Reads a flydrive-backed file as a {@link @nhtio/adk!SpoolReader}.\n *\n * @remarks\n * Constructor is **not** async — but the first method call awaits a private readiness promise\n * that fetches the object's metadata (and in eager mode, its contents). Subsequent calls reuse\n * the cached state. This keeps construction call sites synchronous while still doing real I/O\n * lazily.\n *\n * Implementations of {@link @nhtio/adk!SpoolReader.line}, {@link @nhtio/adk!SpoolReader.byteLength}, and\n * {@link @nhtio/adk!SpoolReader.lineCount} all return promises. The `SpoolReader` contract supports both\n * sync and async return; consumers of `SpooledArtifact` handle either.\n */\nexport class FlydriveSpoolReader implements SpoolReader {\n readonly #disk: Disk\n readonly #key: string\n readonly #threshold: number\n #ready: Promise<ReaderState> | undefined\n\n constructor(disk: Disk, key: string, opts: FlydriveSpoolReaderOptions = {}) {\n this.#disk = disk\n this.#key = key\n const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n // Allow `Infinity` (forces eager) but reject anything non-finite-negative.\n if (typeof raw !== 'number' || Number.isNaN(raw) || raw < 0) {\n throw new TypeError(\n `FlydriveSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`\n )\n }\n this.#threshold = raw\n }\n\n async line(index: number): Promise<string | undefined> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.lines[index]\n if (index < 0 || index >= state.offsets.length - 1) return undefined\n return this.#readRange(state.offsets[index], state.offsets[index + 1])\n }\n\n async byteLength(): Promise<number> {\n const state = await this.#load()\n return state.bytes\n }\n\n async lineCount(): Promise<number> {\n const state = await this.#load()\n return state.mode === 'eager' ? state.lines.length : state.offsets.length - 1\n }\n\n /**\n * Returns the full underlying content as a single decoded string, byte-faithful to the source.\n *\n * @remarks\n * In **eager mode** the content is already cached at construction-time load and this method is\n * effectively a property access. In **streaming mode** there is no cache: the file is\n * re-streamed and concatenated on every call. Use {@link @nhtio/adk!SpooledArtifact.asString} judiciously\n * on large streaming-mode artifacts.\n */\n async readAll(): Promise<string> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.content\n const stream = await this.#disk.getStream(this.#key)\n const chunks: Uint8Array[] = []\n let total = 0\n for await (const chunk of stream as AsyncIterable<Buffer | Uint8Array>) {\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on stream chunks; cross-realm fragility does not apply here\n const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)\n chunks.push(view)\n total += view.length\n }\n const concat = new Uint8Array(total)\n let offset = 0\n for (const view of chunks) {\n concat.set(view, offset)\n offset += view.length\n }\n return new TextDecoder().decode(concat)\n }\n\n /**\n * Lazily initialise the reader's mode-specific state. Called by every public method; the\n * promise is cached so the work runs at most once.\n */\n #load(): Promise<ReaderState> {\n if (!this.#ready) this.#ready = this.#init()\n return this.#ready\n }\n\n async #init(): Promise<ReaderState> {\n const meta = await this.#disk.getMetaData(this.#key)\n const bytes = meta.contentLength\n if (!isNonNegativeFiniteNumber(bytes)) {\n // Defensive — flydrive's contract types this as `number`, but cloud drivers occasionally\n // return NaN/Infinity if the backing store omits the size header.\n throw new Error(\n `FlydriveSpoolReader: disk returned a non-finite contentLength (${String(bytes)}) for key \"${this.#key}\"`\n )\n }\n if (bytes < this.#threshold) {\n // Eager — pull the whole thing into memory.\n const content = await this.#disk.get(this.#key)\n const lines = content === '' ? [] : content.split('\\n')\n return { mode: 'eager', lines, bytes, content }\n }\n // Streaming — build a line-offset index by scanning bytes once.\n return this.#buildStreamingIndex(bytes)\n }\n\n async #buildStreamingIndex(bytes: number): Promise<StreamingState> {\n // Edge case first — an empty file is one offset (the EOF), zero lines.\n if (bytes === 0) return { mode: 'streaming', offsets: [0], bytes }\n\n const stream = await this.#disk.getStream(this.#key)\n // offsets[i] is the byte position where line `i` starts. offsets[lineCount] is one-past-end.\n // For \"a\\nb\\nc\" → offsets=[0, 2, 4, 5] (3 lines).\n // For \"a\\nb\\n\" → offsets=[0, 2, 4, 4] (3 lines, last is the trailing empty line). This\n // mirrors `String.prototype.split('\\n')` semantics so streaming and eager agree.\n const offsets: number[] = [0]\n let position = 0\n let lastByte = -1\n for await (const chunk of stream as AsyncIterable<Buffer | Uint8Array>) {\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on stream chunks; cross-realm fragility does not apply here\n const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)\n for (const byte of view) {\n position++\n if (byte === LF) offsets.push(position)\n lastByte = byte\n }\n }\n // If the file ends on a newline, the byte after the LF is the start of an empty trailing\n // line — record it. If it doesn't, the final line's end is the EOF and we need to push\n // it so line(N-1) can read up to bytes.\n if (lastByte === LF) offsets.push(position)\n else if (offsets[offsets.length - 1] !== position) offsets.push(position)\n return { mode: 'streaming', offsets, bytes }\n }\n\n /**\n * Streams the byte range `[start, end)` from the backing disk and returns it as a UTF-8\n * string, stripping a trailing `\\n` if present.\n *\n * @remarks\n * flydrive doesn't expose native byte-range reads, so we open a fresh stream and skip until\n * we reach the requested start offset, then collect until we reach `end`. This is O(end)\n * per call — fine for occasional reads but worth profiling if a workload performs many\n * sequential `line()` calls on a large file.\n */\n async #readRange(start: number, end: number): Promise<string> {\n if (start === end) return ''\n const stream = await this.#disk.getStream(this.#key)\n const out: number[] = []\n let position = 0\n for await (const chunk of stream as AsyncIterable<Buffer | Uint8Array>) {\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on stream chunks; cross-realm fragility does not apply here\n const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)\n // Past the range entirely → stop early\n if (position >= end) {\n // Destroy the stream if it's a Node Readable; otherwise the for-await will naturally\n // continue, costing extra reads we don't need.\n // eslint-disable-next-line adk/use-is-instance-of -- native Node built-in; flydrive returns a real Readable, no cross-realm risk\n if (stream instanceof Readable) stream.destroy()\n break\n }\n // Fully before the range → skip the whole chunk\n if (position + view.length <= start) {\n position += view.length\n continue\n }\n // Partial overlap — copy the bytes that fall inside [start, end)\n const localStart = Math.max(0, start - position)\n const localEnd = Math.min(view.length, end - position)\n for (let i = localStart; i < localEnd; i++) out.push(view[i])\n position += view.length\n }\n // Strip the trailing newline if the range ended on one. The line-offset index ends each\n // line *after* its terminating LF (so offsets[i+1] points to the start of the next line),\n // and the SpoolReader contract returns lines *without* their trailing newline.\n if (out.length > 0 && out[out.length - 1] === LF) out.pop()\n return new TextDecoder().decode(new Uint8Array(out))\n }\n}\n\n/**\n * Constructor options for {@link FlydriveSpoolStore}.\n */\nexport interface FlydriveSpoolStoreOptions {\n /**\n * Optional key prefix prepended to every `callId`. Useful for namespacing tool-call artifacts\n * inside a shared bucket (e.g. `\"tool-calls/\"`).\n *\n * @defaultValue `\"\"`\n */\n keyPrefix?: string\n\n /**\n * Default `streamThresholdBytes` for readers produced by `write()` and `read()`. Individual\n * calls may override via their own `opts` argument.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\n/**\n * \"Give bytes, get a reader\" persistence layer over a flydrive {@link Disk}.\n *\n * @remarks\n * `write(callId, bytes)` calls `disk.put(key, bytes)` where `key = keyPrefix + callId`, then\n * returns a fresh {@link FlydriveSpoolReader} pointed at the same key. `read(callId)` returns\n * a reader without re-writing; `delete(callId)` calls `disk.delete(key)`.\n *\n * The store is stateless — it owns no in-memory cache of writes. Multiple `FlydriveSpoolStore`\n * instances sharing the same disk + key prefix see the same data.\n *\n * @example\n * ```ts\n * import { Disk } from 'flydrive'\n * import { FSDriver } from 'flydrive/drivers/fs'\n * import { FlydriveSpoolStore } from '@nhtio/adk/batteries/storage/flydrive'\n *\n * const disk = new Disk(new FSDriver({ location: './tmp', visibility: 'public' }))\n * const store = new FlydriveSpoolStore(disk)\n *\n * const bytes = await tool.executor(ctx)(args)\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class FlydriveSpoolStore {\n readonly #disk: Disk\n readonly #prefix: string\n readonly #defaultThreshold: number\n\n constructor(disk: Disk, opts: FlydriveSpoolStoreOptions = {}) {\n this.#disk = disk\n this.#prefix = opts.keyPrefix ?? ''\n this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n }\n\n /**\n * Persists `bytes` under `callId` and returns a reader bound to the stored key.\n *\n * @param callId - Identifier used to retrieve the bytes via {@link FlydriveSpoolStore.read}.\n * @param bytes - The bytes to store, as a `string` or `Uint8Array`.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns A {@link FlydriveSpoolReader} over the stored bytes.\n */\n async write(\n callId: string,\n bytes: string | Uint8Array,\n opts?: FlydriveSpoolReaderOptions\n ): Promise<FlydriveSpoolReader> {\n const key = this.#prefix + callId\n await this.#disk.put(key, bytes)\n return new FlydriveSpoolReader(this.#disk, key, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Returns a reader over the bytes previously written under `callId`.\n *\n * @remarks\n * Returns `undefined` if the underlying key does not exist. Existence is checked via\n * `disk.exists(key)` before the reader is returned, so callers can rely on a defined return\n * value pointing at a real object.\n *\n * @param callId - Identifier supplied to a prior {@link FlydriveSpoolStore.write} call.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns A {@link FlydriveSpoolReader}, or `undefined` if the key is missing.\n */\n async read(\n callId: string,\n opts?: FlydriveSpoolReaderOptions\n ): Promise<FlydriveSpoolReader | undefined> {\n const key = this.#prefix + callId\n if (!(await this.#disk.exists(key))) return undefined\n return new FlydriveSpoolReader(this.#disk, key, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Removes the entry under `callId`.\n *\n * @param callId - Identifier whose entry should be removed.\n * @returns `true` if the key existed and was removed; `false` if it didn't exist.\n */\n async delete(callId: string): Promise<boolean> {\n const key = this.#prefix + callId\n const existed = await this.#disk.exists(key)\n if (!existed) return false\n await this.#disk.delete(key)\n return true\n }\n\n /**\n * Returns the full disk key for a given `callId` (i.e. `keyPrefix + callId`).\n *\n * @remarks\n * Useful for tests or for callers that want to interact with the underlying disk directly.\n */\n keyFor(callId: string): string {\n return this.#prefix + callId\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,249 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
3
+ //#endregion
4
+ //#region src/batteries/storage/flydrive/index.ts
5
+ var import___vite_browser_external = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
6
+ module.exports = {};
7
+ })))();
8
+ var DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024;
9
+ var LF = 10;
10
+ var isNonNegativeFiniteNumber = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
11
+ /**
12
+ * Reads a flydrive-backed file as a {@link @nhtio/adk!SpoolReader}.
13
+ *
14
+ * @remarks
15
+ * Constructor is **not** async — but the first method call awaits a private readiness promise
16
+ * that fetches the object's metadata (and in eager mode, its contents). Subsequent calls reuse
17
+ * the cached state. This keeps construction call sites synchronous while still doing real I/O
18
+ * lazily.
19
+ *
20
+ * Implementations of {@link @nhtio/adk!SpoolReader.line}, {@link @nhtio/adk!SpoolReader.byteLength}, and
21
+ * {@link @nhtio/adk!SpoolReader.lineCount} all return promises. The `SpoolReader` contract supports both
22
+ * sync and async return; consumers of `SpooledArtifact` handle either.
23
+ */
24
+ var FlydriveSpoolReader = class {
25
+ #disk;
26
+ #key;
27
+ #threshold;
28
+ #ready;
29
+ constructor(disk, key, opts = {}) {
30
+ this.#disk = disk;
31
+ this.#key = key;
32
+ const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES;
33
+ if (typeof raw !== "number" || Number.isNaN(raw) || raw < 0) throw new TypeError(`FlydriveSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`);
34
+ this.#threshold = raw;
35
+ }
36
+ async line(index) {
37
+ const state = await this.#load();
38
+ if (state.mode === "eager") return state.lines[index];
39
+ if (index < 0 || index >= state.offsets.length - 1) return void 0;
40
+ return this.#readRange(state.offsets[index], state.offsets[index + 1]);
41
+ }
42
+ async byteLength() {
43
+ return (await this.#load()).bytes;
44
+ }
45
+ async lineCount() {
46
+ const state = await this.#load();
47
+ return state.mode === "eager" ? state.lines.length : state.offsets.length - 1;
48
+ }
49
+ /**
50
+ * Returns the full underlying content as a single decoded string, byte-faithful to the source.
51
+ *
52
+ * @remarks
53
+ * In **eager mode** the content is already cached at construction-time load and this method is
54
+ * effectively a property access. In **streaming mode** there is no cache: the file is
55
+ * re-streamed and concatenated on every call. Use {@link @nhtio/adk!SpooledArtifact.asString} judiciously
56
+ * on large streaming-mode artifacts.
57
+ */
58
+ async readAll() {
59
+ const state = await this.#load();
60
+ if (state.mode === "eager") return state.content;
61
+ const stream = await this.#disk.getStream(this.#key);
62
+ const chunks = [];
63
+ let total = 0;
64
+ for await (const chunk of stream) {
65
+ const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
66
+ chunks.push(view);
67
+ total += view.length;
68
+ }
69
+ const concat = new Uint8Array(total);
70
+ let offset = 0;
71
+ for (const view of chunks) {
72
+ concat.set(view, offset);
73
+ offset += view.length;
74
+ }
75
+ return new TextDecoder().decode(concat);
76
+ }
77
+ /**
78
+ * Lazily initialise the reader's mode-specific state. Called by every public method; the
79
+ * promise is cached so the work runs at most once.
80
+ */
81
+ #load() {
82
+ if (!this.#ready) this.#ready = this.#init();
83
+ return this.#ready;
84
+ }
85
+ async #init() {
86
+ const bytes = (await this.#disk.getMetaData(this.#key)).contentLength;
87
+ if (!isNonNegativeFiniteNumber(bytes)) throw new Error(`FlydriveSpoolReader: disk returned a non-finite contentLength (${String(bytes)}) for key "${this.#key}"`);
88
+ if (bytes < this.#threshold) {
89
+ const content = await this.#disk.get(this.#key);
90
+ return {
91
+ mode: "eager",
92
+ lines: content === "" ? [] : content.split("\n"),
93
+ bytes,
94
+ content
95
+ };
96
+ }
97
+ return this.#buildStreamingIndex(bytes);
98
+ }
99
+ async #buildStreamingIndex(bytes) {
100
+ if (bytes === 0) return {
101
+ mode: "streaming",
102
+ offsets: [0],
103
+ bytes
104
+ };
105
+ const stream = await this.#disk.getStream(this.#key);
106
+ const offsets = [0];
107
+ let position = 0;
108
+ let lastByte = -1;
109
+ for await (const chunk of stream) {
110
+ const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
111
+ for (const byte of view) {
112
+ position++;
113
+ if (byte === LF) offsets.push(position);
114
+ lastByte = byte;
115
+ }
116
+ }
117
+ if (lastByte === LF) offsets.push(position);
118
+ else if (offsets[offsets.length - 1] !== position) offsets.push(position);
119
+ return {
120
+ mode: "streaming",
121
+ offsets,
122
+ bytes
123
+ };
124
+ }
125
+ /**
126
+ * Streams the byte range `[start, end)` from the backing disk and returns it as a UTF-8
127
+ * string, stripping a trailing `\n` if present.
128
+ *
129
+ * @remarks
130
+ * flydrive doesn't expose native byte-range reads, so we open a fresh stream and skip until
131
+ * we reach the requested start offset, then collect until we reach `end`. This is O(end)
132
+ * per call — fine for occasional reads but worth profiling if a workload performs many
133
+ * sequential `line()` calls on a large file.
134
+ */
135
+ async #readRange(start, end) {
136
+ if (start === end) return "";
137
+ const stream = await this.#disk.getStream(this.#key);
138
+ const out = [];
139
+ let position = 0;
140
+ for await (const chunk of stream) {
141
+ const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
142
+ if (position >= end) {
143
+ if (stream instanceof import___vite_browser_external.Readable) stream.destroy();
144
+ break;
145
+ }
146
+ if (position + view.length <= start) {
147
+ position += view.length;
148
+ continue;
149
+ }
150
+ const localStart = Math.max(0, start - position);
151
+ const localEnd = Math.min(view.length, end - position);
152
+ for (let i = localStart; i < localEnd; i++) out.push(view[i]);
153
+ position += view.length;
154
+ }
155
+ if (out.length > 0 && out[out.length - 1] === LF) out.pop();
156
+ return new TextDecoder().decode(new Uint8Array(out));
157
+ }
158
+ };
159
+ /**
160
+ * "Give bytes, get a reader" persistence layer over a flydrive {@link Disk}.
161
+ *
162
+ * @remarks
163
+ * `write(callId, bytes)` calls `disk.put(key, bytes)` where `key = keyPrefix + callId`, then
164
+ * returns a fresh {@link FlydriveSpoolReader} pointed at the same key. `read(callId)` returns
165
+ * a reader without re-writing; `delete(callId)` calls `disk.delete(key)`.
166
+ *
167
+ * The store is stateless — it owns no in-memory cache of writes. Multiple `FlydriveSpoolStore`
168
+ * instances sharing the same disk + key prefix see the same data.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * import { Disk } from 'flydrive'
173
+ * import { FSDriver } from 'flydrive/drivers/fs'
174
+ * import { FlydriveSpoolStore } from '@nhtio/adk/batteries/storage/flydrive'
175
+ *
176
+ * const disk = new Disk(new FSDriver({ location: './tmp', visibility: 'public' }))
177
+ * const store = new FlydriveSpoolStore(disk)
178
+ *
179
+ * const bytes = await tool.executor(ctx)(args)
180
+ * const reader = await store.write(callId, bytes)
181
+ * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact
182
+ * const artifact = new Ctor(reader)
183
+ * ```
184
+ */
185
+ var FlydriveSpoolStore = class {
186
+ #disk;
187
+ #prefix;
188
+ #defaultThreshold;
189
+ constructor(disk, opts = {}) {
190
+ this.#disk = disk;
191
+ this.#prefix = opts.keyPrefix ?? "";
192
+ this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES;
193
+ }
194
+ /**
195
+ * Persists `bytes` under `callId` and returns a reader bound to the stored key.
196
+ *
197
+ * @param callId - Identifier used to retrieve the bytes via {@link FlydriveSpoolStore.read}.
198
+ * @param bytes - The bytes to store, as a `string` or `Uint8Array`.
199
+ * @param opts - Per-call override for `streamThresholdBytes`.
200
+ * @returns A {@link FlydriveSpoolReader} over the stored bytes.
201
+ */
202
+ async write(callId, bytes, opts) {
203
+ const key = this.#prefix + callId;
204
+ await this.#disk.put(key, bytes);
205
+ return new FlydriveSpoolReader(this.#disk, key, { streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold });
206
+ }
207
+ /**
208
+ * Returns a reader over the bytes previously written under `callId`.
209
+ *
210
+ * @remarks
211
+ * Returns `undefined` if the underlying key does not exist. Existence is checked via
212
+ * `disk.exists(key)` before the reader is returned, so callers can rely on a defined return
213
+ * value pointing at a real object.
214
+ *
215
+ * @param callId - Identifier supplied to a prior {@link FlydriveSpoolStore.write} call.
216
+ * @param opts - Per-call override for `streamThresholdBytes`.
217
+ * @returns A {@link FlydriveSpoolReader}, or `undefined` if the key is missing.
218
+ */
219
+ async read(callId, opts) {
220
+ const key = this.#prefix + callId;
221
+ if (!await this.#disk.exists(key)) return void 0;
222
+ return new FlydriveSpoolReader(this.#disk, key, { streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold });
223
+ }
224
+ /**
225
+ * Removes the entry under `callId`.
226
+ *
227
+ * @param callId - Identifier whose entry should be removed.
228
+ * @returns `true` if the key existed and was removed; `false` if it didn't exist.
229
+ */
230
+ async delete(callId) {
231
+ const key = this.#prefix + callId;
232
+ if (!await this.#disk.exists(key)) return false;
233
+ await this.#disk.delete(key);
234
+ return true;
235
+ }
236
+ /**
237
+ * Returns the full disk key for a given `callId` (i.e. `keyPrefix + callId`).
238
+ *
239
+ * @remarks
240
+ * Useful for tests or for callers that want to interact with the underlying disk directly.
241
+ */
242
+ keyFor(callId) {
243
+ return this.#prefix + callId;
244
+ }
245
+ };
246
+ //#endregion
247
+ export { FlydriveSpoolReader, FlydriveSpoolStore };
248
+
249
+ //# sourceMappingURL=flydrive.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flydrive.mjs","names":["#disk","#key","#threshold","#load","#readRange","#ready","#init","#buildStreamingIndex","#prefix","#defaultThreshold"],"sources":["../../../__vite-browser-external","../../../src/batteries/storage/flydrive/index.ts"],"sourcesContent":["module.exports = {}","/**\n * Flydrive-backed spooled artifact storage for Node and server runtimes.\n *\n * @module @nhtio/adk/batteries/storage/flydrive\n *\n * @remarks\n * **Requires Node 24+.** `flydrive` uses the `node:stream` `ReadableStream` web API which is\n * only available from Node 24. This battery does not work in the browser or earlier Node versions.\n *\n * Opt-in storage battery backed by [flydrive](https://flydrive.dev). Provides\n * {@link FlydriveSpoolReader} (a {@link @nhtio/adk!SpoolReader} over a flydrive key) and\n * {@link FlydriveSpoolStore} (a `write(callId, bytes) → reader` persistence layer that wraps an\n * existing `Disk`).\n *\n * The reader has two modes selected at construction time based on the size of the underlying\n * object:\n *\n * - **Eager mode** — when the object's `contentLength` is below `streamThresholdBytes` (default\n * 10 MiB), the reader calls `disk.get(key)` once, splits the content on `\\n`, and caches\n * lines + byte count. All subsequent `line() / byteLength() / lineCount()` calls resolve from\n * memory.\n * - **Streaming mode** — when `contentLength` meets or exceeds the threshold, the reader\n * streams the file once via `disk.getStream(key)` to build a line-offset index (`number[]`\n * of byte offsets per line), then serves each `line(i)` request by streaming the byte range\n * `[offsets[i], offsets[i+1])`. Caps RAM at one index + one line buffer regardless of file\n * size.\n *\n * Set `streamThresholdBytes: 0` to force streaming mode; set it to `Infinity` to force eager\n * mode. The default of 10 MiB matches typical tool output sizes — tune it for your workload.\n *\n * The store and reader are pure-flydrive: they don't know about S3, GCS, or filesystem\n * specifically — they delegate to whatever `Disk` you construct.\n */\n\nimport { Disk } from 'flydrive'\nimport { Readable } from 'node:stream'\nimport type { SpoolReader } from '@nhtio/adk/common'\n\nconst DEFAULT_STREAM_THRESHOLD_BYTES = 10 * 1024 * 1024 // 10 MiB\n\nconst LF = 0x0a // '\\n'\n\n/**\n * Constructor options for {@link FlydriveSpoolReader}.\n */\nexport interface FlydriveSpoolReaderOptions {\n /**\n * Byte-length threshold that switches between eager and streaming modes.\n *\n * @remarks\n * - Below the threshold → eager (whole-file in memory).\n * - At or above the threshold → streaming (line-offset index + per-line streaming reads).\n *\n * Set to `0` to force streaming mode; set to `Number.POSITIVE_INFINITY` to force eager mode.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\ninterface EagerState {\n mode: 'eager'\n lines: string[]\n bytes: number\n content: string\n}\n\ninterface StreamingState {\n mode: 'streaming'\n /**\n * Byte offsets where each line *starts*. Length equals lineCount + 1; the final entry equals\n * the total byte length. So `offsets[i + 1] - offsets[i]` is the byte length of line `i`\n * including any trailing `\\n`.\n */\n offsets: number[]\n bytes: number\n}\n\ntype ReaderState = EagerState | StreamingState\n\nconst isNonNegativeFiniteNumber = (n: unknown): n is number =>\n typeof n === 'number' && Number.isFinite(n) && n >= 0\n\n/**\n * Reads a flydrive-backed file as a {@link @nhtio/adk!SpoolReader}.\n *\n * @remarks\n * Constructor is **not** async — but the first method call awaits a private readiness promise\n * that fetches the object's metadata (and in eager mode, its contents). Subsequent calls reuse\n * the cached state. This keeps construction call sites synchronous while still doing real I/O\n * lazily.\n *\n * Implementations of {@link @nhtio/adk!SpoolReader.line}, {@link @nhtio/adk!SpoolReader.byteLength}, and\n * {@link @nhtio/adk!SpoolReader.lineCount} all return promises. The `SpoolReader` contract supports both\n * sync and async return; consumers of `SpooledArtifact` handle either.\n */\nexport class FlydriveSpoolReader implements SpoolReader {\n readonly #disk: Disk\n readonly #key: string\n readonly #threshold: number\n #ready: Promise<ReaderState> | undefined\n\n constructor(disk: Disk, key: string, opts: FlydriveSpoolReaderOptions = {}) {\n this.#disk = disk\n this.#key = key\n const raw = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n // Allow `Infinity` (forces eager) but reject anything non-finite-negative.\n if (typeof raw !== 'number' || Number.isNaN(raw) || raw < 0) {\n throw new TypeError(\n `FlydriveSpoolReader: streamThresholdBytes must be a non-negative number or Infinity, got ${String(raw)}`\n )\n }\n this.#threshold = raw\n }\n\n async line(index: number): Promise<string | undefined> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.lines[index]\n if (index < 0 || index >= state.offsets.length - 1) return undefined\n return this.#readRange(state.offsets[index], state.offsets[index + 1])\n }\n\n async byteLength(): Promise<number> {\n const state = await this.#load()\n return state.bytes\n }\n\n async lineCount(): Promise<number> {\n const state = await this.#load()\n return state.mode === 'eager' ? state.lines.length : state.offsets.length - 1\n }\n\n /**\n * Returns the full underlying content as a single decoded string, byte-faithful to the source.\n *\n * @remarks\n * In **eager mode** the content is already cached at construction-time load and this method is\n * effectively a property access. In **streaming mode** there is no cache: the file is\n * re-streamed and concatenated on every call. Use {@link @nhtio/adk!SpooledArtifact.asString} judiciously\n * on large streaming-mode artifacts.\n */\n async readAll(): Promise<string> {\n const state = await this.#load()\n if (state.mode === 'eager') return state.content\n const stream = await this.#disk.getStream(this.#key)\n const chunks: Uint8Array[] = []\n let total = 0\n for await (const chunk of stream as AsyncIterable<Buffer | Uint8Array>) {\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on stream chunks; cross-realm fragility does not apply here\n const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)\n chunks.push(view)\n total += view.length\n }\n const concat = new Uint8Array(total)\n let offset = 0\n for (const view of chunks) {\n concat.set(view, offset)\n offset += view.length\n }\n return new TextDecoder().decode(concat)\n }\n\n /**\n * Lazily initialise the reader's mode-specific state. Called by every public method; the\n * promise is cached so the work runs at most once.\n */\n #load(): Promise<ReaderState> {\n if (!this.#ready) this.#ready = this.#init()\n return this.#ready\n }\n\n async #init(): Promise<ReaderState> {\n const meta = await this.#disk.getMetaData(this.#key)\n const bytes = meta.contentLength\n if (!isNonNegativeFiniteNumber(bytes)) {\n // Defensive — flydrive's contract types this as `number`, but cloud drivers occasionally\n // return NaN/Infinity if the backing store omits the size header.\n throw new Error(\n `FlydriveSpoolReader: disk returned a non-finite contentLength (${String(bytes)}) for key \"${this.#key}\"`\n )\n }\n if (bytes < this.#threshold) {\n // Eager — pull the whole thing into memory.\n const content = await this.#disk.get(this.#key)\n const lines = content === '' ? [] : content.split('\\n')\n return { mode: 'eager', lines, bytes, content }\n }\n // Streaming — build a line-offset index by scanning bytes once.\n return this.#buildStreamingIndex(bytes)\n }\n\n async #buildStreamingIndex(bytes: number): Promise<StreamingState> {\n // Edge case first — an empty file is one offset (the EOF), zero lines.\n if (bytes === 0) return { mode: 'streaming', offsets: [0], bytes }\n\n const stream = await this.#disk.getStream(this.#key)\n // offsets[i] is the byte position where line `i` starts. offsets[lineCount] is one-past-end.\n // For \"a\\nb\\nc\" → offsets=[0, 2, 4, 5] (3 lines).\n // For \"a\\nb\\n\" → offsets=[0, 2, 4, 4] (3 lines, last is the trailing empty line). This\n // mirrors `String.prototype.split('\\n')` semantics so streaming and eager agree.\n const offsets: number[] = [0]\n let position = 0\n let lastByte = -1\n for await (const chunk of stream as AsyncIterable<Buffer | Uint8Array>) {\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on stream chunks; cross-realm fragility does not apply here\n const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)\n for (const byte of view) {\n position++\n if (byte === LF) offsets.push(position)\n lastByte = byte\n }\n }\n // If the file ends on a newline, the byte after the LF is the start of an empty trailing\n // line — record it. If it doesn't, the final line's end is the EOF and we need to push\n // it so line(N-1) can read up to bytes.\n if (lastByte === LF) offsets.push(position)\n else if (offsets[offsets.length - 1] !== position) offsets.push(position)\n return { mode: 'streaming', offsets, bytes }\n }\n\n /**\n * Streams the byte range `[start, end)` from the backing disk and returns it as a UTF-8\n * string, stripping a trailing `\\n` if present.\n *\n * @remarks\n * flydrive doesn't expose native byte-range reads, so we open a fresh stream and skip until\n * we reach the requested start offset, then collect until we reach `end`. This is O(end)\n * per call — fine for occasional reads but worth profiling if a workload performs many\n * sequential `line()` calls on a large file.\n */\n async #readRange(start: number, end: number): Promise<string> {\n if (start === end) return ''\n const stream = await this.#disk.getStream(this.#key)\n const out: number[] = []\n let position = 0\n for await (const chunk of stream as AsyncIterable<Buffer | Uint8Array>) {\n // eslint-disable-next-line adk/use-is-instance-of -- native built-in narrowing on stream chunks; cross-realm fragility does not apply here\n const view = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)\n // Past the range entirely → stop early\n if (position >= end) {\n // Destroy the stream if it's a Node Readable; otherwise the for-await will naturally\n // continue, costing extra reads we don't need.\n // eslint-disable-next-line adk/use-is-instance-of -- native Node built-in; flydrive returns a real Readable, no cross-realm risk\n if (stream instanceof Readable) stream.destroy()\n break\n }\n // Fully before the range → skip the whole chunk\n if (position + view.length <= start) {\n position += view.length\n continue\n }\n // Partial overlap — copy the bytes that fall inside [start, end)\n const localStart = Math.max(0, start - position)\n const localEnd = Math.min(view.length, end - position)\n for (let i = localStart; i < localEnd; i++) out.push(view[i])\n position += view.length\n }\n // Strip the trailing newline if the range ended on one. The line-offset index ends each\n // line *after* its terminating LF (so offsets[i+1] points to the start of the next line),\n // and the SpoolReader contract returns lines *without* their trailing newline.\n if (out.length > 0 && out[out.length - 1] === LF) out.pop()\n return new TextDecoder().decode(new Uint8Array(out))\n }\n}\n\n/**\n * Constructor options for {@link FlydriveSpoolStore}.\n */\nexport interface FlydriveSpoolStoreOptions {\n /**\n * Optional key prefix prepended to every `callId`. Useful for namespacing tool-call artifacts\n * inside a shared bucket (e.g. `\"tool-calls/\"`).\n *\n * @defaultValue `\"\"`\n */\n keyPrefix?: string\n\n /**\n * Default `streamThresholdBytes` for readers produced by `write()` and `read()`. Individual\n * calls may override via their own `opts` argument.\n *\n * @defaultValue `10 * 1024 * 1024` (10 MiB)\n */\n streamThresholdBytes?: number\n}\n\n/**\n * \"Give bytes, get a reader\" persistence layer over a flydrive {@link Disk}.\n *\n * @remarks\n * `write(callId, bytes)` calls `disk.put(key, bytes)` where `key = keyPrefix + callId`, then\n * returns a fresh {@link FlydriveSpoolReader} pointed at the same key. `read(callId)` returns\n * a reader without re-writing; `delete(callId)` calls `disk.delete(key)`.\n *\n * The store is stateless — it owns no in-memory cache of writes. Multiple `FlydriveSpoolStore`\n * instances sharing the same disk + key prefix see the same data.\n *\n * @example\n * ```ts\n * import { Disk } from 'flydrive'\n * import { FSDriver } from 'flydrive/drivers/fs'\n * import { FlydriveSpoolStore } from '@nhtio/adk/batteries/storage/flydrive'\n *\n * const disk = new Disk(new FSDriver({ location: './tmp', visibility: 'public' }))\n * const store = new FlydriveSpoolStore(disk)\n *\n * const bytes = await tool.executor(ctx)(args)\n * const reader = await store.write(callId, bytes)\n * const Ctor = tool.artifactConstructor?.() ?? SpooledArtifact\n * const artifact = new Ctor(reader)\n * ```\n */\nexport class FlydriveSpoolStore {\n readonly #disk: Disk\n readonly #prefix: string\n readonly #defaultThreshold: number\n\n constructor(disk: Disk, opts: FlydriveSpoolStoreOptions = {}) {\n this.#disk = disk\n this.#prefix = opts.keyPrefix ?? ''\n this.#defaultThreshold = opts.streamThresholdBytes ?? DEFAULT_STREAM_THRESHOLD_BYTES\n }\n\n /**\n * Persists `bytes` under `callId` and returns a reader bound to the stored key.\n *\n * @param callId - Identifier used to retrieve the bytes via {@link FlydriveSpoolStore.read}.\n * @param bytes - The bytes to store, as a `string` or `Uint8Array`.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns A {@link FlydriveSpoolReader} over the stored bytes.\n */\n async write(\n callId: string,\n bytes: string | Uint8Array,\n opts?: FlydriveSpoolReaderOptions\n ): Promise<FlydriveSpoolReader> {\n const key = this.#prefix + callId\n await this.#disk.put(key, bytes)\n return new FlydriveSpoolReader(this.#disk, key, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Returns a reader over the bytes previously written under `callId`.\n *\n * @remarks\n * Returns `undefined` if the underlying key does not exist. Existence is checked via\n * `disk.exists(key)` before the reader is returned, so callers can rely on a defined return\n * value pointing at a real object.\n *\n * @param callId - Identifier supplied to a prior {@link FlydriveSpoolStore.write} call.\n * @param opts - Per-call override for `streamThresholdBytes`.\n * @returns A {@link FlydriveSpoolReader}, or `undefined` if the key is missing.\n */\n async read(\n callId: string,\n opts?: FlydriveSpoolReaderOptions\n ): Promise<FlydriveSpoolReader | undefined> {\n const key = this.#prefix + callId\n if (!(await this.#disk.exists(key))) return undefined\n return new FlydriveSpoolReader(this.#disk, key, {\n streamThresholdBytes: opts?.streamThresholdBytes ?? this.#defaultThreshold,\n })\n }\n\n /**\n * Removes the entry under `callId`.\n *\n * @param callId - Identifier whose entry should be removed.\n * @returns `true` if the key existed and was removed; `false` if it didn't exist.\n */\n async delete(callId: string): Promise<boolean> {\n const key = this.#prefix + callId\n const existed = await this.#disk.exists(key)\n if (!existed) return false\n await this.#disk.delete(key)\n return true\n }\n\n /**\n * Returns the full disk key for a given `callId` (i.e. `keyPrefix + callId`).\n *\n * @remarks\n * Useful for tests or for callers that want to interact with the underlying disk directly.\n */\n keyFor(callId: string): string {\n return this.#prefix + callId\n }\n}\n"],"mappings":"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"}