tokentracker-cli 1.3.11 → 1.3.14

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 (116) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-CjXQPC-W.js → AchievementBadge-DJK8AWBF.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-xb-g6Jws.js → AchievementsPage-Cle78oKH.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DI4NOt9D.js → AchievementsSection-BPqXDoPP.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-Cn2rV5Bk.js → BadgeDetailModal-HsH_Pwsu.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CFb-gvO7.js → BotAnimated-BWy2-z4Q.js} +1 -1
  6. package/dashboard/dist/assets/{Card-CB2-QAqP.js → Card-DPGBuBDb.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-beBzOiJA.js → CloudCheckoutPage-ohIQnIhe.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-COYm4wdR.js → CloudPage-OYiiZ4c4.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-kCa9Mfpj.js → CommandPalette-BmDZmpMi.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BeUZhxxn.js → CommunityStatsModal-L86uJgyp.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-DiCm2JXX.js → ConfirmModal-COQfPXos.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-BkRVScXl.js → Controls-BkKlT6Dz.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-DiV0ynQc.js → DashboardPage-3dlQBOjz.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-DdmjfdFj.js → DateRangePopover-D0UeP8ig.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-D7E9uy4l.js → DevicePage-K3CdWKaf.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-VDWLqY0o.js → DialogClose-DKQ2ZVA0.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-MbuaPFlj.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-DfBUUBYN.js → DialogTitle-ufFnN-8D.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Cr-WDplA.js → ElectricLogo-Bb79UnTo.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-DPdMW40K.js → FadeIn-DlAKIU4I.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-Ulqd6uC1.js → HeaderGithubStar-CzwSyhEY.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-By6YzN89.js → HoverTooltip-BEpwekkd.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-BLvWuVmL.js → IpCheckPage-C7kpZ9JX.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-BfgBmSGV.js → LandingPage-CfYk0Jzy.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-D7C91BEb.js → LeaderboardPage-XgjAHwpy.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-Mj_-HH8L.js → LeaderboardProBadge-wSlqVjBb.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-BfHTrqHf.js → LeaderboardProfileModal-DaQrNYnu.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CggAvx-w.js → LeaderboardProfilePage-3SWjG8FQ.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-BR8hlK7t.js → LimitsPage-DvM57ri3.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-yW1orlU8.js → LocalOnlyNotice-CY6eDJEk.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-B_7mpZ6Y.js → LoginCard-4bdpPQeD.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-D1ZtIkRD.js → LoginModal-Bmw7-b3f.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-DRThDeqD.js → LoginPage-CEI4nnlT.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-CsIkqyiR.js → PetPage-7Udtc9GY.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-dySzR9xR.js → PopoverPopup-r2BvRKLO.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-oBKAZ-WS.js → ProviderIcon-Cy_pKLdw.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-Bf0-zIl2.js → ResetPasswordPage-QKUaLtO3.js} +1 -1
  38. package/dashboard/dist/assets/{Select-xyNGq71G.js → Select-BsUrn8Ui.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-C8ZB19C7.js → SelectItemText-Nis0uGZT.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-Cf7pLoVz.js → SelfHostPage-DF8EzBEV.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-BH3W_Wxq.js → ServiceStatusPage-BYmKzI_v.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-u8RIKjx9.js → SessionsPage-BsCVe75l.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-Bnh-AxM9.js → SettingsPage-CIZ6WmNR.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CIP3Av7z.js → SkillsPage-cwHyK0Hp.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-DnNhsfa-.js → TokenGalaxy-2S-ud67v.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-BstAYPIH.js → TooltipPopup-GG7xjDOP.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-D4CVzczb.js → TrendMonitor-CrbXrVMe.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-A_gMzap_.js → WidgetsPage-HrhnI3C-.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-Bmm_WjA5.js → WrappedPage-sm7UVFon.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-DS-dnBDP.js → __vite-browser-external-C7JgMYNl.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-BdU0GjQq.js → agent-logos-DGrrmPth.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-Cl8AYaOh.js → arrow-left-DOzsdmqR.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-Cfi2pR1p.js → arrow-right-CHb0kJou.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-DH7vnAf4.js → arrow-up-right-CH8vEEN1.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-BJjjx5Vu.js → calendar-days-_D5qnyH_.js} +1 -1
  56. package/dashboard/dist/assets/{check-DGTejNYt.js → check-DZLesnNy.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-Brz4a22U.js → chevron-down-QL4xkUtL.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-DZbiSnVr.js → cloud-checkout-DiCcWJVl.js} +1 -1
  59. package/dashboard/dist/assets/{composite-rfjkpEna.js → composite-DObGcQmb.js} +1 -1
  60. package/dashboard/dist/assets/{download-axaJjbTW.js → download-BKp8pusR.js} +1 -1
  61. package/dashboard/dist/assets/{external-link-D9dvv4DN.js → external-link-B3p_TVGc.js} +1 -1
  62. package/dashboard/dist/assets/{flame-DuivUwnP.js → flame-Df9OTUym.js} +1 -1
  63. package/dashboard/dist/assets/{format-tokens-Dg_-K3cD.js → format-tokens-DXrEDEYs.js} +1 -1
  64. package/dashboard/dist/assets/{gift-CgE1uDg4.js → gift-DtWubiuR.js} +1 -1
  65. package/dashboard/dist/assets/{host-mode-C2QCLKjw.js → host-mode-C069A_hB.js} +1 -1
  66. package/dashboard/dist/assets/{icons-PNlyYW8C.js → icons-lB1kfROy.js} +1 -1
  67. package/dashboard/dist/assets/{index-DFWE1vbu.js → index-D356L-jw.js} +1 -1
  68. package/dashboard/dist/assets/{index-BbroxaKr.js → index-DsXIWrJx.js} +1 -1
  69. package/dashboard/dist/assets/{index-PiRGMAPd.js → index-M5BiC0jx.js} +1 -1
  70. package/dashboard/dist/assets/{info-BjRBvuNu.js → info-CSG8OkG4.js} +1 -1
  71. package/dashboard/dist/assets/{link-2-CCqYoK3V.js → link-2-BkBGu7FX.js} +1 -1
  72. package/dashboard/dist/assets/{loader-circle-IiB0vjDJ.js → loader-circle-BERDkbsP.js} +1 -1
  73. package/dashboard/dist/assets/{modifiers.esm-DbjjBgLM.js → modifiers.esm-Ce5IvVw2.js} +1 -1
  74. package/dashboard/dist/assets/{monitor-smartphone-BUSQ3-U3.js → monitor-smartphone-zKuoJldz.js} +1 -1
  75. package/dashboard/dist/assets/{play-CDU4bWoE.js → play-C0x-pxft.js} +1 -1
  76. package/dashboard/dist/assets/{plus-BY7jwS0R.js → plus-DJvks-CX.js} +1 -1
  77. package/dashboard/dist/assets/{provider-display-endIONDM.js → provider-display-C0e4tAFu.js} +1 -1
  78. package/dashboard/dist/assets/{react-C0JpiKb9.js → react-Bg0_YfGs.js} +1 -1
  79. package/dashboard/dist/assets/{safePolygon-B5pqmyki.js → safePolygon-CptquMWZ.js} +1 -1
  80. package/dashboard/dist/assets/{search-BWQCqCrK.js → search-CLESf2Tg.js} +1 -1
  81. package/dashboard/dist/assets/{server-DkDn4DQU.js → server-Cdwhp-2m.js} +1 -1
  82. package/dashboard/dist/assets/{shield-check-BPOMBy-3.js → shield-check-8U4XqmW3.js} +1 -1
  83. package/dashboard/dist/assets/{skills-api-Btor2iNW.js → skills-api-ioDaxLLo.js} +1 -1
  84. package/dashboard/dist/assets/{sortable.esm-qA98E6si.js → sortable.esm-CEohF5EK.js} +1 -1
  85. package/dashboard/dist/assets/{store-DRcycxJw.js → store-CyOeiGFE.js} +1 -1
  86. package/dashboard/dist/assets/{terminal-BWOI_yxH.js → terminal-BjgwEAH-.js} +1 -1
  87. package/dashboard/dist/assets/{trash-DgWVzyKs.js → trash-Skj6z7D1.js} +1 -1
  88. package/dashboard/dist/assets/{use-community-stats-odyPsCDi.js → use-community-stats-Cworqjzy.js} +1 -1
  89. package/dashboard/dist/assets/{use-latest-request-guard-CBocPZjh.js → use-latest-request-guard-DMVWXXHE.js} +1 -1
  90. package/dashboard/dist/assets/{use-limits-display-prefs-uKB639iI.js → use-limits-display-prefs-DKJZcYfE.js} +1 -1
  91. package/dashboard/dist/assets/{use-native-settings-EoDW6Dgk.js → use-native-settings-CQF4BLQl.js} +1 -1
  92. package/dashboard/dist/assets/{use-ordered-list-B4Gychu4.js → use-ordered-list-B9yoUGUU.js} +1 -1
  93. package/dashboard/dist/assets/use-reduced-motion-DPUViCfp.js +1 -0
  94. package/dashboard/dist/assets/{use-session-efficiency-pref-C0goPBNl.js → use-session-efficiency-pref-K00bK2sQ.js} +1 -1
  95. package/dashboard/dist/assets/{use-transform-hwE19i1Y.js → use-transform-Cfw11yOU.js} +1 -1
  96. package/dashboard/dist/assets/{use-usage-limits-DnsGayPX.js → use-usage-limits-BE2dJXFd.js} +1 -1
  97. package/dashboard/dist/assets/{useCurrency-DiN1Jj_t.js → useCurrency-BW7y9Tdv.js} +1 -1
  98. package/dashboard/dist/assets/{usePositioner-CM1Isf5e.js → usePositioner-DhtQcPOC.js} +1 -1
  99. package/dashboard/dist/assets/{useTokenFormat-CGhMrRps.js → useTokenFormat-BnvzUc-t.js} +1 -1
  100. package/dashboard/dist/assets/{zap-Bc2wd9eQ.js → zap-BQ3BR9On.js} +1 -1
  101. package/dashboard/dist/index.html +1 -1
  102. package/dashboard/dist/ip-check.html +1 -1
  103. package/dashboard/dist/leaderboard.html +1 -1
  104. package/dashboard/dist/share.html +1 -1
  105. package/package.json +1 -1
  106. package/src/commands/status.js +5 -1
  107. package/src/commands/sync.js +28 -1
  108. package/src/lib/async-queue-reader.js +117 -0
  109. package/src/lib/jsonl-lines.js +34 -4
  110. package/src/lib/local-api.js +88 -226
  111. package/src/lib/pricing/seed-snapshot.json +1 -1
  112. package/src/lib/rollout.js +148 -52
  113. package/src/lib/session-analytics.js +11 -10
  114. package/src/lib/upload-queue.js +1 -1
  115. package/dashboard/dist/assets/DialogDescription-BQ5VSHQi.js +0 -1
  116. package/dashboard/dist/assets/use-reduced-motion-B-JhzSwL.js +0 -1
