depa-actor 0.1.0
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/README.md +372 -0
- package/package.json +28 -0
- package/src/core/ActorSystem.ts +225 -0
- package/src/core/types.ts +136 -0
- package/src/dispatch/ActorDispatchAdapter.ts +84 -0
- package/src/index.ts +92 -0
- package/src/orchestration/index.ts +35 -0
- package/src/orchestration/presets/aiAgent.ts +48 -0
- package/src/orchestration/recovery.ts +144 -0
- package/src/orchestration/reducer.ts +337 -0
- package/src/orchestration/runtimeAdapter.ts +19 -0
- package/src/orchestration/scheduler.ts +111 -0
- package/src/orchestration/types.ts +159 -0
- package/src/pipeline/ActorPipeline.ts +132 -0
- package/src/runtime/ActorRuntime.ts +119 -0
- package/src/runtime/completion.ts +86 -0
- package/src/runtime/indexing.ts +39 -0
- package/src/runtime/snapshot.ts +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# depa-actor
|
|
2
|
+
|
|
3
|
+
`depa-actor` 是一个面向 TypeScript / JavaScript 的轻量 actor 运行时,重点支持以下几类能力:
|
|
4
|
+
|
|
5
|
+
- 基础 actor system:注册 actor、发送消息、广播消息、按邮箱优先级处理
|
|
6
|
+
- typed mailbox:用 `MailboxSchema` 对消息 tag 和 payload 建模
|
|
7
|
+
- selective receive:按 tag 检查和抽取待处理消息
|
|
8
|
+
- runtime 包装:把 actor 系统嵌入更高层运行时
|
|
9
|
+
- pipeline / dispatch 适配:方便与数据流、路由层组合
|
|
10
|
+
- fiber orchestration:把“状态归属”和“调度执行”拆开,支持前后台协作
|
|
11
|
+
|
|
12
|
+
这个库适合用来构建:
|
|
13
|
+
|
|
14
|
+
- AI Agent 运行时
|
|
15
|
+
- 多执行单元协作系统
|
|
16
|
+
- 需要消息驱动 + 可测试调度语义的应用
|
|
17
|
+
|
|
18
|
+
## 设计目标
|
|
19
|
+
|
|
20
|
+
`depa-actor` 不是为了做一个巨大的通用框架,而是聚焦几个明确目标:
|
|
21
|
+
|
|
22
|
+
- **小而清晰**:核心概念尽量少,API 保持直接
|
|
23
|
+
- **类型友好**:tag 和 payload 通过 schema 建模
|
|
24
|
+
- **消息优先**:控制语义和业务语义都优先消息化
|
|
25
|
+
- **可编排**:在 actor 之上支持 fiber 调度
|
|
26
|
+
- **可测试**:复杂行为优先通过仿真测试验证
|
|
27
|
+
|
|
28
|
+
## 核心概念
|
|
29
|
+
|
|
30
|
+
### 1. Actor
|
|
31
|
+
|
|
32
|
+
Actor 是状态与通信边界:
|
|
33
|
+
|
|
34
|
+
- 有唯一 id
|
|
35
|
+
- 有内部 state
|
|
36
|
+
- 通过 mailbox 收消息
|
|
37
|
+
- 用 handler 或 handlers 处理消息
|
|
38
|
+
|
|
39
|
+
### 2. MailboxSchema
|
|
40
|
+
|
|
41
|
+
`MailboxSchema` 是一个 `Record<tag, payload>`:
|
|
42
|
+
|
|
43
|
+
- key:邮箱 tag
|
|
44
|
+
- value:该 tag 对应的 payload 类型
|
|
45
|
+
|
|
46
|
+
示例:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
type ChatSchema = {
|
|
50
|
+
human_input: { text: string }
|
|
51
|
+
cancel: { reason: string }
|
|
52
|
+
tool_result: { callId: string; content: string }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Selective Receive
|
|
57
|
+
|
|
58
|
+
handler 内可以:
|
|
59
|
+
|
|
60
|
+
- `hasPending(tag)`:检查某类消息是否已在队列中
|
|
61
|
+
- `drainMailbox(tag)`:把某类消息一次性取出
|
|
62
|
+
|
|
63
|
+
这对 AI agent 这类需要“先看控制消息,再决定是否继续”的场景非常重要。
|
|
64
|
+
|
|
65
|
+
### 4. Fiber Orchestration
|
|
66
|
+
|
|
67
|
+
Actor 负责:
|
|
68
|
+
|
|
69
|
+
- 状态
|
|
70
|
+
- 身份
|
|
71
|
+
- mailbox
|
|
72
|
+
|
|
73
|
+
Fiber 负责:
|
|
74
|
+
|
|
75
|
+
- 调度
|
|
76
|
+
- 挂起
|
|
77
|
+
- 恢复
|
|
78
|
+
- 完成
|
|
79
|
+
- 取消
|
|
80
|
+
|
|
81
|
+
这让复杂系统可以拆成:
|
|
82
|
+
|
|
83
|
+
- 上层业务语义
|
|
84
|
+
- 中层 actor 通信
|
|
85
|
+
- 底层 fiber 调度
|
|
86
|
+
|
|
87
|
+
更完整的说明见:
|
|
88
|
+
|
|
89
|
+
- `ACTOR-FOR-AI-AGENTS.md`
|
|
90
|
+
- `doc/runtime-foundations.md`
|
|
91
|
+
|
|
92
|
+
## 导出的模块
|
|
93
|
+
|
|
94
|
+
`src/index.ts` 当前导出以下能力:
|
|
95
|
+
|
|
96
|
+
### Core
|
|
97
|
+
|
|
98
|
+
- 类型:
|
|
99
|
+
- `MailboxSchema`
|
|
100
|
+
- `ActorEnvelope`
|
|
101
|
+
- `TaggedEnvelope`
|
|
102
|
+
- `MailboxPriority`
|
|
103
|
+
- `ActorRef`
|
|
104
|
+
- `ActorSelf`
|
|
105
|
+
- `ActorHandler`
|
|
106
|
+
- `TagHandler`
|
|
107
|
+
- `ActorDef`
|
|
108
|
+
- `ActorLogKind`
|
|
109
|
+
- `ActorLogEntry`
|
|
110
|
+
- 实现:
|
|
111
|
+
- `ActorSystem`
|
|
112
|
+
|
|
113
|
+
### Runtime
|
|
114
|
+
|
|
115
|
+
- 类型:
|
|
116
|
+
- `ActorPlugin`
|
|
117
|
+
- `CompletionWaiter`
|
|
118
|
+
- `SnapshotRecoveryState`
|
|
119
|
+
- `RuntimeSnapshotManifestBase`
|
|
120
|
+
- `RuntimeRootSnapshotBase`
|
|
121
|
+
- `ActorSnapshotBase`
|
|
122
|
+
- `FiberSnapshotBase`
|
|
123
|
+
- `SnapshotCodec`
|
|
124
|
+
- `RecoveryHooks`
|
|
125
|
+
- `PersistenceEffectPort`
|
|
126
|
+
- 实现:
|
|
127
|
+
- `ActorRuntime`
|
|
128
|
+
- `CompletionSignalRegistry`
|
|
129
|
+
- `CompletionBindingRegistry`
|
|
130
|
+
- `createCompletionSignalRegistry`
|
|
131
|
+
- `createCompletionBindingRegistry`
|
|
132
|
+
- `createSnapshotCodec`
|
|
133
|
+
- `createRecoveryHooks`
|
|
134
|
+
- `createPersistenceEffectPort`
|
|
135
|
+
- `RuntimeIndexHook`
|
|
136
|
+
- `createRuntimeIndexHook`
|
|
137
|
+
|
|
138
|
+
### Pipeline
|
|
139
|
+
|
|
140
|
+
- 类型:
|
|
141
|
+
- `ActorPipelineDef`
|
|
142
|
+
- `PipelineDerivedAdapter`
|
|
143
|
+
- `PipelineInnerRuntimeAdapter`
|
|
144
|
+
- `PipelineInnerInputAdapter`
|
|
145
|
+
- `PipelineInnerConfigAdapter`
|
|
146
|
+
- `PipelineCoreLogic`
|
|
147
|
+
- `PipelineOutputAdapter`
|
|
148
|
+
- 实现:
|
|
149
|
+
- `createPipelineHandler`
|
|
150
|
+
|
|
151
|
+
### Dispatch
|
|
152
|
+
|
|
153
|
+
- 类型:
|
|
154
|
+
- `DispatchRoute`
|
|
155
|
+
- 实现:
|
|
156
|
+
- `createDispatchHandler`
|
|
157
|
+
|
|
158
|
+
### Orchestration
|
|
159
|
+
|
|
160
|
+
- 类型:
|
|
161
|
+
- `FiberId`
|
|
162
|
+
- `FiberStatus`
|
|
163
|
+
- `FiberWaitingReason`
|
|
164
|
+
- `SuspendPolicy`
|
|
165
|
+
- `SchedulerHooks`
|
|
166
|
+
- `FiberStep`
|
|
167
|
+
- `FiberRecord`
|
|
168
|
+
- `SpawnFiberInput`
|
|
169
|
+
- `DeadLetterRecord`
|
|
170
|
+
- `OrchestratorOptions`
|
|
171
|
+
- `OrchestratorState`
|
|
172
|
+
- `FiberAction`
|
|
173
|
+
- `FiberEffect`
|
|
174
|
+
- `ReduceResult`
|
|
175
|
+
- `ScheduleResult`
|
|
176
|
+
- 实现:
|
|
177
|
+
- `DEFAULT_ORCHESTRATOR_OPTIONS`
|
|
178
|
+
- `createOrchestratorState`
|
|
179
|
+
- `reduceOrchestrator`
|
|
180
|
+
- `applyFailure`
|
|
181
|
+
- `computeEffectivePriority`
|
|
182
|
+
- `selectNextFiberId`
|
|
183
|
+
- `scheduleOne`
|
|
184
|
+
- `createAiAgentSchedulerHooks`
|
|
185
|
+
- `dispatchEffects`
|
|
186
|
+
|
|
187
|
+
## 最小示例
|
|
188
|
+
|
|
189
|
+
下面是一个最小的 actor system 示例:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { ActorSystem, type ActorDef } from "@depa/actor"
|
|
193
|
+
|
|
194
|
+
type DemoSchema = {
|
|
195
|
+
ping: { from: string }
|
|
196
|
+
pong: { from: string }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const system = new ActorSystem<void, DemoSchema>(() => undefined)
|
|
200
|
+
|
|
201
|
+
const pingActor: ActorDef<void, DemoSchema, { count: number }> = {
|
|
202
|
+
initialState: { count: 0 },
|
|
203
|
+
handlers: {
|
|
204
|
+
ping(self, env) {
|
|
205
|
+
self.state.count += 1
|
|
206
|
+
self.send(env.from, "pong", { from: self.id })
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const pongActor: ActorDef<void, DemoSchema, void> = {
|
|
212
|
+
initialState: undefined,
|
|
213
|
+
handlers: {
|
|
214
|
+
pong(_self, env) {
|
|
215
|
+
console.log("received pong from", env.payload.from)
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
system.register("alice", pingActor)
|
|
221
|
+
system.register("bob", pongActor)
|
|
222
|
+
|
|
223
|
+
system.sendFrom("bob", "alice", "ping", { from: "bob" })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Selective Receive 示例
|
|
227
|
+
|
|
228
|
+
如果某些消息优先级更高,可以配合 `priority` 和 `drainMailbox()`:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { ActorSystem, type ActorDef } from "@depa/actor"
|
|
232
|
+
|
|
233
|
+
type Schema = {
|
|
234
|
+
step: { round: number }
|
|
235
|
+
cancel: { reason: string }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const system = new ActorSystem<void, Schema>(() => undefined)
|
|
239
|
+
|
|
240
|
+
const actor: ActorDef<void, Schema, { cancelled: boolean }> = {
|
|
241
|
+
initialState: { cancelled: false },
|
|
242
|
+
priority: {
|
|
243
|
+
cancel: 1,
|
|
244
|
+
step: 100,
|
|
245
|
+
},
|
|
246
|
+
handlers: {
|
|
247
|
+
step(self, env) {
|
|
248
|
+
const pendingCancel = self.hasPending("cancel")
|
|
249
|
+
if (pendingCancel) {
|
|
250
|
+
const [cancel] = self.drainMailbox("cancel")
|
|
251
|
+
if (cancel) {
|
|
252
|
+
self.state.cancelled = true
|
|
253
|
+
console.log("cancelled:", cancel.payload.reason)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!self.state.cancelled) {
|
|
259
|
+
console.log("step", env.payload.round)
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
system.register("worker", actor)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## 适合什么,不适合什么
|
|
269
|
+
|
|
270
|
+
### 适合
|
|
271
|
+
|
|
272
|
+
- 用消息驱动状态机
|
|
273
|
+
- 需要明确 mailbox 边界
|
|
274
|
+
- 需要把状态与调度分开
|
|
275
|
+
- 需要通过仿真测试验证复杂运行时语义
|
|
276
|
+
- 需要为 AI agent、多 worker、多阶段执行建模
|
|
277
|
+
|
|
278
|
+
### 不适合
|
|
279
|
+
|
|
280
|
+
- 只想写一个简单的同步流程
|
|
281
|
+
- 不需要消息边界与调度语义
|
|
282
|
+
- 需要现成的分布式 actor 集群能力
|
|
283
|
+
- 需要持久化 mailbox / durable queue / exactly-once 保证
|
|
284
|
+
|
|
285
|
+
## 测试
|
|
286
|
+
|
|
287
|
+
运行全部测试:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
bun test
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
或:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
bun run test
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
其中有几类值得优先阅读的测试:
|
|
300
|
+
|
|
301
|
+
### 基础 actor / scheduler
|
|
302
|
+
|
|
303
|
+
- `test/fiber-orchestrator.core.test.ts`
|
|
304
|
+
- `test/fiber-orchestrator.runtime.test.ts`
|
|
305
|
+
- `test/scheduler.generic-hooks.test.ts`
|
|
306
|
+
|
|
307
|
+
### AI agent 相关抽象测试
|
|
308
|
+
|
|
309
|
+
- `test/ai-agent-orchestration-simulation.test.ts`
|
|
310
|
+
- `test/ai-agent-human-wait-policy.test.ts`
|
|
311
|
+
- `test/ai-agent-organization-model.test.ts`
|
|
312
|
+
|
|
313
|
+
### 业务语义仿真测试
|
|
314
|
+
|
|
315
|
+
- `test/simulations/background-tasks/daemon_notify_queue.test.ts`
|
|
316
|
+
- `test/simulations/agent-teams/members_jsonl_mailboxes.test.ts`
|
|
317
|
+
- `test/simulations/team-protocols/protocol_fsm_shutdown_plan_approval.test.ts`
|
|
318
|
+
- `test/simulations/autonomous-agents/idle_cycle_auto_claim.test.ts`
|
|
319
|
+
- `test/simulations/cancel/interrupt_cancel_keeps_main_alive.test.ts`
|
|
320
|
+
- `test/simulations/shutdown/shutdown_stops_actor.test.ts`
|
|
321
|
+
|
|
322
|
+
这些仿真测试应以当前的 primary / delegate / detached 与 member / holon + governance 模型理解。
|
|
323
|
+
|
|
324
|
+
## 目录结构
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
src/
|
|
328
|
+
├── core/ Actor 基础类型与 ActorSystem
|
|
329
|
+
├── runtime/ ActorRuntime
|
|
330
|
+
├── dispatch/ dispatch 适配层
|
|
331
|
+
├── pipeline/ pipeline 适配层
|
|
332
|
+
└── orchestration/ fiber 调度与编排
|
|
333
|
+
|
|
334
|
+
test/
|
|
335
|
+
├── simulations/ 面向业务语义的仿真测试
|
|
336
|
+
└── *.test.ts 核心能力测试
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## 构建与开发
|
|
340
|
+
|
|
341
|
+
安装依赖:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
bun install
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
构建:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
bun run build
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
监听编译:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
bun run dev
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
测试:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
bun run test
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## 相关文档
|
|
366
|
+
|
|
367
|
+
- `ACTOR-FOR-AI-AGENTS.md`:如何用 actor / fiber 抽象实现 AI agent 能力
|
|
368
|
+
- `doc/runtime-foundations.md`:runtime foundation、边界与第一轮 adoption 说明
|
|
369
|
+
|
|
370
|
+
## 一句话总结
|
|
371
|
+
|
|
372
|
+
`depa-actor` 的核心价值不是“把函数包装成 actor”,而是提供一套足够小、足够清晰的消息与调度抽象,使复杂系统——尤其是 AI agent 运行时——能够用可组合、可测试的方式实现。
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "depa-actor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"test:watch": "bun test --watch"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.8.3"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* depa-actor — ActorSystem
|
|
3
|
+
*
|
|
4
|
+
* Multi-mailbox actor system with priority-based drain.
|
|
5
|
+
* Evolved from depa-data-graph ActorSystem with MailboxSchema support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
MailboxSchema,
|
|
10
|
+
ActorEnvelope,
|
|
11
|
+
ActorLogEntry,
|
|
12
|
+
ActorDef,
|
|
13
|
+
ActorRef,
|
|
14
|
+
ActorSelf,
|
|
15
|
+
TaggedEnvelope,
|
|
16
|
+
TagHandler,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// ─── ActorCell (internal) ────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
type ActorCell<TRuntime, TSchema extends MailboxSchema, TState = void> = {
|
|
22
|
+
id: string;
|
|
23
|
+
def: ActorDef<TRuntime, TSchema, TState>;
|
|
24
|
+
state: TState;
|
|
25
|
+
queue: ActorEnvelope<TSchema>[];
|
|
26
|
+
processing: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ─── ActorSystem ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export class ActorSystem<TRuntime, TSchema extends MailboxSchema> {
|
|
32
|
+
private seq = 0;
|
|
33
|
+
private cells = new Map<string, ActorCell<TRuntime, TSchema, unknown>>();
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private getRuntime: () => TRuntime,
|
|
37
|
+
private onLog?: (entry: ActorLogEntry<TSchema>) => void,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
// ── Query ──
|
|
41
|
+
|
|
42
|
+
ids(): string[] {
|
|
43
|
+
return Array.from(this.cells.keys()).sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
has(id: string): boolean {
|
|
47
|
+
return this.cells.has(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Registration ──
|
|
51
|
+
|
|
52
|
+
register<TState = void>(
|
|
53
|
+
id: string,
|
|
54
|
+
def: ActorDef<TRuntime, TSchema, TState>,
|
|
55
|
+
): void {
|
|
56
|
+
if (this.cells.has(id)) {
|
|
57
|
+
throw new Error(`Actor already registered: ${id}`);
|
|
58
|
+
}
|
|
59
|
+
if (!def.handler && !def.handlers) {
|
|
60
|
+
throw new Error(`Actor "${id}" must have at least handler or handlers`);
|
|
61
|
+
}
|
|
62
|
+
this.cells.set(id, {
|
|
63
|
+
id,
|
|
64
|
+
def: def as ActorDef<TRuntime, TSchema, unknown>,
|
|
65
|
+
state: def.initialState as unknown,
|
|
66
|
+
queue: [],
|
|
67
|
+
processing: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
unregister(id: string): void {
|
|
72
|
+
this.cells.delete(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Messaging ──
|
|
76
|
+
|
|
77
|
+
refFrom(from: string, to: string): ActorRef<TSchema> | undefined {
|
|
78
|
+
if (!this.cells.has(to)) return undefined;
|
|
79
|
+
return {
|
|
80
|
+
id: to,
|
|
81
|
+
send: (tag, payload) => this.sendFrom(from, to, tag, payload),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sendFrom<TTag extends keyof TSchema & string>(
|
|
86
|
+
from: string,
|
|
87
|
+
to: string,
|
|
88
|
+
tag: TTag,
|
|
89
|
+
payload: TSchema[TTag],
|
|
90
|
+
): void {
|
|
91
|
+
const target = this.cells.get(to);
|
|
92
|
+
const envelope: ActorEnvelope<TSchema> = {
|
|
93
|
+
id: ++this.seq,
|
|
94
|
+
ts: Date.now(),
|
|
95
|
+
from,
|
|
96
|
+
to,
|
|
97
|
+
tag,
|
|
98
|
+
payload,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (!target) {
|
|
102
|
+
this.onLog?.({ kind: 'error', ...envelope, error: `Unknown actor: ${to}` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.onLog?.({ kind: 'send', ...envelope });
|
|
107
|
+
target.queue.push(envelope);
|
|
108
|
+
this.scheduleDrain(target);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
broadcastFrom<TTag extends keyof TSchema & string>(
|
|
112
|
+
from: string,
|
|
113
|
+
tag: TTag,
|
|
114
|
+
payload: TSchema[TTag],
|
|
115
|
+
opts?: { excludeSelf?: boolean },
|
|
116
|
+
): void {
|
|
117
|
+
for (const id of this.cells.keys()) {
|
|
118
|
+
if (opts?.excludeSelf && id === from) continue;
|
|
119
|
+
this.sendFrom(from, id, tag, payload);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Drain ──
|
|
124
|
+
|
|
125
|
+
private scheduleDrain(cell: ActorCell<TRuntime, TSchema, unknown>): void {
|
|
126
|
+
if (cell.processing) return;
|
|
127
|
+
cell.processing = true;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
void this.drain(cell);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Priority-based drain: sorts pending messages by tag priority,
|
|
135
|
+
* then processes sequentially. Per-tag handlers take precedence.
|
|
136
|
+
*/
|
|
137
|
+
private async drain(cell: ActorCell<TRuntime, TSchema, unknown>): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
while (cell.queue.length > 0) {
|
|
140
|
+
// Sort by priority (lower = higher priority, default 100)
|
|
141
|
+
const priorities = cell.def.priority ?? {};
|
|
142
|
+
cell.queue.sort((a, b) => {
|
|
143
|
+
const pa = (priorities as Record<string, number | undefined>)[a.tag] ?? 100;
|
|
144
|
+
const pb = (priorities as Record<string, number | undefined>)[b.tag] ?? 100;
|
|
145
|
+
return pa - pb;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const envelope = cell.queue.shift()!;
|
|
149
|
+
const self = this.makeSelf(cell);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await this.dispatch(cell, self, envelope);
|
|
153
|
+
this.onLog?.({ kind: 'deliver', ...envelope });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
156
|
+
this.onLog?.({ kind: 'error', ...envelope, error: message });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
cell.processing = false;
|
|
161
|
+
if (cell.queue.length > 0) {
|
|
162
|
+
this.scheduleDrain(cell);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Route envelope to per-tag handler or unified handler */
|
|
168
|
+
private async dispatch(
|
|
169
|
+
cell: ActorCell<TRuntime, TSchema, unknown>,
|
|
170
|
+
self: ActorSelf<TRuntime, TSchema, unknown>,
|
|
171
|
+
envelope: ActorEnvelope<TSchema>,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const tagHandler = cell.def.handlers?.[envelope.tag] as
|
|
174
|
+
| TagHandler<TRuntime, TSchema, unknown, string>
|
|
175
|
+
| undefined;
|
|
176
|
+
|
|
177
|
+
if (tagHandler) {
|
|
178
|
+
await tagHandler(self, envelope as TaggedEnvelope<TSchema, string>);
|
|
179
|
+
} else if (cell.def.handler) {
|
|
180
|
+
await cell.def.handler(self, envelope);
|
|
181
|
+
} else {
|
|
182
|
+
// No handler for this tag — log and skip
|
|
183
|
+
this.onLog?.({
|
|
184
|
+
kind: 'error',
|
|
185
|
+
...envelope,
|
|
186
|
+
error: `No handler for tag "${envelope.tag}" on actor "${cell.id}"`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Construct ActorSelf with selective receive capabilities */
|
|
192
|
+
private makeSelf(
|
|
193
|
+
cell: ActorCell<TRuntime, TSchema, unknown>,
|
|
194
|
+
): ActorSelf<TRuntime, TSchema, unknown> {
|
|
195
|
+
return {
|
|
196
|
+
id: cell.id,
|
|
197
|
+
ref: {
|
|
198
|
+
id: cell.id,
|
|
199
|
+
send: (tag, payload) => this.sendFrom(cell.id, cell.id, tag, payload),
|
|
200
|
+
},
|
|
201
|
+
runtime: this.getRuntime(),
|
|
202
|
+
state: cell.state,
|
|
203
|
+
|
|
204
|
+
send: (to, tag, payload) => this.sendFrom(cell.id, to, tag, payload),
|
|
205
|
+
broadcast: (tag, payload, opts) => this.broadcastFrom(cell.id, tag, payload, opts),
|
|
206
|
+
|
|
207
|
+
// Selective receive
|
|
208
|
+
hasPending: (tag) => cell.queue.some((e) => e.tag === tag),
|
|
209
|
+
drainMailbox: <TTag extends keyof TSchema & string>(tag: TTag) => {
|
|
210
|
+
const matching: TaggedEnvelope<TSchema, TTag>[] = [];
|
|
211
|
+
const remaining: ActorEnvelope<TSchema>[] = [];
|
|
212
|
+
for (const e of cell.queue) {
|
|
213
|
+
if (e.tag === tag) {
|
|
214
|
+
matching.push(e as TaggedEnvelope<TSchema, TTag>);
|
|
215
|
+
} else {
|
|
216
|
+
remaining.push(e);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
cell.queue.length = 0;
|
|
220
|
+
cell.queue.push(...remaining);
|
|
221
|
+
return matching;
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|