@zaflun/lumio-sdk 0.6.3 → 0.10.0
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/README.md +5 -4
- package/dist/chunk-GVAYWB5X.js +196 -0
- package/dist/host.d.ts +72 -0
- package/dist/host.js +340 -0
- package/dist/index.d.ts +41 -12
- package/dist/index.js +129 -198
- package/dist/lumio-runtime.global.js +1072 -160
- package/dist/server.d.ts +147 -77
- package/dist/server.js +11 -5
- package/package.json +16 -12
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()` |
|
|
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
|
-
##
|
|
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,72 @@
|
|
|
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 { type InitParams, type LumioBridge, type MountRootOptions, type MountedRoot, destroyBridgeContext, getBridge, getRegistry, mountRoot, setBridgeFactory, setInitParams };
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
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/reconciler.ts
|
|
15
|
+
var instanceCounter = 0;
|
|
16
|
+
var STRIP_PROPS = /* @__PURE__ */ new Set(["children", "key", "ref", "__self", "__source"]);
|
|
17
|
+
function sanitizeProps(rawProps, registry, existingHandlerIds) {
|
|
18
|
+
const sanitized = {};
|
|
19
|
+
for (const [key, value] of Object.entries(rawProps)) {
|
|
20
|
+
if (STRIP_PROPS.has(key)) continue;
|
|
21
|
+
if (typeof value === "symbol") continue;
|
|
22
|
+
if (typeof value === "function") {
|
|
23
|
+
const handlerKey = `__handler_${key}`;
|
|
24
|
+
const existingId = existingHandlerIds?.[key];
|
|
25
|
+
if (existingId && registry.has(existingId)) {
|
|
26
|
+
registry.update(existingId, value);
|
|
27
|
+
sanitized[handlerKey] = existingId;
|
|
28
|
+
} else {
|
|
29
|
+
const id = registry.register(value);
|
|
30
|
+
sanitized[handlerKey] = id;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
sanitized[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return sanitized;
|
|
37
|
+
}
|
|
38
|
+
function createLumioInstance(type, rawProps, registry) {
|
|
39
|
+
const sanitized = sanitizeProps(rawProps, registry);
|
|
40
|
+
const handlerIds = {};
|
|
41
|
+
for (const [key, value] of Object.entries(sanitized)) {
|
|
42
|
+
if (key.startsWith("__handler_")) {
|
|
43
|
+
const propName = key.slice("__handler_".length);
|
|
44
|
+
handlerIds[propName] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
_id: `lumio_${++instanceCounter}`,
|
|
49
|
+
type,
|
|
50
|
+
props: sanitized,
|
|
51
|
+
children: [],
|
|
52
|
+
parent: null,
|
|
53
|
+
handlerIds
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function serializeTree(instances) {
|
|
57
|
+
return instances.map((instance) => ({
|
|
58
|
+
_id: instance._id,
|
|
59
|
+
type: instance.type,
|
|
60
|
+
props: { ...instance.props },
|
|
61
|
+
children: serializeTree(instance.children)
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
function treeFingerprint(nodes) {
|
|
65
|
+
return JSON.stringify(nodes, (key, value) => {
|
|
66
|
+
if (typeof key === "string" && key.startsWith("__handler_")) return void 0;
|
|
67
|
+
return value;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/renderer/host-config.ts
|
|
72
|
+
var NO_EVENT_PRIORITY = 0;
|
|
73
|
+
var DISCRETE_EVENT_PRIORITY = 2;
|
|
74
|
+
var LEGACY_UPDATE_PRIORITY = 16;
|
|
75
|
+
function createHostConfig(registry, options = {}) {
|
|
76
|
+
const syncUpdates = options.syncUpdates === true;
|
|
77
|
+
let currentUpdatePriority = NO_EVENT_PRIORITY;
|
|
78
|
+
return {
|
|
79
|
+
supportsMutation: true,
|
|
80
|
+
supportsPersistence: false,
|
|
81
|
+
supportsHydration: false,
|
|
82
|
+
isPrimaryRenderer: true,
|
|
83
|
+
warnsIfNotActing: false,
|
|
84
|
+
scheduleTimeout: setTimeout,
|
|
85
|
+
cancelTimeout: clearTimeout,
|
|
86
|
+
noTimeout: -1,
|
|
87
|
+
supportsMicrotasks: true,
|
|
88
|
+
scheduleMicrotask: queueMicrotask,
|
|
89
|
+
getRootHostContext() {
|
|
90
|
+
return {};
|
|
91
|
+
},
|
|
92
|
+
getChildHostContext(_parentCtx) {
|
|
93
|
+
return {};
|
|
94
|
+
},
|
|
95
|
+
getPublicInstance(instance) {
|
|
96
|
+
return instance;
|
|
97
|
+
},
|
|
98
|
+
createInstance(type, props) {
|
|
99
|
+
return createLumioInstance(type, props, registry);
|
|
100
|
+
},
|
|
101
|
+
createTextInstance(text) {
|
|
102
|
+
return createLumioInstance("Text", { content: text }, registry);
|
|
103
|
+
},
|
|
104
|
+
appendInitialChild(parent, child) {
|
|
105
|
+
child.parent = parent;
|
|
106
|
+
parent.children.push(child);
|
|
107
|
+
},
|
|
108
|
+
appendChild(parent, child) {
|
|
109
|
+
child.parent = parent;
|
|
110
|
+
parent.children.push(child);
|
|
111
|
+
},
|
|
112
|
+
appendChildToContainer(container, child) {
|
|
113
|
+
child.parent = null;
|
|
114
|
+
container.children.push(child);
|
|
115
|
+
},
|
|
116
|
+
removeChild(parent, child) {
|
|
117
|
+
const idx = parent.children.indexOf(child);
|
|
118
|
+
if (idx !== -1) {
|
|
119
|
+
parent.children.splice(idx, 1);
|
|
120
|
+
}
|
|
121
|
+
child.parent = null;
|
|
122
|
+
removeHandlersRecursive(child, registry);
|
|
123
|
+
},
|
|
124
|
+
removeChildFromContainer(container, child) {
|
|
125
|
+
const idx = container.children.indexOf(child);
|
|
126
|
+
if (idx !== -1) {
|
|
127
|
+
container.children.splice(idx, 1);
|
|
128
|
+
}
|
|
129
|
+
child.parent = null;
|
|
130
|
+
removeHandlersRecursive(child, registry);
|
|
131
|
+
},
|
|
132
|
+
insertBefore(parent, child, before) {
|
|
133
|
+
const idx = parent.children.indexOf(before);
|
|
134
|
+
if (idx !== -1) {
|
|
135
|
+
parent.children.splice(idx, 0, child);
|
|
136
|
+
} else {
|
|
137
|
+
parent.children.push(child);
|
|
138
|
+
}
|
|
139
|
+
child.parent = parent;
|
|
140
|
+
},
|
|
141
|
+
insertInContainerBefore(container, child, before) {
|
|
142
|
+
const idx = container.children.indexOf(before);
|
|
143
|
+
if (idx !== -1) {
|
|
144
|
+
container.children.splice(idx, 0, child);
|
|
145
|
+
} else {
|
|
146
|
+
container.children.push(child);
|
|
147
|
+
}
|
|
148
|
+
child.parent = null;
|
|
149
|
+
},
|
|
150
|
+
commitUpdate(instance, _type, _prevProps, nextProps) {
|
|
151
|
+
const sanitized = sanitizeProps(
|
|
152
|
+
nextProps,
|
|
153
|
+
registry,
|
|
154
|
+
instance.handlerIds
|
|
155
|
+
);
|
|
156
|
+
for (const [key, value] of Object.entries(sanitized)) {
|
|
157
|
+
if (key.startsWith("__handler_")) {
|
|
158
|
+
const propName = key.slice("__handler_".length);
|
|
159
|
+
instance.handlerIds[propName] = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
instance.props = sanitized;
|
|
163
|
+
},
|
|
164
|
+
commitTextUpdate(instance, _oldText, newText) {
|
|
165
|
+
instance.props.content = newText;
|
|
166
|
+
},
|
|
167
|
+
finalizeInitialChildren() {
|
|
168
|
+
return false;
|
|
169
|
+
},
|
|
170
|
+
shouldSetTextContent() {
|
|
171
|
+
return false;
|
|
172
|
+
},
|
|
173
|
+
prepareForCommit() {
|
|
174
|
+
return null;
|
|
175
|
+
},
|
|
176
|
+
resetAfterCommit(container) {
|
|
177
|
+
container.onCommit();
|
|
178
|
+
},
|
|
179
|
+
clearContainer(container) {
|
|
180
|
+
container.children.length = 0;
|
|
181
|
+
},
|
|
182
|
+
preparePortalMount() {
|
|
183
|
+
},
|
|
184
|
+
detachDeletedInstance() {
|
|
185
|
+
},
|
|
186
|
+
getInstanceFromNode() {
|
|
187
|
+
return null;
|
|
188
|
+
},
|
|
189
|
+
beforeActiveInstanceBlur() {
|
|
190
|
+
},
|
|
191
|
+
afterActiveInstanceBlur() {
|
|
192
|
+
},
|
|
193
|
+
prepareScopeUpdate() {
|
|
194
|
+
},
|
|
195
|
+
getInstanceFromScope() {
|
|
196
|
+
return null;
|
|
197
|
+
},
|
|
198
|
+
// Priority methods required by react-reconciler 0.32
|
|
199
|
+
setCurrentUpdatePriority(priority) {
|
|
200
|
+
if (syncUpdates) currentUpdatePriority = priority;
|
|
201
|
+
},
|
|
202
|
+
getCurrentUpdatePriority() {
|
|
203
|
+
return syncUpdates ? currentUpdatePriority : LEGACY_UPDATE_PRIORITY;
|
|
204
|
+
},
|
|
205
|
+
resolveUpdatePriority() {
|
|
206
|
+
if (!syncUpdates) return LEGACY_UPDATE_PRIORITY;
|
|
207
|
+
return currentUpdatePriority !== NO_EVENT_PRIORITY ? currentUpdatePriority : DISCRETE_EVENT_PRIORITY;
|
|
208
|
+
},
|
|
209
|
+
// Transition support (not used — Lumio SDK does not support transitions)
|
|
210
|
+
NotPendingTransition: null,
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
HostTransitionContext: { $$typeof: /* @__PURE__ */ Symbol.for("react.context") },
|
|
213
|
+
// Form instance
|
|
214
|
+
resetFormInstance() {
|
|
215
|
+
},
|
|
216
|
+
// Post paint callback
|
|
217
|
+
requestPostPaintCallback() {
|
|
218
|
+
},
|
|
219
|
+
// Eager transition
|
|
220
|
+
shouldAttemptEagerTransition() {
|
|
221
|
+
return false;
|
|
222
|
+
},
|
|
223
|
+
// Suspend commit methods
|
|
224
|
+
maySuspendCommit() {
|
|
225
|
+
return false;
|
|
226
|
+
},
|
|
227
|
+
preloadInstance() {
|
|
228
|
+
return true;
|
|
229
|
+
},
|
|
230
|
+
startSuspendingCommit() {
|
|
231
|
+
},
|
|
232
|
+
suspendInstance() {
|
|
233
|
+
},
|
|
234
|
+
waitForCommitToBeReady() {
|
|
235
|
+
return null;
|
|
236
|
+
},
|
|
237
|
+
// Scheduler event tracking
|
|
238
|
+
trackSchedulerEvent() {
|
|
239
|
+
},
|
|
240
|
+
// Event resolution
|
|
241
|
+
resolveEventType() {
|
|
242
|
+
return null;
|
|
243
|
+
},
|
|
244
|
+
resolveEventTimeStamp() {
|
|
245
|
+
return Date.now();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function removeHandlersRecursive(instance, registry) {
|
|
250
|
+
for (const handlerId of Object.values(instance.handlerIds)) {
|
|
251
|
+
registry.remove(handlerId);
|
|
252
|
+
}
|
|
253
|
+
for (const child of instance.children) {
|
|
254
|
+
removeHandlersRecursive(child, registry);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/renderer/render.ts
|
|
259
|
+
function mountRoot(element, options = {}) {
|
|
260
|
+
const registry = getRegistry();
|
|
261
|
+
const bridge = getBridge();
|
|
262
|
+
const hostConfig = createHostConfig(registry, { syncUpdates: options.syncUpdates });
|
|
263
|
+
const reconciler = Reconciler(hostConfig);
|
|
264
|
+
const { flushSyncFromReconciler } = reconciler;
|
|
265
|
+
let lastFingerprint = "";
|
|
266
|
+
const container = {
|
|
267
|
+
children: [],
|
|
268
|
+
onCommit() {
|
|
269
|
+
const tree = serializeTree(container.children);
|
|
270
|
+
const fingerprint = treeFingerprint(tree);
|
|
271
|
+
if (fingerprint === lastFingerprint) return;
|
|
272
|
+
lastFingerprint = fingerprint;
|
|
273
|
+
bridge.notify("ui.render", { tree });
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const root = reconciler.createContainer(
|
|
277
|
+
container,
|
|
278
|
+
0,
|
|
279
|
+
// LegacyRoot tag
|
|
280
|
+
null,
|
|
281
|
+
// hydration callbacks
|
|
282
|
+
false,
|
|
283
|
+
// isStrictMode
|
|
284
|
+
null,
|
|
285
|
+
// concurrentUpdatesByDefaultOverride
|
|
286
|
+
"",
|
|
287
|
+
// identifierPrefix
|
|
288
|
+
(_error) => {
|
|
289
|
+
console.error("[Lumio SDK] Uncaught error:", _error);
|
|
290
|
+
},
|
|
291
|
+
(_error) => {
|
|
292
|
+
console.error("[Lumio SDK] Caught error:", _error);
|
|
293
|
+
},
|
|
294
|
+
(_error) => {
|
|
295
|
+
console.error("[Lumio SDK] Recoverable error:", _error);
|
|
296
|
+
},
|
|
297
|
+
() => {
|
|
298
|
+
},
|
|
299
|
+
// onDefaultTransitionIndicator
|
|
300
|
+
null
|
|
301
|
+
// transitionCallbacks
|
|
302
|
+
);
|
|
303
|
+
const stopCallbacks = bridge.onNotification(HOST_METHODS.EVENT_CALLBACK, (params) => {
|
|
304
|
+
const { handlerId, args } = params;
|
|
305
|
+
try {
|
|
306
|
+
registry.invoke(handlerId, args);
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
const flushSync = (fn) => {
|
|
311
|
+
if (options.syncUpdates) {
|
|
312
|
+
flushSyncFromReconciler(fn);
|
|
313
|
+
reconciler.flushPassiveEffects();
|
|
314
|
+
} else {
|
|
315
|
+
fn();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
flushSync(() => {
|
|
319
|
+
reconciler.updateContainer(element, root, null, () => {
|
|
320
|
+
bridge.notify("ui.ready", {});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
flushSync,
|
|
325
|
+
unmount() {
|
|
326
|
+
stopCallbacks();
|
|
327
|
+
flushSync(() => {
|
|
328
|
+
reconciler.updateContainer(null, root, null, null);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
destroyBridgeContext,
|
|
335
|
+
getBridge,
|
|
336
|
+
getRegistry,
|
|
337
|
+
mountRoot,
|
|
338
|
+
setBridgeFactory,
|
|
339
|
+
setInitParams
|
|
340
|
+
};
|