@@ -145,6 +145,8 @@ const {
145
145
  toUtcHalfHourStart,
146
146
  totalsKey,
147
147
  claudeMessageDedupKey,
148
+ hasZcodeCursorState,
149
+ resolveZcodeInstallPaths,
148
150
  } = require("../lib/rollout");
149
151
  const { computeClaudeGroundTruthBuckets } = require("../lib/claude-categorizer");
150
152
  const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
@@ -1733,8 +1735,23 @@ async function cmdSync(argv, context = {}) {
1733
1735
  const wslZcodeDir = process.platform === "win32" && wsl.shouldProbeWsl(process.env)
1734
1736
  ? wsl.discoverWslHome(".zcode")
1735
1737
  : null;
1736
- const zcodePaths = resolveInstallPaths({ nativeValue: zcodeNativeValue, wslValue: wslZcodeDir ? path.join(wslZcodeDir, "cli", "db", "db.sqlite") : null });
1738
+ const zcodeWslValue = wslZcodeDir ? path.join(wslZcodeDir, "cli", "db", "db.sqlite") : null;
1739
+ const zcodePaths = resolveZcodeInstallPaths(
1740
+ { nativeValue: zcodeNativeValue, wslValue: zcodeWslValue }, process.env,
1741
+ { cursors },
1742
+ );
1737
1743
  if (zcodePaths.native || zcodePaths.wsl) {
1744
+ const mode = wsl.getWslMode(process.env);
1745
+ if (process.platform === "win32" && ["native-first", "wsl-first"].includes(mode) &&
1746
+ !cursors.zcodeInstallSelection && hasZcodeCursorState(cursors)) {
1747
+ // Preserve legacy ownership before a repair can clear old buckets,
1748
+ // even when the selected database currently has no rows to parse.
1749
+ cursors.zcodeInstallSelection = {
1750
+ mode, zcodeHome: process.env.ZCODE_HOME?.trim() || null,
1751
+ dbPath: zcodePaths.native || zcodePaths.wsl,
1752
+ };
1753
+ cursorStore.requiresCommit = true;
1754
+ }
1738
1755
  if (progress?.enabled) progress.start(`Parsing ZCode ${renderBar(0)} | buckets 0`);
1739
1756
  try {
1740
1757
  const nativeUsageAvailable = [zcodePaths.native, zcodePaths.wsl]
@@ -1761,6 +1778,16 @@ async function cmdSync(argv, context = {}) {
1761
1778
  cursors, getParams: (p) => ({ dbPath: p, readFn: readZcodeDbMessages, source: "zcode", cursorKey: "zcode" }),
1762
1779
  queuePath, projectQueuePath, onProgress: makeProviderProgress("ZCode"),
1763
1780
  });
1781
+ if (zcodeResult.recordsProcessed > 0) {
1782
+ if (process.platform === "win32" && ["native-first", "wsl-first"].includes(mode)) {
1783
+ cursors.zcodeInstallSelection = {
1784
+ mode, zcodeHome: process.env.ZCODE_HOME?.trim() || null,
1785
+ dbPath: zcodePaths.native || zcodePaths.wsl,
1786
+ };
1787
+ } else {
1788
+ delete cursors.zcodeInstallSelection;
1789
+ }
1790
+ }
1764
1791
  } catch (err) { warnProviderParseFailure("ZCode", err, opts); }
1765
1792
  }
