docs(runtime): clarify ledger authority and scheduler durability boundaries
This commit is contained in:
@@ -26,14 +26,17 @@ export type FrozenRequest = {
|
||||
};
|
||||
/** 意图及其本地解析状态;blocked 不等于丢弃,elided/retired 不等于收到服务器 applied 回执。 */
|
||||
export type OperationRecord = { readonly intent: OperationIntent; readonly phase: 'pending' | 'frozen' | 'resolved' | 'blocked'; readonly requestId?: Id; readonly blockers: readonly string[]; readonly resolution?: 'applied' | 'not-applied' | 'superseded' | 'discarded' | 'satisfied' | 'elided'; readonly elisionId?: Id; readonly elision?: ElisionProof; readonly recoveryId?: Id; readonly replacementId?: Id; readonly basePresence: Readonly<Record<Id, Presence>> };
|
||||
/** generation 随账本事实改变递增,durableGeneration 仅确认已实际存储的前缀。 */
|
||||
/** generation 随账本事实改变递增;durableGeneration 是宿主报告的已存前缀。
|
||||
* markDurable 只检查水位范围,本模块不访问存储或证明 fsync/事务真的完成。 */
|
||||
export type LedgerSnapshot = { readonly generation: number; readonly durableGeneration: number; readonly operations: ReadonlyMap<Id, OperationRecord>; readonly requests: ReadonlyMap<Id, FrozenRequest> };
|
||||
|
||||
/** A field-safe compressor for a wholly unsent segment. Frozen records never enter here. */
|
||||
/** 仅压缩完全未冻结的变化段:create 后字段修改并入新对象,同字段连续变化合并。
|
||||
* preserveLifecycle 保留创建/删除,preserveChecks 保留条件 no-op;不能用最后整节点覆盖替代字段链。 */
|
||||
/** 用于调用方已确认完全未冻结的变化段:合并 create 后字段修改及同字段链。
|
||||
* 本函数不读 ledger、basePresence 或 possiblyApplied,不能单独证明可抵消已发送/未知结果的生命周期。
|
||||
* preserveLifecycle 只阻止 create→delete 抵消,preserveChecks 只保留合并后字段 no-op;不是停用所有压缩。 */
|
||||
export function compressChanges(changes: readonly Change[], options: { preserveLifecycle?: boolean; preserveChecks?: boolean } = {}): readonly Change[] {
|
||||
// positions 按元素/canvas ID 保存结果索引栈,只尝试合并同对象最近变化,保留原条件顺序。
|
||||
// positions 按元素/canvas ID 保存索引栈,只尝试同对象最近一项;其它对象的变化可以夹在两项之间。
|
||||
// 若同对象中间出现不同字段,则不跨过它继续向前找同字段,因此不是任意字段的全段最后值覆盖。
|
||||
const result: (Change | undefined)[] = [], positions = new Map<string, number[]>();
|
||||
for (const change of changes) {
|
||||
const id = change.kind === 'field' ? change.target.id : change.id;
|
||||
@@ -57,7 +60,8 @@ export function compressChanges(changes: readonly Change[], options: { preserveL
|
||||
return freeze(result.filter((change): change is Change => change !== undefined));
|
||||
}
|
||||
|
||||
/** 从已有快照复制可变表,所有有效修改后重新捕获只读快照;旧 Snapshot 的成员不会改变。 */
|
||||
/** 从已有快照复制 Map 成员,修改后发布新的只读表视图;不在此验证完整恢复 DTO。
|
||||
* 初始记录沿用引用,宿主须先解码捕获为不可变记录;构造器不是任意外部对象的深隔离入口。 */
|
||||
export function createLedger(initial?: LedgerSnapshot) {
|
||||
let generation = initial?.generation ?? 0, durableGeneration = initial?.durableGeneration ?? 0;
|
||||
const operations = new Map(initial?.operations), requests = new Map(initial?.requests);
|
||||
@@ -84,7 +88,8 @@ export function createLedger(initial?: LedgerSnapshot) {
|
||||
return Object.freeze({
|
||||
/** 读取最新捕获的只读事实,不暴露内部可变表。 */
|
||||
getSnapshot: () => cached,
|
||||
/** 校验身份/序列/基线与依赖,再添加 pending;同 ID 同意图幂等,改意图则拒绝。 */
|
||||
/** 检查单条身份、正安全整数序列、基线枚举和依赖,再添加 pending。
|
||||
* 不在此证明全账本 sequence 唯一/递增;同 ID 同意图直接返回,也不会更新第一次的 basePresence。 */
|
||||
add(intent: OperationIntent, basePresence: Readonly<Record<Id, Presence>>) {
|
||||
if (!intent.id || !Number.isSafeInteger(intent.sequence) || intent.sequence < 1 || new Set(intent.dependencies).size !== intent.dependencies.length || Object.values(basePresence).some(value => !['absent', 'exists', 'unknown'].includes(value))) throw fault('invalid_operation');
|
||||
const existing = operations.get(intent.id);
|
||||
@@ -99,8 +104,9 @@ export function createLedger(initial?: LedgerSnapshot) {
|
||||
},
|
||||
/** 给可变意图设置/清除 blocker;有原因变 blocked,无原因回 pending,不改冻结请求。 */
|
||||
block(id: Id, reasons: readonly string[]) { const operation = operations.get(id); if (!operation || !['pending', 'blocked'].includes(operation.phase)) return; operations.set(id, freeze({ ...operation, phase: reasons.length ? 'blocked' : 'pending', blockers: [...reasons] })); changed(); },
|
||||
/** 原子绑定多个 ready 意图到同一请求;参与者版本精确覆盖、类型一致、依赖已解决或在同批。
|
||||
* 只在这里序列化 payload 一次,后续 submit/query 都使用保存的 body 和请求 ID。 */
|
||||
/** 完成输入/ready 检查后,同次更新请求和所有操作的绑定:参与版本覆盖、类型一致,依赖已解决或在同批。
|
||||
* payload 在这里 JSON 序列化一次,后续沿用 body/ID;wire 语义、意图到参与者的推导由编译/调度层核对。
|
||||
* 这次内存更新本身不等于一次持久事务,也不自动取得 scheduler claims。 */
|
||||
freeze(input: { id: Id; kind?: FrozenRequest['kind']; ownerCanvasId?: Id; runId?: Id; protocolVersion?: number; operationIds: readonly Id[]; canvasIds: readonly Id[]; expectedVersions: Readonly<Record<Id, number>>; payload: Json }) {
|
||||
if (!input.id || requests.has(input.id) || !Number.isSafeInteger(input.protocolVersion ?? 1) || (input.protocolVersion ?? 1) < 1 || !input.operationIds.length || (!input.canvasIds.length && input.kind!=='asset.upload' && input.kind!=='run.cancel') || !validJson(input.payload) || new Set(input.operationIds).size !== input.operationIds.length || new Set(input.canvasIds).size !== input.canvasIds.length || Object.keys(input.expectedVersions).length !== input.canvasIds.length || input.canvasIds.some(id => !id || !Number.isSafeInteger(input.expectedVersions[id]) || input.expectedVersions[id] < 0)) throw fault('invalid_frozen_request');
|
||||
if (input.kind === 'run.start' && (!input.ownerCanvasId || !input.canvasIds.includes(input.ownerCanvasId))) throw fault('invalid_run_request');
|
||||
@@ -122,11 +128,13 @@ export function createLedger(initial?: LedgerSnapshot) {
|
||||
if (request.phase === 'resolved') throw fault('request_resolved', [id]);
|
||||
const next: FrozenRequest = freeze({ ...request, phase: 'dispatch-marked', attempt: request.attempt + 1, retryAt: undefined, retryAction: undefined }); requests.set(id, next); changed(); return next;
|
||||
},
|
||||
/** 记录执行结果;applied 且 reconciled 才解除逻辑操作,受理/未协调成功仍 waiting。
|
||||
/** 接受上层已解释的 Outcome;本函数不验证协议身份、canonical 模型或 not-applied 的零写入证据。
|
||||
* applied 且 reconciled 才解除逻辑操作,受理/未协调成功仍 waiting;reconciled 默认 true,调用方需按协调状态传入。
|
||||
* not-applied 结束旧请求但阻塞原意图,不能自动换 key 当作用户已重新授权执行。 */
|
||||
result(id: Id, outcome: Outcome, error?: string, reconciled = true) {
|
||||
const request = requireRequest(id);
|
||||
if ((request.phase === 'resolved' || request.outcome === 'applied') && request.outcome !== outcome) throw fault('receipt_conflict', [id]);
|
||||
// 这里只拒绝与既有 applied/resolved outcome 不同的回执;相同 outcome 的阶段准入仍由调用链维持。
|
||||
const phase = (outcome === 'applied' && reconciled) || outcome === 'not-applied' ? 'resolved' : 'waiting';
|
||||
requests.set(id, freeze({ ...request, phase, outcome, error, retryAt: undefined, retryAction: undefined }));
|
||||
if (outcome === 'applied' && reconciled) for (const operationId of request.operationIds) operations.set(operationId, freeze({ ...operations.get(operationId)!, phase: 'resolved', resolution: 'applied' }));
|
||||
@@ -161,7 +169,8 @@ export function createLedger(initial?: LedgerSnapshot) {
|
||||
},
|
||||
/** Local retirement is not an applied receipt. Validate the complete closure before mutating. */
|
||||
/** 用户恢复决策的本地退休:先验证整组与依赖闭包,再标记 superseded/discarded/satisfied。
|
||||
* 上传和执行结果未知的请求不能在这里被静默丢弃;replacement ID 必须是全新的。 */
|
||||
* 上传和执行结果未知的请求不能在这里被静默丢弃;replacement ID 不能撞已有/本批操作。
|
||||
* 只标记原操作退休,不创建 replacement,不改工作文档/history,须由 engine 在同一 Root 组织其它修改。 */
|
||||
retire(recoveryId:Id,decisions:readonly {operationId:Id;resolution:'superseded'|'discarded'|'satisfied';replacementId?:Id}[]) {
|
||||
const ids=new Set(decisions.map(decision=>decision.operationId));
|
||||
if(!recoveryId||ids.size!==decisions.length||new Set(decisions.flatMap(decision=>decision.replacementId?[decision.replacementId]:[])).size!==decisions.filter(decision=>decision.replacementId).length)throw fault('invalid_recovery_decisions');
|
||||
@@ -175,17 +184,20 @@ export function createLedger(initial?: LedgerSnapshot) {
|
||||
for(const decision of decisions)operations.set(decision.operationId,freeze({...operations.get(decision.operationId)!,phase:'resolved',resolution:decision.resolution,recoveryId,...(decision.replacementId?{replacementId:decision.replacementId}:{})}));
|
||||
if(decisions.length)changed();
|
||||
},
|
||||
/** 保存原请求的 dueAt/action,重试计划也是可恢复事实;实际 heap 是 scheduler 的派生索引。 */
|
||||
/** 保存原请求的 dueAt/action;这里只要求 dueAt 有限,不要求大于当前时间。
|
||||
* 重试计划是可恢复事实,实际 heap/timer 是 scheduler 的派生索引;这里不启动 timer。 */
|
||||
scheduleRetry(id: Id, dueAt: number, action: 'submit' | 'query') {
|
||||
const request = requireRequest(id);
|
||||
if (request.phase === 'resolved' || !Number.isFinite(dueAt)) throw fault('invalid_retry', [id]);
|
||||
requests.set(id, freeze({ ...request, retryAt: dueAt, retryAction: action })); changed();
|
||||
},
|
||||
/** 为未解决请求记录解释器/权限等阻碍,并清重试计划;保留 body/key 和 claims 的恢复依据。 */
|
||||
/** 为未解决请求记录解释器/权限等阻碍,发生写入时清 retry 元数据,保留 body/key。
|
||||
* 已 resolved 或 error 相同直接 no-op;相同 error 的早返回不会再清已有 retry 元数据。 */
|
||||
blockRequest(id: Id, error: string) { const request = requireRequest(id); if (request.phase === 'resolved' || request.error === error) return; requests.set(id, freeze({ ...request, error, retryAt: undefined, retryAction: undefined })); changed(); },
|
||||
/** 仅允许 durable 水位单调前进到当前 generation 内;不新增业务 generation。 */
|
||||
markDurable(through: number) { if (!Number.isSafeInteger(through) || through < durableGeneration || through > generation) throw fault('invalid_durable_generation'); durableGeneration = through; refresh(); },
|
||||
/** 发送标记后且未确定 not-applied 均按可能已执行处理;这是保守风险依据,不是成功证明。 */
|
||||
/** 精确条件为 phase!=frozen 且 outcome!=not-applied,按可能已执行处理;applied/resolved 也可为 true。
|
||||
* 这是基于账本标记的保守风险读取,不查询网络,不证明成功,也不取代实际发送前持久化屏障。 */
|
||||
possiblyApplied(id: Id) { const request = requireRequest(id); return request.phase !== 'frozen' && request.outcome !== 'not-applied'; },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 从 ledger 派生执行顺序的调度器:ready FIFO+queued 去重、依赖索引、canvas claims 与重试 min-heap。
|
||||
* 所有权威变化通过 options.mutate 写回 Root;队列本身既不是历史,也不保存另一套任务状态。
|
||||
* 发送前先持久化精确 body/key 和 dispatch 标记,结果协调及持久化成功后才释放占用。
|
||||
* 发送前等待宿主持久化精确 body/key 和 dispatch 标记,结果协调及持久化成功后才释放占用。
|
||||
* claims/holds 只约束本 scheduler;跨标签、进程或服务端的 writer/身份隔离由 engine 与 storage 端口负责。
|
||||
*/
|
||||
import type { Id, Json, OperationIntent } from '../canvas-core/types.ts';
|
||||
import { fault, readMap } from '../canvas-core/values.ts';
|
||||
@@ -21,27 +22,40 @@ type Ledger = ReturnType<typeof createLedger>;
|
||||
/** engine 提供准入、纯编译、原子账本更新、持久屏障、外部端口和完成协调。
|
||||
* complete 必须连回执/基线/working/ledger 一起处理,不允许仅把请求先标成功。 */
|
||||
export type SchedulerOptions = {
|
||||
/** false 进入准入等待集合,由 refreshAdmission 唤醒;不在此把逻辑操作改为 blocked。 */
|
||||
admit?: (intent:OperationIntent)=>boolean;
|
||||
/** 每次准入/唤醒读取当前权威快照,不信任入队时捕获的旧状态。 */
|
||||
getLedger(): LedgerSnapshot;
|
||||
/** 在 engine 的受控发布路径内修改账本,失败不能留下半份持久事实。 */
|
||||
mutate(reduce: (ledger: Ledger) => void): void;
|
||||
/** 等待当前 Root 可靠存储;失败保持请求身份与占用,不退回生成新请求。 */
|
||||
/** 等待当前 Root 可靠存储;失败保持请求身份与占用,不退回生成新请求。
|
||||
* scheduler 信任此 Promise 的持久承诺,本身不核对 durableGeneration/底层事务。 */
|
||||
persist(): Promise<void>;
|
||||
/** 为新冻结请求分配 ID;恢复/重试沿用原 ID,不再次调用此工厂。 */
|
||||
createId(): Id;
|
||||
/** 从当前意图推导需要 claim 的 canvas;编译结果必须覆盖同一集合。 */
|
||||
participants(intent: OperationIntent): readonly Id[];
|
||||
/** 同步编译一个意图;不在这里发送,payload/版本随后由 ledger.freeze 捕获。 */
|
||||
compile(intent: OperationIntent, requestId: Id): CompiledRequest;
|
||||
/** 将选中的连续候选缩为非空前缀,返回值必须是 1..intents.length 的整数。 */
|
||||
batchLimit?: (intents: readonly OperationIntent[]) => number;
|
||||
/** 可选抵消协调钩子:先实际标记 ledger,再返回已抵消的候选前缀 ID;调度器不自行构造证明。 */
|
||||
elide?: (intents: readonly OperationIntent[]) => readonly Id[];
|
||||
/** 多意图编译器仅在最终候选数 >1 时使用,单项仍走 compile。 */
|
||||
compileBatch?: (intents: readonly OperationIntent[], requestId: Id) => CompiledRequest;
|
||||
/** 使用保存的冻结请求发写端口;端口负责协议/权限与回执解释,不得重编 payload。 */
|
||||
submit(request: FrozenRequest, signal: AbortSignal): Promise<OperationStatus>;
|
||||
/** 按原请求身份查结果,端口解释回执;端口抛错时 scheduler 统一记为 unresolved/transport_unknown。 */
|
||||
query(request: FrozenRequest, signal: AbortSignal): Promise<OperationStatus>;
|
||||
/** 恢复时确认冻结格式是否有解释器;省略钩子只默认支持 protocolVersion=1。 */
|
||||
supportsRequest?: (request: FrozenRequest) => boolean;
|
||||
/** Atomically reconcile the receipt/base/working document and the ledger outcome. */
|
||||
/** 同步协调回执/base/working 与 ledger 的宿主边界;不得返回异步准备结果。 */
|
||||
complete(request: FrozenRequest, status: OperationStatus): { readonly blocked: true } | void;
|
||||
/** 报告存储/协调/编译错误,scheduler 隔离此回调抛错,不能用它改写请求结果。 */
|
||||
onError(error: unknown): void;
|
||||
clock?: Clock;
|
||||
concurrency?: number;
|
||||
/** 按 submit attempt 提供有限退避毫秒;正常值取至少 1ms,端口 retryAfterMs 可覆盖此次值。 */
|
||||
retryDelay?: (attempt: number) => number;
|
||||
};
|
||||
|
||||
@@ -58,7 +72,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
const waiters = new Map<Id, Set<Id>>(), dependents = new Map<Id, Set<Id>>(), indexed = new Set<Id>();
|
||||
// 最早 dueAt 优先;同到期按入堆 sequence 稳定排序,token 让旧 timer/旧重试条目惰性失效。
|
||||
const retry = createMinHeap<{ id: Id; dueAt: number; sequence: number; token: number }>((a, b) => a.dueAt - b.dueAt || a.sequence - b.sequence);
|
||||
// active 只跟踪当前异步句柄/AbortController;任务完成事实仍必须回到 ledger。
|
||||
// active 跟踪整条异步流水线(包含发送前/后的持久化等待),不是仅统计正在进行的 HTTP 请求。
|
||||
// 句柄/AbortController 属于调度器,任务完成事实仍必须回到 ledger。
|
||||
const tokens = new Map<Id, number>(), active = new Map<Id, { controller: AbortController; promise: Promise<void>; kind: FrozenRequest['kind'] }>();
|
||||
// 多个恢复/关闭流程可独立 hold;只有最后一个 token 释放才继续准入,已启动工作仍正常收尾。
|
||||
const holds = new Set<symbol>();
|
||||
@@ -73,7 +88,7 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
const report = (error: unknown) => { try { options.onError(error); } catch { /* Reporting cannot change a receipt. */ } };
|
||||
/** 旧冻结协议必须有解释器才重试;缺钩子时仅默认支持协议 1。 */
|
||||
const supported = (request: FrozenRequest) => options.supportsRequest?.(request) ?? request.protocolVersion === 1;
|
||||
/** 总并发上限之外,上传独占一个 lane;其它类别仍可使用剩余网络执行位。 */
|
||||
/** 总 active 上限之外,asset.upload 至多占一个 lane;其它类别可用剩余流水线执行位。 */
|
||||
const canRun = (kind: FrozenRequest['kind']) => active.size < concurrency && (kind !== 'asset.upload' || ![...active.values()].some(task => task.kind === 'asset.upload'));
|
||||
|
||||
/** 先读 ledger 检查 pending/依赖/门禁,再用 queued 去重入 FIFO;不是通知一次就盲发一次。 */
|
||||
@@ -100,7 +115,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
enqueue(id);
|
||||
}
|
||||
/** Only a contiguous mutable document prefix may share a request. Task barriers stay visible. */
|
||||
/** 只选连续未冻结 document.change 前缀,最多 64 意图/10000 changes,遇任务屏障或占用即停。
|
||||
/** 只选连续未冻结 document.change 前缀,最多 64 意图;追加候选时不让累计 changes 超过 10000。
|
||||
* 首意图本身超过 10000 时仍可单独返回,不把它拆成字段请求;遇任务屏障/占用即停。
|
||||
* 仅合并有相交参与者的 lane;独立 canvas 保留各自失败/重试边界。 */
|
||||
function batch(first: OperationIntent, canvases: Set<Id>): readonly OperationIntent[] {
|
||||
const selected = [first], ids = new Set([first.id]);
|
||||
@@ -143,7 +159,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
}
|
||||
for (const id of request.operationIds) for (const dependent of dependents.get(id) ?? []) enqueue(dependent);
|
||||
}
|
||||
/** 可选写回可恢复 retry 元数据,再用新 token 入 heap;重建索引时 persist=false 避免重复写事实。 */
|
||||
/** 可选 mutate 写回 retry 元数据,再用新 token 入 heap;persist=false 仅表示不重复写账本。
|
||||
* 此处没有 await 存储屏障,真正可靠落盘仍由 launch/resume 的 options.persist 完成。 */
|
||||
function schedule(id: Id, dueAt: number, action: 'submit' | 'query', persist = true) {
|
||||
if (persist) options.mutate(ledger => ledger.scheduleRetry(id, dueAt, action));
|
||||
const token = (tokens.get(id) ?? 0) + 1; tokens.set(id, token);
|
||||
@@ -160,7 +177,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
if (stopped || holds.size || active.size >= concurrency || !retry.peek()) return;
|
||||
timer = clock.setTimer(() => { timer = undefined; pump(); }, Math.max(0, retry.peek()!.dueAt - clock.now()));
|
||||
}
|
||||
/** 安装 active 句柄后在微任务中执行:dispatch 标记→持久化→submit/query→原子协调→持久化。 */
|
||||
/** 安装 active 句柄后在微任务中执行:submit 先标 dispatch;query 保留现有发送风险,再共同等待持久化。
|
||||
* 调端口→同步 complete→再次持久化;hold 不撤回已启动流水线,stop 则让晚到结果不再采纳。 */
|
||||
function launch(id: Id, action: 'submit' | 'query') {
|
||||
if (active.has(id) || stopped || holds.size) return;
|
||||
const controller = new AbortController();
|
||||
@@ -186,6 +204,7 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
else {
|
||||
// A missing receipt is unknown, so the only write retry reuses this frozen request.
|
||||
// 首次 submit 未知先 query;query 仍未知才原样重试 submit;accepted 一直查,不换请求身份。
|
||||
// accepted 的查询/submit 都继续 query;其它 submit 结果先 query,非 accepted 的 query 结果才允许同身份 submit。
|
||||
const nextAction = status.outcome === 'accepted' || action === 'submit' ? 'query' : 'submit';
|
||||
schedule(id, clock.now() + Math.max(1, status.retryAfterMs ?? delay(current.attempt)), nextAction);
|
||||
await options.persist();
|
||||
@@ -203,7 +222,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
});
|
||||
active.set(id, { controller, promise, kind: options.getLedger().requests.get(id)?.kind });
|
||||
}
|
||||
/** 可重入保护下消费到期请求和最多 64 个 ready 项;等待者移到索引,不在 FIFO 无界反复轮询。 */
|
||||
/** 防同步重入地处理到期请求,再从本轮 ready 前缀最多取 64 项;retry 循环没有同样的 64 项上限。
|
||||
* 等待者移到索引,ready 尚有余量且有容量时安排下一轮 timer,不代表总队列大小有上限。 */
|
||||
function pump() {
|
||||
if (stopped || holds.size || pumping) return;
|
||||
pumping = true;
|
||||
@@ -250,7 +270,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
}
|
||||
const intents = options.compileBatch ? candidates : [record.intent];
|
||||
const participants = [...new Set(intents.flatMap(intent => options.participants(intent)))].sort();
|
||||
// 编译参与者必须与规划一致,随后 freeze/own/launch;失败阻塞原意图而非偷偷重新编另一请求。
|
||||
// 编译参与者必须与实际 intents 推导的集合一致,随后 freeze/own/launch;没有 compileBatch 时只编译首意图。
|
||||
// 异常走原逻辑 ID 的 block;若已 frozen,ledger.block 不会把它退回 pending,冻结身份仍保留。
|
||||
const requestId = options.createId(), compiled = intents.length > 1 ? options.compileBatch!(intents, requestId) : options.compile(record.intent, requestId);
|
||||
if (participants.length !== compiled.canvasIds.length || participants.some(canvas => !compiled.canvasIds.includes(canvas))) throw fault('participant_mismatch');
|
||||
options.mutate(ledger => ledger.freeze({ ...compiled, id: requestId, operationIds: intents.map(intent => intent.id) }));
|
||||
@@ -267,7 +288,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
/** 首次启动(已运行时幂等):先恢复全部未解决 claims,按原 retry/body/key 继续;解释器缺失仍保留占用。 */
|
||||
/** 首次启动(已运行时幂等):先恢复全部未解决 claims,之后才开始端口工作;claim 冲突会在仍 stopped 时抛错。
|
||||
* 解释器缺失保留占用并写 blocker;此步骤不做完整恢复 DTO 校验,也不从单个错误推断请求未执行。 */
|
||||
start() {
|
||||
if (closed) throw fault('scheduler_closed');
|
||||
if (!stopped) return;
|
||||
@@ -284,7 +306,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
},
|
||||
/** 门禁事实变化后显式重试准入,未满足者继续等待,不制造定时器忙轮询。 */
|
||||
refreshAdmission(){for(const id of [...admissionWaiters])enqueue(id);if(ready.size)requestPump();},
|
||||
/** 新逻辑操作通知只推进派生索引,重复 notify 不重复注册依赖或入队。 */
|
||||
/** 新逻辑操作通知推进派生索引;正在 queued 时不重复入队,indexed 让依赖只注册一次。
|
||||
* 已出队的 pending 操作后续 notify 可再次尝试准入;通知本身不新增 ledger 意图。 */
|
||||
notify(operationIds: readonly Id[]) { for (const id of operationIds) index(id); requestPump(); },
|
||||
/** Acquire synchronously, then settle(): already admitted work completes with its original identity. */
|
||||
/** 同步阻止新准入并清唤醒 timer;返回幂等释放函数,已准入异步工作不被撤销或换身份。 */
|
||||
@@ -316,7 +339,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
},
|
||||
/** 返回当前占用表的只读副本,调用者不能手改 claim 绕过未知请求。 */
|
||||
getClaims: () => readMap(claims),
|
||||
/** 等待当前微任务/pump/active 收尾,不主动等待未来 retry timer,因此不是“全部已同步”保证。 */
|
||||
/** 等待当前微任务/pump/active 收尾,不主动等待未来 retry timer,因此不是“全部已同步”保证。
|
||||
* 端口若长期不完成(包括忽略 abort),active Promise 仍可让 settle 一直等待。 */
|
||||
async settle() {
|
||||
do {
|
||||
if (scheduledPump) await scheduledPump;
|
||||
@@ -324,7 +348,8 @@ export function createScheduler(options: SchedulerOptions) {
|
||||
if (!stopped && !holds.size && pumpTimer !== undefined) { clock.clearTimer(pumpTimer); pumpTimer = undefined; pump(); }
|
||||
} while (scheduledPump || active.size || (!stopped && !holds.size && pumpTimer !== undefined));
|
||||
},
|
||||
/** 永久关闭本 scheduler、取消 timer 并 abort active;晚到响应因 stopped 不再采纳,不能再 start。 */
|
||||
/** 永久关闭本 scheduler、取消 timer 并请求 abort active;晚到响应因 stopped 不再采纳,不能再 start。
|
||||
* abort 不证明外部未执行,也不清 ledger/claims/派生容器或替 engine 关闭存储;活动句柄在 finally 收尾。 */
|
||||
stop() { closed = true; stopped = true; if (timer !== undefined) clock.clearTimer(timer); if (pumpTimer !== undefined) clock.clearTimer(pumpTimer); timer = pumpTimer = undefined; for (const item of active.values()) item.controller.abort(); },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* runtime 使用的基础调度容器,不包含任务状态事实:FIFO 存可执行顺序,min-heap 存优先项。
|
||||
* 两者都是内存结构,ledger/checkpoint 才负责可恢复事实;容器本身不去重、不校验依赖或执行网络。
|
||||
* T 值按引用保存/返回,不复制冻结任务对象,也不以 size/clear 修改外部任务事实。
|
||||
*/
|
||||
/** FIFO with amortized O(1) removal, without Array.shift on each item. */
|
||||
/** 当前接口是先进先出队列:push 入尾、pop 出头,并非 history 的后进先出 stack。
|
||||
@@ -24,12 +25,13 @@ export function createDeque<T>() {
|
||||
},
|
||||
/** 同时丢弃底层数组和游标,释放旧任务引用。 */
|
||||
clear() { items = []; head = 0; },
|
||||
/** 返回有效区间的数组副本,外部改副本不会更改队列成员。 */
|
||||
/** 返回有效区间的浅数组副本,改副本成员位置不改队列;对象成员仍共享引用。 */
|
||||
values() { return items.slice(head); },
|
||||
};
|
||||
}
|
||||
/** 数组实现二叉最小堆,compare<0 表示左项优先;peek O(1),push/pop O(log n)。
|
||||
* 相等优先级不保证插入稳定性,上层若需要 FIFO tie-break 必须把序列写进 comparator。 */
|
||||
* 相等优先级不保证插入稳定性,上层若需要 FIFO tie-break 必须把序列写进 comparator。
|
||||
* compare 应稳定、同步且不抛错;容器不校验 comparator,比较中途异常也不提供事务回滚。 */
|
||||
export function createMinHeap<T>(compare: (left: T, right: T) => number) {
|
||||
// 父下标 (i-1)>>1,左右孩子 2i+1/2i+2;堆序只要求父不大于孩子,不是全数组排序。
|
||||
const items: T[] = [];
|
||||
|
||||
Reference in New Issue
Block a user