@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.
- package/LICENSE +190 -21
- package/README.md +2 -0
- package/dist/action.d.ts +2 -2
- package/dist/action.js +2 -2
- package/dist/bundler-shared.d.ts +11 -0
- package/dist/bundler-shared.js +11 -0
- package/dist/context/event-bus.d.ts +1 -1
- package/dist/context/event-bus.js +1 -1
- package/dist/context/touchstrip-context.d.ts +2 -0
- package/dist/context/touchstrip-context.js +5 -0
- package/dist/devtools/bridge.d.ts +35 -7
- package/dist/devtools/bridge.js +153 -46
- package/dist/devtools/highlight.d.ts +6 -0
- package/dist/devtools/highlight.js +106 -57
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/observers/lifecycle.d.ts +4 -4
- package/dist/devtools/server.d.ts +6 -1
- package/dist/devtools/server.js +6 -1
- package/dist/devtools/types.d.ts +50 -6
- package/dist/font-inline.d.ts +5 -1
- package/dist/font-inline.js +8 -3
- package/dist/hooks/animation.d.ts +154 -0
- package/dist/hooks/animation.js +381 -0
- package/dist/hooks/events.js +1 -5
- package/dist/hooks/touchstrip.d.ts +6 -0
- package/dist/hooks/touchstrip.js +37 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -2
- package/dist/manifest-codegen.d.ts +38 -0
- package/dist/manifest-codegen.js +110 -0
- package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +3157 -0
- package/dist/plugin.js +20 -9
- package/dist/reconciler/host-config.js +19 -1
- package/dist/reconciler/vnode.d.ts +26 -0
- package/dist/reconciler/vnode.js +41 -10
- package/dist/render/buffer-pool.d.ts +19 -0
- package/dist/render/buffer-pool.js +51 -0
- package/dist/render/cache.d.ts +41 -0
- package/dist/render/cache.js +159 -5
- package/dist/render/image-cache.d.ts +53 -0
- package/dist/render/image-cache.js +128 -0
- package/dist/render/metrics.d.ts +58 -0
- package/dist/render/metrics.js +101 -0
- package/dist/render/pipeline.d.ts +46 -1
- package/dist/render/pipeline.js +370 -36
- package/dist/render/png.d.ts +10 -1
- package/dist/render/png.js +31 -13
- package/dist/render/render-pool.d.ts +26 -0
- package/dist/render/render-pool.js +141 -0
- package/dist/render/svg.d.ts +7 -0
- package/dist/render/svg.js +139 -0
- package/dist/render/worker.d.ts +1 -0
- package/dist/rollup.d.ts +23 -9
- package/dist/rollup.js +24 -9
- package/dist/roots/flush-coordinator.d.ts +18 -0
- package/dist/roots/flush-coordinator.js +38 -0
- package/dist/roots/registry.d.ts +6 -4
- package/dist/roots/registry.js +47 -33
- package/dist/roots/root.d.ts +32 -2
- package/dist/roots/root.js +104 -14
- package/dist/roots/settings-equality.d.ts +5 -0
- package/dist/roots/settings-equality.js +24 -0
- package/dist/roots/touchstrip-root.d.ts +93 -0
- package/dist/roots/touchstrip-root.js +383 -0
- package/dist/types.d.ts +62 -16
- package/dist/vite.d.ts +22 -8
- package/dist/vite.js +24 -8
- package/package.json +5 -4
- package/dist/context/touchbar-context.d.ts +0 -2
- package/dist/context/touchbar-context.js +0 -5
- package/dist/hooks/touchbar.d.ts +0 -6
- package/dist/hooks/touchbar.js +0 -37
- package/dist/roots/touchbar-root.d.ts +0 -45
- 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
|
|
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.
|
|
49
|
-
registry.create(ev, 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;
|
package/dist/reconciler/vnode.js
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
|
-
import
|
|
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,
|
|
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 };
|
package/dist/render/cache.d.ts
CHANGED
|
@@ -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;
|
package/dist/render/cache.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
47
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
7
48
|
}
|
|
8
|
-
else
|
|
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,
|
|
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;
|