@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.
Files changed (76) 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 +1 -2
  5. package/dist/adapter/index.d.ts +2 -0
  6. package/dist/adapter/physical-device.d.ts +2 -0
  7. package/dist/adapter/physical-device.js +153 -0
  8. package/dist/adapter/types.d.ts +127 -0
  9. package/dist/bundler-shared.d.ts +11 -0
  10. package/dist/bundler-shared.js +11 -0
  11. package/dist/context/event-bus.d.ts +1 -1
  12. package/dist/context/event-bus.js +1 -1
  13. package/dist/context/touchstrip-context.d.ts +2 -0
  14. package/dist/context/touchstrip-context.js +5 -0
  15. package/dist/devtools/bridge.d.ts +35 -7
  16. package/dist/devtools/bridge.js +152 -46
  17. package/dist/devtools/highlight.d.ts +5 -0
  18. package/dist/devtools/highlight.js +107 -57
  19. package/dist/devtools/index.js +6 -0
  20. package/dist/devtools/observers/lifecycle.d.ts +4 -4
  21. package/dist/devtools/server.d.ts +6 -1
  22. package/dist/devtools/server.js +6 -1
  23. package/dist/devtools/types.d.ts +50 -6
  24. package/dist/font-inline.d.ts +5 -1
  25. package/dist/font-inline.js +8 -3
  26. package/dist/hooks/animation.d.ts +154 -0
  27. package/dist/hooks/animation.js +381 -0
  28. package/dist/hooks/events.js +2 -6
  29. package/dist/hooks/sdk.js +11 -11
  30. package/dist/hooks/touchstrip.d.ts +6 -0
  31. package/dist/hooks/touchstrip.js +37 -0
  32. package/dist/hooks/utility.js +3 -2
  33. package/dist/index.d.ts +9 -2
  34. package/dist/index.js +4 -2
  35. package/dist/manifest-codegen.d.ts +38 -0
  36. package/dist/manifest-codegen.js +110 -0
  37. package/dist/plugin.js +86 -106
  38. package/dist/reconciler/host-config.js +19 -1
  39. package/dist/reconciler/vnode.d.ts +26 -2
  40. package/dist/reconciler/vnode.js +40 -10
  41. package/dist/render/buffer-pool.d.ts +19 -0
  42. package/dist/render/buffer-pool.js +51 -0
  43. package/dist/render/cache.d.ts +29 -0
  44. package/dist/render/cache.js +137 -5
  45. package/dist/render/image-cache.d.ts +54 -0
  46. package/dist/render/image-cache.js +144 -0
  47. package/dist/render/metrics.d.ts +57 -0
  48. package/dist/render/metrics.js +98 -0
  49. package/dist/render/pipeline.d.ts +36 -1
  50. package/dist/render/pipeline.js +304 -34
  51. package/dist/render/png.d.ts +1 -1
  52. package/dist/render/png.js +26 -11
  53. package/dist/render/render-pool.d.ts +24 -0
  54. package/dist/render/render-pool.js +130 -0
  55. package/dist/render/svg.d.ts +7 -0
  56. package/dist/render/svg.js +139 -0
  57. package/dist/render/worker.d.ts +1 -0
  58. package/dist/rollup.d.ts +23 -9
  59. package/dist/rollup.js +24 -9
  60. package/dist/roots/registry.d.ts +9 -11
  61. package/dist/roots/registry.js +39 -42
  62. package/dist/roots/root.d.ts +9 -6
  63. package/dist/roots/root.js +52 -29
  64. package/dist/roots/settings-equality.d.ts +5 -0
  65. package/dist/roots/settings-equality.js +24 -0
  66. package/dist/roots/{touchbar-root.d.ts → touchstrip-root.d.ts} +30 -8
  67. package/dist/roots/touchstrip-root.js +263 -0
  68. package/dist/types.d.ts +73 -23
  69. package/dist/vite.d.ts +22 -8
  70. package/dist/vite.js +24 -8
  71. package/package.json +7 -4
  72. package/dist/context/touchbar-context.d.ts +0 -2
  73. package/dist/context/touchbar-context.js +0 -5
  74. package/dist/hooks/touchbar.d.ts +0 -6
  75. package/dist/hooks/touchbar.js +0 -37
  76. 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 streamDeck, { SingletonAction } from "@elgato/streamdeck";
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: new Renderer({ fonts: config.fonts.map((f) => ({
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, config.renderDebounceMs ?? 16, streamDeck, async (settings) => {
19
- await streamDeck.settings.setGlobalSettings(settings);
27
+ const registry = new RootRegistry(renderConfig, adapter, async (settings) => {
28
+ await adapter.setGlobalSettings(settings);
20
29
  }, config.wrapper);
21
- streamDeck.settings.getGlobalSettings().then((gs) => {
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
- streamDeck.settings.onDidReceiveGlobalSettings((ev) => {
27
- registry.setGlobalSettings(ev.settings);
35
+ adapter.onGlobalSettingsChanged((settings) => {
36
+ registry.setGlobalSettings(settings);
28
37
  });
29
- for (const definition of config.actions) {
30
- const singletonAction = createSingletonAction(definition, registry, config.onActionError);
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: streamDeck.info.plugin.uuid,
41
+ devtoolsName: adapter.pluginUUID,
35
42
  registry,
36
43
  renderConfig
37
44
  });
38
45
  return { async connect() {
39
- await streamDeck.connect();
46
+ if (renderPool != null) renderPool.initialize().catch(() => {});
47
+ await adapter.connect();
40
48
  } };
41
49
  }
42
- function createSingletonAction(definition, registry, onError) {
43
- return new class extends SingletonAction {
44
- manifestId = definition.uuid;
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.touchBar) {
49
- registry.create(ev, definition.touchBar, 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
- this.handleError(ev.action.id, err);
68
+ handleError(ev.action.id, err);
57
69
  }
58
- }
59
- onWillDisappear(ev) {
70
+ },
71
+ onWillDisappear(actionId) {
60
72
  try {
61
- registry.destroy(ev.action.id);
73
+ registry.destroy(actionId);
62
74
  } catch (err) {
63
- this.handleError(ev.action.id, err);
75
+ handleError(actionId, err);
64
76
  }
65
- }
66
- onKeyDown(ev) {
77
+ },
78
+ onKeyDown(actionId, payload) {
67
79
  try {
68
- registry.dispatch(ev.action.id, "keyDown", {
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
- this.handleError(ev.action.id, err);
82
+ handleError(actionId, err);
76
83
  }
77
- }
78
- onKeyUp(ev) {
84
+ },
85
+ onKeyUp(actionId, payload) {
79
86
  try {
80
- registry.dispatch(ev.action.id, "keyUp", {
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
- this.handleError(ev.action.id, err);
89
+ handleError(actionId, err);
88
90
  }
89
- }
90
- onDialRotate(ev) {
91
+ },
92
+ onDialRotate(actionId, payload) {
91
93
  try {
92
- registry.dispatch(ev.action.id, "dialRotate", {
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
- this.handleError(ev.action.id, err);
96
+ handleError(actionId, err);
99
97
  }
100
- }
101
- onDialDown(ev) {
98
+ },
99
+ onDialDown(actionId, payload) {
102
100
  try {
103
- registry.dispatch(ev.action.id, "dialDown", {
104
- settings: ev.payload.settings,
105
- controller: "Encoder"
106
- });
101
+ registry.dispatch(actionId, "dialDown", payload);
107
102
  } catch (err) {
108
- this.handleError(ev.action.id, err);
103
+ handleError(actionId, err);
109
104
  }
110
- }
111
- onDialUp(ev) {
105
+ },
106
+ onDialUp(actionId, payload) {
112
107
  try {
113
- registry.dispatch(ev.action.id, "dialUp", {
114
- settings: ev.payload.settings,
115
- controller: "Encoder"
116
- });
108
+ registry.dispatch(actionId, "dialUp", payload);
117
109
  } catch (err) {
118
- this.handleError(ev.action.id, err);
110
+ handleError(actionId, err);
119
111
  }
120
- }
121
- onTouchTap(ev) {
112
+ },
113
+ onTouchTap(actionId, payload) {
122
114
  try {
123
- registry.dispatch(ev.action.id, "touchTap", {
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
- this.handleError(ev.action.id, err);
117
+ handleError(actionId, err);
130
118
  }
131
- }
132
- onDidReceiveSettings(ev) {
119
+ },
120
+ onDidReceiveSettings(actionId, settings) {
133
121
  try {
134
- registry.updateSettings(ev.action.id, ev.payload.settings);
122
+ registry.updateSettings(actionId, settings);
135
123
  } catch (err) {
136
- this.handleError(ev.action.id, err);
124
+ handleError(actionId, err);
137
125
  }
138
- }
139
- onSendToPlugin(ev) {
126
+ },
127
+ onSendToPlugin(actionId, payload) {
140
128
  try {
141
- registry.dispatch(ev.action.id, "sendToPlugin", ev.payload);
129
+ registry.dispatch(actionId, "sendToPlugin", payload);
142
130
  } catch (err) {
143
- this.handleError(ev.action.id, err);
131
+ handleError(actionId, err);
144
132
  }
145
- }
146
- onPropertyInspectorDidAppear(ev) {
133
+ },
134
+ onPropertyInspectorDidAppear(actionId) {
147
135
  try {
148
- registry.dispatch(ev.action.id, "propertyInspectorDidAppear", void 0);
136
+ registry.dispatch(actionId, "propertyInspectorDidAppear", void 0);
149
137
  } catch (err) {
150
- this.handleError(ev.action.id, err);
138
+ handleError(actionId, err);
151
139
  }
152
- }
153
- onPropertyInspectorDidDisappear(ev) {
140
+ },
141
+ onPropertyInspectorDidDisappear(actionId) {
154
142
  try {
155
- registry.dispatch(ev.action.id, "propertyInspectorDidDisappear", void 0);
143
+ registry.dispatch(actionId, "propertyInspectorDidDisappear", void 0);
156
144
  } catch (err) {
157
- this.handleError(ev.action.id, err);
145
+ handleError(actionId, err);
158
146
  }
159
- }
160
- onTitleParametersDidChange(ev) {
147
+ },
148
+ onTitleParametersDidChange(actionId, payload) {
161
149
  try {
162
- registry.dispatch(ev.action.id, "titleParametersDidChange", {
163
- title: ev.payload.title,
164
- settings: ev.payload.settings
165
- });
150
+ registry.dispatch(actionId, "titleParametersDidChange", payload);
166
151
  } catch (err) {
167
- this.handleError(ev.action.id, err);
152
+ handleError(actionId, err);
168
153
  }
169
154
  }
170
- handleError(actionId, err) {
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;
@@ -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, vnodeToElement };
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 };
@@ -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;