命令确认与资源生命周期
地图命令提交后,应用状态、新帧渲染、网络资源就绪和空闲是不同的状态。脚本应等待对应的确认,不用固定延迟推断成功。
浏览器 SDK 的开发接口
以下接口属于本次开发源码中的 MapView,尚未作为更新后的 npm 包发布。它是现有 SDK 的完成确认扩展;应用共同脚本已接通 map.sqs.flush(),水位属于各自的命令入口。旧 npm SDK 尚无底层确认能力,示例页明确返回能力未接通。
map.mapInstanceId: string
map.flush(options?: { render?: boolean; since?: number }): Promise<MapCommitResult>
flush() 捕获调用时已经提交的地图及应用扩展命令水位。后续命令不延长该等待。默认只确认应用;render: true 还等待一个实际完成的新帧,并更新宿主的相机快照。它不等待网络资源、动画终点或空闲。
| 返回字段 | 含义 |
|---|---|
| mapInstanceId | 本地图的不可复用身份,销毁重建后不同 |
| submittedSequence | 本次捕获的命令水位 |
| appliedSequence | 此时最高成功应用的命令序号 |
| stage | applied 或 rendered |
| camera | 应用时或新帧中的权威相机快照 |
| frameSequence | rendered 状态下的实际帧序号 |
同一张地图的命令序号严格递增。默认 since: 0 包括之前所有命令的错误;出现错误时拒绝,并提供 code、commandSequence 和 submittedSequence。调用方处理错误后,可将已观察的水位作为 since,确认后续修正命令。失败前后已经成功应用的独立命令不会原子回滚。
const before = await map.flush();
map.setBearing(30);
try {
const commit = await map.flush({ render: true, since: before.submittedSequence });
console.log(commit.mapInstanceId, commit.frameSequence, commit.camera.bearing);
} catch (error) {
console.error(error.code, error.commandSequence, error.message);
}
待回复的 Worker 请求最多 256 个,待新帧确认最多 32 个,命令失败记录最多 256 项。超出预算明确拒绝。地图销毁或 Worker 出错时,所有未完成的请求和新帧等待均拒绝;再次调用已销毁的地图也会拒绝。
脚本会话的所有权
停止一次脚本会终止其 Worker/解释器、事件监听与待处理请求。已应用的地图修改保留,停止脚本不等于销毁地图。显式宿主持有的场景需要单独停止或移除。
重跑时使用新的脚本运行实例。旧运行的迟到响应不能删除新运行的取消句柄;已取消订阅的旧事件回调不能进入新运行。事件转发只接受当前地图身份以及递增的安全整数序号,其他地图或过期事件被丢弃。
用户自行运行的调试板脚本沿用现有免确认规则;AI 生成的高影响业务修改仍使用宿主的确认计划。命令应用确认与用户授权确认是两个独立步骤。
实现与验收范围
本子阶段已验证主线程/Worker 公共命令协议、实际运行时帧调度、失败、销毁,以及脚本停止后重跑的迟到响应和事件隔离。这些是确定性的协议回归;本轮真实浏览器连接失败,未把替身渲染器结果标为 GPU 实测。
C ABI 与独立原生脚本的命令水位已接通并完成 Linux 真实 GPU 验收;应用共同门面、图源修订和完整跨端资源身份继续在后续子阶段接通。Windows/macOS 留待外机验收,移动端留待真实宿主和设备接入。参见 持续脚本会话 与 开发者接口。
原生动态库与独立脚本的回执
先检查既有 sqsm_map_runtime_flags() & SQSM_MAP_COMMAND_BARRIERS(8)判断是否提供新屏障,再解析或调用该符号;同 ABI v1 的旧库未设置该能力位。ABI v1 新增 sqsm_map_flush_json(map, since, flags, out),flags=0 确认应用,flags=1 同步确认一帧 GPU 读回。无变化的有效修改也消耗命令序号;错误线程、无效指针等前置拒绝尚未提交。回执字段与上表一致,camera 当前保留原生世界坐标快照,不冒充后续精确公共相机。状态额外提供三个水位。
独立一次性与持续原生脚本使用 await map.sqs.flush({ since, render })。屏障在调用点直接排入该脚本的有序请求队列,后续同队列修改不会改变该回执。原生排队调用由宿主 pump 提交到地图;pump 前的独立 C 修改可以先执行,本阶段不提供混合入口的全局入队顺序。C 与脚本的修改共用同一日志;默认确认所有历史失败。C 失败时输出清空,last_error 为包含命令水位的 JSON,消息限制为 128 字符以保证不会截断 JSON;原生脚本拒绝保留同名错误字段。已返回回执在销毁后仍有效,C 调用方须释放其独立缓冲。
完整 C/C++ 示例 · 共同原生脚本 · 匹配声明
import { map } from 'sqsmap';
await map.sqs.ready();
const before = await map.sqs.state();
map.setBearing(10);
const first = map.sqs.flush({ since: before.submittedSequence });
map.setBearing(20);
const applied = await first;
const rendered = await map.sqs.flush({ since: applied.submittedSequence, render: true });
export default { applied, rendered };
同一份脚本在 Linux debug 与 release-capi 的真实 QuickJS/GPU、一次性与持续入口通过,首次回执捕获方向 10,第二次确认方向 20 及新帧;另有独立 C 宿主验证失败恢复、无效参数和销毁后缓冲寿命。未将其当成尚未接通的应用/网页共同门面验收。
应用共同脚本的确认
桌面应用和配套开发 SDK 的网页宿主都提供以下调用。地图修改仍通过兼容模块执行;完整 MapLibre 风格相机门面继续按相机阶段接通。
// @gis-api 0.5-session
// @permissions ["project.read", "map.write"]
import { map } from 'sqsmap';
import { map as compatibilityMap } from 'gis';
await map.sqs.ready();
await compatibilityMap.camera.setView({ bearing: 30 });
const commit = await map.sqs.flush({ render: true });
console.log(commit.mapInstanceId, commit.submittedSequence, commit.frameSequence);
应用宿主按地图修改请求计数,SDK/C 库按后端命令计数。一条宿主请求可能包含多个后端命令;两套水位不能互换。连续运行同一宿主保留命令历史,since 只使用该入口曾返回的水位。确认不会把后来的宿主修改纳入等待;桌面异步请求在完成前保留身份,随后修改暂存,取消确认不会伪造成功。
桌面的 render: true 确认屏幕可见地图的一次实际离屏绘制及完成的像素快照,而非操作系统窗口呈现或全部瓦片就绪。当前回执相机仅支持明确的 EPSG:4326 / EPSG:3857 二维视图;其他视图明确报能力未接通。桌面应用原始相机按逻辑视口像素,浏览器 SDK 按画布像素;完整投影和像素归一化留在相机阶段。动态图源修订和移动设备验收仍在后续任务中。
网页宿主仅在实际 SDK 同时提供 flush 与实例身份时启用确认队列,每次修改都由引擎确认后才返回。旧版 SDK 的其他既有操作继续使用原路径;调用 flush 明确拒绝。新版源码与旧 npm 包不能混合当作已发布能力。
失败确认不会清除历史。捕获确认错误后,把该入口返回的 submittedSequence 作为新的 since,并在后续每次确认中持续携带或推进这个水位;再次省略 since 会重新检查旧失败。不能使用另一个宿主或底层 SDK 的序号。
保留失败的实际原因
共同接口使用 INVALID_ARGUMENT、STALE_HANDLE、CONFLICT、RESOURCE_LIMIT、ENGINE_ERROR、NOT_FOUND、CAPABILITY_UNAVAILABLE、CANCELLED。原生修改和后续 flush 返回同一原因;C 状态0~7保留,新增8取消,确认失败的 last_error 是含 code/message/commandSequence/submittedSequence 的 JSON。不要把所有确认失败当作GPU错误。原生QuickJS与WASM SDK也保留这些类型,不根据消息文案猜测。
失败未回滚。since只使用此入口此前返回的submittedSequence,不清除历史也不与其它计数域互换。以下完整脚本依次验证参数无效、重复资源、缺失资源和已删除源的旧句柄,并显式恢复后继续。未知引擎异常仍为ENGINE_ERROR,普通JS语法错误为INVALID_ARGUMENT。停止按既有会话取消规则清理,不等同于用户自己throw一个带CANCELLED的异常。
// @gis-api 0.5-session
// @permissions ["project.read", "map.write"]
import {map} from 'sqsmap';
const empty={type:'FeatureCollection',features:[]};
let since=0;const reasons=[];
async function rejected(invoke,code){
let direct;try{await invoke();}catch(error){direct=error;}
let failure;try{await map.sqs.flush({render:false,since});}catch(error){failure=error;}
if(!failure||failure.code!==code||direct&&direct.code!==code)throw Error('Command reason was lost: '+code+'/'+direct?.code+'/'+failure?.code);
if(!Number.isSafeInteger(failure.commandSequence)||!Number.isSafeInteger(failure.submittedSequence)||failure.commandSequence<=since||failure.submittedSequence<failure.commandSequence)throw Error('Failed command watermark missing');
since=failure.submittedSequence;await map.sqs.flush({render:false,since});reasons.push(code);
}
await rejected(()=>map.addSource('invalid',{type:'geojson',data:{type:'FeatureCollection',features:'invalid'}}),'INVALID_ARGUMENT');
map.addSource('reason',{type:'geojson',data:empty});await map.sqs.flush({render:false,since});
const stale=await map.getSource('reason');
await rejected(()=>map.addSource('reason',{type:'geojson',data:empty}),'CONFLICT');
await rejected(()=>map.removeSource('missing-reason'),'NOT_FOUND');
map.removeSource('reason');await map.sqs.flush({render:false,since});
await rejected(()=>stale.setData(empty),'STALE_HANDLE');
if(await map.getSource('reason')!==undefined||await map.getSource('invalid')!==undefined)throw Error('Failed command changed sources');
export default {reasons,recovered:true};
共同错误脚本 · 完整C示例。Rust开发源码中SessionError::CommandFailed增加code字段,外部匹配可使用..或读取code;C符号与布局未变。