@zaflun/lumio-sdk 0.6.6 → 0.11.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@zaflun/lumio-sdk` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## 0.11.1
9
+
10
+ ### Changed
11
+
12
+ - The package is built with tsup 9 (esbuild 0.28). The bundled Worker runtime
13
+ (`lumio-runtime.global.js`) differs only in generated helper code, and the
14
+ type declarations are unchanged for consumers.
15
+
16
+ ## 0.11.0
17
+
18
+ ### Added
19
+
20
+ - The `IconPicker` Lucide tab offers 2,296 icons (Lucide 1.67, before: 2,022).
21
+ No icon name was removed or renamed, so stored icon names keep working.
22
+
23
+ ### Changed
24
+
25
+ - **Breaking: React 19.3 or newer is required.** The peer dependencies are now
26
+ `react ^19.3.0` and `react-dom ^19.3.0` (before: `^18.0.0 || ^19.0.0`). The
27
+ renderer runs on `react-reconciler` 0.34, which requires React 19.3. To
28
+ upgrade, raise `react` (and `@types/react`) in your extension's
29
+ `package.json` to `^19.3.0` and reinstall. Code written against React 18 or
30
+ 19 needs no other change. The Worker runtime (`@zaflun/lumio-sdk/runtime`)
31
+ bundles its own React, so deployed extensions are not affected.
32
+ - The renderer reports itself to React DevTools as `@zaflun/lumio-sdk`.
33
+
34
+ ### Fixed
35
+
36
+ - State updates wrapped in `startTransition` (or `useTransition`) commit
37
+ instead of failing in the renderer.
38
+ - Updates scheduled outside React run at React's default priority.
package/README.md CHANGED
@@ -44,14 +44,14 @@ Lumio.render(<MyWidget />, { target: "editor" });
44
44
 
45
45
  | Hook | Purpose |
46
46
  |------|---------|
47
- | `useExtensionStorage()` | Persistent key-value storage synced across surfaces |
47
+ | `useExtensionStorage(key, initialValue)` | Persistent key-value storage, one key per call, synced across surfaces |
48
48
  | `useVisionState()` | Per-key state with real-time sync |
49
49
  | `useExtensionContext()` | Extension metadata (ID, target, overlay) |
50
50
  | `useLumioEvent()` | Subscribe to platform events (49 event types) |
51
51
  | `useLumioConfig()` | Read/write extension config |
52
52
  | `useLumioTheme()` | Active overlay/widget theme (colors, fonts) |
53
53
  | `useLumioIdentity()` | Current user/account context |
54
- | `useLumioAction()` | Trigger platform actions (21 action types) |
54
+ | `useLumioAction()` | Run a Lumio action (`events:emit`, `overlay:update_layer`, …) through the host |
55
55
  | `useQuery()` | Call server query functions |
56
56
  | `useMutation()` | Call server mutation functions |
57
57
 
@@ -94,16 +94,17 @@ const scoreFont = localFont({
94
94
  });
95
95
  ```
96
96
 
97
- ## Three Surfaces
97
+ ## Surfaces
98
98
 
99
99
  | Surface | File | Where it renders |
100
100
  |---------|------|-----------------|
101
101
  | `editor` | `src/editor.tsx` | Dashboard sidebar panel |
102
102
  | `layer` | `src/layer.tsx` | OBS overlay (transparent) |
103
- | `interactive` | `src/interactive.tsx` | Audience page |
104
103
 
105
104
  All surfaces share storage — changes in the editor propagate to the overlay in real time.
106
105
 
106
+ Viewers interact through chat commands and channel point rewards (`twitch:reward`, `kick:reward`), handled by bot-module handlers that write `ctx.db` and call `ctx.realtime.push(topic, payload)`. The layer and editor pick that up with `useRealtime(topic, cb)`.
107
+
107
108
  ## Security
108
109
 
109
110
  Extensions run in a sandboxed Web Worker with zero DOM access. A custom React reconciler serializes the component tree to JSON, which the host renders. Extension code cannot access cookies, JWT tokens, localStorage, or any host resources.
