mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(spec): shared runtime-entry installs next-action over pick.ts
This commit is contained in:
1 parent
06ae965387
commit
e18feff5fe
1 file changed
+127
@@ -0,0 +1,127 @@
|
|||||||
|
// The single next-action entry shared by both engines (goja verifier and V8 web
|
||||||
|
// runtime). installRuntime wires the spec's root generator, the shared Pcg, and
|
||||||
|
// a Host into the globals the host invokes each tick:
|
||||||
|
// __sanderlingNextAction__() -> one serialized action (unified wire contract)
|
||||||
|
// __sanderlingExtractors__() -> the engine's extractor snapshot
|
||||||
|
//
|
||||||
|
// Both engines call THIS picker over the SAME Pcg, so a given seed yields an
|
||||||
|
// identical action stream by construction.
|
||||||
|
|
||||||
|
import { Pcg } from "./pcg.ts";
|
||||||
|
import { nextAction } from "./pick.ts";
|
||||||
|
import type { ActionDescriptor, GeneratorNode, Host } from "./action-tree.ts";
|
||||||
|
import type { Point } from "./types.ts";
|
||||||
|
|
||||||
|
// SerializedAction is the flat, camelCase wire shape JS emits and Go decodes
|
||||||
|
// (ONE decoder on each side). Builtin targets are already resolved to a Point,
|
||||||
|
// and serializeAction collapses author targets that carry {x, y}; a target that
|
||||||
|
// does not resolve to coordinates drops the action (returns null).
|
||||||
|
export type SerializedAction =
|
||||||
|
| { kind: "Tap" | "DoubleTap" | "LongPress"; x: number; y: number; selector?: string }
|
||||||
|
| { kind: "InputText"; x: number; y: number; text: string; selector?: string }
|
||||||
|
| { kind: "Swipe"; fromX: number; fromY: number; toX: number; toY: number; durationMillis: number }
|
||||||
|
| {
|
||||||
|
kind: "Scroll";
|
||||||
|
direction: string;
|
||||||
|
fromX: number;
|
||||||
|
fromY: number;
|
||||||
|
toX: number;
|
||||||
|
toY: number;
|
||||||
|
durationMillis: number;
|
||||||
|
}
|
||||||
|
| { kind: "PressKey"; key: string }
|
||||||
|
| { kind: "Wait"; durationMillis: number };
|
||||||
|
|
||||||
|
const DEFAULT_SWIPE_DURATION = 250;
|
||||||
|
|
||||||
|
// pointOf resolves a target to {x, y, selector?}. Builtins already supply a
|
||||||
|
// Point; author targets that carry numeric x/y (or a resolved element handle)
|
||||||
|
// collapse the same way. A string/selector target the host could not resolve
|
||||||
|
// returns undefined so the action is dropped.
|
||||||
|
function pointOf(target: unknown): (Point & { selector?: string }) | undefined {
|
||||||
|
if (!target || typeof target !== "object") return undefined;
|
||||||
|
const obj = target as Record<string, unknown>;
|
||||||
|
if (typeof obj.x === "number" && typeof obj.y === "number") {
|
||||||
|
const point: Point & { selector?: string } = { x: obj.x, y: obj.y };
|
||||||
|
if (typeof obj.selector === "string") point.selector = obj.selector;
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeAction(action: ActionDescriptor | null): SerializedAction | null {
|
||||||
|
if (!action) return null;
|
||||||
|
switch (action.kind) {
|
||||||
|
case "Tap":
|
||||||
|
case "DoubleTap":
|
||||||
|
case "LongPress": {
|
||||||
|
const point = pointOf(action.on);
|
||||||
|
if (!point) return null;
|
||||||
|
const out: SerializedAction = { kind: action.kind, x: point.x, y: point.y };
|
||||||
|
if (point.selector) out.selector = point.selector;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case "InputText": {
|
||||||
|
const point = pointOf(action.into);
|
||||||
|
if (!point) return null;
|
||||||
|
const out: SerializedAction = { kind: "InputText", x: point.x, y: point.y, text: action.text };
|
||||||
|
if (point.selector) out.selector = point.selector;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case "Swipe": {
|
||||||
|
const from = pointOf(action.from);
|
||||||
|
const to = pointOf(action.to);
|
||||||
|
if (!from || !to) return null;
|
||||||
|
return {
|
||||||
|
kind: "Swipe",
|
||||||
|
fromX: from.x,
|
||||||
|
fromY: from.y,
|
||||||
|
toX: to.x,
|
||||||
|
toY: to.y,
|
||||||
|
durationMillis: action.durationMillis ?? DEFAULT_SWIPE_DURATION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "Scroll": {
|
||||||
|
const from = pointOf(action.in);
|
||||||
|
if (!from) return null;
|
||||||
|
return {
|
||||||
|
kind: "Scroll",
|
||||||
|
direction: action.direction,
|
||||||
|
fromX: from.x,
|
||||||
|
fromY: from.y,
|
||||||
|
toX: from.x,
|
||||||
|
toY: from.y,
|
||||||
|
durationMillis: DEFAULT_SWIPE_DURATION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "PressKey":
|
||||||
|
return { kind: "PressKey", key: action.key };
|
||||||
|
case "Wait":
|
||||||
|
return { kind: "Wait", durationMillis: action.durationMillis };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installRuntime defines the next-action and extractor globals for one engine.
|
||||||
|
// root is the generator the spec assigned (globalThis.actions); evaluateExtractors
|
||||||
|
// is the engine's snapshot of the spec's extract() handles.
|
||||||
|
export function installRuntime(
|
||||||
|
host: Host,
|
||||||
|
root: GeneratorNode | null,
|
||||||
|
evaluateExtractors: () => Record<number, unknown>,
|
||||||
|
): void {
|
||||||
|
const rng = new Pcg(host.seedHi(), host.seedLo());
|
||||||
|
defineLockedGlobal("__sanderlingExtractors__", () => evaluateExtractors());
|
||||||
|
defineLockedGlobal("__sanderlingNextAction__", () => {
|
||||||
|
if (!root) return null;
|
||||||
|
return serializeAction(nextAction(root, rng, host));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineLockedGlobal(name: string, value: unknown): void {
|
||||||
|
Object.defineProperty(globalThis, name, {
|
||||||
|
value,
|
||||||
|
writable: false,
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user