@fcannizzaro/streamdeck-react 0.1.10 → 0.1.12
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 +1 -2
- package/dist/adapter/index.d.ts +2 -0
- package/dist/adapter/physical-device.d.ts +2 -0
- package/dist/adapter/physical-device.js +153 -0
- package/dist/adapter/types.d.ts +127 -0
- 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 +152 -46
- package/dist/devtools/highlight.d.ts +5 -0
- package/dist/devtools/highlight.js +107 -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 +2 -6
- package/dist/hooks/sdk.js +11 -11
- package/dist/hooks/touchstrip.d.ts +6 -0
- package/dist/hooks/touchstrip.js +37 -0
- package/dist/hooks/utility.js +3 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +4 -2
- package/dist/manifest-codegen.d.ts +38 -0
- package/dist/manifest-codegen.js +110 -0
- package/dist/plugin.js +86 -106
- package/dist/reconciler/host-config.js +19 -1
- package/dist/reconciler/vnode.d.ts +26 -2
- package/dist/reconciler/vnode.js +40 -10
- package/dist/render/buffer-pool.d.ts +19 -0
- package/dist/render/buffer-pool.js +51 -0
- package/dist/render/cache.d.ts +29 -0
- package/dist/render/cache.js +137 -5
- package/dist/render/image-cache.d.ts +54 -0
- package/dist/render/image-cache.js +144 -0
- package/dist/render/metrics.d.ts +57 -0
- package/dist/render/metrics.js +98 -0
- package/dist/render/pipeline.d.ts +36 -1
- package/dist/render/pipeline.js +304 -34
- package/dist/render/png.d.ts +1 -1
- package/dist/render/png.js +26 -11
- package/dist/render/render-pool.d.ts +24 -0
- package/dist/render/render-pool.js +130 -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/registry.d.ts +9 -11
- package/dist/roots/registry.js +39 -42
- package/dist/roots/root.d.ts +9 -6
- package/dist/roots/root.js +52 -29
- package/dist/roots/settings-equality.d.ts +5 -0
- package/dist/roots/settings-equality.js +24 -0
- package/dist/roots/{touchbar-root.d.ts → touchstrip-root.d.ts} +30 -8
- package/dist/roots/touchstrip-root.js +263 -0
- package/dist/types.d.ts +73 -23
- package/dist/vite.d.ts +22 -8
- package/dist/vite.js +24 -8
- package/package.json +7 -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.js +0 -175
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
//#region src/manifest-codegen.ts
|
|
4
|
+
/**
|
|
5
|
+
* Find the manifest.json inside a `*.sdPlugin` directory.
|
|
6
|
+
*
|
|
7
|
+
* - If `explicit` is a string, it is resolved against `root`.
|
|
8
|
+
* - If `explicit` is `false`, codegen is disabled (returns `null`).
|
|
9
|
+
* - Otherwise, auto-detects by scanning `root` for `*.sdPlugin/manifest.json`.
|
|
10
|
+
*/
|
|
11
|
+
function findManifestPath(root, explicit, warn) {
|
|
12
|
+
if (explicit === false) return null;
|
|
13
|
+
if (typeof explicit === "string") {
|
|
14
|
+
const resolved = resolve(root, explicit);
|
|
15
|
+
if (!existsSync(resolved)) {
|
|
16
|
+
warn?.(`[@fcannizzaro/streamdeck-react] Manifest not found at ${resolved}`);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
const sdPluginDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.endsWith(".sdPlugin"));
|
|
22
|
+
if (sdPluginDirs.length === 0) return null;
|
|
23
|
+
if (sdPluginDirs.length > 1) warn?.(`[@fcannizzaro/streamdeck-react] Multiple .sdPlugin directories found. Using "${sdPluginDirs[0].name}". Set the \`manifest\` option to specify one explicitly.`);
|
|
24
|
+
const manifestPath = join(root, sdPluginDirs[0].name, "manifest.json");
|
|
25
|
+
return existsSync(manifestPath) ? manifestPath : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a Stream Deck plugin manifest and extract action metadata.
|
|
29
|
+
* Returns an empty array on failure (with a warning).
|
|
30
|
+
*/
|
|
31
|
+
function parseManifest(path, warn) {
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(path, "utf-8");
|
|
34
|
+
const manifest = JSON.parse(content);
|
|
35
|
+
if (!Array.isArray(manifest.Actions)) {
|
|
36
|
+
warn?.(`[@fcannizzaro/streamdeck-react] manifest.json has no Actions array`);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const actions = [];
|
|
40
|
+
for (const action of manifest.Actions) {
|
|
41
|
+
if (!action.UUID) {
|
|
42
|
+
warn?.(`[@fcannizzaro/streamdeck-react] Skipping manifest action with missing UUID`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
actions.push({
|
|
46
|
+
uuid: action.UUID,
|
|
47
|
+
controllers: action.Controllers ?? ["Keypad"]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return actions;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
warn?.(`[@fcannizzaro/streamdeck-react] Failed to parse manifest: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var HEADER = ["// This file is auto-generated by @fcannizzaro/streamdeck-react", "// Do not edit this file manually. It is regenerated from the plugin manifest."].join("\n");
|
|
57
|
+
/**
|
|
58
|
+
* Generate the `declare module` augmentation string from parsed actions.
|
|
59
|
+
* Returns an empty string when there are no actions.
|
|
60
|
+
*/
|
|
61
|
+
function generateManifestDts(actions) {
|
|
62
|
+
if (actions.length === 0) return "";
|
|
63
|
+
return [
|
|
64
|
+
HEADER,
|
|
65
|
+
"",
|
|
66
|
+
"import \"@fcannizzaro/streamdeck-react\";",
|
|
67
|
+
"",
|
|
68
|
+
"declare module \"@fcannizzaro/streamdeck-react\" {",
|
|
69
|
+
" interface ManifestActions {",
|
|
70
|
+
...actions.map((action) => {
|
|
71
|
+
const controllers = action.controllers.map((c) => `"${c}"`).join(", ");
|
|
72
|
+
return ` "${action.uuid}": {\n controllers: readonly [${controllers}];\n };`;
|
|
73
|
+
}),
|
|
74
|
+
" }",
|
|
75
|
+
"}",
|
|
76
|
+
""
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Write the generated `.d.ts` file only when the content has changed.
|
|
81
|
+
* Creates the parent directory if it does not exist.
|
|
82
|
+
*
|
|
83
|
+
* @returns `true` if the file was written, `false` if content was unchanged.
|
|
84
|
+
*/
|
|
85
|
+
function writeManifestDtsIfChanged(outPath, content) {
|
|
86
|
+
if (content === "") return false;
|
|
87
|
+
if (existsSync(outPath)) {
|
|
88
|
+
if (readFileSync(outPath, "utf-8") === content) return false;
|
|
89
|
+
}
|
|
90
|
+
const dir = dirname(outPath);
|
|
91
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
92
|
+
writeFileSync(outPath, content);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* High-level helper: find, parse, generate, and write manifest types.
|
|
97
|
+
*
|
|
98
|
+
* @returns The resolved manifest path (for watch registration), or `null`.
|
|
99
|
+
*/
|
|
100
|
+
function generateManifestTypes(root, manifestOption, warn) {
|
|
101
|
+
const manifestPath = findManifestPath(root, manifestOption, warn);
|
|
102
|
+
if (!manifestPath) return null;
|
|
103
|
+
const content = generateManifestDts(parseManifest(manifestPath, warn));
|
|
104
|
+
return {
|
|
105
|
+
manifestPath,
|
|
106
|
+
written: writeManifestDtsIfChanged(resolve(root, "src/streamdeck-env.d.ts"), content)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
export { generateManifestTypes };
|
package/dist/plugin.js
CHANGED
|
@@ -1,178 +1,158 @@
|
|
|
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
|
-
import
|
|
5
|
+
import { physicalDevice } from "./adapter/physical-device.js";
|
|
4
6
|
import { Renderer } from "@takumi-rs/core";
|
|
5
7
|
//#region src/plugin.ts
|
|
6
8
|
function createPlugin(config) {
|
|
9
|
+
const adapter = config.adapter ?? physicalDevice();
|
|
10
|
+
const renderer = new Renderer({ fonts: config.fonts.map((f) => ({
|
|
11
|
+
name: f.name,
|
|
12
|
+
data: f.data,
|
|
13
|
+
weight: f.weight,
|
|
14
|
+
style: f.style
|
|
15
|
+
})) });
|
|
16
|
+
const renderPool = config.useWorker !== false ? new RenderPool(config.fonts) : null;
|
|
7
17
|
const renderConfig = {
|
|
8
|
-
renderer
|
|
9
|
-
name: f.name,
|
|
10
|
-
data: f.data,
|
|
11
|
-
weight: f.weight,
|
|
12
|
-
style: f.style
|
|
13
|
-
})) }),
|
|
18
|
+
renderer,
|
|
14
19
|
imageFormat: config.imageFormat ?? "png",
|
|
15
20
|
caching: config.caching ?? true,
|
|
16
|
-
devicePixelRatio: config.devicePixelRatio ?? 1
|
|
21
|
+
devicePixelRatio: config.devicePixelRatio ?? 1,
|
|
22
|
+
debug: config.debug ?? process.env.NODE_ENV !== "production",
|
|
23
|
+
imageCacheMaxBytes: config.imageCacheMaxBytes ?? 16 * 1024 * 1024,
|
|
24
|
+
touchStripCacheMaxBytes: config.touchStripCacheMaxBytes ?? 8 * 1024 * 1024,
|
|
25
|
+
renderPool
|
|
17
26
|
};
|
|
18
|
-
const registry = new RootRegistry(renderConfig,
|
|
19
|
-
await
|
|
27
|
+
const registry = new RootRegistry(renderConfig, adapter, async (settings) => {
|
|
28
|
+
await adapter.setGlobalSettings(settings);
|
|
20
29
|
}, config.wrapper);
|
|
21
|
-
|
|
30
|
+
adapter.getGlobalSettings().then((gs) => {
|
|
22
31
|
registry.setGlobalSettings(gs);
|
|
23
32
|
}).catch((err) => {
|
|
24
33
|
console.error("[@fcannizzaro/streamdeck-react] Failed to load global settings:", err);
|
|
25
34
|
});
|
|
26
|
-
|
|
27
|
-
registry.setGlobalSettings(
|
|
35
|
+
adapter.onGlobalSettingsChanged((settings) => {
|
|
36
|
+
registry.setGlobalSettings(settings);
|
|
28
37
|
});
|
|
29
|
-
for (const definition of config.actions)
|
|
30
|
-
|
|
31
|
-
streamDeck.actions.registerAction(singletonAction);
|
|
32
|
-
}
|
|
38
|
+
for (const definition of config.actions) registerActionWithAdapter(adapter, definition, registry, config.onActionError);
|
|
39
|
+
if (renderConfig.debug) metrics.enable();
|
|
33
40
|
if (config.devtools) startDevtoolsServer({
|
|
34
|
-
devtoolsName:
|
|
41
|
+
devtoolsName: adapter.pluginUUID,
|
|
35
42
|
registry,
|
|
36
43
|
renderConfig
|
|
37
44
|
});
|
|
38
45
|
return { async connect() {
|
|
39
|
-
|
|
46
|
+
if (renderPool != null) renderPool.initialize().catch(() => {});
|
|
47
|
+
await adapter.connect();
|
|
40
48
|
} };
|
|
41
49
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
function registerActionWithAdapter(adapter, definition, registry, onError) {
|
|
51
|
+
const handleError = (actionId, err) => {
|
|
52
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
53
|
+
console.error(`[@fcannizzaro/streamdeck-react] Error in action ${definition.uuid} (${actionId}):`, error);
|
|
54
|
+
onError?.(definition.uuid, actionId, error);
|
|
55
|
+
};
|
|
56
|
+
adapter.registerAction(definition.uuid, {
|
|
45
57
|
onWillAppear(ev) {
|
|
46
58
|
try {
|
|
47
59
|
const isEncoder = ev.payload.controller === "Encoder";
|
|
48
|
-
if (isEncoder && definition.
|
|
49
|
-
registry.create(ev, definition.
|
|
60
|
+
if (isEncoder && definition.touchStrip) {
|
|
61
|
+
registry.create(ev, definition.touchStrip, definition);
|
|
50
62
|
return;
|
|
51
63
|
}
|
|
52
64
|
const component = isEncoder ? definition.dial ?? definition.key : definition.key;
|
|
53
65
|
if (!component) return;
|
|
54
66
|
registry.create(ev, component, definition);
|
|
55
67
|
} catch (err) {
|
|
56
|
-
|
|
68
|
+
handleError(ev.action.id, err);
|
|
57
69
|
}
|
|
58
|
-
}
|
|
59
|
-
onWillDisappear(
|
|
70
|
+
},
|
|
71
|
+
onWillDisappear(actionId) {
|
|
60
72
|
try {
|
|
61
|
-
registry.destroy(
|
|
73
|
+
registry.destroy(actionId);
|
|
62
74
|
} catch (err) {
|
|
63
|
-
|
|
75
|
+
handleError(actionId, err);
|
|
64
76
|
}
|
|
65
|
-
}
|
|
66
|
-
onKeyDown(
|
|
77
|
+
},
|
|
78
|
+
onKeyDown(actionId, payload) {
|
|
67
79
|
try {
|
|
68
|
-
registry.dispatch(
|
|
69
|
-
settings: ev.payload.settings,
|
|
70
|
-
isInMultiAction: ev.payload.isInMultiAction,
|
|
71
|
-
state: ev.payload.state,
|
|
72
|
-
userDesiredState: "userDesiredState" in ev.payload ? ev.payload.userDesiredState : void 0
|
|
73
|
-
});
|
|
80
|
+
registry.dispatch(actionId, "keyDown", payload);
|
|
74
81
|
} catch (err) {
|
|
75
|
-
|
|
82
|
+
handleError(actionId, err);
|
|
76
83
|
}
|
|
77
|
-
}
|
|
78
|
-
onKeyUp(
|
|
84
|
+
},
|
|
85
|
+
onKeyUp(actionId, payload) {
|
|
79
86
|
try {
|
|
80
|
-
registry.dispatch(
|
|
81
|
-
settings: ev.payload.settings,
|
|
82
|
-
isInMultiAction: ev.payload.isInMultiAction,
|
|
83
|
-
state: ev.payload.state,
|
|
84
|
-
userDesiredState: "userDesiredState" in ev.payload ? ev.payload.userDesiredState : void 0
|
|
85
|
-
});
|
|
87
|
+
registry.dispatch(actionId, "keyUp", payload);
|
|
86
88
|
} catch (err) {
|
|
87
|
-
|
|
89
|
+
handleError(actionId, err);
|
|
88
90
|
}
|
|
89
|
-
}
|
|
90
|
-
onDialRotate(
|
|
91
|
+
},
|
|
92
|
+
onDialRotate(actionId, payload) {
|
|
91
93
|
try {
|
|
92
|
-
registry.dispatch(
|
|
93
|
-
ticks: ev.payload.ticks,
|
|
94
|
-
pressed: ev.payload.pressed,
|
|
95
|
-
settings: ev.payload.settings
|
|
96
|
-
});
|
|
94
|
+
registry.dispatch(actionId, "dialRotate", payload);
|
|
97
95
|
} catch (err) {
|
|
98
|
-
|
|
96
|
+
handleError(actionId, err);
|
|
99
97
|
}
|
|
100
|
-
}
|
|
101
|
-
onDialDown(
|
|
98
|
+
},
|
|
99
|
+
onDialDown(actionId, payload) {
|
|
102
100
|
try {
|
|
103
|
-
registry.dispatch(
|
|
104
|
-
settings: ev.payload.settings,
|
|
105
|
-
controller: "Encoder"
|
|
106
|
-
});
|
|
101
|
+
registry.dispatch(actionId, "dialDown", payload);
|
|
107
102
|
} catch (err) {
|
|
108
|
-
|
|
103
|
+
handleError(actionId, err);
|
|
109
104
|
}
|
|
110
|
-
}
|
|
111
|
-
onDialUp(
|
|
105
|
+
},
|
|
106
|
+
onDialUp(actionId, payload) {
|
|
112
107
|
try {
|
|
113
|
-
registry.dispatch(
|
|
114
|
-
settings: ev.payload.settings,
|
|
115
|
-
controller: "Encoder"
|
|
116
|
-
});
|
|
108
|
+
registry.dispatch(actionId, "dialUp", payload);
|
|
117
109
|
} catch (err) {
|
|
118
|
-
|
|
110
|
+
handleError(actionId, err);
|
|
119
111
|
}
|
|
120
|
-
}
|
|
121
|
-
onTouchTap(
|
|
112
|
+
},
|
|
113
|
+
onTouchTap(actionId, payload) {
|
|
122
114
|
try {
|
|
123
|
-
registry.dispatch(
|
|
124
|
-
tapPos: ev.payload.tapPos,
|
|
125
|
-
hold: ev.payload.hold,
|
|
126
|
-
settings: ev.payload.settings
|
|
127
|
-
});
|
|
115
|
+
registry.dispatch(actionId, "touchTap", payload);
|
|
128
116
|
} catch (err) {
|
|
129
|
-
|
|
117
|
+
handleError(actionId, err);
|
|
130
118
|
}
|
|
131
|
-
}
|
|
132
|
-
onDidReceiveSettings(
|
|
119
|
+
},
|
|
120
|
+
onDidReceiveSettings(actionId, settings) {
|
|
133
121
|
try {
|
|
134
|
-
registry.updateSettings(
|
|
122
|
+
registry.updateSettings(actionId, settings);
|
|
135
123
|
} catch (err) {
|
|
136
|
-
|
|
124
|
+
handleError(actionId, err);
|
|
137
125
|
}
|
|
138
|
-
}
|
|
139
|
-
onSendToPlugin(
|
|
126
|
+
},
|
|
127
|
+
onSendToPlugin(actionId, payload) {
|
|
140
128
|
try {
|
|
141
|
-
registry.dispatch(
|
|
129
|
+
registry.dispatch(actionId, "sendToPlugin", payload);
|
|
142
130
|
} catch (err) {
|
|
143
|
-
|
|
131
|
+
handleError(actionId, err);
|
|
144
132
|
}
|
|
145
|
-
}
|
|
146
|
-
onPropertyInspectorDidAppear(
|
|
133
|
+
},
|
|
134
|
+
onPropertyInspectorDidAppear(actionId) {
|
|
147
135
|
try {
|
|
148
|
-
registry.dispatch(
|
|
136
|
+
registry.dispatch(actionId, "propertyInspectorDidAppear", void 0);
|
|
149
137
|
} catch (err) {
|
|
150
|
-
|
|
138
|
+
handleError(actionId, err);
|
|
151
139
|
}
|
|
152
|
-
}
|
|
153
|
-
onPropertyInspectorDidDisappear(
|
|
140
|
+
},
|
|
141
|
+
onPropertyInspectorDidDisappear(actionId) {
|
|
154
142
|
try {
|
|
155
|
-
registry.dispatch(
|
|
143
|
+
registry.dispatch(actionId, "propertyInspectorDidDisappear", void 0);
|
|
156
144
|
} catch (err) {
|
|
157
|
-
|
|
145
|
+
handleError(actionId, err);
|
|
158
146
|
}
|
|
159
|
-
}
|
|
160
|
-
onTitleParametersDidChange(
|
|
147
|
+
},
|
|
148
|
+
onTitleParametersDidChange(actionId, payload) {
|
|
161
149
|
try {
|
|
162
|
-
registry.dispatch(
|
|
163
|
-
title: ev.payload.title,
|
|
164
|
-
settings: ev.payload.settings
|
|
165
|
-
});
|
|
150
|
+
registry.dispatch(actionId, "titleParametersDidChange", payload);
|
|
166
151
|
} catch (err) {
|
|
167
|
-
|
|
152
|
+
handleError(actionId, err);
|
|
168
153
|
}
|
|
169
154
|
}
|
|
170
|
-
|
|
171
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
172
|
-
console.error(`[@fcannizzaro/streamdeck-react] Error in action ${definition.uuid} (${actionId}):`, error);
|
|
173
|
-
onError?.(definition.uuid, actionId, error);
|
|
174
|
-
}
|
|
175
|
-
}();
|
|
155
|
+
});
|
|
176
156
|
}
|
|
177
157
|
//#endregion
|
|
178
158
|
export { createPlugin };
|
|
@@ -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,
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { ReactElement } from 'react';
|
|
2
1
|
export interface VNode {
|
|
3
2
|
type: string;
|
|
4
3
|
props: Record<string, unknown>;
|
|
5
4
|
children: VNode[];
|
|
6
5
|
text?: string;
|
|
6
|
+
/** @internal Back-pointer to parent VNode or VContainer for dirty propagation. */
|
|
7
|
+
_parent?: VNode | VContainer;
|
|
8
|
+
/** @internal True when this node or a descendant has been mutated since last flush. */
|
|
9
|
+
_dirty?: boolean;
|
|
10
|
+
/** @internal Cached Merkle hash for this subtree. */
|
|
11
|
+
_hash?: number;
|
|
12
|
+
/** @internal True when `_hash` is valid (invalidated on mutation). */
|
|
13
|
+
_hashValid?: boolean;
|
|
14
|
+
/** @internal Cached sorted prop keys for deterministic hashing. Invalidated when props change (commitUpdate). */
|
|
15
|
+
_sortedPropKeys?: string[];
|
|
7
16
|
}
|
|
8
17
|
export interface VContainer {
|
|
9
18
|
children: VNode[];
|
|
@@ -11,8 +20,23 @@ export interface VContainer {
|
|
|
11
20
|
lastSvgHash: number;
|
|
12
21
|
renderCallback: () => void;
|
|
13
22
|
renderTimer: ReturnType<typeof setTimeout> | null;
|
|
23
|
+
/** @internal Consecutive identical render count for duplicate detection. */
|
|
24
|
+
_dupCount: number;
|
|
25
|
+
/** @internal True when any child VNode has been mutated since last flush. */
|
|
26
|
+
_dirty: boolean;
|
|
14
27
|
}
|
|
28
|
+
/** Mark a node (and its ancestors up to the container) as dirty. */
|
|
29
|
+
export declare function markDirty(node: VNode): void;
|
|
30
|
+
/** Mark the container itself as dirty (for structural mutations at container level). */
|
|
31
|
+
export declare function markContainerDirty(container: VContainer): void;
|
|
32
|
+
/** Check if the container has any pending mutations. */
|
|
33
|
+
export declare function isContainerDirty(container: VContainer): boolean;
|
|
34
|
+
/** Clear all dirty flags in the tree after a successful render. */
|
|
35
|
+
export declare function clearDirtyFlags(container: VContainer): void;
|
|
36
|
+
/** Set back-pointer on a child node. */
|
|
37
|
+
export declare function setParent(child: VNode, parent: VNode | VContainer): void;
|
|
38
|
+
/** Clear back-pointer (e.g., when removing from tree). */
|
|
39
|
+
export declare function clearParent(child: VNode): void;
|
|
15
40
|
export declare function createVNode(type: string, props: Record<string, unknown>): VNode;
|
|
16
41
|
export declare function createTextVNode(text: string): VNode;
|
|
17
42
|
export declare function createVContainer(renderCallback: () => void): VContainer;
|
|
18
|
-
export declare function vnodeToElement(node: VNode): ReactElement | string;
|
package/dist/reconciler/vnode.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
|
-
import { createElement } from "react";
|
|
2
1
|
//#region src/reconciler/vnode.ts
|
|
2
|
+
/** Mark a node (and its ancestors up to the container) as dirty. */
|
|
3
|
+
function markDirty(node) {
|
|
4
|
+
let current = node;
|
|
5
|
+
while (current != null) {
|
|
6
|
+
if ("_dirty" in current && current._dirty) break;
|
|
7
|
+
current._dirty = true;
|
|
8
|
+
if ("type" in current) current._hashValid = false;
|
|
9
|
+
current = current._parent;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Mark the container itself as dirty (for structural mutations at container level). */
|
|
13
|
+
function markContainerDirty(container) {
|
|
14
|
+
container._dirty = true;
|
|
15
|
+
}
|
|
16
|
+
/** Check if the container has any pending mutations. */
|
|
17
|
+
function isContainerDirty(container) {
|
|
18
|
+
return container._dirty;
|
|
19
|
+
}
|
|
20
|
+
/** Clear all dirty flags in the tree after a successful render. */
|
|
21
|
+
function clearDirtyFlags(container) {
|
|
22
|
+
container._dirty = false;
|
|
23
|
+
for (const child of container.children) clearNodeDirty(child);
|
|
24
|
+
}
|
|
25
|
+
function clearNodeDirty(node) {
|
|
26
|
+
if (!node._dirty) return;
|
|
27
|
+
node._dirty = false;
|
|
28
|
+
for (const child of node.children) clearNodeDirty(child);
|
|
29
|
+
}
|
|
30
|
+
/** Set back-pointer on a child node. */
|
|
31
|
+
function setParent(child, parent) {
|
|
32
|
+
child._parent = parent;
|
|
33
|
+
}
|
|
34
|
+
/** Clear back-pointer (e.g., when removing from tree). */
|
|
35
|
+
function clearParent(child) {
|
|
36
|
+
child._parent = void 0;
|
|
37
|
+
}
|
|
3
38
|
function createVNode(type, props) {
|
|
4
39
|
return {
|
|
5
40
|
type,
|
|
@@ -21,15 +56,10 @@ function createVContainer(renderCallback) {
|
|
|
21
56
|
scheduledRender: false,
|
|
22
57
|
lastSvgHash: 0,
|
|
23
58
|
renderCallback,
|
|
24
|
-
renderTimer: null
|
|
59
|
+
renderTimer: null,
|
|
60
|
+
_dupCount: 0,
|
|
61
|
+
_dirty: true
|
|
25
62
|
};
|
|
26
63
|
}
|
|
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
64
|
//#endregion
|
|
35
|
-
export { createTextVNode, createVContainer, createVNode,
|
|
65
|
+
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,30 @@
|
|
|
1
|
+
import { VNode, VContainer } from '../reconciler/vnode';
|
|
2
|
+
/**
|
|
3
|
+
* Hash a raw byte buffer (Uint8Array or Buffer) or string.
|
|
4
|
+
*
|
|
5
|
+
* For buffers larger than {@link STRIDE_THRESHOLD} bytes, uses strided
|
|
6
|
+
* FNV-1a sampling (every 16th byte) — at 30fps this adds <1ms even
|
|
7
|
+
* for 320KB TouchStrip buffers.
|
|
8
|
+
*
|
|
9
|
+
* Strings and small buffers always use full byte-by-byte FNV-1a.
|
|
10
|
+
*/
|
|
1
11
|
export declare function fnv1a(input: string | Uint8Array | Buffer): number;
|
|
12
|
+
/** Feed a string into a running FNV-1a hash. */
|
|
13
|
+
export declare function fnv1aString(str: string, hash: number): number;
|
|
14
|
+
/** Feed a uint32 value into a running FNV-1a hash (4 bytes, big-endian). */
|
|
15
|
+
export declare function fnv1aU32(value: number, hash: number): number;
|
|
16
|
+
/** Hash an arbitrary JS value into a running FNV-1a hash. */
|
|
17
|
+
export declare function hashValue(value: unknown, hash: number, depth?: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Compute (or return cached) Merkle hash for a single VNode.
|
|
20
|
+
* The hash incorporates: type, text, props (sorted, functions skipped),
|
|
21
|
+
* children count, and children hashes (recursive).
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeHash(node: VNode): number;
|
|
24
|
+
/**
|
|
25
|
+
* Compute the Merkle hash for an entire VContainer tree.
|
|
26
|
+
* Returns 0 for empty containers.
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeTreeHash(container: VContainer): number;
|
|
29
|
+
export declare function computeCacheKey(treeHash: number, width: number, height: number, dpr: number, format: string): number;
|
|
30
|
+
export declare function computeTouchStripSegmentCacheKey(treeHash: number, width: number, height: number, dpr: number, columns: number[]): number;
|