1766
1793
  }
@@ -0,0 +1,117 @@
1
+ const fs = require("node:fs/promises");
2
+ const { StringDecoder } = require("node:string_decoder");
3
+ const { setImmediate: yieldToRequests } = require("node:timers/promises");
4
+
5
+ const CHUNK_BYTES = 64 * 1024;
6
+ const PROBE_BYTES = 256;
7
+
8
+ function signature(stat) {
9
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
10
+ }
11
+
12
+ // One in-flight read per path shares the dashboard's request fan-out. Parsing
13
+ // yields between bounded chunks so a cold/replaced queue cannot monopolize the
14
+ // HTTP event loop. Keep committed newline-terminated rows separate from a tail
15
+ // that a concurrent writer may still be completing.
16
+ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
17
+ const cache = new Map();
18
+ const pending = new Map();
19
+
20
+ async function probe(file, position, length) {
21
+ const buffer = Buffer.alloc(length);
22
+ const { bytesRead } = await file.read(buffer, 0, length, position);
23
+ return buffer.subarray(0, bytesRead);
24
+ }
25
+
26
+ async function load(queuePath) {
27
+ const cached = cache.get(queuePath);
28
+ let file;
29
+ try {
30
+ file = await fs.open(queuePath, "r");
31
+ // Stat the opened file, rather than a path which can be atomically replaced
32
+ // between stat and open. Its signature always describes the bytes we read.
33
+ const stat = await file.stat({ bigint: true });
34
+ const nextSignature = signature(stat);
35
+ if (cached?.signature === nextSignature) return cached.rows;
36
+ const identity = `${stat.dev}:${stat.ino}`;
37
+ const size = Number(stat.size);
38
+ let offset = 0;
39
+ let seen = new Map();
40
+ if (cached && cached.identity === identity && size > cached.size && cached.consumedBytes > 0) {
41
+ const head = await probe(file, 0, cached.head.length);
42
+ const anchor = await probe(file, cached.consumedBytes - cached.anchor.length, cached.anchor.length);
43
+ if (head.equals(cached.head) && anchor.equals(cached.anchor)) {
44
+ offset = cached.consumedBytes;
45
+ seen = cached.seen;
46
+ }
47
+ }
48
+
49
+ const changes = new Map();
50
+ let malformed = 0;
51
+ function parse(line, target) {
52
+ if (!line.trim()) return;
53
+ try {
54
+ const entry = parseRow(JSON.parse(line));
55
+ if (entry) target.set(entry.key, entry.row);
56
+ } catch { malformed++; }
57
+ }
58
+
59
+ const decoder = new StringDecoder("utf8");
60
+ const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
61
+ let remainder = "";
62
+ let position = offset;
63
+ let consumedBytes = offset;
64
+ while (position < size) {
65
+ const length = Math.min(CHUNK_BYTES, size - position);
66
+ const { bytesRead } = await file.read(buffer, 0, length, position);
67
+ if (!bytesRead) throw new Error("Queue truncated during read");
68
+ const newline = buffer.lastIndexOf(0x0a, bytesRead - 1);
69
+ if (newline >= 0) consumedBytes = position + newline + 1;
70
+ position += bytesRead;
71
+ remainder += decoder.write(buffer.subarray(0, bytesRead));
72
+ let start = 0;
73
+ let end;
74
+ while ((end = remainder.indexOf("\n", start)) !== -1) {
75
+ parse(remainder.slice(start, end), changes);
76
+ start = end + 1;
77
+ }
78
+ remainder = remainder.slice(start);
79
+ await yieldToRequests();
80
+ }
81
+ remainder += decoder.end();
82
+ // Capture probes before committing any state: a failed read must not mutate
83
+ // a snapshot still being used by another endpoint.
84
+ const head = await probe(file, 0, Math.min(PROBE_BYTES, consumedBytes));
85
+ const anchor = await probe(file, Math.max(0, consumedBytes - PROBE_BYTES), Math.min(PROBE_BYTES, consumedBytes));
86
+ for (const [key, row] of changes) seen.set(key, row);
87
+ const tail = new Map();
88
+ parse(remainder, tail);
89
+ // Legacy files without a final newline are readable, but their provisional
90
+ // key never enters the committed map (completion may change that key).
91
+ const rows = tail.size ? Array.from(new Map([...seen, ...tail]).values()) : Array.from(seen.values());
92
+ cache.delete(queuePath);
93
+ cache.set(queuePath, { signature: nextSignature, identity, size, consumedBytes, head, anchor, seen, rows });
94
+ while (cache.size > maxEntries) cache.delete(cache.keys().next().value);
95
+ if (malformed) console.error(`[LocalAPI] ${label}: skipped ${malformed} malformed line(s)`);
96
+ return rows;
97
+ } catch (error) {
98
+ if (error.code === "ENOENT") {
99
+ cache.delete(queuePath);
100
+ return [];
101
+ }
102
+ console.error(`[LocalAPI] ${label}: failed to read queue:`, error.message);
103
+ return cached?.rows || [];
104
+ } finally {
105
+ if (file) await file.close();
106
+ }
107
+ }
108
+
109
+ return function read(queuePath) {
110
+ if (pending.has(queuePath)) return pending.get(queuePath);
111
+ const task = load(queuePath).finally(() => pending.delete(queuePath));
112
+ pending.set(queuePath, task);
113
+ return task;
114
+ };
115
+ }
116
+
117
+ module.exports = { createAsyncQueueReader };
@@ -5,6 +5,9 @@
5
5
  * only LF as a record separator so valid U+2028/U+2029 characters inside JSON
