@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 +38 -0
- package/README.md +5 -4
- package/dist/chunk-GVAYWB5X.js +196 -0
- package/dist/host.d.ts +73 -0
- package/dist/host.js +368 -0
- package/dist/index.d.ts +42 -12
- package/dist/index.js +108 -195
- package/dist/lumio-runtime.global.js +14806 -5257
- package/dist/server.d.ts +148 -77
- package/dist/server.js +11 -5
- package/package.json +22 -17
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()` |
|
|
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,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
|
+
};
|