@fcannizzaro/streamdeck-react 0.1.10 → 0.1.11

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 (74) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +2 -0
  3. package/dist/action.d.ts +2 -2
  4. package/dist/action.js +2 -2
  5. package/dist/bundler-shared.d.ts +11 -0
  6. package/dist/bundler-shared.js +11 -0
  7. package/dist/context/event-bus.d.ts +1 -1
  8. package/dist/context/event-bus.js +1 -1
  9. package/dist/context/touchstrip-context.d.ts +2 -0
  10. package/dist/context/touchstrip-context.js +5 -0
  11. package/dist/devtools/bridge.d.ts +35 -7
  12. package/dist/devtools/bridge.js +153 -46
  13. package/dist/devtools/highlight.d.ts +6 -0
  14. package/dist/devtools/highlight.js +106 -57
  15. package/dist/devtools/index.js +6 -0
  16. package/dist/devtools/observers/lifecycle.d.ts +4 -4
  17. package/dist/devtools/server.d.ts +6 -1
  18. package/dist/devtools/server.js +6 -1
  19. package/dist/devtools/types.d.ts +50 -6
  20. package/dist/font-inline.d.ts +5 -1
  21. package/dist/font-inline.js +8 -3
  22. package/dist/hooks/animation.d.ts +154 -0
  23. package/dist/hooks/animation.js +381 -0
  24. package/dist/hooks/events.js +1 -5
  25. package/dist/hooks/touchstrip.d.ts +6 -0
  26. package/dist/hooks/touchstrip.js +37 -0
  27. package/dist/index.d.ts +7 -2
  28. package/dist/index.js +3 -2
  29. package/dist/manifest-codegen.d.ts +38 -0
  30. package/dist/manifest-codegen.js +110 -0
  31. package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +3157 -0
  32. package/dist/plugin.js +20 -9
  33. package/dist/reconciler/host-config.js +19 -1
  34. package/dist/reconciler/vnode.d.ts +26 -0
  35. package/dist/reconciler/vnode.js +41 -10
  36. package/dist/render/buffer-pool.d.ts +19 -0
  37. package/dist/render/buffer-pool.js +51 -0
  38. package/dist/render/cache.d.ts +41 -0
  39. package/dist/render/cache.js +159 -5
  40. package/dist/render/image-cache.d.ts +53 -0
  41. package/dist/render/image-cache.js +128 -0
  42. package/dist/render/metrics.d.ts +58 -0
  43. package/dist/render/metrics.js +101 -0
  44. package/dist/render/pipeline.d.ts +46 -1
  45. package/dist/render/pipeline.js +370 -36
  46. package/dist/render/png.d.ts +10 -1
  47. package/dist/render/png.js +31 -13
  48. package/dist/render/render-pool.d.ts +26 -0
  49. package/dist/render/render-pool.js +141 -0
  50. package/dist/render/svg.d.ts +7 -0
  51. package/dist/render/svg.js +139 -0
  52. package/dist/render/worker.d.ts +1 -0
  53. package/dist/rollup.d.ts +23 -9
  54. package/dist/rollup.js +24 -9
  55. package/dist/roots/flush-coordinator.d.ts +18 -0
  56. package/dist/roots/flush-coordinator.js +38 -0
  57. package/dist/roots/registry.d.ts +6 -4
  58. package/dist/roots/registry.js +47 -33
  59. package/dist/roots/root.d.ts +32 -2
  60. package/dist/roots/root.js +104 -14
  61. package/dist/roots/settings-equality.d.ts +5 -0
  62. package/dist/roots/settings-equality.js +24 -0
  63. package/dist/roots/touchstrip-root.d.ts +93 -0
  64. package/dist/roots/touchstrip-root.js +383 -0
  65. package/dist/types.d.ts +62 -16
  66. package/dist/vite.d.ts +22 -8
  67. package/dist/vite.js +24 -8
  68. package/package.json +5 -4
  69. package/dist/context/touchbar-context.d.ts +0 -2
  70. package/dist/context/touchbar-context.js +0 -5
  71. package/dist/hooks/touchbar.d.ts +0 -6
  72. package/dist/hooks/touchbar.js +0 -37
  73. package/dist/roots/touchbar-root.d.ts +0 -45
  74. package/dist/roots/touchbar-root.js +0 -175