6
6
  * strings remain part of the record. Reading raw bytes keeps byte budgets exact
7
7
  * and lets malformed UTF-8 fail closed.
8
+ * Records longer than maxRecordBytes content bytes (excluding LF) are reported
9
+ * as { line: null, utf8Valid: null, oversized: true } without being buffered
10
+ * beyond the limit or decoded.
8
11
  */
9
12
  class PhysicalJsonlLimitError extends Error {
10
13
  constructor(maxPhysicalBytes) {
@@ -26,6 +29,7 @@ function decodePhysicalLine(buffer, { stripCr = false } = {}) {
26
29
  async function* physicalJsonlRecords(input, {
27
30
  invalidUtf8 = "throw",
28
31
  maxPhysicalBytes = Infinity,
32
+ maxRecordBytes = Infinity,
29
33
  } = {}) {
30
34
  if (invalidUtf8 !== "throw" && invalidUtf8 !== "record") {
31
35
  throw new TypeError(`unsupported invalidUtf8 policy: ${invalidUtf8}`);
@@ -33,8 +37,12 @@ async function* physicalJsonlRecords(input, {
33
37
  const byteLimit = Number.isFinite(maxPhysicalBytes)
34
38
  ? Math.max(0, Number(maxPhysicalBytes))
35
39
  : Infinity;
40
+ const recordLimit = Number.isFinite(maxRecordBytes)
41
+ ? Math.max(0, Number(maxRecordBytes))
42
+ : Infinity;
36
43
  const fragments = [];
37
44
  let fragmentsBytes = 0;
45
+ let oversized = false;
38
46
  let physicalBytesRead = 0;
39
47
 
40
48
  for await (const value of input) {
@@ -53,6 +61,17 @@ async function* physicalJsonlRecords(input, {
53
61
  if (physicalBytesRead + physicalBytes > byteLimit) {
54
62
  throw new PhysicalJsonlLimitError(byteLimit);
55
63
  }
64
+ const contentBytes = fragmentsBytes + fragment.length;
65
+ if (oversized || contentBytes > recordLimit) {
66
+ fragments.length = 0;
67
+ fragmentsBytes = 0;
68
+ oversized = false;
69
+ physicalBytesRead += physicalBytes;
70
+ start = newline + 1;
71
+ newline = chunk.indexOf(0x0a, start);
72
+ yield { line: null, utf8Valid: null, oversized: true, physicalBytes, terminated: true };
73
+ continue;
74
+ }
56
75
  const lineBuffer = fragments.length === 0
57
76
  ? fragment
58
77
  : Buffer.concat([...fragments, fragment], fragmentsBytes + fragment.length);
@@ -66,10 +85,10 @@ async function* physicalJsonlRecords(input, {
66
85
  line = decodePhysicalLine(lineBuffer, { stripCr: true });
67
86
  } catch (error) {
68
87
  if (invalidUtf8 === "throw") throw error;
69
- yield { line: null, utf8Valid: false, physicalBytes, terminated: true };
88
+ yield { line: null, utf8Valid: false, oversized: false, physicalBytes, terminated: true };
70
89
  continue;
71
90
  }
72
- yield { line, utf8Valid: true, physicalBytes, terminated: true };
91
+ yield { line, utf8Valid: true, oversized: false, physicalBytes, terminated: true };
73
92
  }
74
93
 
75
94
  if (start < chunk.length) {
@@ -77,12 +96,23 @@ async function* physicalJsonlRecords(input, {
77
96
  if (physicalBytesRead + fragmentsBytes + fragment.length > byteLimit) {
78
97
  throw new PhysicalJsonlLimitError(byteLimit);
79
98
  }
80
- fragments.push(Buffer.from(fragment));
99
+ if (!oversized) {
100
+ if (fragmentsBytes + fragment.length > recordLimit) {
101
+ fragments.length = 0;
102
+ oversized = true;
103
+ } else {
104
+ fragments.push(Buffer.from(fragment));
105
+ }
106
+ }
81
107
  fragmentsBytes += fragment.length;
82
108
  }
83
109
  }
84
110
 
85
111
  if (fragmentsBytes > 0) {
112
+ if (oversized) {
113
+ yield { line: null, utf8Valid: null, oversized: true, physicalBytes: fragmentsBytes, terminated: false };
114
+ return;
115
+ }
86
116
  const lineBuffer = fragments.length === 1
87
117
  ? fragments[0]
88
118
  : Buffer.concat(fragments, fragmentsBytes);
@@ -94,7 +124,7 @@ async function* physicalJsonlRecords(input, {
94
124
  if (invalidUtf8 === "throw") throw error;
95
125
  utf8Valid = false;
96
126
  }
97
- yield { line, utf8Valid, physicalBytes: fragmentsBytes, terminated: false };
127
+ yield { line, utf8Valid, oversized: false, physicalBytes: fragmentsBytes, terminated: false };
98
128
  }
99
129
  }
100
130