@@ -0,0 +1,196 @@
1
+ // src/bridge/WorkerBridge.ts
2
+ var WorkerBridge = class {
3
+ notificationHandlers = /* @__PURE__ */ new Map();
4
+ requestHandlers = /* @__PURE__ */ new Map();
5
+ pendingRequests = /* @__PURE__ */ new Map();
6
+ requestCounter = 0;
7
+ requestTimeout;
8
+ messageListener = null;
9
+ destroyed = false;
10
+ constructor(options) {
11
+ this.requestTimeout = options?.requestTimeout ?? 3e4;
12
+ this.messageListener = this.handleMessage.bind(this);
13
+ globalThis.addEventListener("message", this.messageListener);
14
+ }
15
+ notify(method, params) {
16
+ const msg = {
17
+ jsonrpc: "2.0",
18
+ method,
19
+ ...params !== void 0 ? { params } : {}
20
+ };
21
+ globalThis.postMessage(msg);
22
+ }
23
+ request(method, params) {
24
+ const id = `req_${++this.requestCounter}`;
25
+ const msg = {
26
+ jsonrpc: "2.0",
27
+ id,
28
+ method,
29
+ ...params !== void 0 ? { params } : {}
30
+ };
31
+ return new Promise((resolve, reject) => {
32
+ const timer = setTimeout(() => {
33
+ this.pendingRequests.delete(id);
34
+ reject(new Error("Request timeout"));
35
+ }, this.requestTimeout);
36
+ this.pendingRequests.set(id, { resolve, reject, timer });
37
+ globalThis.postMessage(msg);
38
+ });
39
+ }
40
+ onNotification(method, handler) {
41
+ if (!this.notificationHandlers.has(method)) {
42
+ this.notificationHandlers.set(method, /* @__PURE__ */ new Set());
43
+ }
44
+ this.notificationHandlers.get(method).add(handler);
45
+ return () => {
46
+ this.notificationHandlers.get(method)?.delete(handler);
47
+ };
48
+ }
49
+ onRequest(method, handler) {
50
+ this.requestHandlers.set(method, handler);
51
+ }
52
+ destroy() {
53
+ this.destroyed = true;
54
+ if (this.messageListener) {
55
+ globalThis.removeEventListener("message", this.messageListener);
56
+ this.messageListener = null;
57
+ }
58
+ for (const { timer, reject } of this.pendingRequests.values()) {
59
+ if (timer) clearTimeout(timer);
60
+ reject(new Error("Bridge destroyed"));
61
+ }
62
+ this.pendingRequests.clear();
63
+ this.notificationHandlers.clear();
64
+ this.requestHandlers.clear();
65
+ }
66
+ handleMessage(event) {
67
+ if (this.destroyed) return;
68
+ const data = event.data;
69
+ if (!data || typeof data !== "object" || data.jsonrpc !== "2.0") return;
70
+ if ("id" in data && !("method" in data)) {
71
+ const pending = this.pendingRequests.get(data.id);
72
+ if (pending) {
73
+ this.pendingRequests.delete(data.id);
74
+ if (pending.timer) clearTimeout(pending.timer);
75
+ if (data.error) {
76
+ pending.reject(new Error(data.error.message));
77
+ } else {
78
+ pending.resolve(data.result);
79
+ }
80
+ }
81
+ return;
82
+ }
83
+ if ("id" in data && "method" in data) {
84
+ const handler = this.requestHandlers.get(data.method);
85
+ if (handler) {
86
+ handler(data.params).then((result) => {
87
+ const response = { jsonrpc: "2.0", id: data.id, result };
88
+ globalThis.postMessage(response);
89
+ }).catch((err) => {
90
+ const response = {
91
+ jsonrpc: "2.0",
92
+ id: data.id,
93
+ error: { code: -32603, message: err.message }
94
+ };
95
+ globalThis.postMessage(response);
96
+ });
97
+ }
98
+ return;
99
+ }
100
+ if ("method" in data && !("id" in data)) {
101
+ const handlers = this.notificationHandlers.get(data.method);
102
+ if (handlers) {
103
+ for (const handler of handlers) {
104
+ handler(data.params);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ };
110
+
111
+ // src/renderer/callback-registry.ts
112
+ var CallbackRegistry = class {
113
+ handlers = /* @__PURE__ */ new Map();
114
+ counter = 0;
115
+ register(fn) {
116
+ const id = `cb_${++this.counter}`;
117
+ this.handlers.set(id, fn);
118
+ return id;
119
+ }
120
+ invoke(id, args) {
121
+ const handler = this.handlers.get(id);
122
+ if (!handler) {
123
+ throw new Error(`CallbackRegistry: no handler for ID "${id}"`);
124
+ }
125
+ return handler(...args);
126
+ }
127
+ update(id, fn) {
128
+ if (!this.handlers.has(id)) {
129
+ throw new Error(`CallbackRegistry: cannot update non-existent handler "${id}"`);
130
+ }
131
+ this.handlers.set(id, fn);
132
+ }
133
+ remove(id) {
134
+ this.handlers.delete(id);
135
+ }
136
+ has(id) {
137
+ return this.handlers.has(id);
138
+ }
139
+ clear() {
140
+ this.handlers.clear();
141
+ }
142
+ get size() {
143
+ return this.handlers.size;
144
+ }
145
+ };
146
+
147
+ // src/renderer/bridge-context.ts
148
+ var bridgeInstance = null;
149
+ var registryInstance = null;
150
+ var bridgeFactory = null;
151
+ var initParams = null;
152
+ function getBridge() {
153
+ if (!bridgeInstance) {
154
+ bridgeInstance = bridgeFactory ? bridgeFactory() : new WorkerBridge();
155
+ }
156
+ return bridgeInstance;
157
+ }
158
+ function getRegistry() {
159
+ if (!registryInstance) {
160
+ registryInstance = new CallbackRegistry();
161
+ }
162
+ return registryInstance;
163
+ }
164
+ function setInitParams(params) {
165
+ const rest = { ...params };
166
+ delete rest.bundleCode;
167
+ delete rest.bundleUrl;
168
+ initParams = rest;
169
+ }
170
+ function getInitParams() {
171
+ return initParams;
172
+ }
173
+ function setBridgeFactory(factory) {
174
+ bridgeFactory = factory;
175
+ }
176
+ function destroyBridgeContext() {
177
+ if (bridgeInstance) {
178
+ bridgeInstance.destroy();
179
+ bridgeInstance = null;
180
+ }
181
+ if (registryInstance) {
182
+ registryInstance.clear();
183
+ registryInstance = null;
184
+ }
185
+ initParams = null;
186
+ }
187
+
188
+ export {
189
+ WorkerBridge,
190
+ getBridge,
191
+ getRegistry,
192
+ setInitParams,
193
+ getInitParams,
194
+ setBridgeFactory,
195
+ destroyBridgeContext
196
+ };
package/dist/host.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { LifecycleInitParams } from '@zaflun/lumio-protocol';
2
+ import { ReactElement } from 'react';
3
+
4
+ type AnyFunction = (...args: unknown[]) => unknown;
5
+ declare class CallbackRegistry {
6
+ private handlers;
7
+ private counter;
8
+ register(fn: AnyFunction): string;
9
+ invoke(id: string, args: unknown[]): unknown;
10
+ update(id: string, fn: AnyFunction): void;
11
+ remove(id: string): void;
12
+ has(id: string): boolean;
13
+ clear(): void;
14
+ get size(): number;
15
+ }
16
+
17
+ /**
18
+ * `lifecycle.init` as the host sent it, minus the bundle. Hosts may add fields
19
+ * beyond the protocol type (`settings`, `permissions`, …).
20
+ */
21
+ type InitParams = Partial<LifecycleInitParams> & Record<string, unknown>;
22
+ /**
23
+ * The JSON-RPC transport every hook talks to. In production it is the
24
+ * `WorkerBridge` (postMessage inside the extension Worker); the devkit's mock
25
+ * host plugs in an in-process transport through {@link setBridgeFactory}.
26
+ */
27
+ interface LumioBridge {
28
+ notify(method: string, params?: unknown): void;
29
+ request(method: string, params?: unknown): Promise<unknown>;
30
+ onNotification(method: string, handler: (params: unknown) => void): () => void;
31
+ onRequest(method: string, handler: (params: unknown) => Promise<unknown>): void;
32
+ destroy(): void;
33
+ }
34
+ declare function getBridge(): LumioBridge;
35
+ declare function getRegistry(): CallbackRegistry;
36
+ /**
37
+ * Records the `lifecycle.init` params. The worker entry calls this before it
38
+ * evaluates the bundle: hooks subscribe in an effect, after the first render,
39
+ * so they would miss the one-shot notification and read it from here instead.
40
+ */
41
+ declare function setInitParams(params: Partial<LifecycleInitParams>): void;
42
+ /**
43
+ * Replaces the transport `getBridge()` creates; `null` restores the default
44
+ * `WorkerBridge`. Takes effect for the next bridge, so call it before
45
+ * anything renders or after {@link destroyBridgeContext}.
46
+ */
47
+ declare function setBridgeFactory(factory: (() => LumioBridge) | null): void;
48
+ declare function destroyBridgeContext(): void;
49
+
50
+ interface MountRootOptions {
51
+ /**
52
+ * Commit updates from outside React in a microtask (see
53
+ * `HostConfigOptions.syncUpdates`). Used by the devkit's mock host.
54
+ */
55
+ syncUpdates?: boolean;
56
+ }
57
+ /** A rendered extension root on the current bridge. */
58
+ interface MountedRoot {
59
+ /** Runs `fn` and commits the updates it schedules before returning. */
60
+ flushSync(fn: () => void): void;
61
+ /** Unmounts the tree and stops listening for `event.callback`. */
62
+ unmount(): void;
63
+ }
64
+ /**
65
+ * Renders `element` with the Lumio reconciler: every commit sends the
66
+ * serialized tree as `ui.render`, the first one is followed by `ui.ready`, and
67
+ * `event.callback` invokes the registered handler. `Lumio.render()` in the
68
+ * Worker runtime calls it once; the devkit mounts a fresh root per test.
69
+ */
70
+ declare function mountRoot(element: ReactElement, options?: MountRootOptions): MountedRoot;
71
+
72
+ export { destroyBridgeContext, getBridge, getRegistry, mountRoot, setBridgeFactory, setInitParams };
73
+ export type { InitParams, LumioBridge, MountRootOptions, MountedRoot };
package/dist/host.js ADDED
@@ -0,0 +1,368 @@
1
+ import {
2
+ destroyBridgeContext,
3
+ getBridge,
4
+ getRegistry,
5
+ setBridgeFactory,
6
+ setInitParams
7
+ } from "./chunk-GVAYWB5X.js";
8
+
9
+ // src/renderer/render.ts
10
+ import { createElement } from "react";
11
+ import Reconciler from "react-reconciler";
12
+ import { HOST_METHODS } from "@zaflun/lumio-protocol";
13
+
14
+ // src/renderer/host-config.ts
15
+ import { createContext } from "react";
16
+
17
+ // src/renderer/reconciler.ts
18
+ var instanceCounter = 0;
19
+ var STRIP_PROPS = /* @__PURE__ */ new Set(["children", "key", "ref", "__self", "__source"]);
20
+ function sanitizeProps(rawProps, registry, existingHandlerIds) {
21
+ const sanitized = {};
22
+ for (const [key, value] of Object.entries(rawProps)) {
23
+ if (STRIP_PROPS.has(key)) continue;
24
+ if (typeof value === "symbol") continue;
25
+ if (typeof value === "function") {
26
+ const handlerKey = `__handler_${key}`;
27
+ const existingId = existingHandlerIds?.[key];
28
+ if (existingId && registry.has(existingId)) {
29
+ registry.update(existingId, value);
30
+ sanitized[handlerKey] = existingId;
31
+ } else {
32
+ const id = registry.register(value);
33
+ sanitized[handlerKey] = id;
34
+ }
35
+ continue;
36
+ }
37
+ sanitized[key] = value;
38
+ }
39
+ return sanitized;
40
+ }
41
+ function createLumioInstance(type, rawProps, registry) {
42
+ const sanitized = sanitizeProps(rawProps, registry);
43
+ const handlerIds = {};
44
+ for (const [key, value] of Object.entries(sanitized)) {
45
+ if (key.startsWith("__handler_")) {
46
+ const propName = key.slice("__handler_".length);
47
+ handlerIds[propName] = value;
48
+ }
49
+ }
50
+ return {
51
+ _id: `lumio_${++instanceCounter}`,
52
+ type,
53
+ props: sanitized,
54
+ children: [],
55
+ parent: null,
56
+ handlerIds
57
+ };
58
+ }
59
+ function serializeTree(instances) {
60
+ return instances.map((instance) => ({
61
+ _id: instance._id,
62
+ type: instance.type,
63
+ props: { ...instance.props },
64
+ children: serializeTree(instance.children)
65
+ }));
66
+ }
67
+ function treeFingerprint(nodes) {
68
+ return JSON.stringify(nodes, (key, value) => {
69
+ if (typeof key === "string" && key.startsWith("__handler_")) return void 0;
70
+ return value;
71
+ });
72
+ }
73
+
74
+ // src/renderer/host-config.ts
75
+ var NoEventPriority = 0;
76
+ var DiscreteEventPriority = 2;
77
+ var DefaultEventPriority = 32;
78
+ var LegacyRoot = 0;
79
+ var RENDERER_PACKAGE_NAME = "@zaflun/lumio-sdk";
80
+ var RENDERER_VERSION = "0.11.1";
81
+ var HostTransitionContext = createContext(null);
82
+ function createHostConfig(registry, options = {}) {
83
+ const syncUpdates = options.syncUpdates === true;
84
+ let currentUpdatePriority = NoEventPriority;
85
+ return {
86
+ rendererVersion: RENDERER_VERSION,
87
+ rendererPackageName: RENDERER_PACKAGE_NAME,
88
+ extraDevToolsConfig: null,
89
+ // Replays Server Component console logs; Lumio has no Server Components.
90
+ bindToConsole() {
91
+ return () => {
92
+ };
93
+ },
94
+ supportsMutation: true,
95
+ supportsPersistence: false,
96
+ supportsHydration: false,
97
+ isPrimaryRenderer: true,
98
+ warnsIfNotActing: false,
99
+ scheduleTimeout: setTimeout,
100
+ cancelTimeout: clearTimeout,
101
+ noTimeout: -1,
102
+ supportsMicrotasks: true,
103
+ scheduleMicrotask: queueMicrotask,
104
+ getRootHostContext() {
105
+ return {};
106
+ },
107
+ getChildHostContext(_parentCtx) {
108
+ return {};
109
+ },
110
+ getPublicInstance(instance) {
111
+ return instance;
112
+ },
113
+ createInstance(type, props) {
114
+ return createLumioInstance(type, props, registry);
115
+ },
116
+ createTextInstance(text) {
117
+ return createLumioInstance("Text", { content: text }, registry);
118
+ },
119
+ appendInitialChild(parent, child) {
120
+ child.parent = parent;
121
+ parent.children.push(child);
122
+ },
123
+ appendChild(parent, child) {
124
+ child.parent = parent;
125
+ parent.children.push(child);
126
+ },
127
+ appendChildToContainer(container, child) {
128
+ child.parent = null;
129
+ container.children.push(child);
130
+ },
131
+ removeChild(parent, child) {
132
+ const idx = parent.children.indexOf(child);
133
+ if (idx !== -1) {
134
+ parent.children.splice(idx, 1);
135
+ }
136
+ child.parent = null;
137
+ removeHandlersRecursive(child, registry);
138
+ },
139
+ removeChildFromContainer(container, child) {
140
+ const idx = container.children.indexOf(child);
141
+ if (idx !== -1) {
142
+ container.children.splice(idx, 1);
143
+ }
144
+ child.parent = null;
145
+ removeHandlersRecursive(child, registry);
146
+ },
147
+ insertBefore(parent, child, before) {
148
+ const idx = parent.children.indexOf(before);
149
+ if (idx !== -1) {
150
+ parent.children.splice(idx, 0, child);
151
+ } else {
152
+ parent.children.push(child);
153
+ }
154
+ child.parent = parent;
155
+ },
156
+ insertInContainerBefore(container, child, before) {
157
+ const idx = container.children.indexOf(before);
158
+ if (idx !== -1) {
159
+ container.children.splice(idx, 0, child);
160
+ } else {
161
+ container.children.push(child);
162
+ }
163
+ child.parent = null;
164
+ },
165
+ commitUpdate(instance, _type, _prevProps, nextProps) {
166
+ const sanitized = sanitizeProps(
167
+ nextProps,
168
+ registry,
169
+ instance.handlerIds
170
+ );
171
+ for (const [key, value] of Object.entries(sanitized)) {
172
+ if (key.startsWith("__handler_")) {
173
+ const propName = key.slice("__handler_".length);
174
+ instance.handlerIds[propName] = value;
175
+ }
176
+ }
177
+ instance.props = sanitized;
178
+ },
179
+ commitTextUpdate(instance, _oldText, newText) {
180
+ instance.props.content = newText;
181
+ },
182
+ finalizeInitialChildren() {
183
+ return false;
184
+ },
185
+ shouldSetTextContent() {
186
+ return false;
187
+ },
188
+ prepareForCommit() {
189
+ return null;
190
+ },
191
+ resetAfterCommit(container) {
192
+ container.onCommit();
193
+ },
194
+ clearContainer(container) {
195
+ container.children.length = 0;
196
+ },
197
+ preparePortalMount() {
198
+ },
199
+ detachDeletedInstance() {
200
+ },
201
+ getInstanceFromNode() {
202
+ return null;
203
+ },
204
+ beforeActiveInstanceBlur() {
205
+ },
206
+ afterActiveInstanceBlur() {
207
+ },
208
+ prepareScopeUpdate() {
209
+ },
210
+ getInstanceFromScope() {
211
+ return null;
212
+ },
213
+ // Update priorities. The Worker runtime resolves every update to the
214
+ // default priority; `syncUpdates` tracks React's priority and resolves
215
+ // updates from outside React to the discrete (sync) one.
216
+ setCurrentUpdatePriority(priority) {
217
+ if (syncUpdates) currentUpdatePriority = priority;
218
+ },
219
+ getCurrentUpdatePriority() {
220
+ return syncUpdates ? currentUpdatePriority : DefaultEventPriority;
221
+ },
222
+ resolveUpdatePriority() {
223
+ if (!syncUpdates) return DefaultEventPriority;
224
+ return currentUpdatePriority !== NoEventPriority ? currentUpdatePriority : DiscreteEventPriority;
225
+ },
226
+ // Form actions (not supported — the context only ever holds `null`)
227
+ NotPendingTransition: null,
228
+ HostTransitionContext,
229
+ // Form instance
230
+ resetFormInstance() {
231
+ },
232
+ // Post paint callback
233
+ requestPostPaintCallback() {
234
+ },
235
+ // Eager transition
236
+ shouldAttemptEagerTransition() {
237
+ return false;
238
+ },
239
+ // Suspensey commits. Lumio instances never suspend a commit, but React
240
+ // still calls `startSuspendingCommit` → `suspendOnActiveViewTransition` →
241
+ // `waitForCommitToBeReady` for every transition-lane commit.
242
+ maySuspendCommit() {
243
+ return false;
244
+ },
245
+ maySuspendCommitOnUpdate() {
246
+ return false;
247
+ },
248
+ maySuspendCommitInSyncRender() {
249
+ return false;
250
+ },
251
+ preloadInstance() {
252
+ return true;
253
+ },
254
+ startSuspendingCommit() {
255
+ return null;
256
+ },
257
+ suspendInstance() {
258
+ },
259
+ suspendOnActiveViewTransition() {
260
+ },
261
+ waitForCommitToBeReady() {
262
+ return null;
263
+ },
264
+ getSuspendedCommitReason() {
265
+ return null;
266
+ },
267
+ // Scheduler event tracking
268
+ trackSchedulerEvent() {
269
+ },
270
+ // Event resolution
271
+ resolveEventType() {
272
+ return null;
273
+ },
274
+ resolveEventTimeStamp() {
275
+ return Date.now();
276
+ }
277
+ };
278
+ }
279
+ function removeHandlersRecursive(instance, registry) {
280
+ for (const handlerId of Object.values(instance.handlerIds)) {
281
+ registry.remove(handlerId);
282
+ }
283
+ for (const child of instance.children) {
284
+ removeHandlersRecursive(child, registry);
285
+ }
286
+ }
287
+
288
+ // src/renderer/render.ts
289
+ function mountRoot(element, options = {}) {
290
+ const registry = getRegistry();
291
+ const bridge = getBridge();
292
+ const hostConfig = createHostConfig(registry, { syncUpdates: options.syncUpdates });
293
+ const reconciler = Reconciler(hostConfig);
294
+ let lastFingerprint = "";
295
+ const container = {
296
+ children: [],
297
+ onCommit() {
298
+ const tree = serializeTree(container.children);
299
+ const fingerprint = treeFingerprint(tree);
300
+ if (fingerprint === lastFingerprint) return;
301
+ lastFingerprint = fingerprint;
302
+ bridge.notify("ui.render", { tree });
303
+ }
304
+ };
305
+ const root = reconciler.createContainer(
306
+ container,
307
+ LegacyRoot,
308
+ null,
309
+ // hydration callbacks
310
+ false,
311
+ // isStrictMode
312
+ null,
313
+ // concurrentUpdatesByDefaultOverride
314
+ "",
315
+ // identifierPrefix
316
+ (error) => {
317
+ console.error("[Lumio SDK] Uncaught error:", error);
318
+ },
319
+ (error) => {
320
+ console.error("[Lumio SDK] Caught error:", error);
321
+ },
322
+ (error) => {
323
+ console.error("[Lumio SDK] Recoverable error:", error);
324
+ },
325
+ () => {
326
+ },
327
+ // onDefaultTransitionIndicator
328
+ null
329
+ // transitionCallbacks (typed, but no longer read by 0.34)
330
+ );
331
+ const stopCallbacks = bridge.onNotification(HOST_METHODS.EVENT_CALLBACK, (params) => {
332
+ const { handlerId, args } = params;
333
+ try {
334
+ registry.invoke(handlerId, args);
335
+ } catch {
336
+ }
337
+ });
338
+ const flushSync = (fn) => {
339
+ if (options.syncUpdates) {
340
+ reconciler.flushSyncFromReconciler(fn);
341
+ reconciler.flushPassiveEffects();
342
+ } else {
343
+ fn();
344
+ }
345
+ };
346
+ flushSync(() => {
347
+ reconciler.updateContainer(element, root, null, () => {
348
+ bridge.notify("ui.ready", {});
349
+ });
350
+ });
351
+ return {
352
+ flushSync,
353
+ unmount() {
354
+ stopCallbacks();
355
+ flushSync(() => {
356
+ reconciler.updateContainer(null, root, null, null);
357
+ });
358
+ }
359
+ };
360
+ }
361
+ export {
362
+ destroyBridgeContext,
363
+ getBridge,
364
+ getRegistry,
365
+ mountRoot,
366
+ setBridgeFactory,
367
+ setInitParams
368
+ };