package/dist/plugin.js CHANGED
@@ -1,19 +1,28 @@
1
+ import { metrics } from "./render/metrics.js";
1
2
  import { RootRegistry } from "./roots/registry.js";
3
+ import { RenderPool } from "./render/render-pool.js";
2
4
  import { startDevtoolsServer } from "./devtools/index.js";
3
5
  import streamDeck, { SingletonAction } from "@elgato/streamdeck";
4
6
  import { Renderer } from "@takumi-rs/core";
5
7
  //#region src/plugin.ts
6
8
  function createPlugin(config) {
9
+ const renderer = new Renderer({ fonts: config.fonts.map((f) => ({
10
+ name: f.name,
11
+ data: f.data,
12
+ weight: f.weight,
13
+ style: f.style
14
+ })) });
15
+ const renderPool = config.useWorker !== false ? new RenderPool(config.fonts) : null;
7
16
  const renderConfig = {
8
- renderer: new Renderer({ fonts: config.fonts.map((f) => ({
9
- name: f.name,
10
- data: f.data,
11
- weight: f.weight,
12
- style: f.style
13
- })) }),
17
+ renderer,
14
18
  imageFormat: config.imageFormat ?? "png",
15
19
  caching: config.caching ?? true,
16
- devicePixelRatio: config.devicePixelRatio ?? 1
20
+ devicePixelRatio: config.devicePixelRatio ?? 1,
21
+ debug: config.debug ?? process.env.NODE_ENV !== "production",
22
+ imageCacheMaxBytes: config.imageCacheMaxBytes ?? 16 * 1024 * 1024,
23
+ touchstripCacheMaxBytes: config.touchstripCacheMaxBytes ?? 8 * 1024 * 1024,
24
+ renderPool,
25
+ touchstripImageFormat: config.touchstripImageFormat ?? "webp"
17
26
  };
18
27
  const registry = new RootRegistry(renderConfig, config.renderDebounceMs ?? 16, streamDeck, async (settings) => {
19
28
  await streamDeck.settings.setGlobalSettings(settings);
@@ -30,12 +39,14 @@ function createPlugin(config) {
30
39
  const singletonAction = createSingletonAction(definition, registry, config.onActionError);
31
40
  streamDeck.actions.registerAction(singletonAction);
32
41
  }
42
+ if (renderConfig.debug) metrics.enable();
33
43
  if (config.devtools) startDevtoolsServer({
34
44
  devtoolsName: streamDeck.info.plugin.uuid,
35
45
  registry,
36
46
  renderConfig
37
47
  });
38
48
  return { async connect() {
49
+ if (renderPool != null) renderPool.initialize().catch(() => {});
39
50
  await streamDeck.connect();
40
51
  } };
41
52
  }
@@ -45,8 +56,8 @@ function createSingletonAction(definition, registry, onError) {
45
56
  onWillAppear(ev) {
46
57
  try {
47
58
  const isEncoder = ev.payload.controller === "Encoder";
48
- if (isEncoder && definition.touchBar) {
49
- registry.create(ev, definition.touchBar, definition);
59
+ if (isEncoder && definition.touchStrip) {
60
+ registry.create(ev, definition.touchStrip, definition);
50
61
  return;
51
62
  }
52
63
  const component = isEncoder ? definition.dial ?? definition.key : definition.key;
@@ -1,4 +1,4 @@
1
- import { createTextVNode, createVNode } from "./vnode.js";
1
+ import { clearParent, createTextVNode, createVNode, markContainerDirty, markDirty, setParent } from "./vnode.js";
2
2
  import { createContext } from "react";
3
3
  import { DefaultEventPriority } from "react-reconciler/constants.js";
4
4
  //#region src/reconciler/host-config.ts
@@ -46,6 +46,7 @@ var hostConfig = {
46
46
  return false;
47
47
  },
48
48
  appendInitialChild(parent, child) {
49
+ child._parent = parent;
49
50
  parent.children.push(child);
50
51
  },
51
52
  finalizeInitialChildren() {
@@ -81,42 +82,59 @@ var hostConfig = {
81
82
  return null;
82
83
  },
83
84
  appendChild(parent, child) {
85
+ setParent(child, parent);
84
86
  parent.children.push(child);
87
+ markDirty(parent);
85
88
  },
86
89
  appendChildToContainer(container, child) {
90
+ setParent(child, container);
87
91
  container.children.push(child);
92
+ markContainerDirty(container);
88
93
  },
89
94
  insertBefore(parent, child, beforeChild) {
95
+ setParent(child, parent);
90
96
  const index = parent.children.indexOf(beforeChild);
91
97
  if (index >= 0) parent.children.splice(index, 0, child);
92
98
  else parent.children.push(child);
99
+ markDirty(parent);
93
100
  },
94
101
  insertInContainerBefore(container, child, beforeChild) {
102
+ setParent(child, container);
95
103
  const index = container.children.indexOf(beforeChild);
96
104
  if (index >= 0) container.children.splice(index, 0, child);
97
105
  else container.children.push(child);
106
+ markContainerDirty(container);
98
107
  },
99
108
  removeChild(parent, child) {
100
109
  const index = parent.children.indexOf(child);
101
110
  if (index >= 0) parent.children.splice(index, 1);
111
+ clearParent(child);
112
+ markDirty(parent);
102
113
  },
103
114
  removeChildFromContainer(container, child) {
104
115
  const index = container.children.indexOf(child);
105
116
  if (index >= 0) container.children.splice(index, 1);
117
+ clearParent(child);
118
+ markContainerDirty(container);
106
119
  },
107
120
  commitUpdate(instance, _type, _oldProps, newProps) {
108
121
  const { children: _, ...cleanProps } = newProps;
109
122
  instance.props = cleanProps;
123
+ instance._sortedPropKeys = void 0;
124
+ markDirty(instance);
110
125
  },
111
126
  commitTextUpdate(textInstance, _oldText, newText) {
112
127
  textInstance.text = newText;
128
+ markDirty(textInstance);
113
129
  },
114
130
  hideInstance() {},
115
131
  unhideInstance() {},
116
132
  hideTextInstance() {},
117
133
  unhideTextInstance() {},
118
134
  clearContainer(container) {
135
+ for (const child of container.children) clearParent(child);
119
136
  container.children = [];
137
+ markContainerDirty(container);
120
138
  },
121
139
  scheduleTimeout: setTimeout,
122
140
  cancelTimeout: clearTimeout,
@@ -4,6 +4,16 @@ export interface VNode {
4
4
  props: Record<string, unknown>;
5
5
  children: VNode[];
6
6
  text?: string;
7
+ /** @internal Back-pointer to parent VNode or VContainer for dirty propagation. */
8
+ _parent?: VNode | VContainer;
9
+ /** @internal True when this node or a descendant has been mutated since last flush. */
10
+ _dirty?: boolean;
11
+ /** @internal Cached Merkle hash for this subtree. */
12
+ _hash?: number;
13
+ /** @internal True when `_hash` is valid (invalidated on mutation). */
14
+ _hashValid?: boolean;
15
+ /** @internal Cached sorted prop keys for deterministic hashing. Invalidated when props change (commitUpdate). */
16
+ _sortedPropKeys?: string[];
7
17
  }
8
18
  export interface VContainer {
9
19
  children: VNode[];
@@ -11,7 +21,23 @@ export interface VContainer {
11
21
  lastSvgHash: number;
12
22
  renderCallback: () => void;
13
23
  renderTimer: ReturnType<typeof setTimeout> | null;
24
+ /** @internal Consecutive identical render count for duplicate detection. */
25
+ _dupCount: number;
26
+ /** @internal True when any child VNode has been mutated since last flush. */
27
+ _dirty: boolean;
14
28
  }
29
+ /** Mark a node (and its ancestors up to the container) as dirty. */
30
+ export declare function markDirty(node: VNode): void;
31
+ /** Mark the container itself as dirty (for structural mutations at container level). */
32
+ export declare function markContainerDirty(container: VContainer): void;
33
+ /** Check if the container has any pending mutations. */
34
+ export declare function isContainerDirty(container: VContainer): boolean;
35
+ /** Clear all dirty flags in the tree after a successful render. */
36
+ export declare function clearDirtyFlags(container: VContainer): void;
37
+ /** Set back-pointer on a child node. */
38
+ export declare function setParent(child: VNode, parent: VNode | VContainer): void;
39
+ /** Clear back-pointer (e.g., when removing from tree). */
40
+ export declare function clearParent(child: VNode): void;
15
41
  export declare function createVNode(type: string, props: Record<string, unknown>): VNode;
16
42
  export declare function createTextVNode(text: string): VNode;
17
43
  export declare function createVContainer(renderCallback: () => void): VContainer;
@@ -1,5 +1,41 @@
1
- import { createElement } from "react";
1
+ import "react";
2
2
  //#region src/reconciler/vnode.ts
3
+ /** Mark a node (and its ancestors up to the container) as dirty. */
4
+ function markDirty(node) {
5
+ let current = node;
6
+ while (current != null) {
7
+ if ("_dirty" in current && current._dirty) break;
8
+ current._dirty = true;
9
+ if ("type" in current) current._hashValid = false;
10
+ current = current._parent;
11
+ }
12
+ }
13
+ /** Mark the container itself as dirty (for structural mutations at container level). */
14
+ function markContainerDirty(container) {
15
+ container._dirty = true;
16
+ }
17
+ /** Check if the container has any pending mutations. */
18
+ function isContainerDirty(container) {
19
+ return container._dirty;
20
+ }
21
+ /** Clear all dirty flags in the tree after a successful render. */
22
+ function clearDirtyFlags(container) {
23
+ container._dirty = false;
24
+ for (const child of container.children) clearNodeDirty(child);
25
+ }
26
+ function clearNodeDirty(node) {
27
+ if (!node._dirty) return;
28
+ node._dirty = false;
29
+ for (const child of node.children) clearNodeDirty(child);
30
+ }
31
+ /** Set back-pointer on a child node. */
32
+ function setParent(child, parent) {
33
+ child._parent = parent;
34
+ }
35
+ /** Clear back-pointer (e.g., when removing from tree). */
36
+ function clearParent(child) {
37
+ child._parent = void 0;
38
+ }
3
39
  function createVNode(type, props) {
4
40
  return {
5
41
  type,
@@ -21,15 +57,10 @@ function createVContainer(renderCallback) {
21
57
  scheduledRender: false,
22
58
  lastSvgHash: 0,
23
59
  renderCallback,
24
- renderTimer: null
60
+ renderTimer: null,
61
+ _dupCount: 0,
62
+ _dirty: true
25
63
  };
26
64
  }
27
- function vnodeToElement(node) {
28
- if (node.type === "#text") return node.text ?? "";
29
- const { children: _children, className, ...restProps } = node.props;
30
- if (typeof className === "string" && className.length > 0) restProps.tw = (typeof restProps.tw === "string" ? restProps.tw + " " : "") + className;
31
- const childElements = node.children.map(vnodeToElement);
32
- return createElement(node.type, restProps, ...childElements);
33
- }
34
65
  //#endregion
35
- export { createTextVNode, createVContainer, createVNode, vnodeToElement };
66
+ export { clearDirtyFlags, clearParent, createTextVNode, createVContainer, createVNode, isContainerDirty, markContainerDirty, markDirty, setParent };
@@ -0,0 +1,19 @@
1
+ export declare class BufferPool {
2
+ private pools;
3
+ /** Acquire a zeroed buffer of the given size. Reuses a pooled buffer if available. */
4
+ acquire(size: number): Buffer;
5
+ /** Return a buffer to the pool for future reuse. */
6
+ release(buf: Buffer): void;
7
+ /** Clear all pooled buffers. */
8
+ clear(): void;
9
+ /** Current pool statistics. */
10
+ get stats(): {
11
+ buckets: number;
12
+ totalBuffers: number;
13
+ totalBytes: number;
14
+ };
15
+ }
16
+ /** Get the shared buffer pool. */
17
+ export declare function getBufferPool(): BufferPool;
18
+ /** Reset the shared pool (for testing). */
19
+ export declare function resetBufferPool(): void;
@@ -0,0 +1,51 @@
1
+ //#region src/render/buffer-pool.ts
2
+ /** Maximum number of buffers to retain per size bucket (prevents unbounded growth). */
3
+ var MAX_POOL_SIZE_PER_BUCKET = 8;
4
+ var BufferPool = class {
5
+ pools = /* @__PURE__ */ new Map();
6
+ /** Acquire a zeroed buffer of the given size. Reuses a pooled buffer if available. */
7
+ acquire(size) {
8
+ const pool = this.pools.get(size);
9
+ if (pool != null && pool.length > 0) {
10
+ const buf = pool.pop();
11
+ buf.fill(0);
12
+ return buf;
13
+ }
14
+ return Buffer.alloc(size);
15
+ }
16
+ /** Return a buffer to the pool for future reuse. */
17
+ release(buf) {
18
+ let pool = this.pools.get(buf.length);
19
+ if (pool == null) {
20
+ pool = [];
21
+ this.pools.set(buf.length, pool);
22
+ }
23
+ if (pool.length < MAX_POOL_SIZE_PER_BUCKET) pool.push(buf);
24
+ }
25
+ /** Clear all pooled buffers. */
26
+ clear() {
27
+ this.pools.clear();
28
+ }
29
+ /** Current pool statistics. */
30
+ get stats() {
31
+ let totalBuffers = 0;
32
+ let totalBytes = 0;
33
+ for (const [size, pool] of this.pools) {
34
+ totalBuffers += pool.length;
35
+ totalBytes += size * pool.length;
36
+ }
37
+ return {
38
+ buckets: this.pools.size,
39
+ totalBuffers,
40
+ totalBytes
41
+ };
42
+ }
43
+ };
44
+ var sharedPool = null;
45
+ /** Get the shared buffer pool. */
46
+ function getBufferPool() {
47
+ if (sharedPool == null) sharedPool = new BufferPool();
48
+ return sharedPool;
49
+ }
50
+ //#endregion
51
+ export { getBufferPool };
@@ -1 +1,42 @@
1
+ import { VNode, VContainer } from '../reconciler/vnode';
2
+ /**
3
+ * Initialize the xxHash-wasm module. Call is idempotent — subsequent
4
+ * calls return the same promise. Resolves once `fnv1a()` will use the
5
+ * WASM fast path for large buffers.
6
+ */
7
+ export declare function initBufferHasher(): Promise<void>;
8
+ /** Reset the xxHash singleton — for testing only. */
9
+ export declare function resetBufferHasher(): void;
10
+ /**
11
+ * Hash a raw byte buffer (Uint8Array or Buffer) or string.
12
+ *
13
+ * For buffers larger than {@link STRIDE_THRESHOLD} bytes:
14
+ * - **Primary path**: xxHash-wasm `h32Raw()` — hashes the entire buffer
15
+ * in native WASM code. Faster than JS strided sampling even for
16
+ * 320 KB touchstrip frames, with superior hash distribution.
17
+ * - **Fallback path**: FNV-1a with strided sampling (every 16th byte)
18
+ * when WASM hasn't compiled yet (startup) or is unavailable.
19
+ *
20
+ * Strings and small buffers always use JS FNV-1a (fast enough at those
21
+ * sizes, and avoids the overhead of calling into WASM for tiny inputs).
22
+ */
1
23
  export declare function fnv1a(input: string | Uint8Array | Buffer): number;
24
+ /** Feed a string into a running FNV-1a hash. */
25
+ export declare function fnv1aString(str: string, hash: number): number;
26
+ /** Feed a uint32 value into a running FNV-1a hash (4 bytes, big-endian). */
27
+ export declare function fnv1aU32(value: number, hash: number): number;
28
+ /** Hash an arbitrary JS value into a running FNV-1a hash. */
29
+ export declare function hashValue(value: unknown, hash: number, depth?: number): number;
30
+ /**
31
+ * Compute (or return cached) Merkle hash for a single VNode.
32
+ * The hash incorporates: type, text, props (sorted, functions skipped),
33
+ * children count, and children hashes (recursive).
34
+ */
35
+ export declare function computeHash(node: VNode): number;
36
+ /**
37
+ * Compute the Merkle hash for an entire VContainer tree.
38
+ * Returns 0 for empty containers.
39
+ */
40
+ export declare function computeTreeHash(container: VContainer): number;
41
+ export declare function computeCacheKey(treeHash: number, width: number, height: number, dpr: number, format: string): number;
42
+ export declare function computeNativeTouchstripCacheKey(treeHash: number, width: number, height: number, dpr: number, format: string, columns: number[]): number;
@@ -1,15 +1,169 @@
1
+ import { e } from "../node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js";
1
2
  //#region src/render/cache.ts
3
+ var FNV_OFFSET_BASIS = 2166136261;
4
+ var FNV_PRIME = 16777619;
5
+ var SENTINEL_NULL = 1314212940;
6
+ var SENTINEL_UNDEF = 1431192646;
7
+ var SENTINEL_NAN = 1314999841;
8
+ var SENTINEL_TRUE = 1414681925;
9
+ var SENTINEL_FALSE = 1178684499;
10
+ var SENTINEL_ARRAY = 1095914073;
11
+ var SENTINEL_OBJECT = 1329744468;
12
+ /** @internal WASM-accelerated buffer hash function, null before init. */
13
+ var bufferHashFn = null;
14
+ var xxHashInitPromise = null;
15
+ /**
16
+ * Initialize the xxHash-wasm module. Call is idempotent — subsequent
17
+ * calls return the same promise. Resolves once `fnv1a()` will use the
18
+ * WASM fast path for large buffers.
19
+ */
20
+ function initBufferHasher() {
21
+ if (xxHashInitPromise != null) return xxHashInitPromise;
22
+ xxHashInitPromise = e().then((api) => {
23
+ bufferHashFn = api.h32Raw;
24
+ }).catch(() => {});
25
+ return xxHashInitPromise;
26
+ }
27
+ initBufferHasher();
28
+ var STRIDE_THRESHOLD = 4096;
29
+ var STRIDE = 16;
30
+ /**
31
+ * Hash a raw byte buffer (Uint8Array or Buffer) or string.
32
+ *
33
+ * For buffers larger than {@link STRIDE_THRESHOLD} bytes:
34
+ * - **Primary path**: xxHash-wasm `h32Raw()` — hashes the entire buffer
35
+ * in native WASM code. Faster than JS strided sampling even for
36
+ * 320 KB touchstrip frames, with superior hash distribution.
37
+ * - **Fallback path**: FNV-1a with strided sampling (every 16th byte)
38
+ * when WASM hasn't compiled yet (startup) or is unavailable.
39
+ *
40
+ * Strings and small buffers always use JS FNV-1a (fast enough at those
41
+ * sizes, and avoids the overhead of calling into WASM for tiny inputs).
42
+ */
2
43
  function fnv1a(input) {
3
- let hash = 2166136261;
44
+ let hash = FNV_OFFSET_BASIS;
4
45
  if (typeof input === "string") for (let i = 0; i < input.length; i++) {
5
46
  hash ^= input.charCodeAt(i);
6
- hash = Math.imul(hash, 16777619);
47
+ hash = Math.imul(hash, FNV_PRIME);
7
48
  }
8
- else for (let i = 0; i < input.length; i++) {
49
+ else if (input.length > STRIDE_THRESHOLD) {
50
+ if (bufferHashFn != null) return bufferHashFn(input);
51
+ hash = fnv1aU32(input.length, hash);
52
+ for (let i = 0; i < input.length; i += STRIDE) {
53
+ const end = Math.min(i + 4, input.length);
54
+ for (let j = i; j < end; j++) {
55
+ hash ^= input[j];
56
+ hash = Math.imul(hash, FNV_PRIME);
57
+ }
58
+ }
59
+ } else for (let i = 0; i < input.length; i++) {
9
60
  hash ^= input[i];
10
- hash = Math.imul(hash, 16777619);
61
+ hash = Math.imul(hash, FNV_PRIME);
62
+ }
63
+ return hash >>> 0;
64
+ }
65
+ /** Feed a string into a running FNV-1a hash. */
66
+ function fnv1aString(str, hash) {
67
+ for (let i = 0; i < str.length; i++) {
68
+ hash ^= str.charCodeAt(i);
69
+ hash = Math.imul(hash, FNV_PRIME);
70
+ }
71
+ return hash;
72
+ }
73
+ /** Feed a uint32 value into a running FNV-1a hash (4 bytes, big-endian). */
74
+ function fnv1aU32(value, hash) {
75
+ hash ^= value >>> 24 & 255;
76
+ hash = Math.imul(hash, FNV_PRIME);
77
+ hash ^= value >>> 16 & 255;
78
+ hash = Math.imul(hash, FNV_PRIME);
79
+ hash ^= value >>> 8 & 255;
80
+ hash = Math.imul(hash, FNV_PRIME);
81
+ hash ^= value & 255;
82
+ hash = Math.imul(hash, FNV_PRIME);
83
+ return hash;
84
+ }
85
+ var MAX_HASH_DEPTH = 10;
86
+ /** Hash an arbitrary JS value into a running FNV-1a hash. */
87
+ function hashValue(value, hash, depth = 0) {
88
+ if (depth > MAX_HASH_DEPTH) return hash;
89
+ if (value === null) return fnv1aU32(SENTINEL_NULL, hash);
90
+ if (value === void 0) return fnv1aU32(SENTINEL_UNDEF, hash);
91
+ switch (typeof value) {
92
+ case "string": return fnv1aString(value, hash);
93
+ case "number":
94
+ if (Number.isNaN(value)) return fnv1aU32(SENTINEL_NAN, hash);
95
+ return fnv1aString(String(value), hash);
96
+ case "boolean": return fnv1aU32(value ? SENTINEL_TRUE : SENTINEL_FALSE, hash);
97
+ case "function":
98
+ case "symbol": return hash;
99
+ case "object": {
100
+ if (Array.isArray(value)) {
101
+ hash = fnv1aU32(SENTINEL_ARRAY, hash);
102
+ hash = fnv1aU32(value.length, hash);
103
+ for (let i = 0; i < value.length; i++) hash = hashValue(value[i], hash, depth + 1);
104
+ return hash;
105
+ }
106
+ hash = fnv1aU32(SENTINEL_OBJECT, hash);
107
+ const keys = Object.keys(value).sort();
108
+ hash = fnv1aU32(keys.length, hash);
109
+ for (const key of keys) {
110
+ const v = value[key];
111
+ if (typeof v === "function" || typeof v === "symbol") continue;
112
+ hash = fnv1aString(key, hash);
113
+ hash = hashValue(v, hash, depth + 1);
114
+ }
115
+ return hash;
116
+ }
117
+ default: return fnv1aString(String(value), hash);
118
+ }
119
+ }
120
+ /**
121
+ * Compute (or return cached) Merkle hash for a single VNode.
122
+ * The hash incorporates: type, text, props (sorted, functions skipped),
123
+ * children count, and children hashes (recursive).
124
+ */
125
+ function computeHash(node) {
126
+ if (node._hashValid && node._hash !== void 0) return node._hash;
127
+ let hash = FNV_OFFSET_BASIS;
128
+ hash = fnv1aString(node.type, hash);
129
+ if (node.text !== void 0) hash = fnv1aString(node.text, hash);
130
+ const keys = node._sortedPropKeys ??= Object.keys(node.props).sort();
131
+ for (const key of keys) {
132
+ const value = node.props[key];
133
+ if (typeof value === "function" || typeof value === "symbol") continue;
134
+ hash = fnv1aString(key, hash);
135
+ hash = hashValue(value, hash);
11
136
  }
137
+ hash = fnv1aU32(node.children.length, hash);
138
+ for (const child of node.children) hash = fnv1aU32(computeHash(child), hash);
139
+ node._hash = hash >>> 0;
140
+ node._hashValid = true;
141
+ return node._hash;
142
+ }
143
+ /**
144
+ * Compute the Merkle hash for an entire VContainer tree.
145
+ * Returns 0 for empty containers.
146
+ */
147
+ function computeTreeHash(container) {
148
+ if (container.children.length === 0) return 0;
149
+ let hash = FNV_OFFSET_BASIS;
150
+ hash = fnv1aU32(container.children.length, hash);
151
+ for (const child of container.children) hash = fnv1aU32(computeHash(child), hash);
12
152
  return hash >>> 0;
13
153
  }
154
+ function computeCacheKey(treeHash, width, height, dpr, format) {
155
+ let key = treeHash;
156
+ key = fnv1aU32(width, key);
157
+ key = fnv1aU32(height, key);
158
+ key = fnv1aU32(Math.round(dpr * 100), key);
159
+ key = fnv1aString(format, key);
160
+ return key >>> 0;
161
+ }
162
+ function computeNativeTouchstripCacheKey(treeHash, width, height, dpr, format, columns) {
163
+ let key = computeCacheKey(treeHash, width, height, dpr, format);
164
+ key = fnv1aU32(columns.length, key);
165
+ for (const col of columns) key = fnv1aU32(col, key);
166
+ return key >>> 0;
167
+ }
14
168
  //#endregion
15
- export { fnv1a };
169
+ export { computeCacheKey, computeNativeTouchstripCacheKey, computeTreeHash, fnv1a };
@@ -0,0 +1,53 @@
1
+ export interface CacheStats {
2
+ /** Number of entries currently in the cache. */
3
+ entries: number;
4
+ /** Current memory usage in bytes. */
5
+ bytes: number;
6
+ /** Maximum memory budget in bytes. */
7
+ maxBytes: number;
8
+ /** Total cache hits since creation or last reset. */
9
+ hits: number;
10
+ /** Total cache misses since creation or last reset. */
11
+ misses: number;
12
+ }
13
+ /**
14
+ * Byte-bounded LRU cache. Evicts least-recently-used entries when the
15
+ * total byte size exceeds `maxBytes`.
16
+ *
17
+ * Generic over value type: use `string` for data URI caching (keys/dials),
18
+ * `Buffer` for raw RGBA caching (touchstrip).
19
+ */
20
+ export declare class ImageCache<V = string> {
21
+ private maxBytes;
22
+ private map;
23
+ private head;
24
+ private tail;
25
+ private currentBytes;
26
+ private _hits;
27
+ private _misses;
28
+ constructor(maxBytes: number);
29
+ /** Retrieve a cached value. Returns `undefined` on miss. Promotes to MRU on hit. */
30
+ get(key: number): V | undefined;
31
+ /** Insert or update a cache entry. Evicts LRU entries if over budget. */
32
+ set(key: number, value: V, byteSize: number): void;
33
+ /** Clear all entries and reset stats. */
34
+ clear(): void;
35
+ /** Current cache statistics. */
36
+ get stats(): CacheStats;
37
+ private moveToHead;
38
+ private evictTail;
39
+ private evictUntilUnderBudget;
40
+ }
41
+ /** Get or create the shared image cache for data URIs. */
42
+ export declare function getImageCache(maxBytes?: number): ImageCache<string>;
43
+ /** Get or create the shared touchstrip raw buffer cache. */
44
+ export declare function getTouchstripCache(maxBytes?: number): ImageCache<Buffer>;
45
+ /**
46
+ * Get or create the shared touchstrip native-format segment cache.
47
+ * Stores sorted `[column, dataUri]` tuples per tree hash + column config.
48
+ * Uses the same default budget as the raw touchstrip cache since only one
49
+ * touchstrip rendering path is active at a time.
50
+ */
51
+ export declare function getTouchstripNativeCache(maxBytes?: number): ImageCache<Array<[number, string]>>;
52
+ /** Reset all caches (for testing or config changes). */
53
+ export declare function resetCaches(): void;