Archived
M1: 底座 —— @contracts 首版 + event / ledger / org / llm
@contracts 首版(§8.1 九个文件): - id 品牌 ID / terminal 终态五名与归侧四标签 / envelope 信封六字段与四类事件 - card 工单两模式 + 对话卡族(形状对齐 2026-07-26 原型定稿) - contract7 契约七件 / ledger 三级寻址与写三分 / process-package 流程包 DSL 与触发器三类 - manifest 活动包清单六区(含 CMMN·DMN·步序声明的资产形状)/ org / api 路由契约 模块(L1): - event 信封存储、归属解析三级短路、投递五诺、监听注册表、三级消费裁定 - ledger schema 自生长(最大化合并/最宽类型/只增不破坏)、任务视图叠加、写三分、冲突检查 - org 岗位授权(客体三类×动作三种)、停用前置检查、会话、种子播种 - llm 四元绑定、ModelGateway 三模式(live/record/replay)—— 两缝之一 app 装配:Hono 服务 + 鉴权中间件 + 治理台/工作台路由;drizzle 迁移;AES-256-GCM 凭证密封 ADR-007 工具链与门禁 · ADR-008 投递队列 · ADR-009 凭证加密 (ADR 编号自 007 续:编码规范 §14 已用到 006,README 写「从 005 续」是笔误) 门禁五道全绿;96 个测试通过,含对真实 PG 的 ledger 集成测试 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
7cd664af3a
commit
14d4fa046a
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* event 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
|
||||
*/
|
||||
import type { Delivery, Envelope, EnvelopeDraft, EventId, ListenerSpec, RegisteredListener, TaskId } from '@contracts';
|
||||
import { deliveryIdSchema, eventIdSchema, listenerIdSchema, taskIdSchema, triggerIdSchema } from '@contracts';
|
||||
import type { EventService, InboundResult, PublishResult } from '../../contract.ts';
|
||||
import { ListenerDeadlineError } from '../../contract.ts';
|
||||
import type {
|
||||
Clock,
|
||||
ConditionEvaluator,
|
||||
EventStore,
|
||||
IdGenerator,
|
||||
MessageFallback,
|
||||
TriggerMatcher,
|
||||
} from '../../port.ts';
|
||||
import { decideRetry, planConsumption } from '../domain/delivery.ts';
|
||||
import { deadlineMillis, matchingListeners } from '../domain/listener-match.ts';
|
||||
import { extractKeys, resolveOwnership } from '../domain/ownership.ts';
|
||||
|
||||
export type EventServiceDeps = {
|
||||
readonly store: EventStore;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
readonly evaluate: ConditionEvaluator;
|
||||
readonly triggers: TriggerMatcher;
|
||||
readonly messages: MessageFallback;
|
||||
/** 地址+时间窗的默认窗口(毫秒);通道实例配置可覆盖 */
|
||||
readonly addressWindowMs?: number;
|
||||
};
|
||||
|
||||
export function createEventService(deps: EventServiceDeps): EventService {
|
||||
function seal(draft: EnvelopeDraft, id: EventId): Envelope {
|
||||
return { ...draft, id, trace: { storeRef: `sys.event/${id}` } };
|
||||
}
|
||||
|
||||
/**
|
||||
* 三级消费的裁定与投递。消息兜底在此**唯一**发生 —— 调用方传入无主兜底岗位,
|
||||
* 避免 publish / ingestInbound 两处各发一次(OS 6.3:消息只兜无主)。
|
||||
*/
|
||||
async function dispatch(envelope: Envelope, fallbackPosition?: string): Promise<PublishResult> {
|
||||
const [armed, triggerHits] = await Promise.all([
|
||||
deps.store.armedListeners(),
|
||||
deps.triggers.match(envelope),
|
||||
]);
|
||||
const hits = matchingListeners(armed, envelope, deps.evaluate).map((l) => ({ taskId: String(l.taskId) }));
|
||||
const ownedTaskId =
|
||||
envelope.correlation.target?.kind === 'task' ? String(envelope.correlation.target.id) : undefined;
|
||||
|
||||
const plan = planConsumption({ listenerHits: hits, triggerHits, ownedTaskId });
|
||||
|
||||
// 扇出:每个命中目标一份独立投递,信封「关联 = 单目标」保持单目标
|
||||
const deliveries: Delivery[] = [
|
||||
...plan.taskTargets.map(
|
||||
(taskId): Delivery => ({
|
||||
id: deliveryIdSchema.parse(deps.ids.next()),
|
||||
eventId: envelope.id,
|
||||
target: { kind: 'task', id: taskIdSchema.parse(taskId) },
|
||||
attempt: 1,
|
||||
state: 'pending',
|
||||
}),
|
||||
),
|
||||
...plan.triggerTargets.map(
|
||||
(triggerId): Delivery => ({
|
||||
id: deliveryIdSchema.parse(deps.ids.next()),
|
||||
eventId: envelope.id,
|
||||
target: { kind: 'trigger', id: triggerIdSchema.parse(triggerId) },
|
||||
attempt: 1,
|
||||
state: 'pending',
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
// 先持久后投递 —— 落库与入队同一事务
|
||||
await deps.store.persistAndEnqueue(envelope, deliveries);
|
||||
|
||||
// 消息兜底:仅当既无监听命中亦无触发命中、且事件无主
|
||||
if (plan.messageFallback) {
|
||||
await deps.messages.unowned(envelope, fallbackPosition);
|
||||
}
|
||||
|
||||
return { eventId: envelope.id, plan };
|
||||
}
|
||||
|
||||
return {
|
||||
async publish(draft) {
|
||||
const id = eventIdSchema.parse(draft.id ?? deps.ids.next());
|
||||
return dispatch(seal(draft, id));
|
||||
},
|
||||
|
||||
async ingestInbound({ facts, rules, draft }): Promise<InboundResult> {
|
||||
const keys = extractKeys(facts, rules);
|
||||
const windowMs = rules.addressWindowMs || deps.addressWindowMs || 7 * 86_400_000;
|
||||
const [outbound, armed] = await Promise.all([
|
||||
deps.store.recentOutbound(windowMs, facts.receivedAt),
|
||||
deps.store.armedListeners(),
|
||||
]);
|
||||
|
||||
const resolution = resolveOwnership({
|
||||
facts,
|
||||
rules: { ...rules, addressWindowMs: windowMs },
|
||||
keys,
|
||||
outbound,
|
||||
listeners: armed.map((l) => ({ taskId: l.taskId, keyFilter: l.keyFilter })),
|
||||
});
|
||||
|
||||
const id = eventIdSchema.parse(draft.id ?? deps.ids.next());
|
||||
const envelope = seal(
|
||||
{
|
||||
...draft,
|
||||
correlation: {
|
||||
...draft.correlation,
|
||||
...(resolution.outcome === 'owned'
|
||||
? { target: { kind: 'task' as const, id: resolution.taskId } }
|
||||
: {}),
|
||||
keys: { ...keys, ...draft.correlation.keys },
|
||||
},
|
||||
},
|
||||
id,
|
||||
);
|
||||
|
||||
// 无主入站的相关人 = 通道实例配置的默认认领岗位(OS 6.2)
|
||||
const result = await dispatch(
|
||||
envelope,
|
||||
resolution.outcome === 'unowned' ? resolution.fallbackPosition : undefined,
|
||||
);
|
||||
return { ...result, resolution };
|
||||
},
|
||||
|
||||
async getEvent(id) {
|
||||
return deps.store.get(id);
|
||||
},
|
||||
|
||||
async listByTask(taskId, limit = 200) {
|
||||
return deps.store.listByTask(taskId, limit);
|
||||
},
|
||||
|
||||
async registerListener(taskId: TaskId, spec: ListenerSpec): Promise<RegisteredListener> {
|
||||
let ms: number;
|
||||
try {
|
||||
ms = deadlineMillis(spec.deadline);
|
||||
} catch (error) {
|
||||
throw new ListenerDeadlineError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
const now = deps.clock.now();
|
||||
const listener: RegisteredListener = {
|
||||
...spec,
|
||||
id: listenerIdSchema.parse(deps.ids.next()),
|
||||
taskId,
|
||||
registeredAt: now.toISOString(),
|
||||
expiresAt: new Date(now.getTime() + ms).toISOString(),
|
||||
state: 'armed',
|
||||
};
|
||||
await deps.store.saveListener(listener);
|
||||
return listener;
|
||||
},
|
||||
|
||||
async cancelListener(id) {
|
||||
await deps.store.setListenerState(id, 'cancelled');
|
||||
},
|
||||
|
||||
async listListeners(taskId) {
|
||||
return deps.store.listenersOf(taskId);
|
||||
},
|
||||
|
||||
async sweepExpiredListeners() {
|
||||
const now = deps.clock.now();
|
||||
const expired = await deps.store.expiredListeners(now);
|
||||
for (const listener of expired) {
|
||||
await deps.store.setListenerState(listener.id, 'expired');
|
||||
// 到点抛时间事件 —— 无死等(OS 6.3)
|
||||
await this.publish({
|
||||
type: 'time',
|
||||
correlation: {
|
||||
target: { kind: 'task', id: listener.taskId },
|
||||
cause: { kind: 'timer', ref: listener.id },
|
||||
keys: listener.keyFilter,
|
||||
},
|
||||
payload: {
|
||||
data: {
|
||||
listenerId: listener.id,
|
||||
timerRef: listener.id,
|
||||
escalation: listener.escalation[0] ?? 'nudge',
|
||||
},
|
||||
},
|
||||
occurredAt: now.toISOString(),
|
||||
source: { kind: 'engine', detail: 'listener-sweep' },
|
||||
});
|
||||
}
|
||||
return expired;
|
||||
},
|
||||
|
||||
async recordOutbound(ref) {
|
||||
await deps.store.saveOutbound(ref);
|
||||
},
|
||||
|
||||
async claimConsumption(eventId, consumer) {
|
||||
return deps.store.claimConsumption(eventId, consumer);
|
||||
},
|
||||
|
||||
async reportFailure(deliveryId, error) {
|
||||
const delivery = await deps.store.getDelivery(deliveryId);
|
||||
if (delivery === undefined) throw new Error(`投递不存在:${deliveryId}`);
|
||||
const decision = decideRetry(delivery.attempt);
|
||||
if (decision.kind === 'retry') {
|
||||
const next: Delivery = { ...delivery, attempt: decision.attempt, state: 'pending', lastError: error };
|
||||
await deps.store.updateDelivery(next);
|
||||
await deps.store.scheduleRetry(deliveryId, decision.delayMs);
|
||||
return next;
|
||||
}
|
||||
const dead: Delivery = { ...delivery, state: 'dead-letter', lastError: error };
|
||||
await deps.store.updateDelivery(dead);
|
||||
const envelope = await deps.store.get(delivery.eventId);
|
||||
// 死信以系统通知送达相关人的消息要素与治理台(OS 6.3)
|
||||
if (envelope !== undefined) await deps.messages.deadLetter(envelope, error);
|
||||
return dead;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user