Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ tests/goldens/web/*.actual.png
tests/goldens/psp/*.actual.png
# irecovery readline history, written when the iPhone 2G runbook drives iBoot
.irecovery
engine/apple/dist/
90 changes: 90 additions & 0 deletions apps/nsengine/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// @title NS Engine — pocketjs guest talking to a NativeScript host
import { createSignal, onMount } from "solid-js";
import { Image, Screen, Text, View } from "@pocketjs/framework/components";
import { runEffect } from "@pocketjs/framework/effects";
import { createSpriteAnimation, onFrame } from "@pocketjs/framework/lifecycle";
import { pumpHostLines } from "./channel.ts";

const SPINNER_FRAMES = [
"spinner-00.svg",
"spinner-01.svg",
"spinner-02.svg",
"spinner-03.svg",
"spinner-04.svg",
"spinner-05.svg",
"spinner-06.svg",
"spinner-07.svg",
];

// Bakes the glyphs dynamic host strings may use (digits, punctuation).
const GLYPH_SEED = "0123456789 #:{}\"pong hello from NativeScript,.!?-_iOS via sandboxed realm";

// In a sidecar realm (Direction A) no platform globals exist; when the
// NativeScript runtime is the guest engine (Direction B), the whole iOS
// surface is one identifier away.
declare const UIDevice: { currentDevice: { systemVersion: string } } | undefined;
const platformReach = typeof UIDevice !== "undefined"
? `iOS ${UIDevice!.currentDevice.systemVersion} via NativeScript`
: "sandboxed realm";

function Stat(props: { label: string; value: string; valueClass: string }) {
return (
<View class="flex-col flex-1 gap-1 rounded-lg bg-slate-800 p-3 shadow">
<Text class="text-xs text-slate-400 tracking-wide">{props.label}</Text>
<Text class={props.valueClass}>{props.value}</Text>
</View>
);
}

export default function App() {
const [reply, setReply] = createSignal("waiting");
const [hostEvent, setHostEvent] = createSignal("none yet");
const [count, setCount] = createSignal(0);
const spinnerSrc = createSpriteAnimation(SPINNER_FRAMES, { frameStep: 5 });

onFrame(() => pumpHostLines((message) => {
setHostEvent(String(message["msg"] ?? JSON.stringify(message)));
}));

const ping = () => {
const n = count() + 1;
setCount(n);
runEffect("ns.ping", { n }, (result) => setReply(String(result)));
};

// Fire one round trip unprompted so the channel proves itself on boot.
onMount(() => ping());

return (
<Screen class="relative flex-col w-full h-full overflow-hidden justify-between p-5 bg-gradient-to-b from-slate-950 to-slate-900">
<View class="flex-row items-center justify-between">
<View class="flex-row items-center gap-3">
<Image class="w-10 h-10 rounded-lg shadow" src="logo.png" />
<View class="flex-col">
<Text class="text-base text-white font-bold tracking-wide">
PocketJS × NativeScript
</Text>
<Text class="text-xs text-slate-400 tracking-wide">
one Rust core · two JS worlds
</Text>
</View>
</View>
<Image class="w-8 h-8" src={spinnerSrc()} />
</View>

<View class="flex-row gap-3">
<Stat label="guest → host" value={reply()} valueClass="text-xs font-bold text-cyan-400" />
<Stat label="host → guest" value={hostEvent()} valueClass="text-xs font-bold text-blue-400" />
<Stat label="platform" value={platformReach} valueClass="text-xs font-bold text-emerald-400" />
</View>

<View class="flex-row items-center gap-4">
<View class="rounded-xl bg-blue-600 px-5 py-2 shadow" focusable onPress={ping}>
<Text class="text-sm font-bold text-white">Ping host · {count()}</Text>
</View>
<View class="h-1 flex-1 rounded bg-gradient-to-r from-blue-500 to-cyan-500" />
</View>
<Text class="text-slate-900">{GLYPH_SEED}</Text>
</Screen>
);
}
57 changes: 57 additions & 0 deletions apps/nsengine/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Effect driver over the ui.svc* host-service channel: commands go out as
// JSON lines (svcSend), results and host-initiated events come back through
// the per-frame poll pump (svcPoll). Protocol:
// guest -> host {t:"cmd", id, kind, payload}
// host -> guest {t:"result", id, result} | {t:"event", ...anything}

import { getOps } from "@pocketjs/framework";
import { installEffectDriver } from "@pocketjs/framework/effects";

type SvcOps = {
svcSend?: (line: string) => void;
svcPoll?: () => string | null;
};

const pendingDeliver = new Map<number, (result: unknown) => void>();

export function installSvcEffectDriver(): void {
installEffectDriver((cmd, deliver) => {
const ops = getOps() as SvcOps;
if (typeof ops.svcSend !== "function") {
return; // host without a service channel: commands drop, app stays pure
}
pendingDeliver.set(cmd.id, deliver);
ops.svcSend(JSON.stringify({ t: "cmd", id: cmd.id, kind: cmd.kind, payload: cmd.payload }));
});
}

/** Run once per frame: matches results to pending effects, forwards events. */
export function pumpHostLines(onEvent: (event: Record<string, unknown>) => void): void {
const ops = getOps() as SvcOps;
if (typeof ops.svcPoll !== "function") {
return;
}
const batch = ops.svcPoll();
if (!batch) {
return;
}
for (const line of batch.split("\n")) {
if (!line) {
continue;
}
let message: Record<string, unknown>;
try {
message = JSON.parse(line) as Record<string, unknown>;
} catch {
continue;
}
const id = message["id"];
if (message["t"] === "result" && typeof id === "number" && pendingDeliver.has(id)) {
const deliver = pendingDeliver.get(id)!;
pendingDeliver.delete(id);
deliver(message["result"]);
} else {
onEvent(message);
}
}
}
7 changes: 7 additions & 0 deletions apps/nsengine/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// @title NS Engine
import { mount } from "@pocketjs/framework/solid";
import App from "./app.tsx";
import { installSvcEffectDriver } from "./channel.ts";

installSvcEffectDriver();
mount(() => <App />);
Loading