@mindees/renderer 0.1.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/LICENSE +31 -0
- package/README.md +129 -0
- package/dist/backend.d.ts +68 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +9 -0
- package/dist/backend.js.map +1 -0
- package/dist/dom.d.ts +39 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +125 -0
- package/dist/dom.js.map +1 -0
- package/dist/headless.d.ts +25 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +116 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/native-command-backend.d.ts +71 -0
- package/dist/native-command-backend.d.ts.map +1 -0
- package/dist/native-command-backend.js +246 -0
- package/dist/native-command-backend.js.map +1 -0
- package/dist/native-host.d.ts +56 -0
- package/dist/native-host.d.ts.map +1 -0
- package/dist/native-host.js +125 -0
- package/dist/native-host.js.map +1 -0
- package/dist/native-protocol.d.ts +141 -0
- package/dist/native-protocol.d.ts.map +1 -0
- package/dist/native-protocol.js +135 -0
- package/dist/native-protocol.js.map +1 -0
- package/dist/native.d.ts +42 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +59 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +28 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +145 -0
- package/dist/render.js.map +1 -0
- package/dist/ssr.d.ts +40 -0
- package/dist/ssr.d.ts.map +1 -0
- package/dist/ssr.js +31 -0
- package/dist/ssr.js.map +1 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isSerializable } from "./backend.js";
|
|
2
|
+
import { createDomBackend, domTagFor } from "./dom.js";
|
|
3
|
+
import { createHeadlessBackend, createHeadlessRoot, isEventProp } from "./headless.js";
|
|
4
|
+
import { createNativeNodeIdFactory, isNativeCommand, isNativePropValue, normalizeNativeProp } from "./native-protocol.js";
|
|
5
|
+
import { createNativeCommandBackend } from "./native-command-backend.js";
|
|
6
|
+
import { createCanvasBackend, createNativeBackend } from "./native.js";
|
|
7
|
+
import { NativeHostError, createReferenceHost } from "./native-host.js";
|
|
8
|
+
import { render } from "./render.js";
|
|
9
|
+
import { hydrate, renderToString } from "./ssr.js";
|
|
10
|
+
import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
11
|
+
//#region src/index.ts
|
|
12
|
+
/** The npm package name. */
|
|
13
|
+
const name = "@mindees/renderer";
|
|
14
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
15
|
+
const VERSION = "0.1.0";
|
|
16
|
+
/**
|
|
17
|
+
* Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,
|
|
18
|
+
* headless backend, SSR + hydration) is implemented and tested. Native
|
|
19
|
+
* (iOS/Android) and the GPU canvas are research tracks (throw
|
|
20
|
+
* `NotImplementedError`). See the repository `STATUS.md`.
|
|
21
|
+
*/
|
|
22
|
+
const maturity = "experimental";
|
|
23
|
+
/**
|
|
24
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
25
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
26
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
27
|
+
*/
|
|
28
|
+
const info = Object.freeze({
|
|
29
|
+
name,
|
|
30
|
+
version: VERSION,
|
|
31
|
+
maturity
|
|
32
|
+
});
|
|
33
|
+
//#endregion
|
|
34
|
+
export { NativeHostError, NotImplementedError, VERSION, createCanvasBackend, createDomBackend, createHeadlessBackend, createHeadlessRoot, createNativeBackend, createNativeCommandBackend, createNativeNodeIdFactory, createReferenceHost, domTagFor, hydrate, info, isEventProp, isNativeCommand, isNativePropValue, isSerializable, maturity, name, normalizeNativeProp, notImplemented, render, renderToString };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Host-backend contract + capability detection. */\nexport {\n type HostBackend,\n isSerializable,\n type SerializableBackend,\n} from './backend'\n/** DOM (web) backend. */\nexport {\n createDomBackend,\n type DomDocument,\n type DomElement,\n type DomNode,\n type DomText,\n domTagFor,\n} from './dom'\n/** Headless (in-memory) backend — the reference/test target. */\nexport {\n createHeadlessBackend,\n createHeadlessRoot,\n type HeadlessNode,\n isEventProp,\n} from './headless'\n/**\n * Native backends. `createNativeCommandBackend` is implemented (emits a native\n * command stream); `createNativeBackend`/`createCanvasBackend` are research\n * tracks that throw `NotImplementedError`.\n */\nexport {\n type CanvasBackend,\n createCanvasBackend,\n createNativeBackend,\n createNativeCommandBackend,\n type NativeBackend,\n type NativeCommandBackend,\n type NativeCommandBackendOptions,\n type NativeCommandNode,\n} from './native'\n/**\n * The strict reference native host — applies a command stream to a model tree and\n * validates it (the executable conformance contract real native hosts implement).\n */\nexport {\n createReferenceHost,\n NativeHostError,\n type ReferenceHost,\n type ReferenceHostNode,\n} from './native-host'\n/** The native command protocol: command types + serialization-safe helpers. */\nexport {\n type CreateNodeCommand,\n type CreateTextCommand,\n createNativeNodeIdFactory,\n type DisposeNodeCommand,\n type InsertChildCommand,\n isNativeCommand,\n isNativePropValue,\n type NativeCommand,\n type NativeNodeId,\n type NativePropValue,\n normalizeNativeProp,\n type RegisterEventCommand,\n type RemoveChildCommand,\n type RemovePropCommand,\n type SetPropCommand,\n type UnregisterEventCommand,\n type UpdateTextCommand,\n} from './native-protocol'\n/** The fine-grained reactive reconciler. */\nexport { type Mounted, render } from './render'\n/** Server-side rendering + hydration (web). */\nexport { hydrate, renderToString } from './ssr'\n\n/** The npm package name. */\nexport const name = '@mindees/renderer'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.1.0'\n\n/**\n * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,\n * headless backend, SSR + hydration) is implemented and tested. Native\n * (iOS/Android) and the GPU canvas are research tracks (throw\n * `NotImplementedError`). See the repository `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;AA4EA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;AAQvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { HostBackend } from "./backend.js";
|
|
2
|
+
import { NativeCommand, NativeNodeId } from "./native-protocol.js";
|
|
3
|
+
|
|
4
|
+
//#region src/native-command-backend.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* An opaque native host node. The reconciler treats it as a handle; the backend
|
|
7
|
+
* tracks structure (parent/children) on it to translate the {@link HostBackend}
|
|
8
|
+
* tree operations into index-based {@link NativeCommand}s.
|
|
9
|
+
*/
|
|
10
|
+
interface NativeCommandNode {
|
|
11
|
+
/** Stable id, shared with the host via the command stream. */
|
|
12
|
+
readonly id: NativeNodeId;
|
|
13
|
+
/** `"element"` or `"text"`. */
|
|
14
|
+
kind: 'element' | 'text';
|
|
15
|
+
/** Element tag (empty for text nodes). */
|
|
16
|
+
tag: string;
|
|
17
|
+
/** Text content (text nodes). */
|
|
18
|
+
text: string;
|
|
19
|
+
/** Parent node, or `null` when detached / the root. */
|
|
20
|
+
parent: NativeCommandNode | null;
|
|
21
|
+
/** Ordered child nodes. */
|
|
22
|
+
children: NativeCommandNode[];
|
|
23
|
+
}
|
|
24
|
+
/** Options for {@link createNativeCommandBackend}. */
|
|
25
|
+
interface NativeCommandBackendOptions {
|
|
26
|
+
/** Id of the host's pre-existing root container. Defaults to a per-instance id. */
|
|
27
|
+
rootId?: NativeNodeId;
|
|
28
|
+
/**
|
|
29
|
+
* Custom node-id generator. Must return **unique, finite** ids (a non-finite
|
|
30
|
+
* number is rejected at the boundary). Defaults to a collision-free per-instance
|
|
31
|
+
* factory; uniqueness of a custom factory is the caller's responsibility.
|
|
32
|
+
*/
|
|
33
|
+
idFactory?: () => NativeNodeId;
|
|
34
|
+
/** Called synchronously for every emitted command. */
|
|
35
|
+
onCommand?: (command: NativeCommand) => void;
|
|
36
|
+
/** Called by {@link NativeCommandBackend.flushCommands} with the flushed batch. */
|
|
37
|
+
onBatch?: (commands: readonly NativeCommand[]) => void;
|
|
38
|
+
}
|
|
39
|
+
/** A {@link HostBackend} that emits a {@link NativeCommand} stream. */
|
|
40
|
+
interface NativeCommandBackend<N> extends HostBackend<N> {
|
|
41
|
+
/** Discriminator for backend kind. */
|
|
42
|
+
readonly kind: 'native-command';
|
|
43
|
+
/** Id of the host root container (the `parentId` of top-level inserts). */
|
|
44
|
+
readonly rootId: NativeNodeId;
|
|
45
|
+
/** The root node to pass as the `container` to `render()`. */
|
|
46
|
+
readonly root: N;
|
|
47
|
+
/** A readonly snapshot of all commands buffered since the last flush/clear. */
|
|
48
|
+
getCommands(): readonly NativeCommand[];
|
|
49
|
+
/** Return the buffered commands as a batch, fire `onBatch`, and clear the buffer. */
|
|
50
|
+
flushCommands(): readonly NativeCommand[];
|
|
51
|
+
/** Drop all buffered commands without firing `onBatch`. */
|
|
52
|
+
clearCommands(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Invoke a registered event handler by id (what a host calls when a native
|
|
55
|
+
* event fires). Returns `true` if a handler was found and called.
|
|
56
|
+
*/
|
|
57
|
+
dispatchEvent(handlerId: string, event?: unknown): boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a {@link NativeCommandBackend}. Render against it to capture the native
|
|
61
|
+
* command stream:
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const backend = createNativeCommandBackend()
|
|
65
|
+
* const app = render(MyComponent, {}, backend, backend.root)
|
|
66
|
+
* const commands = backend.flushCommands() // replay these on a native host
|
|
67
|
+
*/
|
|
68
|
+
declare function createNativeCommandBackend(options?: NativeCommandBackendOptions): NativeCommandBackend<NativeCommandNode>;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { NativeCommandBackend, NativeCommandBackendOptions, NativeCommandNode, createNativeCommandBackend };
|
|
71
|
+
//# sourceMappingURL=native-command-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-command-backend.d.ts","names":[],"sources":["../src/native-command-backend.ts"],"mappings":";;;;;;AA6C6B;AAI7B;;UAhBiB,iBAAA;EAkBN;EAAA,SAhBA,EAAA,EAAI,YAAA;EAwBS;EAtBtB,IAAA;EAwB2C;EAtB3C,GAAA;EAYA;EAVA,IAAA;EAgBA;EAdA,MAAA,EAAQ,iBAAA;EAgBR;EAdA,QAAA,EAAU,iBAAA;AAAA;;UAIK,2BAAA;EAYJ;EAVX,MAAA,GAAS,YAAA;EAUoC;AAI/C;;;;EARE,SAAA,SAAkB,YAAA;EAcH;EAZf,SAAA,IAAa,OAAA,EAAS,aAAA;EAgBI;EAd1B,OAAA,IAAW,QAAA,WAAmB,aAAA;AAAA;;UAIf,oBAAA,YAAgC,WAAA,CAAY,CAAA;EAAZ;EAAA,SAEtC,IAAA;EAAA;EAAA,SAEA,MAAA,EAAQ,YAAA;EAAA;EAAA,SAER,IAAA,EAAM,CAAA;EAAA;EAEf,WAAA,aAAwB,aAAA;EAAA;EAExB,aAAA,aAA0B,aAAA;EAAA;EAE1B,aAAA;EAKA;;;;EAAA,aAAA,CAAc,SAAA,UAAmB,KAAA;AAAA;;;;;;;;;;iBAoCnB,0BAAA,CACd,OAAA,GAAS,2BAAA,GACR,oBAAA,CAAqB,iBAAA"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { isEventProp } from "./headless.js";
|
|
2
|
+
import { createNativeNodeIdFactory, normalizeNativeProp } from "./native-protocol.js";
|
|
3
|
+
//#region src/native-command-backend.ts
|
|
4
|
+
/**
|
|
5
|
+
* Private, monotonic instance counter used only to give each backend a distinct
|
|
6
|
+
* id prefix so default node ids never collide across instances. Not observable
|
|
7
|
+
* outside the module; callers that want stable ids pass their own `idFactory`.
|
|
8
|
+
*/
|
|
9
|
+
let backendInstanceSeq = 0;
|
|
10
|
+
/** `onPress` → `press`, `onPointerDown` → `pointerdown`. */
|
|
11
|
+
function eventNameFor(key) {
|
|
12
|
+
return key.slice(2).toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Enforce the protocol's id invariant at the backend boundary: a non-finite number
|
|
16
|
+
* id would silently corrupt to `null` through JSON and break node identity on the
|
|
17
|
+
* wire. Throws on misuse (e.g. a custom `idFactory`/`rootId` yielding `NaN`).
|
|
18
|
+
*/
|
|
19
|
+
function validateNodeId(id) {
|
|
20
|
+
if (typeof id === "number" && !Number.isFinite(id)) throw new TypeError(`native node id must be a string or finite number, received ${String(id)}`);
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a {@link NativeCommandBackend}. Render against it to capture the native
|
|
25
|
+
* command stream:
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const backend = createNativeCommandBackend()
|
|
29
|
+
* const app = render(MyComponent, {}, backend, backend.root)
|
|
30
|
+
* const commands = backend.flushCommands() // replay these on a native host
|
|
31
|
+
*/
|
|
32
|
+
function createNativeCommandBackend(options = {}) {
|
|
33
|
+
const prefix = `b${backendInstanceSeq++}`;
|
|
34
|
+
const rawNextId = options.idFactory ?? createNativeNodeIdFactory(`${prefix}n`);
|
|
35
|
+
const nextId = () => validateNodeId(rawNextId());
|
|
36
|
+
const nextHandlerId = createNativeNodeIdFactory(`${prefix}h`);
|
|
37
|
+
const rootId = validateNodeId(options.rootId ?? `${prefix}root`);
|
|
38
|
+
const root = {
|
|
39
|
+
id: rootId,
|
|
40
|
+
kind: "element",
|
|
41
|
+
tag: "root",
|
|
42
|
+
text: "",
|
|
43
|
+
parent: null,
|
|
44
|
+
children: []
|
|
45
|
+
};
|
|
46
|
+
const pending = [];
|
|
47
|
+
/** handlerId → handler function. The function never enters the command stream. */
|
|
48
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
49
|
+
/** node → (eventName → handlerId), so we can unregister on change/dispose. */
|
|
50
|
+
const nodeEvents = /* @__PURE__ */ new WeakMap();
|
|
51
|
+
function emit(command) {
|
|
52
|
+
pending.push(command);
|
|
53
|
+
options.onCommand?.(command);
|
|
54
|
+
}
|
|
55
|
+
function applyEvent(node, eventName, value) {
|
|
56
|
+
let events = nodeEvents.get(node);
|
|
57
|
+
const existing = events?.get(eventName);
|
|
58
|
+
if (existing !== void 0) {
|
|
59
|
+
handlers.delete(existing);
|
|
60
|
+
events?.delete(eventName);
|
|
61
|
+
emit({
|
|
62
|
+
type: "unregisterEvent",
|
|
63
|
+
id: node.id,
|
|
64
|
+
eventName,
|
|
65
|
+
handlerId: existing
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "function") {
|
|
69
|
+
const handlerId = nextHandlerId();
|
|
70
|
+
handlers.set(handlerId, value);
|
|
71
|
+
if (!events) {
|
|
72
|
+
events = /* @__PURE__ */ new Map();
|
|
73
|
+
nodeEvents.set(node, events);
|
|
74
|
+
}
|
|
75
|
+
events.set(eventName, handlerId);
|
|
76
|
+
emit({
|
|
77
|
+
type: "registerEvent",
|
|
78
|
+
id: node.id,
|
|
79
|
+
eventName,
|
|
80
|
+
handlerId
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Tear down a removed subtree: unregister its events, dispose deepest-first. */
|
|
85
|
+
function disposeSubtree(node) {
|
|
86
|
+
for (const child of node.children) disposeSubtree(child);
|
|
87
|
+
const events = nodeEvents.get(node);
|
|
88
|
+
if (events) {
|
|
89
|
+
for (const [eventName, handlerId] of events) {
|
|
90
|
+
handlers.delete(handlerId);
|
|
91
|
+
emit({
|
|
92
|
+
type: "unregisterEvent",
|
|
93
|
+
id: node.id,
|
|
94
|
+
eventName,
|
|
95
|
+
handlerId
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
nodeEvents.delete(node);
|
|
99
|
+
}
|
|
100
|
+
emit({
|
|
101
|
+
type: "disposeNode",
|
|
102
|
+
id: node.id
|
|
103
|
+
});
|
|
104
|
+
node.parent = null;
|
|
105
|
+
node.children = [];
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
kind: "native-command",
|
|
109
|
+
rootId,
|
|
110
|
+
root,
|
|
111
|
+
createElement(type) {
|
|
112
|
+
const node = {
|
|
113
|
+
id: nextId(),
|
|
114
|
+
kind: "element",
|
|
115
|
+
tag: type,
|
|
116
|
+
text: "",
|
|
117
|
+
parent: null,
|
|
118
|
+
children: []
|
|
119
|
+
};
|
|
120
|
+
emit({
|
|
121
|
+
type: "createNode",
|
|
122
|
+
id: node.id,
|
|
123
|
+
tag: type
|
|
124
|
+
});
|
|
125
|
+
return node;
|
|
126
|
+
},
|
|
127
|
+
createText(value) {
|
|
128
|
+
const node = {
|
|
129
|
+
id: nextId(),
|
|
130
|
+
kind: "text",
|
|
131
|
+
tag: "",
|
|
132
|
+
text: value,
|
|
133
|
+
parent: null,
|
|
134
|
+
children: []
|
|
135
|
+
};
|
|
136
|
+
emit({
|
|
137
|
+
type: "createText",
|
|
138
|
+
id: node.id,
|
|
139
|
+
text: value
|
|
140
|
+
});
|
|
141
|
+
return node;
|
|
142
|
+
},
|
|
143
|
+
setProp(node, key, value, prev) {
|
|
144
|
+
if (isEventProp(key)) {
|
|
145
|
+
applyEvent(node, eventNameFor(key), value);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const normalized = normalizeNativeProp(value);
|
|
149
|
+
if (normalized === void 0) {
|
|
150
|
+
if (normalizeNativeProp(prev) !== void 0) emit({
|
|
151
|
+
type: "removeProp",
|
|
152
|
+
id: node.id,
|
|
153
|
+
name: key
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
emit({
|
|
158
|
+
type: "setProp",
|
|
159
|
+
id: node.id,
|
|
160
|
+
name: key,
|
|
161
|
+
value: normalized
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
setText(node, value) {
|
|
165
|
+
node.text = value;
|
|
166
|
+
emit({
|
|
167
|
+
type: "updateText",
|
|
168
|
+
id: node.id,
|
|
169
|
+
text: value
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
insert(parent, node, anchor) {
|
|
173
|
+
if (node.parent) {
|
|
174
|
+
const old = node.parent;
|
|
175
|
+
const oldIndex = old.children.indexOf(node);
|
|
176
|
+
if (oldIndex >= 0) old.children.splice(oldIndex, 1);
|
|
177
|
+
emit({
|
|
178
|
+
type: "removeChild",
|
|
179
|
+
parentId: old.id,
|
|
180
|
+
childId: node.id
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
let index;
|
|
184
|
+
if (anchor === null) {
|
|
185
|
+
index = parent.children.length;
|
|
186
|
+
parent.children.push(node);
|
|
187
|
+
} else {
|
|
188
|
+
const at = parent.children.indexOf(anchor);
|
|
189
|
+
index = at < 0 ? parent.children.length : at;
|
|
190
|
+
parent.children.splice(index, 0, node);
|
|
191
|
+
}
|
|
192
|
+
node.parent = parent;
|
|
193
|
+
emit({
|
|
194
|
+
type: "insertChild",
|
|
195
|
+
parentId: parent.id,
|
|
196
|
+
childId: node.id,
|
|
197
|
+
index
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
remove(parent, node) {
|
|
201
|
+
const at = parent.children.indexOf(node);
|
|
202
|
+
if (at >= 0) parent.children.splice(at, 1);
|
|
203
|
+
node.parent = null;
|
|
204
|
+
emit({
|
|
205
|
+
type: "removeChild",
|
|
206
|
+
parentId: parent.id,
|
|
207
|
+
childId: node.id
|
|
208
|
+
});
|
|
209
|
+
disposeSubtree(node);
|
|
210
|
+
},
|
|
211
|
+
parentOf(node) {
|
|
212
|
+
return node.parent;
|
|
213
|
+
},
|
|
214
|
+
nextSibling(node) {
|
|
215
|
+
const parent = node.parent;
|
|
216
|
+
if (!parent) return null;
|
|
217
|
+
const at = parent.children.indexOf(node);
|
|
218
|
+
return at >= 0 && at + 1 < parent.children.length ? parent.children[at + 1] ?? null : null;
|
|
219
|
+
},
|
|
220
|
+
isText(node) {
|
|
221
|
+
return node.kind === "text";
|
|
222
|
+
},
|
|
223
|
+
getCommands() {
|
|
224
|
+
return pending.slice();
|
|
225
|
+
},
|
|
226
|
+
flushCommands() {
|
|
227
|
+
const batch = pending.slice();
|
|
228
|
+
pending.length = 0;
|
|
229
|
+
options.onBatch?.(batch);
|
|
230
|
+
return batch;
|
|
231
|
+
},
|
|
232
|
+
clearCommands() {
|
|
233
|
+
pending.length = 0;
|
|
234
|
+
},
|
|
235
|
+
dispatchEvent(handlerId, event) {
|
|
236
|
+
const handler = handlers.get(handlerId);
|
|
237
|
+
if (!handler) return false;
|
|
238
|
+
handler(event);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { createNativeCommandBackend };
|
|
245
|
+
|
|
246
|
+
//# sourceMappingURL=native-command-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-command-backend.js","names":[],"sources":["../src/native-command-backend.ts"],"sourcesContent":["/**\n * The **native command backend** — a {@link HostBackend} that, instead of\n * mutating a DOM, records a stream of {@link NativeCommand}s describing how to\n * build and update a native view tree.\n *\n * This is the bridge between Helix (the reconciler + fine-grained reactivity) and\n * a native host: render an app against this backend and you get a deterministic,\n * serializable command stream a UIKit or Android View host can replay. It depends\n * only on `@mindees/core` reactivity\n * (via the reconciler) — no DOM, no browser globals — so it runs in Node tests.\n *\n * It is **not** an end-to-end native app bridge. It produces the protocol that the\n * verified host projects in `examples/native-hosts/` replay/render in CI; the direct\n * runtime backend and embedded JS engine remain research tracks\n * ({@link createNativeBackend}).\n *\n * @module\n */\n\nimport type { HostBackend } from './backend'\nimport { isEventProp } from './headless'\nimport {\n createNativeNodeIdFactory,\n type NativeCommand,\n type NativeNodeId,\n normalizeNativeProp,\n} from './native-protocol'\n\n/**\n * An opaque native host node. The reconciler treats it as a handle; the backend\n * tracks structure (parent/children) on it to translate the {@link HostBackend}\n * tree operations into index-based {@link NativeCommand}s.\n */\nexport interface NativeCommandNode {\n /** Stable id, shared with the host via the command stream. */\n readonly id: NativeNodeId\n /** `\"element\"` or `\"text\"`. */\n kind: 'element' | 'text'\n /** Element tag (empty for text nodes). */\n tag: string\n /** Text content (text nodes). */\n text: string\n /** Parent node, or `null` when detached / the root. */\n parent: NativeCommandNode | null\n /** Ordered child nodes. */\n children: NativeCommandNode[]\n}\n\n/** Options for {@link createNativeCommandBackend}. */\nexport interface NativeCommandBackendOptions {\n /** Id of the host's pre-existing root container. Defaults to a per-instance id. */\n rootId?: NativeNodeId\n /**\n * Custom node-id generator. Must return **unique, finite** ids (a non-finite\n * number is rejected at the boundary). Defaults to a collision-free per-instance\n * factory; uniqueness of a custom factory is the caller's responsibility.\n */\n idFactory?: () => NativeNodeId\n /** Called synchronously for every emitted command. */\n onCommand?: (command: NativeCommand) => void\n /** Called by {@link NativeCommandBackend.flushCommands} with the flushed batch. */\n onBatch?: (commands: readonly NativeCommand[]) => void\n}\n\n/** A {@link HostBackend} that emits a {@link NativeCommand} stream. */\nexport interface NativeCommandBackend<N> extends HostBackend<N> {\n /** Discriminator for backend kind. */\n readonly kind: 'native-command'\n /** Id of the host root container (the `parentId` of top-level inserts). */\n readonly rootId: NativeNodeId\n /** The root node to pass as the `container` to `render()`. */\n readonly root: N\n /** A readonly snapshot of all commands buffered since the last flush/clear. */\n getCommands(): readonly NativeCommand[]\n /** Return the buffered commands as a batch, fire `onBatch`, and clear the buffer. */\n flushCommands(): readonly NativeCommand[]\n /** Drop all buffered commands without firing `onBatch`. */\n clearCommands(): void\n /**\n * Invoke a registered event handler by id (what a host calls when a native\n * event fires). Returns `true` if a handler was found and called.\n */\n dispatchEvent(handlerId: string, event?: unknown): boolean\n}\n\n/**\n * Private, monotonic instance counter used only to give each backend a distinct\n * id prefix so default node ids never collide across instances. Not observable\n * outside the module; callers that want stable ids pass their own `idFactory`.\n */\nlet backendInstanceSeq = 0\n\n/** `onPress` → `press`, `onPointerDown` → `pointerdown`. */\nfunction eventNameFor(key: string): string {\n return key.slice(2).toLowerCase()\n}\n\n/**\n * Enforce the protocol's id invariant at the backend boundary: a non-finite number\n * id would silently corrupt to `null` through JSON and break node identity on the\n * wire. Throws on misuse (e.g. a custom `idFactory`/`rootId` yielding `NaN`).\n */\nfunction validateNodeId(id: NativeNodeId): NativeNodeId {\n if (typeof id === 'number' && !Number.isFinite(id)) {\n throw new TypeError(`native node id must be a string or finite number, received ${String(id)}`)\n }\n return id\n}\n\n/**\n * Create a {@link NativeCommandBackend}. Render against it to capture the native\n * command stream:\n *\n * @example\n * const backend = createNativeCommandBackend()\n * const app = render(MyComponent, {}, backend, backend.root)\n * const commands = backend.flushCommands() // replay these on a native host\n */\nexport function createNativeCommandBackend(\n options: NativeCommandBackendOptions = {},\n): NativeCommandBackend<NativeCommandNode> {\n const prefix = `b${backendInstanceSeq++}`\n const rawNextId = options.idFactory ?? createNativeNodeIdFactory(`${prefix}n`)\n // Validate every id at the boundary. Uniqueness is the idFactory's contract (the\n // default factory guarantees it); we deliberately don't track every id forever to\n // detect duplicates, which would leak memory in the long-running apps this targets.\n const nextId = (): NativeNodeId => validateNodeId(rawNextId())\n const nextHandlerId = createNativeNodeIdFactory(`${prefix}h`)\n const rootId = validateNodeId(options.rootId ?? `${prefix}root`)\n\n const root: NativeCommandNode = {\n id: rootId,\n kind: 'element',\n tag: 'root',\n text: '',\n parent: null,\n children: [],\n }\n\n const pending: NativeCommand[] = []\n /** handlerId → handler function. The function never enters the command stream. */\n const handlers = new Map<string, (event?: unknown) => void>()\n /** node → (eventName → handlerId), so we can unregister on change/dispose. */\n const nodeEvents = new WeakMap<NativeCommandNode, Map<string, string>>()\n\n function emit(command: NativeCommand): void {\n pending.push(command)\n options.onCommand?.(command)\n }\n\n function applyEvent(node: NativeCommandNode, eventName: string, value: unknown): void {\n let events = nodeEvents.get(node)\n const existing = events?.get(eventName)\n if (existing !== undefined) {\n handlers.delete(existing)\n events?.delete(eventName)\n emit({ type: 'unregisterEvent', id: node.id, eventName, handlerId: existing })\n }\n if (typeof value === 'function') {\n const handlerId = nextHandlerId()\n handlers.set(handlerId, value as (event?: unknown) => void)\n if (!events) {\n events = new Map()\n nodeEvents.set(node, events)\n }\n events.set(eventName, handlerId)\n emit({ type: 'registerEvent', id: node.id, eventName, handlerId })\n }\n }\n\n /** Tear down a removed subtree: unregister its events, dispose deepest-first. */\n function disposeSubtree(node: NativeCommandNode): void {\n for (const child of node.children) disposeSubtree(child)\n const events = nodeEvents.get(node)\n if (events) {\n for (const [eventName, handlerId] of events) {\n handlers.delete(handlerId)\n emit({ type: 'unregisterEvent', id: node.id, eventName, handlerId })\n }\n nodeEvents.delete(node)\n }\n emit({ type: 'disposeNode', id: node.id })\n // Detach every disposed node so parentOf() reports it removed. The reconciler's\n // region cleanup re-checks parentOf before removing (render.ts bindReactiveChild),\n // so without this a descendant whose parent pointer still pointed at the (already\n // removed) parent would be removed + disposed a SECOND time — a host double-free.\n node.parent = null\n node.children = []\n }\n\n return {\n kind: 'native-command',\n rootId,\n root,\n\n createElement(type: string): NativeCommandNode {\n const node: NativeCommandNode = {\n id: nextId(),\n kind: 'element',\n tag: type,\n text: '',\n parent: null,\n children: [],\n }\n emit({ type: 'createNode', id: node.id, tag: type })\n return node\n },\n\n createText(value: string): NativeCommandNode {\n const node: NativeCommandNode = {\n id: nextId(),\n kind: 'text',\n tag: '',\n text: value,\n parent: null,\n children: [],\n }\n emit({ type: 'createText', id: node.id, text: value })\n return node\n },\n\n setProp(node: NativeCommandNode, key: string, value: unknown, prev: unknown): void {\n if (isEventProp(key)) {\n applyEvent(node, eventNameFor(key), value)\n return\n }\n const normalized = normalizeNativeProp(value)\n if (normalized === undefined) {\n // Only emit a removal if there was actually a representable value before.\n if (normalizeNativeProp(prev) !== undefined) {\n emit({ type: 'removeProp', id: node.id, name: key })\n }\n return\n }\n emit({ type: 'setProp', id: node.id, name: key, value: normalized })\n },\n\n setText(node: NativeCommandNode, value: string): void {\n node.text = value\n emit({ type: 'updateText', id: node.id, text: value })\n },\n\n insert(\n parent: NativeCommandNode,\n node: NativeCommandNode,\n anchor: NativeCommandNode | null,\n ): void {\n // A move: detach from the old parent first so indices stay correct.\n if (node.parent) {\n const old = node.parent\n const oldIndex = old.children.indexOf(node)\n if (oldIndex >= 0) old.children.splice(oldIndex, 1)\n emit({ type: 'removeChild', parentId: old.id, childId: node.id })\n }\n let index: number\n if (anchor === null) {\n index = parent.children.length\n parent.children.push(node)\n } else {\n const at = parent.children.indexOf(anchor)\n index = at < 0 ? parent.children.length : at\n parent.children.splice(index, 0, node)\n }\n node.parent = parent\n emit({ type: 'insertChild', parentId: parent.id, childId: node.id, index })\n },\n\n remove(parent: NativeCommandNode, node: NativeCommandNode): void {\n const at = parent.children.indexOf(node)\n if (at >= 0) parent.children.splice(at, 1)\n node.parent = null\n emit({ type: 'removeChild', parentId: parent.id, childId: node.id })\n // The reconciler discards removed nodes, so free the whole subtree + handlers.\n disposeSubtree(node)\n },\n\n parentOf(node: NativeCommandNode): NativeCommandNode | null {\n return node.parent\n },\n\n nextSibling(node: NativeCommandNode): NativeCommandNode | null {\n const parent = node.parent\n if (!parent) return null\n const at = parent.children.indexOf(node)\n return at >= 0 && at + 1 < parent.children.length ? (parent.children[at + 1] ?? null) : null\n },\n\n isText(node: NativeCommandNode): boolean {\n return node.kind === 'text'\n },\n\n getCommands(): readonly NativeCommand[] {\n return pending.slice()\n },\n\n flushCommands(): readonly NativeCommand[] {\n const batch = pending.slice()\n pending.length = 0\n options.onBatch?.(batch)\n return batch\n },\n\n clearCommands(): void {\n pending.length = 0\n },\n\n dispatchEvent(handlerId: string, event?: unknown): boolean {\n const handler = handlers.get(handlerId)\n if (!handler) return false\n handler(event)\n return true\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NativeCommand, NativeNodeId, NativePropValue } from "./native-protocol.js";
|
|
2
|
+
|
|
3
|
+
//#region src/native-host.d.ts
|
|
4
|
+
/** A reconstructed host node. */
|
|
5
|
+
interface ReferenceHostNode {
|
|
6
|
+
/** Stable id from the command stream. */
|
|
7
|
+
readonly id: NativeNodeId;
|
|
8
|
+
/** `"root"` (the pre-existing container), `"element"`, or `"text"`. */
|
|
9
|
+
readonly kind: 'root' | 'element' | 'text';
|
|
10
|
+
/** Element tag (empty for text nodes). */
|
|
11
|
+
tag: string;
|
|
12
|
+
/** Text content (text nodes). */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Applied props (elements). */
|
|
15
|
+
props: Record<string, NativePropValue>;
|
|
16
|
+
/** Wired events: eventName → handlerId. */
|
|
17
|
+
events: Map<string, string>;
|
|
18
|
+
/** Parent, or `null` when detached / the root. */
|
|
19
|
+
parent: ReferenceHostNode | null;
|
|
20
|
+
/** Ordered children. */
|
|
21
|
+
children: ReferenceHostNode[];
|
|
22
|
+
}
|
|
23
|
+
/** A reference host that applies a {@link NativeCommand} stream to a model tree. */
|
|
24
|
+
interface ReferenceHost {
|
|
25
|
+
/** Id of the root container. */
|
|
26
|
+
readonly rootId: NativeNodeId;
|
|
27
|
+
/** The root node; its `children` mirror the rendered top-level nodes. */
|
|
28
|
+
readonly root: ReferenceHostNode;
|
|
29
|
+
/** Apply a single command (throws {@link NativeHostError} on a malformed one). */
|
|
30
|
+
apply(command: NativeCommand): void;
|
|
31
|
+
/** Apply a batch in order. */
|
|
32
|
+
applyBatch(commands: readonly NativeCommand[]): void;
|
|
33
|
+
/** Look up a live node by id. */
|
|
34
|
+
getNode(id: NativeNodeId): ReferenceHostNode | undefined;
|
|
35
|
+
/** Number of live (created, not yet disposed) nodes, excluding the root. */
|
|
36
|
+
liveNodeCount(): number;
|
|
37
|
+
/** A compact structural string of the tree (tags + text; inspect props via {@link getNode}). */
|
|
38
|
+
serialize(): string;
|
|
39
|
+
}
|
|
40
|
+
/** Thrown when a command stream violates the host contract. */
|
|
41
|
+
declare class NativeHostError extends Error {
|
|
42
|
+
constructor(message: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a {@link ReferenceHost}. Wire it to a backend to validate + reconstruct:
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const host = createReferenceHost()
|
|
49
|
+
* const backend = createNativeCommandBackend({ rootId: host.rootId, onCommand: (c) => host.apply(c) })
|
|
50
|
+
* render(MyComponent, {}, backend, backend.root)
|
|
51
|
+
* host.serialize() // the reconstructed tree
|
|
52
|
+
*/
|
|
53
|
+
declare function createReferenceHost(rootId?: NativeNodeId): ReferenceHost;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { NativeHostError, ReferenceHost, ReferenceHostNode, createReferenceHost };
|
|
56
|
+
//# sourceMappingURL=native-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-host.d.ts","names":[],"sources":["../src/native-host.ts"],"mappings":";;;;UAyBiB,iBAAA;EAcf;EAAA,SAZS,EAAA,EAAI,YAAA;EAcb;EAAA,SAZS,IAAA;EAYkB;EAV3B,GAAA;EAce;EAZf,IAAA;;EAEA,KAAA,EAAO,MAAA,SAAe,eAAA;EAcP;EAZf,MAAA,EAAQ,GAAA;EAgBsB;EAd9B,MAAA,EAAQ,iBAAA;EAgBmB;EAd3B,QAAA,EAAU,iBAAA;AAAA;;UAIK,aAAA;EAIN;EAAA,SAFA,MAAA,EAAQ,YAAA;EAIjB;EAAA,SAFS,IAAA,EAAM,iBAAA;EAET;EAAN,KAAA,CAAM,OAAA,EAAS,aAAA;EAEe;EAA9B,UAAA,CAAW,QAAA,WAAmB,aAAA;EAE9B;EAAA,OAAA,CAAQ,EAAA,EAAI,YAAA,GAAe,iBAAA;EAAnB;EAER,aAAA;EAAA;EAEA,SAAA;AAAA;AAAS;AAAA,cAIE,eAAA,SAAwB,KAAK;cAC5B,OAAA;AAAA;;;;;;AAAe;AAe7B;;;iBAAgB,mBAAA,CAAoB,MAAA,GAAQ,YAAA,GAA6B,aAAa"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//#region src/native-host.ts
|
|
2
|
+
/** Thrown when a command stream violates the host contract. */
|
|
3
|
+
var NativeHostError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "NativeHostError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Create a {@link ReferenceHost}. Wire it to a backend to validate + reconstruct:
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const host = createReferenceHost()
|
|
14
|
+
* const backend = createNativeCommandBackend({ rootId: host.rootId, onCommand: (c) => host.apply(c) })
|
|
15
|
+
* render(MyComponent, {}, backend, backend.root)
|
|
16
|
+
* host.serialize() // the reconstructed tree
|
|
17
|
+
*/
|
|
18
|
+
function createReferenceHost(rootId = "host-root") {
|
|
19
|
+
const root = {
|
|
20
|
+
id: rootId,
|
|
21
|
+
kind: "root",
|
|
22
|
+
tag: "root",
|
|
23
|
+
text: "",
|
|
24
|
+
props: {},
|
|
25
|
+
events: /* @__PURE__ */ new Map(),
|
|
26
|
+
parent: null,
|
|
27
|
+
children: []
|
|
28
|
+
};
|
|
29
|
+
const nodes = new Map([[rootId, root]]);
|
|
30
|
+
function requireNode(id) {
|
|
31
|
+
const node = nodes.get(id);
|
|
32
|
+
if (!node) throw new NativeHostError(`command references unknown node ${String(id)}`);
|
|
33
|
+
return node;
|
|
34
|
+
}
|
|
35
|
+
function create(id, kind, tag, text) {
|
|
36
|
+
if (nodes.has(id)) throw new NativeHostError(`duplicate node id ${String(id)}`);
|
|
37
|
+
nodes.set(id, {
|
|
38
|
+
id,
|
|
39
|
+
kind,
|
|
40
|
+
tag,
|
|
41
|
+
text,
|
|
42
|
+
props: {},
|
|
43
|
+
events: /* @__PURE__ */ new Map(),
|
|
44
|
+
parent: null,
|
|
45
|
+
children: []
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function detach(node) {
|
|
49
|
+
const parent = node.parent;
|
|
50
|
+
if (!parent) return;
|
|
51
|
+
const at = parent.children.indexOf(node);
|
|
52
|
+
if (at >= 0) parent.children.splice(at, 1);
|
|
53
|
+
node.parent = null;
|
|
54
|
+
}
|
|
55
|
+
function apply(command) {
|
|
56
|
+
switch (command.type) {
|
|
57
|
+
case "createNode":
|
|
58
|
+
create(command.id, "element", command.tag, "");
|
|
59
|
+
break;
|
|
60
|
+
case "createText":
|
|
61
|
+
create(command.id, "text", "", command.text);
|
|
62
|
+
break;
|
|
63
|
+
case "setProp":
|
|
64
|
+
requireNode(command.id).props[command.name] = command.value;
|
|
65
|
+
break;
|
|
66
|
+
case "removeProp":
|
|
67
|
+
delete requireNode(command.id).props[command.name];
|
|
68
|
+
break;
|
|
69
|
+
case "updateText": {
|
|
70
|
+
const node = requireNode(command.id);
|
|
71
|
+
if (node.kind !== "text") throw new NativeHostError(`updateText on non-text node ${String(command.id)}`);
|
|
72
|
+
node.text = command.text;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "insertChild": {
|
|
76
|
+
const parent = requireNode(command.parentId);
|
|
77
|
+
const child = requireNode(command.childId);
|
|
78
|
+
if (child.parent) throw new NativeHostError(`insertChild: ${String(command.childId)} already has a parent (detach it first)`);
|
|
79
|
+
if (command.index < 0 || command.index > parent.children.length) throw new NativeHostError(`insertChild: index ${command.index} out of range`);
|
|
80
|
+
parent.children.splice(command.index, 0, child);
|
|
81
|
+
child.parent = parent;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "removeChild": {
|
|
85
|
+
const parent = requireNode(command.parentId);
|
|
86
|
+
const child = requireNode(command.childId);
|
|
87
|
+
if (child.parent !== parent) throw new NativeHostError(`removeChild: ${String(command.childId)} is not a child of ${String(command.parentId)}`);
|
|
88
|
+
detach(child);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "disposeNode":
|
|
92
|
+
if (command.id === rootId) throw new NativeHostError("cannot dispose the root node");
|
|
93
|
+
detach(requireNode(command.id));
|
|
94
|
+
nodes.delete(command.id);
|
|
95
|
+
break;
|
|
96
|
+
case "registerEvent":
|
|
97
|
+
requireNode(command.id).events.set(command.eventName, command.handlerId);
|
|
98
|
+
break;
|
|
99
|
+
case "unregisterEvent":
|
|
100
|
+
requireNode(command.id).events.delete(command.eventName);
|
|
101
|
+
break;
|
|
102
|
+
default: throw new NativeHostError(`unknown command ${JSON.stringify(command)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function serializeNode(node) {
|
|
106
|
+
if (node.kind === "text") return node.text;
|
|
107
|
+
const inner = node.children.map(serializeNode).join("");
|
|
108
|
+
return `<${node.tag}>${inner}</${node.tag}>`;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
rootId,
|
|
112
|
+
root,
|
|
113
|
+
apply,
|
|
114
|
+
applyBatch(commands) {
|
|
115
|
+
for (const command of commands) apply(command);
|
|
116
|
+
},
|
|
117
|
+
getNode: (id) => nodes.get(id),
|
|
118
|
+
liveNodeCount: () => nodes.size - 1,
|
|
119
|
+
serialize: () => root.children.map(serializeNode).join("")
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { NativeHostError, createReferenceHost };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=native-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-host.js","names":["_exhaustive"],"sources":["../src/native-host.ts"],"sourcesContent":["/**\n * A **strict reference native host** — the inverse of\n * {@link import('./native-command-backend').createNativeCommandBackend}.\n *\n * It consumes a {@link NativeCommand} stream and reconstructs an in-memory view\n * tree, **strictly validating** the stream as it goes: it throws\n * {@link NativeHostError} on any malformed sequence (unknown/duplicate id,\n * `removeChild` of a non-child, double `disposeNode`, out-of-range insert index,\n * …). This makes it the executable **conformance spec** for a native host: the\n * verified iOS/UIKit and Android View hosts implement these semantics but build\n * platform views instead of these model nodes.\n *\n * Piping the command backend's output through this host is a powerful end-to-end\n * check: it proves the backend never emits an invalid or leaking stream (a lenient\n * host would silently hide a double-free; this one fails loudly).\n *\n * This is **not** a renderer — it draws nothing. It is the host-side contract,\n * verifiable in Node, that the compiled native hosts satisfy in CI.\n *\n * @module\n */\n\nimport type { NativeCommand, NativeNodeId, NativePropValue } from './native-protocol'\n\n/** A reconstructed host node. */\nexport interface ReferenceHostNode {\n /** Stable id from the command stream. */\n readonly id: NativeNodeId\n /** `\"root\"` (the pre-existing container), `\"element\"`, or `\"text\"`. */\n readonly kind: 'root' | 'element' | 'text'\n /** Element tag (empty for text nodes). */\n tag: string\n /** Text content (text nodes). */\n text: string\n /** Applied props (elements). */\n props: Record<string, NativePropValue>\n /** Wired events: eventName → handlerId. */\n events: Map<string, string>\n /** Parent, or `null` when detached / the root. */\n parent: ReferenceHostNode | null\n /** Ordered children. */\n children: ReferenceHostNode[]\n}\n\n/** A reference host that applies a {@link NativeCommand} stream to a model tree. */\nexport interface ReferenceHost {\n /** Id of the root container. */\n readonly rootId: NativeNodeId\n /** The root node; its `children` mirror the rendered top-level nodes. */\n readonly root: ReferenceHostNode\n /** Apply a single command (throws {@link NativeHostError} on a malformed one). */\n apply(command: NativeCommand): void\n /** Apply a batch in order. */\n applyBatch(commands: readonly NativeCommand[]): void\n /** Look up a live node by id. */\n getNode(id: NativeNodeId): ReferenceHostNode | undefined\n /** Number of live (created, not yet disposed) nodes, excluding the root. */\n liveNodeCount(): number\n /** A compact structural string of the tree (tags + text; inspect props via {@link getNode}). */\n serialize(): string\n}\n\n/** Thrown when a command stream violates the host contract. */\nexport class NativeHostError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'NativeHostError'\n }\n}\n\n/**\n * Create a {@link ReferenceHost}. Wire it to a backend to validate + reconstruct:\n *\n * @example\n * const host = createReferenceHost()\n * const backend = createNativeCommandBackend({ rootId: host.rootId, onCommand: (c) => host.apply(c) })\n * render(MyComponent, {}, backend, backend.root)\n * host.serialize() // the reconstructed tree\n */\nexport function createReferenceHost(rootId: NativeNodeId = 'host-root'): ReferenceHost {\n const root: ReferenceHostNode = {\n id: rootId,\n kind: 'root',\n tag: 'root',\n text: '',\n props: {},\n events: new Map(),\n parent: null,\n children: [],\n }\n const nodes = new Map<NativeNodeId, ReferenceHostNode>([[rootId, root]])\n\n function requireNode(id: NativeNodeId): ReferenceHostNode {\n const node = nodes.get(id)\n if (!node) throw new NativeHostError(`command references unknown node ${String(id)}`)\n return node\n }\n\n function create(id: NativeNodeId, kind: 'element' | 'text', tag: string, text: string): void {\n if (nodes.has(id)) throw new NativeHostError(`duplicate node id ${String(id)}`)\n nodes.set(id, { id, kind, tag, text, props: {}, events: new Map(), parent: null, children: [] })\n }\n\n function detach(node: ReferenceHostNode): void {\n const parent = node.parent\n if (!parent) return\n const at = parent.children.indexOf(node)\n if (at >= 0) parent.children.splice(at, 1)\n node.parent = null\n }\n\n function apply(command: NativeCommand): void {\n switch (command.type) {\n case 'createNode':\n create(command.id, 'element', command.tag, '')\n break\n case 'createText':\n create(command.id, 'text', '', command.text)\n break\n case 'setProp':\n requireNode(command.id).props[command.name] = command.value\n break\n case 'removeProp':\n delete requireNode(command.id).props[command.name]\n break\n case 'updateText': {\n const node = requireNode(command.id)\n if (node.kind !== 'text') {\n throw new NativeHostError(`updateText on non-text node ${String(command.id)}`)\n }\n node.text = command.text\n break\n }\n case 'insertChild': {\n const parent = requireNode(command.parentId)\n const child = requireNode(command.childId)\n if (child.parent) {\n throw new NativeHostError(\n `insertChild: ${String(command.childId)} already has a parent (detach it first)`,\n )\n }\n if (command.index < 0 || command.index > parent.children.length) {\n throw new NativeHostError(`insertChild: index ${command.index} out of range`)\n }\n parent.children.splice(command.index, 0, child)\n child.parent = parent\n break\n }\n case 'removeChild': {\n const parent = requireNode(command.parentId)\n const child = requireNode(command.childId)\n if (child.parent !== parent) {\n throw new NativeHostError(\n `removeChild: ${String(command.childId)} is not a child of ${String(command.parentId)}`,\n )\n }\n detach(child)\n break\n }\n case 'disposeNode': {\n if (command.id === rootId) throw new NativeHostError('cannot dispose the root node')\n const node = requireNode(command.id) // throws if already freed → catches double dispose\n detach(node) // interior subtree nodes are freed without an explicit removeChild\n nodes.delete(command.id)\n break\n }\n case 'registerEvent':\n requireNode(command.id).events.set(command.eventName, command.handlerId)\n break\n case 'unregisterEvent':\n requireNode(command.id).events.delete(command.eventName)\n break\n default: {\n // Exhaustiveness: every NativeCommand variant is handled above.\n const _exhaustive: never = command\n throw new NativeHostError(`unknown command ${JSON.stringify(_exhaustive)}`)\n }\n }\n }\n\n function serializeNode(node: ReferenceHostNode): string {\n if (node.kind === 'text') return node.text\n const inner = node.children.map(serializeNode).join('')\n return `<${node.tag}>${inner}</${node.tag}>`\n }\n\n return {\n rootId,\n root,\n apply,\n applyBatch(commands) {\n for (const command of commands) apply(command)\n },\n getNode: (id) => nodes.get(id),\n liveNodeCount: () => nodes.size - 1,\n serialize: () => root.children.map(serializeNode).join(''),\n }\n}\n"],"mappings":"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"}
|