hyperprop-charting-library 0.1.132 → 0.1.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hyperprop-charting-library.cjs +319 -8
- package/dist/hyperprop-charting-library.d.ts +8 -1
- package/dist/hyperprop-charting-library.js +317 -7
- package/dist/index.cjs +319 -8
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +317 -7
- package/docs/API.md +15 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -603,14 +603,23 @@ var withCachedSeries = (key, data, compute) => {
|
|
|
603
603
|
return values;
|
|
604
604
|
};
|
|
605
605
|
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
606
|
+
var COMPUTATION_CACHE_MAX_ENTRIES = 128;
|
|
606
607
|
var withCachedComputation = (key, data, compute) => {
|
|
607
608
|
const fingerprint = getSeriesFingerprint(data);
|
|
608
609
|
const existing = builtInComputationCache.get(key);
|
|
609
610
|
if (existing && existing.fingerprint === fingerprint) {
|
|
611
|
+
builtInComputationCache.delete(key);
|
|
612
|
+
builtInComputationCache.set(key, existing);
|
|
610
613
|
return existing.value;
|
|
611
614
|
}
|
|
612
615
|
const value = compute();
|
|
616
|
+
builtInComputationCache.delete(key);
|
|
613
617
|
builtInComputationCache.set(key, { fingerprint, value });
|
|
618
|
+
while (builtInComputationCache.size > COMPUTATION_CACHE_MAX_ENTRIES) {
|
|
619
|
+
const oldest = builtInComputationCache.keys().next().value;
|
|
620
|
+
if (oldest === void 0) break;
|
|
621
|
+
builtInComputationCache.delete(oldest);
|
|
622
|
+
}
|
|
614
623
|
return value;
|
|
615
624
|
};
|
|
616
625
|
var rollingExtremeSeries = (values, length, mode) => {
|
|
@@ -1542,14 +1551,304 @@ var hashScriptSource = (source) => {
|
|
|
1542
1551
|
}
|
|
1543
1552
|
return (hash >>> 0).toString(36);
|
|
1544
1553
|
};
|
|
1545
|
-
var
|
|
1546
|
-
|
|
1554
|
+
var SCRIPT_GUARD_BUDGET_MS = 1e3;
|
|
1555
|
+
var ScriptBudgetError = class extends Error {
|
|
1556
|
+
constructor() {
|
|
1557
|
+
super(
|
|
1558
|
+
`Script exceeded the ${SCRIPT_GUARD_BUDGET_MS}ms execution budget (possible infinite loop) \u2014 edit the script to retry`
|
|
1559
|
+
);
|
|
1560
|
+
this.name = "ScriptBudgetError";
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
var createScriptGuard = () => {
|
|
1564
|
+
let deadline = Date.now() + SCRIPT_GUARD_BUDGET_MS;
|
|
1565
|
+
let count = 0;
|
|
1566
|
+
const guard = (() => {
|
|
1567
|
+
count += 1;
|
|
1568
|
+
if ((count & 1023) === 0 && Date.now() > deadline) {
|
|
1569
|
+
throw new ScriptBudgetError();
|
|
1570
|
+
}
|
|
1571
|
+
return true;
|
|
1572
|
+
});
|
|
1573
|
+
guard.reset = () => {
|
|
1574
|
+
count = 0;
|
|
1575
|
+
deadline = Date.now() + SCRIPT_GUARD_BUDGET_MS;
|
|
1576
|
+
};
|
|
1577
|
+
return guard;
|
|
1578
|
+
};
|
|
1579
|
+
var injectLoopGuards = (source, guardName) => {
|
|
1580
|
+
const n = source.length;
|
|
1581
|
+
const isIdChar = (ch) => ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "$";
|
|
1582
|
+
const scanString = (start) => {
|
|
1583
|
+
const quote = source[start];
|
|
1584
|
+
let j = start + 1;
|
|
1585
|
+
while (j < n) {
|
|
1586
|
+
const cj = source[j];
|
|
1587
|
+
if (cj === "\\") {
|
|
1588
|
+
j += 2;
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (cj === quote) {
|
|
1592
|
+
return j + 1;
|
|
1593
|
+
}
|
|
1594
|
+
if (quote === "`" && cj === "$" && source[j + 1] === "{") {
|
|
1595
|
+
let depth = 1;
|
|
1596
|
+
j += 2;
|
|
1597
|
+
while (j < n && depth > 0) {
|
|
1598
|
+
const ce = source[j];
|
|
1599
|
+
if (ce === "\\") {
|
|
1600
|
+
j += 2;
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (ce === '"' || ce === "'" || ce === "`") {
|
|
1604
|
+
j = scanString(j);
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if (ce === "{") depth += 1;
|
|
1608
|
+
else if (ce === "}") depth -= 1;
|
|
1609
|
+
j += 1;
|
|
1610
|
+
}
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
j += 1;
|
|
1614
|
+
}
|
|
1615
|
+
return n;
|
|
1616
|
+
};
|
|
1617
|
+
const scanRegex = (start) => {
|
|
1618
|
+
let j = start + 1;
|
|
1619
|
+
let inClass = false;
|
|
1620
|
+
while (j < n) {
|
|
1621
|
+
const cj = source[j];
|
|
1622
|
+
if (cj === "\\") {
|
|
1623
|
+
j += 2;
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
if (cj === "\n") return j;
|
|
1627
|
+
if (cj === "[") inClass = true;
|
|
1628
|
+
else if (cj === "]") inClass = false;
|
|
1629
|
+
else if (cj === "/" && !inClass) return j + 1;
|
|
1630
|
+
j += 1;
|
|
1631
|
+
}
|
|
1632
|
+
return n;
|
|
1633
|
+
};
|
|
1634
|
+
const skipWsAndComments = (k) => {
|
|
1635
|
+
let j = k;
|
|
1636
|
+
while (j < n) {
|
|
1637
|
+
const cj = source[j];
|
|
1638
|
+
if (cj === " " || cj === " " || cj === "\n" || cj === "\r") {
|
|
1639
|
+
j += 1;
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (cj === "/" && source[j + 1] === "/") {
|
|
1643
|
+
const nl = source.indexOf("\n", j);
|
|
1644
|
+
j = nl === -1 ? n : nl + 1;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (cj === "/" && source[j + 1] === "*") {
|
|
1648
|
+
const end = source.indexOf("*/", j + 2);
|
|
1649
|
+
j = end === -1 ? n : end + 2;
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
return j;
|
|
1653
|
+
}
|
|
1654
|
+
return n;
|
|
1655
|
+
};
|
|
1656
|
+
const matchParen = (openIdx) => {
|
|
1657
|
+
let depth = 0;
|
|
1658
|
+
let j = openIdx;
|
|
1659
|
+
while (j < n) {
|
|
1660
|
+
const cj = source[j];
|
|
1661
|
+
if (cj === '"' || cj === "'" || cj === "`") {
|
|
1662
|
+
j = scanString(j);
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
if (cj === "/" && source[j + 1] === "/") {
|
|
1666
|
+
const nl = source.indexOf("\n", j);
|
|
1667
|
+
j = nl === -1 ? n : nl + 1;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
if (cj === "/" && source[j + 1] === "*") {
|
|
1671
|
+
const end = source.indexOf("*/", j + 2);
|
|
1672
|
+
j = end === -1 ? n : end + 2;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (cj === "(") depth += 1;
|
|
1676
|
+
else if (cj === ")") {
|
|
1677
|
+
depth -= 1;
|
|
1678
|
+
if (depth === 0) return j;
|
|
1679
|
+
}
|
|
1680
|
+
j += 1;
|
|
1681
|
+
}
|
|
1682
|
+
return -1;
|
|
1683
|
+
};
|
|
1684
|
+
const splitTopLevelSemicolons = (inner) => {
|
|
1685
|
+
const parts = [];
|
|
1686
|
+
let depth = 0;
|
|
1687
|
+
let j = 0;
|
|
1688
|
+
let last = 0;
|
|
1689
|
+
const m = inner.length;
|
|
1690
|
+
while (j < m) {
|
|
1691
|
+
const cj = inner[j];
|
|
1692
|
+
if (cj === '"' || cj === "'" || cj === "`") {
|
|
1693
|
+
const quote = cj;
|
|
1694
|
+
j += 1;
|
|
1695
|
+
while (j < m) {
|
|
1696
|
+
if (inner[j] === "\\") {
|
|
1697
|
+
j += 2;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (inner[j] === quote) {
|
|
1701
|
+
j += 1;
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
j += 1;
|
|
1705
|
+
}
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
if (cj === "(" || cj === "[" || cj === "{") depth += 1;
|
|
1709
|
+
else if (cj === ")" || cj === "]" || cj === "}") depth -= 1;
|
|
1710
|
+
else if (cj === ";" && depth === 0) {
|
|
1711
|
+
parts.push(inner.slice(last, j));
|
|
1712
|
+
last = j + 1;
|
|
1713
|
+
}
|
|
1714
|
+
j += 1;
|
|
1715
|
+
}
|
|
1716
|
+
parts.push(inner.slice(last));
|
|
1717
|
+
return parts;
|
|
1718
|
+
};
|
|
1719
|
+
const REGEX_PRECEDING_WORDS = /* @__PURE__ */ new Set([
|
|
1720
|
+
"return",
|
|
1721
|
+
"typeof",
|
|
1722
|
+
"case",
|
|
1723
|
+
"do",
|
|
1724
|
+
"else",
|
|
1725
|
+
"in",
|
|
1726
|
+
"of",
|
|
1727
|
+
"new",
|
|
1728
|
+
"delete",
|
|
1729
|
+
"void",
|
|
1730
|
+
"instanceof",
|
|
1731
|
+
"yield",
|
|
1732
|
+
"await"
|
|
1733
|
+
]);
|
|
1734
|
+
let out = "";
|
|
1735
|
+
let i = 0;
|
|
1736
|
+
let lastSig = "";
|
|
1737
|
+
let lastWord = "";
|
|
1738
|
+
while (i < n) {
|
|
1739
|
+
const ch = source[i];
|
|
1740
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
1741
|
+
const nl = source.indexOf("\n", i);
|
|
1742
|
+
const end = nl === -1 ? n : nl;
|
|
1743
|
+
out += source.slice(i, end);
|
|
1744
|
+
i = end;
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
1748
|
+
const close = source.indexOf("*/", i + 2);
|
|
1749
|
+
const end = close === -1 ? n : close + 2;
|
|
1750
|
+
out += source.slice(i, end);
|
|
1751
|
+
i = end;
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1755
|
+
const end = scanString(i);
|
|
1756
|
+
out += source.slice(i, end);
|
|
1757
|
+
lastSig = ch;
|
|
1758
|
+
lastWord = "";
|
|
1759
|
+
i = end;
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (ch === "/") {
|
|
1763
|
+
const regexLikely = lastSig === "" || "(,=:;!&|?{}[+-*%~^<>".includes(lastSig) || REGEX_PRECEDING_WORDS.has(lastWord);
|
|
1764
|
+
if (regexLikely) {
|
|
1765
|
+
const end = scanRegex(i);
|
|
1766
|
+
out += source.slice(i, end);
|
|
1767
|
+
lastSig = "/";
|
|
1768
|
+
lastWord = "";
|
|
1769
|
+
i = end;
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
out += ch;
|
|
1773
|
+
lastSig = ch;
|
|
1774
|
+
lastWord = "";
|
|
1775
|
+
i += 1;
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
if (isIdChar(ch)) {
|
|
1779
|
+
let j = i;
|
|
1780
|
+
while (j < n && isIdChar(source[j])) j += 1;
|
|
1781
|
+
const word = source.slice(i, j);
|
|
1782
|
+
if ((word === "while" || word === "for") && lastSig !== ".") {
|
|
1783
|
+
const k = skipWsAndComments(j);
|
|
1784
|
+
if (source[k] === "(") {
|
|
1785
|
+
const close = matchParen(k);
|
|
1786
|
+
if (close !== -1) {
|
|
1787
|
+
const inner = source.slice(k + 1, close);
|
|
1788
|
+
if (word === "while") {
|
|
1789
|
+
out += `while (${guardName}() && (${inner}))`;
|
|
1790
|
+
lastSig = ")";
|
|
1791
|
+
lastWord = "";
|
|
1792
|
+
i = close + 1;
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const parts = splitTopLevelSemicolons(inner);
|
|
1796
|
+
if (parts.length === 3) {
|
|
1797
|
+
const cond = parts[1].trim();
|
|
1798
|
+
const guardedCond = cond ? `${guardName}() && (${cond})` : `${guardName}()`;
|
|
1799
|
+
out += `for (${parts[0]}; ${guardedCond}; ${parts[2]})`;
|
|
1800
|
+
lastSig = ")";
|
|
1801
|
+
lastWord = "";
|
|
1802
|
+
i = close + 1;
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
out += source.slice(i, close + 1);
|
|
1806
|
+
lastSig = ")";
|
|
1807
|
+
lastWord = "";
|
|
1808
|
+
i = close + 1;
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
out += word;
|
|
1814
|
+
lastSig = word[word.length - 1];
|
|
1815
|
+
lastWord = word;
|
|
1816
|
+
i = j;
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
out += ch;
|
|
1820
|
+
if (ch !== " " && ch !== " " && ch !== "\n" && ch !== "\r") {
|
|
1821
|
+
lastSig = ch;
|
|
1822
|
+
lastWord = "";
|
|
1823
|
+
}
|
|
1824
|
+
i += 1;
|
|
1825
|
+
}
|
|
1826
|
+
return out;
|
|
1827
|
+
};
|
|
1828
|
+
var compileScriptCompute = (source, guard) => {
|
|
1829
|
+
const makeFactory = (body) => new Function(
|
|
1830
|
+
"__hpGuard",
|
|
1547
1831
|
`"use strict";
|
|
1548
|
-
${
|
|
1832
|
+
${body}
|
|
1549
1833
|
;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
|
|
1550
1834
|
return compute;`
|
|
1551
1835
|
);
|
|
1552
|
-
|
|
1836
|
+
let factory;
|
|
1837
|
+
try {
|
|
1838
|
+
factory = makeFactory(injectLoopGuards(source, "__hpGuard"));
|
|
1839
|
+
} catch {
|
|
1840
|
+
factory = makeFactory(source);
|
|
1841
|
+
}
|
|
1842
|
+
guard.reset();
|
|
1843
|
+
return factory(guard);
|
|
1844
|
+
};
|
|
1845
|
+
var validateScriptSource = (source) => {
|
|
1846
|
+
try {
|
|
1847
|
+
compileScriptCompute(source, createScriptGuard());
|
|
1848
|
+
return null;
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
return error instanceof Error ? error.message : String(error);
|
|
1851
|
+
}
|
|
1553
1852
|
};
|
|
1554
1853
|
var drawScriptError = (ctx, renderContext, name, message) => {
|
|
1555
1854
|
ctx.save();
|
|
@@ -1569,17 +1868,23 @@ var compileScriptIndicator = (definition) => {
|
|
|
1569
1868
|
}
|
|
1570
1869
|
let compiled = null;
|
|
1571
1870
|
let compileError = null;
|
|
1871
|
+
const guard = createScriptGuard();
|
|
1572
1872
|
try {
|
|
1573
|
-
compiled = compileScriptCompute(definition.source);
|
|
1873
|
+
compiled = compileScriptCompute(definition.source, guard);
|
|
1574
1874
|
} catch (error) {
|
|
1575
1875
|
compileError = error instanceof Error ? error.message : String(error);
|
|
1576
1876
|
}
|
|
1577
1877
|
const sourceKey = hashScriptSource(definition.source);
|
|
1878
|
+
let trippedError = null;
|
|
1578
1879
|
const runCompute = (data, inputs) => {
|
|
1579
1880
|
if (!compiled) {
|
|
1580
1881
|
return { error: compileError ?? "Script failed to compile" };
|
|
1581
1882
|
}
|
|
1883
|
+
if (trippedError) {
|
|
1884
|
+
return { error: trippedError };
|
|
1885
|
+
}
|
|
1582
1886
|
try {
|
|
1887
|
+
guard.reset();
|
|
1583
1888
|
const result = compiled(data, inputs, SCRIPT_HELPERS);
|
|
1584
1889
|
if (!result || !Array.isArray(result.plots)) {
|
|
1585
1890
|
return { error: "compute() must return { plots: [...] }" };
|
|
@@ -1591,7 +1896,11 @@ var compileScriptIndicator = (definition) => {
|
|
|
1591
1896
|
}
|
|
1592
1897
|
return { result };
|
|
1593
1898
|
} catch (error) {
|
|
1594
|
-
|
|
1899
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1900
|
+
if (error instanceof ScriptBudgetError) {
|
|
1901
|
+
trippedError = message;
|
|
1902
|
+
}
|
|
1903
|
+
return { error: message };
|
|
1595
1904
|
}
|
|
1596
1905
|
};
|
|
1597
1906
|
const cachedCompute = (data, inputs) => withCachedComputation(
|
|
@@ -8040,5 +8349,6 @@ export {
|
|
|
8040
8349
|
FIB_DEFAULT_PALETTE,
|
|
8041
8350
|
POSITION_DEFAULT_COLORS,
|
|
8042
8351
|
compileScriptIndicator,
|
|
8043
|
-
createChart
|
|
8352
|
+
createChart,
|
|
8353
|
+
validateScriptSource
|
|
8044
8354
|
};
|
package/docs/API.md
CHANGED
|
@@ -607,3 +607,18 @@ Script contract:
|
|
|
607
607
|
Errors never break the chart: compile or runtime errors render as an inline red message in the
|
|
608
608
|
pane. Scripts are plain JavaScript executed in the page context — treat them like any other code
|
|
609
609
|
you'd paste into your app (only run scripts you trust).
|
|
610
|
+
|
|
611
|
+
### Execution budget
|
|
612
|
+
|
|
613
|
+
Loop conditions are instrumented at compile time, and each `compute()` invocation gets a
|
|
614
|
+
1000 ms wall-clock budget. A script that exceeds it (e.g. `while (true) {}`) throws
|
|
615
|
+
`"Script exceeded the 1000ms execution budget"`, which renders as the usual inline pane error
|
|
616
|
+
instead of freezing the tab. After a budget trip the script is circuit-broken — it is not run
|
|
617
|
+
again for that compiled instance, so live ticks don't re-trigger the stall. Recompiling (editing
|
|
618
|
+
the source and calling `compileScriptIndicator` again) resets the breaker. String/template/comment
|
|
619
|
+
contents are never instrumented; `for...of` / `for...in` loops are left untouched.
|
|
620
|
+
|
|
621
|
+
`validateScriptSource(source: string): string | null` — compile-checks a script without
|
|
622
|
+
registering it, returning an error message or `null`. It runs under the same execution budget, so
|
|
623
|
+
validating a script with a top-level infinite loop returns a budget error instead of hanging the
|
|
624
|
+
caller. Use this for editor save-validation instead of running `new Function` yourself.
|