Skip to content

Commit f593e9a

Browse files
committed
feat: Initial attempt at wrapping Runtime in a worker within shell.
1 parent 2216fa9 commit f593e9a

35 files changed

+3818
-652
lines changed

packages/cli/lib/charm-render.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { render, vdomSchema, VNode } from "@commontools/html";
2-
import { Cell, UI } from "@commontools/runner";
2+
import { UI } from "@commontools/runner";
3+
import { type RemoteCell } from "@commontools/runner/worker";
34
import { loadManager } from "./charm.ts";
45
import { CharmsController } from "@commontools/charm/ops";
56
import type { CharmConfig } from "./charm.ts";
@@ -53,7 +54,11 @@ export async function renderCharm(
5354
if (options.watch) {
5455
// 4a. Reactive rendering - pass the Cell directly
5556
const uiCell = cell.key(UI);
56-
const cancel = render(container, uiCell as Cell<VNode>, renderOptions); // FIXME: types
57+
const cancel = render(
58+
container,
59+
uiCell as unknown as RemoteCell<VNode>,
60+
renderOptions,
61+
); // FIXME: types
5762

5863
// 5a. Set up monitoring for changes
5964
let updateCount = 0;

packages/html/deno.jsonc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
".": "./src/index.ts",
88
"./utils": "./src/utils.ts",
99
"./jsx-runtime": "./src/jsx-runtime.ts",
10-
"./jsx-dev-runtime": "./src/jsx-dev-runtime.ts"
10+
"./jsx-dev-runtime": "./src/jsx-dev-runtime.ts",
11+
"./mock-doc": "./src/mock-doc.ts"
1112
},
1213
"imports": {
1314
"htmlparser2": "npm:htmlparser2",

packages/html/src/mock-doc.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export class MockDoc {
8080
return DomUtils.removeElement(this as any);
8181
},
8282
},
83+
replaceWith: {
84+
value(newNode: any) {
85+
return DomUtils.replaceElement(this as any, newNode);
86+
},
87+
},
8388
innerHTML: {
8489
get() {
8590
return domserializer.render((this as any).children);

packages/html/src/render.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { isObject, isRecord } from "@commontools/utils/types";
22
import {
33
type Cancel,
4-
type Cell,
54
convertCellsToLinks,
65
effect,
7-
isCell,
86
type JSONSchema,
97
UI,
108
useCancelGroup,
119
} from "@commontools/runner";
10+
import { isRemoteCell, type RemoteCell } from "@commontools/runner/worker";
1211
import { isVNode, type Props, type RenderNode, type VNode } from "./jsx.ts";
1312
import * as logger from "./logger.ts";
1413

@@ -22,7 +21,7 @@ export interface RenderOptions {
2221
setProp?: SetPropHandler;
2322
document?: Document;
2423
/** The root cell for auto-wrapping with ct-cell-context on [UI] traversal */
25-
rootCell?: Cell;
24+
rootCell?: RemoteCell<VNode>;
2625
}
2726

2827
export const vdomSchema: JSONSchema = {
@@ -60,25 +59,30 @@ export const vdomSchema: JSONSchema = {
6059
*/
6160
export const render = (
6261
parent: HTMLElement,
63-
view: VNode | Cell<VNode>,
62+
view: VNode | RemoteCell<VNode>,
6463
options: RenderOptions = {},
6564
): Cancel => {
66-
// Initialize visited set with the original cell for cycle detection
67-
const visited = new Set<object>();
68-
let rootCell: Cell | undefined;
69-
70-
if (isCell(view)) {
71-
visited.add(view);
72-
rootCell = view; // Capture the original cell for ct-cell-context wrapping
73-
view = view.asSchema(vdomSchema);
65+
let rootCell: RemoteCell<VNode> | undefined;
66+
67+
if (isRemoteCell(view)) {
68+
rootCell = view as RemoteCell<VNode>; // Capture the original cell for ct-cell-context wrapping
69+
view = rootCell.asSchema(vdomSchema) as RemoteCell<VNode>;
7470
}
7571

7672
// Pass rootCell through options if we have one
7773
const optionsWithCell = rootCell ? { ...options, rootCell } : options;
7874

7975
return effect(
80-
view,
81-
(view: VNode) => renderImpl(parent, view, optionsWithCell, visited),
76+
view as VNode,
77+
(view: VNode) => {
78+
// Create a fresh visited set for each render pass.
79+
// This prevents false cycle detection when re-rendering with updated values.
80+
const visited = new Set<object>();
81+
if (rootCell) {
82+
visited.add(rootCell);
83+
}
84+
return renderImpl(parent, view, optionsWithCell, visited);
85+
},
8286
);
8387
};
8488

@@ -131,7 +135,7 @@ const createCyclePlaceholder = (document: Document): HTMLSpanElement => {
131135
/** Check if a cell has been visited, using .equals() for cell comparison */
132136
const hasVisitedCell = (
133137
visited: Set<object>,
134-
cell: Cell<unknown>,
138+
cell: { equals(other: unknown): boolean },
135139
): boolean => {
136140
for (const item of visited) {
137141
if (cell.equals(item)) {
@@ -207,7 +211,7 @@ const renderNode = (
207211
if (cellForContext && element) {
208212
const wrapper = document.createElement(
209213
"ct-cell-context",
210-
) as HTMLElement & { cell?: Cell };
214+
) as HTMLElement & { cell?: RemoteCell<VNode> };
211215
wrapper.cell = cellForContext;
212216
wrapper.appendChild(element);
213217
return [wrapper, cancel];
@@ -235,13 +239,16 @@ const bindChildren = (
235239
const document = options.document ?? globalThis.document;
236240

237241
// Check for cell cycle before setting up effect (using .equals() for comparison)
238-
if (isCell(child) && hasVisitedCell(visited, child)) {
242+
if (
243+
isRenderableCell(child) &&
244+
hasVisitedCell(visited, child as unknown as RemoteCell<unknown>)
245+
) {
239246
logger.warn("render", "Cycle detected in cell graph", child);
240247
return { node: createCyclePlaceholder(document), cancel: () => {} };
241248
}
242249

243250
// Track if this child is a cell for the visited set
244-
const childIsCell = isCell(child);
251+
const childIsCell = isRenderableCell(child);
245252

246253
let currentNode: ChildNode | null = null;
247254
const cancel = effect(child, (childValue) => {
@@ -377,7 +384,7 @@ const bindProps = (
377384
const setProperty = options.setProp ?? setProp;
378385
const [cancel, addCancel] = useCancelGroup();
379386
for (const [propKey, propValue] of Object.entries(props)) {
380-
if (isCell(propValue)) {
387+
if (isRenderableCell(propValue)) {
381388
// If prop is an event, we need to add an event listener
382389
if (isEventProp(propKey)) {
383390
const key = cleanEventProp(propKey);
@@ -538,10 +545,10 @@ const sanitizeScripts = (node: VNode): VNode | null => {
538545
if (node.name === "script") {
539546
return null;
540547
}
541-
if (!isCell(node.props) && !isObject(node.props)) {
548+
if (!isRenderableCell(node.props) && !isObject(node.props)) {
542549
node = { ...node, props: {} };
543550
}
544-
if (!isCell(node.children) && !Array.isArray(node.children)) {
551+
if (!isRenderableCell(node.children) && !Array.isArray(node.children)) {
545552
node = { ...node, children: [] };
546553
}
547554

@@ -654,3 +661,12 @@ function isSelectElement(value: unknown): value is HTMLSelectElement {
654661
typeof value.tagName === "string" &&
655662
value.tagName.toUpperCase() === "SELECT");
656663
}
664+
665+
type RenderableCell = {
666+
send(value: unknown): void;
667+
};
668+
function isRenderableCell(value: unknown): value is RenderableCell {
669+
// Check for any object with a send() method (Cell, RemoteCell, or Stream)
670+
return !!value && typeof value === "object" && "send" in value &&
671+
typeof (value as RenderableCell).send === "function";
672+
}

packages/html/test/html-recipes.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type IExtendedStorageTransaction,
88
Runtime,
99
} from "@commontools/runner";
10+
import { type RemoteCell } from "@commontools/runner/worker";
1011
import { StorageManager } from "@commontools/runner/storage/cache.deno";
1112
import * as assert from "./assert.ts";
1213
import { Identity } from "@commontools/identity";
@@ -167,7 +168,7 @@ describe("recipes with HTML", () => {
167168

168169
const root = document.getElementById("root")!;
169170
const cell = result.key(UI);
170-
render(root, cell, renderOptions);
171+
render(root, cell as unknown as RemoteCell<VNode>, renderOptions);
171172
assert.equal(root.innerHTML, "<div><div>test</div></div>");
172173
});
173174

@@ -277,7 +278,11 @@ describe("recipes with HTML", () => {
277278
await runtime.idle();
278279

279280
const root = document.getElementById("root")!;
280-
render(root, cell1.key("ui"), renderOptions);
281+
render(
282+
root,
283+
cell1.key("ui") as unknown as RemoteCell<VNode>,
284+
renderOptions,
285+
);
281286

282287
// Should detect the cycle and render placeholder, not infinite loop
283288
// MockDoc doesn't properly reflect textContent/title in innerHTML,

packages/identity/src/identity.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,17 @@ export class Identity<ID extends DIDKey = DIDKey> implements Signer<ID> {
4646
}
4747

4848
// Derive a new `Identity` given a seed string.
49-
async derive<ID extends DIDKey>(name: string): Promise<Identity<ID>> {
49+
async derive<ID extends DIDKey>(
50+
name: string,
51+
config: IdentityCreateConfig = {},
52+
): Promise<Identity<ID>> {
5053
const seed = textEncoder.encode(name);
5154
const { ok: signed, error } = await this.sign(seed);
5255
if (error) {
5356
throw error;
5457
}
5558
const signedHash = await hash(signed);
56-
return await Identity.fromRaw(new Uint8Array(signedHash));
59+
return await Identity.fromRaw(new Uint8Array(signedHash), config);
5760
}
5861

5962
// Derive PKCS8/PEM bytes from this identity.

packages/runner/deno.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
"name": "@commontools/runner",
33
"tasks": {
44
"test": "deno test --allow-ffi --allow-env --allow-read test/*.test.ts",
5-
"integration": "LOG_LEVEL=warn deno test -A ./integration/*.test.ts"
5+
"integration": "LOG_LEVEL=warn deno test -A ./integration/worker.test.ts"
66
},
77
"exports": {
88
".": "./src/index.ts",
9+
"./worker": "./src/worker/index.ts",
10+
"./worker-script": "./src/worker/worker-runtime.ts",
911
"./storage/inspector": "./src/storage/inspector.ts",
1012
"./storage/telemetry": "./src/storage/telemetry.ts",
1113
"./traverse": "./src/traverse.ts",

0 commit comments

Comments
 (0)