@proteus-vue/render-backend 0.3.0-beta.21 → 0.3.0-beta.26
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/dist/app-navigation.d.ts +57 -0
- package/dist/app-navigation.js +3017 -0
- package/dist/host-conformance.d.ts +7 -0
- package/dist/host-invoke-contract.d.ts +8 -0
- package/dist/host-invoke-contract.js +47 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +3007 -56
- package/dist/native.d.ts +67 -1
- package/dist/quickjs-host.d.ts +77 -0
- package/dist/screen-executor-host.d.ts +67 -0
- package/dist/screen-executor-host.js +151 -0
- package/dist/screen-executor.d.ts +239 -0
- package/dist/screen-executor.js +165 -0
- package/dist/screen-runtime.d.ts +96 -0
- package/dist/selfdraw-batch.d.ts +47 -0
- package/dist/superapp-runtime.d.ts +100 -0
- package/dist/tab-bar-spec.d.ts +35 -0
- package/dist/tab-bar-spec.js +17 -0
- package/package.json +23 -5
package/dist/native.d.ts
CHANGED
|
@@ -28,6 +28,69 @@ export interface MockNativeAdapter extends NativeViewAdapter {
|
|
|
28
28
|
/** 操作日志(断言 create/insert/update/remove/setText 顺序) */
|
|
29
29
|
ops: string[];
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* ★★C1:**宿主操作批次**(批量协议的元素)——一次跨边界调用携带整批变更。
|
|
33
|
+
*
|
|
34
|
+
* 【为什么必须有(Host ABI 的批处理红线)】`docs/Proteus_HostABI宿主抽象层设计方案.md` §3 原文:
|
|
35
|
+
* 「**所有跨边界调用必须是批处理的**」+ 红线「跨边界调用 = 帧数」。
|
|
36
|
+
* 而逐节点命令式适配器(`NativeViewAdapter`)在 4050 节点树上会产生
|
|
37
|
+
* **数千次跨边界调用**(每节点 create + insert + ...)⇒ 直接违反该红线。
|
|
38
|
+
*
|
|
39
|
+
* 【真机链路本来就是这样做的(I6 评估的实测证据)】真机宿主协议是**批量**的:
|
|
40
|
+
* `mount(treeJson)` / `updatePatches(patchesJson)` / `applyOps(bytesJson)` —— 每帧**一次**调用。
|
|
41
|
+
* ⇒ 本类型把"真机已用的批量形态"**显式建模进 SPI**,让 NativeBackend 能表达它。
|
|
42
|
+
*/
|
|
43
|
+
export type NativeHostOp =
|
|
44
|
+
/** 建视图(宿主侧创建 UIView/View/ArkUI Node) */
|
|
45
|
+
{
|
|
46
|
+
op: 'create';
|
|
47
|
+
id: number;
|
|
48
|
+
type: string;
|
|
49
|
+
props: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/** 插入子视图(anchor 可选——同批次内按序应用) */
|
|
52
|
+
| {
|
|
53
|
+
op: 'insert';
|
|
54
|
+
id: number;
|
|
55
|
+
parentId: number | null;
|
|
56
|
+
anchorId?: number;
|
|
57
|
+
}
|
|
58
|
+
/** 移除视图 */
|
|
59
|
+
| {
|
|
60
|
+
op: 'remove';
|
|
61
|
+
id: number;
|
|
62
|
+
}
|
|
63
|
+
/** 属性/样式变更 */
|
|
64
|
+
| {
|
|
65
|
+
op: 'patch';
|
|
66
|
+
id: number;
|
|
67
|
+
key: string;
|
|
68
|
+
prev: unknown;
|
|
69
|
+
next: unknown;
|
|
70
|
+
}
|
|
71
|
+
/** 文本同步 */
|
|
72
|
+
| {
|
|
73
|
+
op: 'text';
|
|
74
|
+
id: number;
|
|
75
|
+
text: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* ★★C1:**批量宿主适配器**(与逐节点 `NativeViewAdapter` 并列的第二形态)。
|
|
79
|
+
*
|
|
80
|
+
* 【两者的分工(本评估的结论,勿混用)】
|
|
81
|
+
* · `NativeViewAdapter`(逐节点)= **原型形态**:接口直观,适合简单宿主/测试;
|
|
82
|
+
* 但**不满足批处理红线** ⇒ 不能作为生产形态。
|
|
83
|
+
* · `NativeBatchAdapter`(批量)= **生产形态**:一次 `commit` 携带整批 ⇒ 跨边界调用数 = flush 次数。
|
|
84
|
+
*
|
|
85
|
+
* 【与真机链路的对应】`commit(ops)` 即真机 `mount`/`updatePatches`/`applyOps` 的统一抽象:
|
|
86
|
+
* 宿主侧把批次翻译成自己的渲染调用(iOS CALayer / Android Canvas / ArkUI)。
|
|
87
|
+
*/
|
|
88
|
+
export interface NativeBatchAdapter {
|
|
89
|
+
/** 提交一批变更(★**一次跨边界调用**——调用方保证一批 = 一帧的变更) */
|
|
90
|
+
commit(ops: readonly NativeHostOp[]): void;
|
|
91
|
+
/** 已提交批次数(诊断/判据用——跨边界调用计数) */
|
|
92
|
+
commitCount?(): number;
|
|
93
|
+
}
|
|
31
94
|
/** 内置 mock 适配器(无宿主环境验证接线;真实平台 B4 后接 SDK 实现替换) */
|
|
32
95
|
export declare function createMockNativeAdapter(): MockNativeAdapter;
|
|
33
96
|
/** 原生平台(iOS UIKit / Android Jetpack / 鸿蒙 ArkUI) */
|
|
@@ -39,4 +102,7 @@ export type NativePlatform = 'ios' | 'android' | 'harmony';
|
|
|
39
102
|
* - adapter 缺省 mock(ops 日志);真实平台注入 SDK 桥
|
|
40
103
|
* - platform:ios(UIKit 基准)/ android(Jetpack)/ harmony(ArkUI)——决定 id + semantic 映射表
|
|
41
104
|
*/
|
|
42
|
-
export declare function createNativeBackend(adapter?: NativeViewAdapter, platform?: NativePlatform): ProteusRenderBackend
|
|
105
|
+
export declare function createNativeBackend(adapter?: NativeViewAdapter | NativeBatchAdapter, platform?: NativePlatform): ProteusRenderBackend & {
|
|
106
|
+
flush(): void;
|
|
107
|
+
hostCalls(): number;
|
|
108
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { HostRuntimeLike } from './host-conformance';
|
|
2
|
+
/** 宿主原生调用通道(宿主壳注入:Android = proteusHost.* / iOS = JSExport;**同步**返回 JSON 串) */
|
|
3
|
+
export interface NativeTransport {
|
|
4
|
+
/** 调用宿主(同步;返回 JSON 字符串;抛错 = 原生调用失败,语义与 JNI 异常对齐) */
|
|
5
|
+
call(name: string, argsJson: string): string;
|
|
6
|
+
}
|
|
7
|
+
/** 职责边界拒绝记录(G-39.1「唯一拥有」的机器证据——非法操作**被拒绝**而不是被静默吞掉) */
|
|
8
|
+
export interface RuntimeRefusal {
|
|
9
|
+
op: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
/** 拒绝时的状态(诊断用) */
|
|
12
|
+
state: string;
|
|
13
|
+
}
|
|
14
|
+
/** 能力自描述(G-39.3 诚实声明——框架据此决定降级,而不是运行时假装支持) */
|
|
15
|
+
export interface QuickJsHostCapabilities {
|
|
16
|
+
threads: {
|
|
17
|
+
main: boolean;
|
|
18
|
+
background: boolean;
|
|
19
|
+
count: number;
|
|
20
|
+
};
|
|
21
|
+
engine: 'quickjs' | 'jsc' | 'node' | 'none';
|
|
22
|
+
nativeBridge: boolean;
|
|
23
|
+
lifecycle: 'full' | 'basic' | 'none';
|
|
24
|
+
/** 帧驱动源(诊断:谁在调 pumpFrame) */
|
|
25
|
+
frameDriver: string;
|
|
26
|
+
}
|
|
27
|
+
/** 逻辑 worker 域(**无真并行**——诚实声明;`real=false` 是"单线程宿主"的机器可读标记) */
|
|
28
|
+
export interface LogicalWorkerHandle {
|
|
29
|
+
id: string;
|
|
30
|
+
thread: string;
|
|
31
|
+
/** ★恒为 false:本宿主无真并行(有真 Worker 的宿主见 web-host 的 WebWorkerHandle.raw) */
|
|
32
|
+
real: false;
|
|
33
|
+
}
|
|
34
|
+
export interface QuickJsHostRuntime extends HostRuntimeLike {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly capabilities: QuickJsHostCapabilities;
|
|
37
|
+
/** ★宿主帧驱动入口(Android Choreographer / iOS CADisplayLink 调用)——返回本帧执行的任务数 */
|
|
38
|
+
pumpFrame(): number;
|
|
39
|
+
/** 生命周期由宿主壳转发(Activity onPause/onResume → 这里)——事件流是治理证据 */
|
|
40
|
+
readonly lifecycleEvents: ReadonlyArray<'suspend' | 'resume' | 'destroy'>;
|
|
41
|
+
/** 职责边界拒绝日志(非法操作被拒绝的机器证据;不断言"我们不会犯错",断言"犯错会被拒绝") */
|
|
42
|
+
readonly refusals: ReadonlyArray<RuntimeRefusal>;
|
|
43
|
+
/** 注册 JS→原生方向的原生处理器(宿主壳也可以注册;业务不得绕过 runtime 直连宿主——G-39.1) */
|
|
44
|
+
registerNativeHandler(name: string, handler: (args: unknown) => unknown): void;
|
|
45
|
+
/** 调原生:注册的处理器优先,否则走 transport;未注册/未导出 ⇒ **拒绝并记账**(不静默) */
|
|
46
|
+
invokeNative(name: string, args?: unknown): Promise<unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* 线程委派(G-39 计划面):`'main'` ⇒ 入队(由 pumpFrame 消费);
|
|
49
|
+
* `'background'` ⇒ **诚实拒绝**(capabilities.threads.background=false)——
|
|
50
|
+
* ★这正是 G-39 要治的病:「各 Backend 自己 pthread_create」在本宿主里**没有后门**,
|
|
51
|
+
* 要么查 capabilities 走降级,要么被 runtime 拒绝(记账可观测)。
|
|
52
|
+
*/
|
|
53
|
+
runOnThread(thread: 'main' | 'background', task: () => void): void;
|
|
54
|
+
/** 已注册的原生处理器名(诊断) */
|
|
55
|
+
readonly nativeHandlers: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface QuickJsHostOptions {
|
|
58
|
+
/** 宿主标识('android' | 'ios' | 'harmony' | 诊断用) */
|
|
59
|
+
id?: string;
|
|
60
|
+
/** JS 引擎标识(诚实声明) */
|
|
61
|
+
engine?: QuickJsHostCapabilities['engine'];
|
|
62
|
+
/** 帧驱动源名(诊断:'Choreographer' / 'CADisplayLink' / 'manual') */
|
|
63
|
+
frameDriver?: string;
|
|
64
|
+
/** 原生调用通道(缺省无桥:invokeNative 未命中处理器时**明确拒绝**) */
|
|
65
|
+
transport?: NativeTransport;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* ★★G-39 B4:创建单线程 JS 引擎宿主运行时(Android QuickJS / iOS JSC 同族骨架)
|
|
69
|
+
*
|
|
70
|
+
* 职责边界(G-39.1「唯一拥有」在本实现的落点):
|
|
71
|
+
* · **生命周期**:只有 runtime 拥有状态机;宿主壳(Activity)只能**转发**事件;非法转换被拒绝并记账。
|
|
72
|
+
* · **线程**:只有 runtime.createWorker 能产生新的执行域;`runOnThread('background')` 在本宿主
|
|
73
|
+
* 诚实拒绝(threads.background=false)——调用方应先查 capabilities(G-39.3)。
|
|
74
|
+
* · **事件循环**:只有 runtime.pumpFrame 消费队列(宿主帧驱动);destroy 清空队列(不悬空)。
|
|
75
|
+
* · **原生桥**:唯一出口 invokeNative;未注册 ⇒ 拒绝 + 记账(不静默返回 undefined)。
|
|
76
|
+
*/
|
|
77
|
+
export declare function createQuickJsHostRuntime(opts?: QuickJsHostOptions): QuickJsHostRuntime;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ScreenAnimHost, ScreenTreeHost } from './screen-executor';
|
|
2
|
+
/** 宿主通道(与 `capability-app.ts` 的 `invokeHost` 同形:同步请求/响应字符串) */
|
|
3
|
+
export type HostInvokeChannel = (method: string, argsJson: string) => string;
|
|
4
|
+
/** 完成回调的全局键(宿主回推用——与 `__proteusHostAppEvent` 同一"约定即接口"模式) */
|
|
5
|
+
export declare const SCREEN_ANIM_DONE_KEY = "__proteusHostScreenAnimDone";
|
|
6
|
+
export interface HostScreenPortsOptions {
|
|
7
|
+
/** 宿主通道(生产:`invokeHost`;测试:记录桩) */
|
|
8
|
+
invoke: HostInvokeChannel;
|
|
9
|
+
/**
|
|
10
|
+
* 是否安装完成回调钩子(缺省 true)。
|
|
11
|
+
* ★与 `installAppEventSource` 同一防御姿态:QuickJS 的全局可能不可写 ⇒ try + defineProperty 回退,
|
|
12
|
+
* 且**失败不阻断**(动画完成退化为"宿主不回调"——调用方需自行兜底)。
|
|
13
|
+
*/
|
|
14
|
+
installAnimDoneHook?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface HostScreenPorts {
|
|
17
|
+
/** 树操作端口(执行器用) */
|
|
18
|
+
tree: ScreenTreeHost;
|
|
19
|
+
/** 动画端口(执行器用) */
|
|
20
|
+
anim: ScreenAnimHost;
|
|
21
|
+
/**
|
|
22
|
+
* ★★**跨页面共享元素**(2026-10-01 收诚实边界):两棵内核树之间的飞行。
|
|
23
|
+
*
|
|
24
|
+
* 与同树共享元素的差别:源几何由**调用方注入**(跨页面的稳态起点只有页面栈层知道——
|
|
25
|
+
* 内核只认识"当前树")。
|
|
26
|
+
*
|
|
27
|
+
* 用法(页面栈层):目标页 mount 后 → `rect(目标屏, 目标节点)` 取终点基准 →
|
|
28
|
+
* 以「源页当前矩形」为 `sourceRect` 调 `fly(...)` ⇒ 内核算 dx/dy/scale 并写首帧 ⇒
|
|
29
|
+
* 宿主帧循环推进(与转场**同一条**完成链)。
|
|
30
|
+
*/
|
|
31
|
+
shared: {
|
|
32
|
+
/** 单节点绝对矩形(内核算;跨页面几何回传的入口) */
|
|
33
|
+
rect(screenId: string, nodeId: number): Promise<{
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
w: number;
|
|
37
|
+
h: number;
|
|
38
|
+
}>;
|
|
39
|
+
/** 启动跨页面飞行(源=系统坐标矩形;目标=目标屏的节点) */
|
|
40
|
+
fly(opts: {
|
|
41
|
+
targetScreenId: string;
|
|
42
|
+
targetNodeId: number;
|
|
43
|
+
sourceRect: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
w: number;
|
|
47
|
+
h: number;
|
|
48
|
+
};
|
|
49
|
+
durMs?: number;
|
|
50
|
+
curve?: number;
|
|
51
|
+
fadeIn?: boolean;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
fromRect?: unknown;
|
|
54
|
+
toRect?: unknown;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
/** 在途动画 promise 数(诊断:应回落到 0) */
|
|
58
|
+
readonly pendingAnimations: number;
|
|
59
|
+
/** 完成回调是否已装(判据读——未装时"动画播完"不可达) */
|
|
60
|
+
readonly animDoneHookInstalled: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 创建**生产端口**(执行器 ↔ 宿主通道)。
|
|
64
|
+
*
|
|
65
|
+
* ★返回值是端口对 + 读数;执行器照常 `createScreenExecutor({host: ports.tree, anim: ports.anim, plan})`。
|
|
66
|
+
*/
|
|
67
|
+
export declare function createHostScreenPorts(opts: HostScreenPortsOptions): HostScreenPorts;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/screen-executor-host.ts
|
|
2
|
+
var SCREEN_ANIM_DONE_KEY = "__proteusHostScreenAnimDone";
|
|
3
|
+
var instSeq = 0;
|
|
4
|
+
function call(channel, method, args) {
|
|
5
|
+
const raw = channel(method, JSON.stringify(args ?? null));
|
|
6
|
+
let parsed;
|
|
7
|
+
try {
|
|
8
|
+
parsed = JSON.parse(raw);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
throw new Error(`[screen-host] ${method} \u56DE\u6267\u975E JSON\uFF1A${String(raw).slice(0, 120)}`);
|
|
11
|
+
}
|
|
12
|
+
if (parsed && parsed.ok === false) {
|
|
13
|
+
throw new Error(`[screen-host] ${method} \u5931\u8D25${parsed.missing ? "\uFF08\u672A\u5B9E\u73B0\uFF09" : ""}\uFF1A${parsed.reason ?? "\u65E0\u539F\u56E0"}`);
|
|
14
|
+
}
|
|
15
|
+
return parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : parsed;
|
|
16
|
+
}
|
|
17
|
+
function createHostScreenPorts(opts) {
|
|
18
|
+
const channel = opts.invoke;
|
|
19
|
+
const pending = /* @__PURE__ */ new Map();
|
|
20
|
+
let tokenSeq = 0;
|
|
21
|
+
let hookInstalled = false;
|
|
22
|
+
const INSTANCE_ID = `s${++instSeq}-`;
|
|
23
|
+
const resolveOne = (token, payload) => {
|
|
24
|
+
const r = pending.get(token);
|
|
25
|
+
if (!r) return false;
|
|
26
|
+
pending.delete(token);
|
|
27
|
+
r(payload);
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
if (opts.installAnimDoneHook !== false) {
|
|
31
|
+
const g = globalThis;
|
|
32
|
+
const REGISTRY_KEY = `${SCREEN_ANIM_DONE_KEY}__registry`;
|
|
33
|
+
let registry = g[REGISTRY_KEY];
|
|
34
|
+
if (!Array.isArray(registry)) {
|
|
35
|
+
registry = [];
|
|
36
|
+
g[REGISTRY_KEY] = registry;
|
|
37
|
+
}
|
|
38
|
+
const handler = (token, resultJson) => {
|
|
39
|
+
if (typeof token !== "string") return "bad-token";
|
|
40
|
+
const r = registry;
|
|
41
|
+
for (let i = 0; i < r.length; i++) {
|
|
42
|
+
if (r[i](token, resultJson)) return "ok";
|
|
43
|
+
}
|
|
44
|
+
return "unknown-token";
|
|
45
|
+
};
|
|
46
|
+
registry.push((token, payload) => resolveOne(token, payload));
|
|
47
|
+
try {
|
|
48
|
+
g[SCREEN_ANIM_DONE_KEY] = handler;
|
|
49
|
+
hookInstalled = true;
|
|
50
|
+
} catch {
|
|
51
|
+
try {
|
|
52
|
+
Object.defineProperty(g, SCREEN_ANIM_DONE_KEY, { value: handler, writable: true, configurable: true });
|
|
53
|
+
hookInstalled = true;
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const tree = {
|
|
59
|
+
mountScreen(screen) {
|
|
60
|
+
const d = call(channel, "screen.mount", {
|
|
61
|
+
screenId: screen.screenId,
|
|
62
|
+
name: screen.name,
|
|
63
|
+
path: screen.path,
|
|
64
|
+
params: screen.params,
|
|
65
|
+
rebuild: screen.rebuild,
|
|
66
|
+
// ★★★GP3-c:**三层容器计划**随 mount 下发(宿主照此建树——偏移量,宿主加到自己分配的根 id 上)。
|
|
67
|
+
// 执行器已按契约算好(见 screen-executor.ts);本层只做**透传**(不再重复计算——
|
|
68
|
+
// 本仓纪律:同一件事两份实现 = 修一份等于没修)。
|
|
69
|
+
...screen.layerContainers ? { layerContainers: screen.layerContainers } : {},
|
|
70
|
+
// ★★★阶段 1(2026-10-04 · App 三端对齐 B1+B2):**屏内容**(真实页面渲染产物)透传。
|
|
71
|
+
// 执行器按 `contentOf` 提供者解析后放入 mountScreen 入参;本层只做转发(与上方同款纪律)。
|
|
72
|
+
...screen.content ? { content: screen.content } : {}
|
|
73
|
+
});
|
|
74
|
+
const rootNodeId = Number(d?.rootNodeId ?? 0);
|
|
75
|
+
if (!rootNodeId) {
|
|
76
|
+
throw new Error(`[screen-host] screen.mount \u672A\u8FD4\u56DE rootNodeId\uFF08${JSON.stringify(d)}\uFF09\u2014\u2014\u5BBF\u4E3B\u5B9E\u73B0\u4E0D\u5B8C\u6574`);
|
|
77
|
+
}
|
|
78
|
+
return rootNodeId;
|
|
79
|
+
},
|
|
80
|
+
setScreenVisible(screenId, visible, rootNodeId) {
|
|
81
|
+
call(channel, "screen.visible", { screenId, visible, rootNodeId });
|
|
82
|
+
},
|
|
83
|
+
destroyScreen(screenId, reason, rootNodeId) {
|
|
84
|
+
call(channel, "screen.destroy", { screenId, reason, rootNodeId });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const anim = {
|
|
88
|
+
playRouteTransition(plan, ctx) {
|
|
89
|
+
const anims = [...plan.incoming.anims, ...plan.outgoing.anims];
|
|
90
|
+
if (anims.length === 0) return;
|
|
91
|
+
const token = `${INSTANCE_ID}screen-anim-${++tokenSeq}`;
|
|
92
|
+
const d = call(channel, "screen.anim", {
|
|
93
|
+
anims,
|
|
94
|
+
durationMs: plan.durationMs,
|
|
95
|
+
direction: ctx.direction,
|
|
96
|
+
transition: ctx.transition,
|
|
97
|
+
token
|
|
98
|
+
});
|
|
99
|
+
if (d?.immediate === true) return;
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
pending.set(token, () => resolve());
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
tree,
|
|
107
|
+
anim,
|
|
108
|
+
shared: {
|
|
109
|
+
async rect(screenId, nodeId) {
|
|
110
|
+
const d = call(channel, "screen.rect", { screenId, nodeId });
|
|
111
|
+
if (!d || typeof d.x !== "number") {
|
|
112
|
+
throw new Error(`[screen-host] screen.rect \u672A\u8FD4\u56DE\u51E0\u4F55\uFF08${JSON.stringify(d)}\uFF09`);
|
|
113
|
+
}
|
|
114
|
+
return { x: d.x, y: d.y ?? 0, w: d.width ?? 0, h: d.height ?? 0 };
|
|
115
|
+
},
|
|
116
|
+
fly(opts2) {
|
|
117
|
+
const token = `screen-shared-${++tokenSeq}`;
|
|
118
|
+
const d = call(channel, "screen.shared", { ...opts2, token });
|
|
119
|
+
if (!d || (d.started ?? 0) < 1) {
|
|
120
|
+
return Promise.reject(new Error(`[screen-host] screen.shared \u672A\u542F\u52A8\uFF08${JSON.stringify(d)}\uFF09`));
|
|
121
|
+
}
|
|
122
|
+
const geom = { fromRect: d.fromRect, toRect: d.toRect };
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
pending.set(token, (v) => {
|
|
125
|
+
let extra = {};
|
|
126
|
+
if (typeof v === "string") {
|
|
127
|
+
try {
|
|
128
|
+
const o = JSON.parse(v);
|
|
129
|
+
if (o && typeof o === "object") extra = o;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
} else if (v && typeof v === "object") {
|
|
133
|
+
extra = v;
|
|
134
|
+
}
|
|
135
|
+
resolve({ ...geom, ...extra });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
get pendingAnimations() {
|
|
141
|
+
return pending.size;
|
|
142
|
+
},
|
|
143
|
+
get animDoneHookInstalled() {
|
|
144
|
+
return hookInstalled;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
SCREEN_ANIM_DONE_KEY,
|
|
150
|
+
createHostScreenPorts
|
|
151
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { type MountLayerContainerPlan } from '@proteus-vue/contracts';
|
|
2
|
+
/** 屏命令(**结构类型**——与 `@proteus-vue/router` 的 `ScreenCommand` 同形;本文件不 import router) */
|
|
3
|
+
export interface ScreenCommandLike {
|
|
4
|
+
op: 'mount' | 'enter' | 'exit' | 'unmount';
|
|
5
|
+
screenId: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
params?: unknown;
|
|
9
|
+
rebuild?: boolean;
|
|
10
|
+
transition?: unknown;
|
|
11
|
+
reason?: 'pop' | 'reset' | 'freeze';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 引擎动画指令的最小结构(与 `EngineAnim` **结构兼容**——执行器只透传 + 读少量字段)。
|
|
15
|
+
* ★不要加索引签名(`[k: string]: unknown`):那会让 `EngineAnim`(无该签名)**不可赋值**
|
|
16
|
+
* (真机装置编译期实测抓到)。按需字段显式列出即可。
|
|
17
|
+
*/
|
|
18
|
+
export interface AnimLike {
|
|
19
|
+
nodeId: number;
|
|
20
|
+
from?: number;
|
|
21
|
+
to?: number;
|
|
22
|
+
}
|
|
23
|
+
/** 转场批次计划(与 `routeTransitionBatches` 的产物结构兼容) */
|
|
24
|
+
export interface RouteTransitionPlanLike {
|
|
25
|
+
incoming: {
|
|
26
|
+
anims: readonly AnimLike[];
|
|
27
|
+
};
|
|
28
|
+
outgoing: {
|
|
29
|
+
anims: readonly AnimLike[];
|
|
30
|
+
};
|
|
31
|
+
durationMs: number;
|
|
32
|
+
opaque: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** 转场规划器(注入端口;生产 = `@proteus-vue/animation` 的 `routeTransitionBatches`) */
|
|
35
|
+
export type RouteTransitionPlanner = (transition: unknown, targets: {
|
|
36
|
+
incoming?: number;
|
|
37
|
+
outgoing?: number;
|
|
38
|
+
}, opts?: {
|
|
39
|
+
direction?: 'forward' | 'back';
|
|
40
|
+
}) => RouteTransitionPlanLike;
|
|
41
|
+
/**
|
|
42
|
+
* ★★★**屏内容节点**(2026-10-04 · App 三端对齐 阶段 1):一页渲染产物的**内核节点描述**。
|
|
43
|
+
*
|
|
44
|
+
* 【字段名与内核契约同源】与 `@proteus-vue/renderer-app` 的 `SelfDrawNodeSpec` 结构兼容
|
|
45
|
+
* (`id/parentId/width/height/text/flexDirection/justifyContent/alignItems/gap/display/position/…`),
|
|
46
|
+
* 也与 Rust 内核 `create` 请求的 `NodeDto`(camelCase)同形——**一处命名,三处消费**。
|
|
47
|
+
* 【为什么用宽松类型】本包**不 import renderer-app**(依赖方向:renderer-app → render-backend 的反向会成环);
|
|
48
|
+
* 故按**结构**声明 `id`/`parentId` + 其余内核字段(`Record<string, unknown>`)。定义处的强类型在
|
|
49
|
+
* `SelfDrawNodeSpec`(SSOT)——本处只承诺"这是能被宿主透传给内核的节点描述"。
|
|
50
|
+
*/
|
|
51
|
+
export interface ScreenContentNode extends Record<string, unknown> {
|
|
52
|
+
id: number;
|
|
53
|
+
parentId: number | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* ★★★**屏内容**(一页的渲染产物)——`screen.mount` 的 `content` 字段载荷。
|
|
57
|
+
*
|
|
58
|
+
* 【它解决什么用户可见问题】此前屏 = 空壳容器(宿主塞占位几何节点)⇒「路由在走、页面没渲染」。
|
|
59
|
+
* 本载荷把**真实页面内容**(由页面渲染器从 SFC 产物产出)送到宿主建树路径 ⇒ 屏里真有页面。
|
|
60
|
+
* 【id 空间】`nodes[].id`/`parentId` 是**内容局部**空间;宿主建屏时重映射到屏 id 空间(见 mountScreen 注释)。
|
|
61
|
+
* 【viewport 可选】缺省用屏尺寸(宿主已知);提供时用于内容按视口求解(如百分比基准)。
|
|
62
|
+
*/
|
|
63
|
+
export interface ScreenContent {
|
|
64
|
+
viewport?: {
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
};
|
|
68
|
+
nodes: readonly ScreenContentNode[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 屏内容提供者(执行器选项):按屏名/路径取该页渲染产物。
|
|
72
|
+
*
|
|
73
|
+
* 【为什么是"提供者"而不是塞进屏注册表】屏注册表(`AppScreenSpec`)在 **router 包**——
|
|
74
|
+
* 若把它与"渲染产物"(render-backend 概念)耦合,会让 router 依赖渲染层(方向错)。
|
|
75
|
+
* ⇒ 由**装配层**(宿主桥/应用入口)注入 `name → ScreenContent` 解析(缺省无内容 ⇒ 向后兼容老行为)。
|
|
76
|
+
*/
|
|
77
|
+
export type ScreenContentProvider = (screen: {
|
|
78
|
+
name: string;
|
|
79
|
+
path: string;
|
|
80
|
+
}) => ScreenContent | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* ★★★**LayoutTemplate → ScreenContent**(2026-10-04 · App 三端对齐 阶段 1b · B5):
|
|
83
|
+
* 把编译器产出的**页面布局模板**(`buildLayoutTemplate` 的 `LayoutTemplate`,来自 SFC)
|
|
84
|
+
* 转成宿主建树用的屏内容节点。
|
|
85
|
+
*
|
|
86
|
+
* 【为什么需要(把"链路证明"变成"真实页面")】阶段 1a 的 `contentOf` 用**装置内联**内容
|
|
87
|
+
* (手写 4 个节点)证明链路;生产形态必须让内容来自 **SFC 产物**。本函数是那条"从产物到内容"
|
|
88
|
+
* 的**唯一转换点**(一处实现,两端宿主 + 测试共用)。
|
|
89
|
+
*
|
|
90
|
+
* 【转换规则(LayoutNode → 内核节点)】
|
|
91
|
+
* · `id`/`parentId`/`text` 直接搬(内容局部 id 空间——宿主会重映射到屏 id 空间);
|
|
92
|
+
* · `style`(嵌套引擎字段对象)**展平到节点顶层**(内核 `create` 的 NodeDto 是**顶层**键:
|
|
93
|
+
* `width`/`flexDirection`/`padding`/`backgroundColor`…)——这是本函数存在的**关键**:
|
|
94
|
+
* `LayoutNode.style` 是嵌套的,内核要顶层,直接搬会静默丢样式(本仓踩过同款"字段形态不对"坑)。
|
|
95
|
+
* · `tag` → `semantic`(诊断用;内核不解析标签,只透传)。
|
|
96
|
+
*
|
|
97
|
+
* 【★结构类型(不 import 编译器/slot-runtime)】本包**不依赖** compiler/slot-runtime
|
|
98
|
+
* (依赖方向纪律)⇒ 入参用**结构**描述 `LayoutTemplate` 的最小子集(`nodes` 的
|
|
99
|
+
* `{id,parentId,style,text?,tag?}`)——编译器产物天然满足(多字段无妨)。
|
|
100
|
+
*
|
|
101
|
+
* 【诚实边界】本函数只做**结构转换**;`LayoutNode.style` 已由编译器折叠成**引擎字段**
|
|
102
|
+
* (px/数值/比例——见 `parseStaticStyle`),本函数**不做 CSS 解析**(那是编译期 C1 的职责)。
|
|
103
|
+
* 即:CSS class → style 的展开**尚未**支持(见 App 三端对齐缺口 C1)——本函数吃的是
|
|
104
|
+
* 已折叠的 `style`(来自 inline style / 结构化 paint 声明)。
|
|
105
|
+
*/
|
|
106
|
+
export declare function screenContentFromLayoutTemplate(template: {
|
|
107
|
+
nodes: ReadonlyArray<{
|
|
108
|
+
id: number;
|
|
109
|
+
parentId: number | null;
|
|
110
|
+
style?: Record<string, unknown>;
|
|
111
|
+
text?: string;
|
|
112
|
+
tag?: string;
|
|
113
|
+
}>;
|
|
114
|
+
}, viewport?: {
|
|
115
|
+
width: number;
|
|
116
|
+
height: number;
|
|
117
|
+
}): ScreenContent;
|
|
118
|
+
/**
|
|
119
|
+
* 树操作端口(宿主实现——Android/iOS 各自对接 Host ABI 的树接口)。
|
|
120
|
+
* ★三个方法都是"屏粒度":宿主内部怎么落子树(load_tree / splice / 五原子销毁)不由本层约束。
|
|
121
|
+
*/
|
|
122
|
+
export interface ScreenTreeHost {
|
|
123
|
+
/** 建屏子树(首次挂载 rebuild=false;冻结后重建 rebuild=true)。返回**屏子树根节点 id**(转场动画的目标) */
|
|
124
|
+
mountScreen(screen: {
|
|
125
|
+
screenId: string;
|
|
126
|
+
name: string;
|
|
127
|
+
path: string;
|
|
128
|
+
params: unknown;
|
|
129
|
+
rebuild: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* ★★★**屏内容**(2026-10-04 · App 三端对齐 阶段 1 · B1+B2):该页**渲染产物**——内核节点描述列表。
|
|
132
|
+
*
|
|
133
|
+
* 【这是什么(消灭"屏 = 空壳容器"的关键)】此前 `screen.mount` 建的是**空子树**(宿主塞 3 个
|
|
134
|
+
* 占位几何节点)⇒ "路由在走、页面没渲染"(缺口 B2)。本字段携带**真实页面内容**:
|
|
135
|
+
* 由页面渲染器(`@proteus-vue/renderer-app` 的 selfdraw 适配器 `buildRequest`)从 SFC 产物
|
|
136
|
+
* 产出——`{ nodes: SelfDrawNodeSpec[] }`,字段名与内核 `create` 契约**同源**
|
|
137
|
+
* (`id/parentId/width/text/flexDirection/...`)。
|
|
138
|
+
*
|
|
139
|
+
* 【★ id 空间(宿主职责)】content 里的 `id`/`parentId` 属**内容局部**空间(通常 1..N)。
|
|
140
|
+
* 宿主建屏时**必须重映射到屏 id 空间**(与层容器/屏根不冲突)——契约只承诺
|
|
141
|
+
* "节点间的父子关系由 parentId 表达",不约束宿主怎么分配最终 id(与
|
|
142
|
+
* `mountLayerNodeId` 的"偏移量而非绝对 id"同一哲学)。`parentId === null` ⇒ 挂到
|
|
143
|
+
* **page 内容容器**(层计划里的 page 层;无层计划则挂屏根)。
|
|
144
|
+
* 【诚实边界】本字段只保证"内容被下发";内核对它建没建、建得对不对,由宿主读数
|
|
145
|
+
* (`contentNodes` 数 / 几何探针)与判据证明——本层不声称。
|
|
146
|
+
*/
|
|
147
|
+
content?: ScreenContent;
|
|
148
|
+
/**
|
|
149
|
+
* ★★★GP3-c(2026-10-03):**三层挂载容器计划**(宿主据此在屏根下建三层容器)。
|
|
150
|
+
*
|
|
151
|
+
* 【为什么由执行器下发而不是宿主自造】层容器的偏移/树序/几何口径是**契约**
|
|
152
|
+
* (`@proteus-vue/contracts` 的 `mountLayerContainerPlans`);两端宿主各写一遍
|
|
153
|
+
* = **同一件事两份实现**(本仓纪律:修一份等于没修)。⇒ 执行器(TS,两端同一份)
|
|
154
|
+
* 下发计划,宿主**只做"照此建树"**。
|
|
155
|
+
*
|
|
156
|
+
* 【宿主要做什么】对每项建一个容器:`id = rootId + nodeOffset`、`parentId = rootId`、
|
|
157
|
+
* `position: absolute` + 宽高 = 屏尺寸 ⇒ **按数组顺序**加入 nodes(内核树序即层序)。
|
|
158
|
+
* ★`global` 层容器**不随屏销毁**(见契约 `MOUNT_LAYER_HOST_CONTRACT.survivesRouteChange`)。
|
|
159
|
+
* ★可选字段:宿主不认识它可忽略(向后兼容——老宿主照常工作,只是没有三层结构)。
|
|
160
|
+
*/
|
|
161
|
+
layerContainers?: readonly MountLayerContainerPlan[];
|
|
162
|
+
}): number | Promise<number>;
|
|
163
|
+
/** 可见性切换(true = 显示;false = 隐藏但**树保留**——虚拟栈的核心语义) */
|
|
164
|
+
setScreenVisible(screenId: string, visible: boolean, rootNodeId: number | undefined): void | Promise<void>;
|
|
165
|
+
/** 销毁屏子树(reason 区分 pop/reset/freeze;五原子销毁由宿主实现负责) */
|
|
166
|
+
destroyScreen(screenId: string, reason: 'pop' | 'reset' | 'freeze', rootNodeId: number | undefined): void | Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
/** 动画播放端口(宿主实现——内核 `anim_start` 或平台零参与提交路径) */
|
|
169
|
+
export interface ScreenAnimHost {
|
|
170
|
+
/**
|
|
171
|
+
* 播放一次路由转场(**两页批次并发**——一次导航 = 一次跨边界提交);
|
|
172
|
+
* promise resolve 时视为动画完成(无动画的宿主实现可同步返回)。
|
|
173
|
+
*/
|
|
174
|
+
playRouteTransition(plan: RouteTransitionPlanLike, ctx: {
|
|
175
|
+
direction: 'forward' | 'back';
|
|
176
|
+
transition: string;
|
|
177
|
+
}): void | Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
/** 执行器运行读数(判据/诊断读它——如"3 次导航 = 3 次转场提交") */
|
|
180
|
+
export interface ScreenExecutorStats {
|
|
181
|
+
/** 消费的命令总数 */
|
|
182
|
+
commands: number;
|
|
183
|
+
/** 实际播放的转场次数(空批次跳过的不计) */
|
|
184
|
+
transitions: number;
|
|
185
|
+
/** 方向分档计数 */
|
|
186
|
+
forward: number;
|
|
187
|
+
back: number;
|
|
188
|
+
/** 空规格(none/无动画)跳过的"转场"次数 */
|
|
189
|
+
skippedNone: number;
|
|
190
|
+
/** 被销毁的屏数(按 reason 计数) */
|
|
191
|
+
destroyed: {
|
|
192
|
+
pop: number;
|
|
193
|
+
reset: number;
|
|
194
|
+
freeze: number;
|
|
195
|
+
};
|
|
196
|
+
/** 可见性变更次数(true / false) */
|
|
197
|
+
visibility: {
|
|
198
|
+
shown: number;
|
|
199
|
+
hidden: number;
|
|
200
|
+
};
|
|
201
|
+
/** 建屏次数(含 rebuild) */
|
|
202
|
+
mounts: {
|
|
203
|
+
first: number;
|
|
204
|
+
rebuild: number;
|
|
205
|
+
};
|
|
206
|
+
/** 编排层错误(如 enter 的屏没有子树——不静默) */
|
|
207
|
+
errors: string[];
|
|
208
|
+
}
|
|
209
|
+
export interface ScreenExecutorOptions {
|
|
210
|
+
host: ScreenTreeHost;
|
|
211
|
+
anim: ScreenAnimHost;
|
|
212
|
+
plan: RouteTransitionPlanner;
|
|
213
|
+
/**
|
|
214
|
+
* ★★★(2026-10-04 · App 三端对齐 阶段 1):**屏内容提供者**——按屏名/路径取该页渲染产物。
|
|
215
|
+
* · 提供 ⇒ 屏里是**真实页面内容**(`screen.mount` 带 `content`);
|
|
216
|
+
* · 缺省(未提供)⇒ 向后兼容:不带 `content`,宿主按老行为建(空壳/占位)——
|
|
217
|
+
* 老宿主与老装置不受影响(零行为变化)。
|
|
218
|
+
* ★诚实边界:本项只负责"把内容送到宿主建树路径";内容**从哪来**(SFC 产物 → 节点描述)
|
|
219
|
+
* 由装配层(`@proteus-vue/renderer-app` 的 selfdraw `buildRequest`)决定,本层不声称。
|
|
220
|
+
*/
|
|
221
|
+
contentOf?: ScreenContentProvider;
|
|
222
|
+
/** 建屏完成通知(接 `AppStack.markRebuilt`——契约要求"执行器建完调 markRebuilt") */
|
|
223
|
+
onScreenMounted?: (screenId: string, rebuild: boolean) => void;
|
|
224
|
+
/** 诊断事件(记录端口调用顺序——真机判据/测试断言读它) */
|
|
225
|
+
onEvent?: (e: {
|
|
226
|
+
type: string;
|
|
227
|
+
screenId?: string;
|
|
228
|
+
detail?: unknown;
|
|
229
|
+
}) => void;
|
|
230
|
+
}
|
|
231
|
+
export interface ScreenExecutor {
|
|
232
|
+
/** 消费一批命令(一次 `stack.drainCommands()` 的产物)。★串行化:并发调用按调用序排队 */
|
|
233
|
+
applyCommands(commands: readonly ScreenCommandLike[]): Promise<void>;
|
|
234
|
+
/** 读数快照 */
|
|
235
|
+
stats(): ScreenExecutorStats;
|
|
236
|
+
/** 屏子树根节点(screenId → nodeId;诊断/转场目标查询) */
|
|
237
|
+
subtreeNode(screenId: string): number | undefined;
|
|
238
|
+
}
|
|
239
|
+
export declare function createScreenExecutor(opts: ScreenExecutorOptions): ScreenExecutor;
|