@resonatehq/sdk 0.9.6 → 0.10.1
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 +84 -12
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +12 -0
- package/dist/clock.js.map +1 -0
- package/dist/codec.d.ts +11 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +115 -0
- package/dist/codec.js.map +1 -0
- package/dist/computation.d.ts +40 -0
- package/dist/computation.d.ts.map +1 -0
- package/dist/computation.js +152 -0
- package/dist/computation.js.map +1 -0
- package/dist/{src/context.d.ts → context.d.ts} +70 -40
- package/dist/context.d.ts.map +1 -0
- package/dist/{src/context.js → context.js} +140 -155
- package/dist/context.js.map +1 -0
- package/dist/core.d.ts +45 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +140 -0
- package/dist/core.js.map +1 -0
- package/dist/coroutine.d.ts +37 -0
- package/dist/coroutine.d.ts.map +1 -0
- package/dist/coroutine.js +259 -0
- package/dist/coroutine.js.map +1 -0
- package/dist/{src/decorator.d.ts → decorator.d.ts} +9 -8
- package/dist/decorator.d.ts.map +1 -0
- package/dist/decorator.js +150 -0
- package/dist/decorator.js.map +1 -0
- package/dist/encryptor.d.ts +10 -0
- package/dist/encryptor.d.ts.map +1 -0
- package/dist/encryptor.js +9 -0
- package/dist/encryptor.js.map +1 -0
- package/dist/{src/exceptions.d.ts → exceptions.d.ts} +4 -1
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/{src/exceptions.js → exceptions.js} +11 -11
- package/dist/exceptions.js.map +1 -0
- package/dist/{src/heartbeat.d.ts → heartbeat.d.ts} +5 -3
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +50 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +33 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +54 -0
- package/dist/logger.js.map +1 -0
- package/dist/network/http.d.ts +86 -0
- package/dist/network/http.d.ts.map +1 -0
- package/dist/network/http.js +350 -0
- package/dist/network/http.js.map +1 -0
- package/dist/network/local.d.ts +188 -0
- package/dist/network/local.d.ts.map +1 -0
- package/dist/network/local.js +1349 -0
- package/dist/network/local.js.map +1 -0
- package/dist/network/network.d.ts +17 -0
- package/dist/network/network.d.ts.map +1 -0
- package/dist/network/network.js +2 -0
- package/dist/network/network.js.map +1 -0
- package/dist/network/types.d.ts +1280 -0
- package/dist/network/types.d.ts.map +1 -0
- package/dist/network/types.js +920 -0
- package/dist/network/types.js.map +1 -0
- package/dist/{src/options.d.ts → options.d.ts} +15 -6
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +34 -0
- package/dist/options.js.map +1 -0
- package/dist/promises.d.ts +54 -0
- package/dist/promises.d.ts.map +1 -0
- package/dist/promises.js +122 -0
- package/dist/promises.js.map +1 -0
- package/dist/{src/registry.d.ts → registry.d.ts} +1 -1
- package/dist/registry.d.ts.map +1 -0
- package/dist/{src/registry.js → registry.js} +6 -13
- package/dist/registry.js.map +1 -0
- package/dist/resonate.d.ts +168 -0
- package/dist/resonate.d.ts.map +1 -0
- package/dist/resonate.js +475 -0
- package/dist/resonate.js.map +1 -0
- package/dist/retries.d.ts.map +1 -0
- package/dist/{src/retries.js → retries.js} +4 -11
- package/dist/retries.js.map +1 -0
- package/dist/schedules.d.ts +22 -0
- package/dist/schedules.d.ts.map +1 -0
- package/dist/schedules.js +65 -0
- package/dist/schedules.js.map +1 -0
- package/dist/trace.d.ts +79 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +224 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +28 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +196 -0
- package/dist/util.js.map +1 -0
- package/package.json +18 -10
- package/dist/dev/network.d.ts +0 -34
- package/dist/dev/network.d.ts.map +0 -1
- package/dist/dev/network.js +0 -115
- package/dist/dev/network.js.map +0 -1
- package/dist/dev/server.d.ts +0 -89
- package/dist/dev/server.d.ts.map +0 -1
- package/dist/dev/server.js +0 -890
- package/dist/dev/server.js.map +0 -1
- package/dist/sim/example-1.d.ts +0 -2
- package/dist/sim/example-1.d.ts.map +0 -1
- package/dist/sim/example-1.js +0 -95
- package/dist/sim/example-1.js.map +0 -1
- package/dist/sim/example-2.d.ts +0 -2
- package/dist/sim/example-2.d.ts.map +0 -1
- package/dist/sim/example-2.js +0 -97
- package/dist/sim/example-2.js.map +0 -1
- package/dist/sim/example-3.d.ts +0 -2
- package/dist/sim/example-3.d.ts.map +0 -1
- package/dist/sim/example-3.js +0 -97
- package/dist/sim/example-3.js.map +0 -1
- package/dist/sim/main.d.ts +0 -14
- package/dist/sim/main.d.ts.map +0 -1
- package/dist/sim/main.js +0 -207
- package/dist/sim/main.js.map +0 -1
- package/dist/sim/src/server.d.ts +0 -15
- package/dist/sim/src/server.d.ts.map +0 -1
- package/dist/sim/src/server.js +0 -85
- package/dist/sim/src/server.js.map +0 -1
- package/dist/sim/src/simulator.d.ts +0 -65
- package/dist/sim/src/simulator.d.ts.map +0 -1
- package/dist/sim/src/simulator.js +0 -236
- package/dist/sim/src/simulator.js.map +0 -1
- package/dist/sim/src/worker.d.ts +0 -23
- package/dist/sim/src/worker.d.ts.map +0 -1
- package/dist/sim/src/worker.js +0 -217
- package/dist/sim/src/worker.js.map +0 -1
- package/dist/src/clock.d.ts.map +0 -1
- package/dist/src/clock.js +0 -17
- package/dist/src/clock.js.map +0 -1
- package/dist/src/computation.d.ts +0 -49
- package/dist/src/computation.d.ts.map +0 -1
- package/dist/src/computation.js +0 -314
- package/dist/src/computation.js.map +0 -1
- package/dist/src/context.d.ts.map +0 -1
- package/dist/src/context.js.map +0 -1
- package/dist/src/core.d.ts +0 -12
- package/dist/src/core.d.ts.map +0 -1
- package/dist/src/core.js +0 -28
- package/dist/src/core.js.map +0 -1
- package/dist/src/coroutine.d.ts +0 -42
- package/dist/src/coroutine.d.ts.map +0 -1
- package/dist/src/coroutine.js +0 -364
- package/dist/src/coroutine.js.map +0 -1
- package/dist/src/decorator.d.ts.map +0 -1
- package/dist/src/decorator.js +0 -220
- package/dist/src/decorator.js.map +0 -1
- package/dist/src/encoder.d.ts +0 -12
- package/dist/src/encoder.d.ts.map +0 -1
- package/dist/src/encoder.js +0 -101
- package/dist/src/encoder.js.map +0 -1
- package/dist/src/encryptor.d.ts +0 -10
- package/dist/src/encryptor.d.ts.map +0 -1
- package/dist/src/encryptor.js +0 -13
- package/dist/src/encryptor.js.map +0 -1
- package/dist/src/exceptions.d.ts.map +0 -1
- package/dist/src/exceptions.js.map +0 -1
- package/dist/src/handler.d.ts +0 -49
- package/dist/src/handler.d.ts.map +0 -1
- package/dist/src/handler.js +0 -316
- package/dist/src/handler.js.map +0 -1
- package/dist/src/heartbeat.d.ts.map +0 -1
- package/dist/src/heartbeat.js +0 -87
- package/dist/src/heartbeat.js.map +0 -1
- package/dist/src/index.d.ts +0 -15
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -61
- package/dist/src/index.js.map +0 -1
- package/dist/src/network/network.d.ts +0 -254
- package/dist/src/network/network.d.ts.map +0 -1
- package/dist/src/network/network.js +0 -4
- package/dist/src/network/network.js.map +0 -1
- package/dist/src/network/remote.d.ts +0 -85
- package/dist/src/network/remote.d.ts.map +0 -1
- package/dist/src/network/remote.js +0 -523
- package/dist/src/network/remote.js.map +0 -1
- package/dist/src/nursery.d.ts +0 -19
- package/dist/src/nursery.d.ts.map +0 -1
- package/dist/src/nursery.js +0 -102
- package/dist/src/nursery.js.map +0 -1
- package/dist/src/options.d.ts.map +0 -1
- package/dist/src/options.js +0 -70
- package/dist/src/options.js.map +0 -1
- package/dist/src/processor/processor.d.ts +0 -12
- package/dist/src/processor/processor.d.ts.map +0 -1
- package/dist/src/processor/processor.js +0 -43
- package/dist/src/processor/processor.js.map +0 -1
- package/dist/src/promises.d.ts +0 -54
- package/dist/src/promises.d.ts.map +0 -1
- package/dist/src/promises.js +0 -169
- package/dist/src/promises.js.map +0 -1
- package/dist/src/registry.d.ts.map +0 -1
- package/dist/src/registry.js.map +0 -1
- package/dist/src/resonate-inner.d.ts +0 -60
- package/dist/src/resonate-inner.d.ts.map +0 -1
- package/dist/src/resonate-inner.js +0 -97
- package/dist/src/resonate-inner.js.map +0 -1
- package/dist/src/resonate.d.ts +0 -341
- package/dist/src/resonate.d.ts.map +0 -1
- package/dist/src/resonate.js +0 -591
- package/dist/src/resonate.js.map +0 -1
- package/dist/src/retries.d.ts.map +0 -1
- package/dist/src/retries.js.map +0 -1
- package/dist/src/schedules.d.ts +0 -19
- package/dist/src/schedules.d.ts.map +0 -1
- package/dist/src/schedules.js +0 -85
- package/dist/src/schedules.js.map +0 -1
- package/dist/src/tasks.d.ts +0 -9
- package/dist/src/tasks.d.ts.map +0 -1
- package/dist/src/tasks.js +0 -61
- package/dist/src/tasks.js.map +0 -1
- package/dist/src/tracer.d.ts +0 -23
- package/dist/src/tracer.d.ts.map +0 -1
- package/dist/src/tracer.js +0 -25
- package/dist/src/tracer.js.map +0 -1
- package/dist/src/types.d.ts +0 -28
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -11
- package/dist/src/types.js.map +0 -1
- package/dist/src/util.d.ts +0 -20
- package/dist/src/util.d.ts.map +0 -1
- package/dist/src/util.js +0 -120
- package/dist/src/util.js.map +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- /package/dist/{src/clock.d.ts → clock.d.ts} +0 -0
- /package/dist/{src/retries.d.ts → retries.d.ts} +0 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CronExpressionParser } from "cron-parser";
|
|
3
|
+
import { assert, VERSION } from "../util.js";
|
|
4
|
+
import { isResponse } from "./types.js";
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// CONSTANTS
|
|
7
|
+
// =============================================================================
|
|
8
|
+
const PENDING_RETRY_TTL = 30000;
|
|
9
|
+
export class Server {
|
|
10
|
+
promises = new Map();
|
|
11
|
+
tasks = new Map();
|
|
12
|
+
schedules = new Map();
|
|
13
|
+
pTimeouts = [];
|
|
14
|
+
tTimeouts = [];
|
|
15
|
+
sTimeouts = [];
|
|
16
|
+
outgoing = [];
|
|
17
|
+
// When "eager", every action handler converges physical state to logical
|
|
18
|
+
// state by running tryEagerTimeout for the relevant id(s) before the
|
|
19
|
+
// handler body. When "none", convergence happens only via debug.tick (or
|
|
20
|
+
// an equivalent background mechanism) — read paths must rely on
|
|
21
|
+
// projection to return the logical state.
|
|
22
|
+
timeoutMode;
|
|
23
|
+
constructor(opts = {}) {
|
|
24
|
+
this.timeoutMode = opts.timeoutMode ?? "eager";
|
|
25
|
+
}
|
|
26
|
+
apply(now, req) {
|
|
27
|
+
const changes = [];
|
|
28
|
+
const error = this.validate(req);
|
|
29
|
+
if (error !== null) {
|
|
30
|
+
return { response: { kind: req.kind, head: { status: 400 }, data: error }, changes };
|
|
31
|
+
}
|
|
32
|
+
let result;
|
|
33
|
+
switch (req.kind) {
|
|
34
|
+
case "promise.get": {
|
|
35
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
36
|
+
result = this.promiseGet(now, req);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "promise.create": {
|
|
40
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
41
|
+
result = this.promiseCreate(now, req);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "promise.settle": {
|
|
45
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
46
|
+
result = this.promiseSettle(now, req);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "promise.register_callback": {
|
|
50
|
+
changes.push(...this.tryEagerTimeout(now, req.data.awaited));
|
|
51
|
+
changes.push(...this.tryEagerTimeout(now, req.data.awaiter));
|
|
52
|
+
result = this.promiseRegisterCallback(now, req);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "promise.register_listener": {
|
|
56
|
+
changes.push(...this.tryEagerTimeout(now, req.data.awaited));
|
|
57
|
+
result = this.promiseRegisterListener(now, req);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "promise.search": {
|
|
61
|
+
result = { response: this.response("promise.search", 501, "Not implemented"), changes: [] };
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "task.get": {
|
|
65
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
66
|
+
result = this.taskGet(now, req);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "task.create": {
|
|
70
|
+
changes.push(...this.tryEagerTimeout(now, req.data.action.data.id));
|
|
71
|
+
result = this.taskCreate(now, req);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "task.acquire": {
|
|
75
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
76
|
+
result = this.taskAcquire(now, req);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "task.release": {
|
|
80
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
81
|
+
result = this.taskRelease(now, req);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "task.fulfill": {
|
|
85
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
86
|
+
result = this.taskFulfill(now, req);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "task.suspend": {
|
|
90
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
91
|
+
for (const action of req.data.actions) {
|
|
92
|
+
changes.push(...this.tryEagerTimeout(now, action.data.awaited));
|
|
93
|
+
}
|
|
94
|
+
result = this.taskSuspend(now, req);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "task.halt": {
|
|
98
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
99
|
+
result = this.taskHalt(now, req);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "task.continue": {
|
|
103
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
104
|
+
result = this.taskContinue(now, req);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "task.fence": {
|
|
108
|
+
changes.push(...this.tryEagerTimeout(now, req.data.id));
|
|
109
|
+
changes.push(...this.tryEagerTimeout(now, req.data.action.data.id));
|
|
110
|
+
result = this.taskFence(now, req);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "task.heartbeat": {
|
|
114
|
+
result = this.taskHeartbeat(now, req);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "task.search": {
|
|
118
|
+
result = { response: this.response("task.search", 501, "Not implemented"), changes: [] };
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "debug.start": {
|
|
122
|
+
result = this.debugStart();
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "debug.reset": {
|
|
126
|
+
result = this.debugReset();
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "debug.snap": {
|
|
130
|
+
result = this.debugSnap();
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "debug.tick": {
|
|
134
|
+
result = this.debugTick(req);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "debug.stop": {
|
|
138
|
+
result = this.debugStop();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "schedule.get": {
|
|
142
|
+
result = this.scheduleGet(req);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "schedule.create": {
|
|
146
|
+
result = this.scheduleCreate(now, req);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "schedule.delete": {
|
|
150
|
+
result = this.scheduleDelete(req);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "schedule.search": {
|
|
154
|
+
result = { response: this.response("schedule.search", 501, "Not implemented"), changes: [] };
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
changes.push(...result.changes);
|
|
159
|
+
return { response: result.response, changes };
|
|
160
|
+
}
|
|
161
|
+
validate(req) {
|
|
162
|
+
switch (req.kind) {
|
|
163
|
+
case "promise.register_callback":
|
|
164
|
+
if (req.data.awaited === req.data.awaiter) {
|
|
165
|
+
return "Awaited and awaiter must be different";
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
case "task.suspend":
|
|
169
|
+
if (req.data.actions.length === 0) {
|
|
170
|
+
return "Actions list must not be empty";
|
|
171
|
+
}
|
|
172
|
+
if (req.data.actions.some((a) => a.data.awaiter !== req.data.id)) {
|
|
173
|
+
return "Awaiter must be the suspending task";
|
|
174
|
+
}
|
|
175
|
+
if (req.data.actions.some((a) => a.data.awaited === req.data.id)) {
|
|
176
|
+
return "Task cannot await its own promise";
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
case "task.create":
|
|
180
|
+
if (!req.data.action.data.tags?.["resonate:target"]) {
|
|
181
|
+
return "Action must have a resonate:target tag";
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
case "task.fulfill":
|
|
185
|
+
if (req.data.action.data.id !== req.data.id) {
|
|
186
|
+
return "Promise ID must match task ID";
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
default:
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ===========================================================================
|
|
194
|
+
// PROMISE OPERATIONS
|
|
195
|
+
// ===========================================================================
|
|
196
|
+
promiseGet(now, req) {
|
|
197
|
+
const promise = this.promises.get(req.data.id);
|
|
198
|
+
if (!promise) {
|
|
199
|
+
return { response: this.response("promise.get", 404, "Promise not found"), changes: [] };
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
response: this.response("promise.get", 200, {
|
|
203
|
+
promise: this.toPromiseRecord(this.projectedPromise(now, promise)),
|
|
204
|
+
}),
|
|
205
|
+
changes: [],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
promiseCreate(now, req) {
|
|
209
|
+
const existingPromise = this.promises.get(req.data.id);
|
|
210
|
+
if (existingPromise) {
|
|
211
|
+
return {
|
|
212
|
+
response: this.response("promise.create", 200, {
|
|
213
|
+
promise: this.toPromiseRecord(this.projectedPromise(now, existingPromise)),
|
|
214
|
+
}),
|
|
215
|
+
changes: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const changes = [];
|
|
219
|
+
// Check if the promise is already timed out at creation
|
|
220
|
+
if (now >= req.data.timeoutAt) {
|
|
221
|
+
const promise = {
|
|
222
|
+
id: req.data.id,
|
|
223
|
+
state: this.timeoutState(req.data.tags),
|
|
224
|
+
param: req.data.param ?? { data: undefined, headers: undefined },
|
|
225
|
+
value: {},
|
|
226
|
+
tags: req.data.tags,
|
|
227
|
+
createdAt: req.data.timeoutAt,
|
|
228
|
+
settledAt: req.data.timeoutAt,
|
|
229
|
+
timeoutAt: req.data.timeoutAt,
|
|
230
|
+
callbacks: new Set(),
|
|
231
|
+
listeners: new Set(),
|
|
232
|
+
};
|
|
233
|
+
changes.push(this.setPromise(promise));
|
|
234
|
+
// Create fulfilled task inline if promise has a target
|
|
235
|
+
const address = req.data.tags["resonate:target"];
|
|
236
|
+
if (address) {
|
|
237
|
+
changes.push(this.setTask({
|
|
238
|
+
id: req.data.id,
|
|
239
|
+
state: "fulfilled",
|
|
240
|
+
version: 0,
|
|
241
|
+
pid: undefined,
|
|
242
|
+
ttl: undefined,
|
|
243
|
+
resumes: new Set(),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
changes.push(...this.triggerSettlement(req.data.id, now));
|
|
247
|
+
return {
|
|
248
|
+
response: this.response("promise.create", 200, { promise: this.toPromiseRecord(promise) }),
|
|
249
|
+
changes,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const promise = {
|
|
253
|
+
id: req.data.id,
|
|
254
|
+
state: "pending",
|
|
255
|
+
param: req.data.param ?? { data: undefined, headers: undefined },
|
|
256
|
+
value: {},
|
|
257
|
+
tags: req.data.tags,
|
|
258
|
+
createdAt: now,
|
|
259
|
+
settledAt: null,
|
|
260
|
+
timeoutAt: req.data.timeoutAt,
|
|
261
|
+
callbacks: new Set(),
|
|
262
|
+
listeners: new Set(),
|
|
263
|
+
};
|
|
264
|
+
changes.push(this.setPromise(promise));
|
|
265
|
+
changes.push(this.setPTimeout({ id: req.data.id, timeout: req.data.timeoutAt }));
|
|
266
|
+
const address = req.data.tags["resonate:target"];
|
|
267
|
+
if (address) {
|
|
268
|
+
const delay = Number(req.data.tags["resonate:delay"]);
|
|
269
|
+
const deferred = Number.isFinite(delay) && now < delay;
|
|
270
|
+
const task = {
|
|
271
|
+
id: req.data.id,
|
|
272
|
+
state: "pending",
|
|
273
|
+
version: 0,
|
|
274
|
+
resumes: new Set(),
|
|
275
|
+
};
|
|
276
|
+
changes.push(this.setTask(task));
|
|
277
|
+
changes.push(this.setTTimeout({
|
|
278
|
+
id: req.data.id,
|
|
279
|
+
type: 0,
|
|
280
|
+
timeout: deferred ? delay : now + PENDING_RETRY_TTL,
|
|
281
|
+
}));
|
|
282
|
+
if (!deferred) {
|
|
283
|
+
changes.push(this.sendMessage(address, { kind: "execute", head: {}, data: { task: { id: req.data.id, version: 0 } } }));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
response: this.response("promise.create", 200, { promise: this.toPromiseRecord(promise) }),
|
|
288
|
+
changes,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
promiseSettle(now, req) {
|
|
292
|
+
const promise = this.promises.get(req.data.id);
|
|
293
|
+
if (!promise) {
|
|
294
|
+
return { response: this.response("promise.settle", 404, "Promise not found"), changes: [] };
|
|
295
|
+
}
|
|
296
|
+
// Idempotent return: if the promise is already physically terminal OR
|
|
297
|
+
// pending past its deadline (logically settled), return the projected
|
|
298
|
+
// logical state without writing.
|
|
299
|
+
const projected = this.projectedPromise(now, promise);
|
|
300
|
+
if (projected.state !== "pending") {
|
|
301
|
+
return {
|
|
302
|
+
response: this.response("promise.settle", 200, { promise: this.toPromiseRecord(projected) }),
|
|
303
|
+
changes: [],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const changes = [];
|
|
307
|
+
const settledPromise = {
|
|
308
|
+
...promise,
|
|
309
|
+
state: req.data.state,
|
|
310
|
+
value: req.data.value,
|
|
311
|
+
settledAt: now,
|
|
312
|
+
};
|
|
313
|
+
changes.push(this.setPromise(settledPromise));
|
|
314
|
+
changes.push(this.delPTimeout(req.data.id));
|
|
315
|
+
changes.push(...this.triggerSettlement(req.data.id, now));
|
|
316
|
+
return {
|
|
317
|
+
response: this.response("promise.settle", 200, { promise: this.toPromiseRecord(settledPromise) }),
|
|
318
|
+
changes,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
promiseRegisterCallback(now, req) {
|
|
322
|
+
const awaitedPromise = this.promises.get(req.data.awaited);
|
|
323
|
+
if (!awaitedPromise) {
|
|
324
|
+
return { response: this.response("promise.register_callback", 404, "Awaited promise not found"), changes: [] };
|
|
325
|
+
}
|
|
326
|
+
const awaiterPromise = this.promises.get(req.data.awaiter);
|
|
327
|
+
if (!awaiterPromise) {
|
|
328
|
+
return { response: this.response("promise.register_callback", 422, "Awaiter promise not found"), changes: [] };
|
|
329
|
+
}
|
|
330
|
+
// HasAddress check: awaiter must have a resonate:target tag
|
|
331
|
+
if (!awaiterPromise.tags["resonate:target"]) {
|
|
332
|
+
return { response: this.response("promise.register_callback", 422, "Awaiter has no address"), changes: [] };
|
|
333
|
+
}
|
|
334
|
+
const changes = [];
|
|
335
|
+
// Project for the logically-pending check and for the response — the
|
|
336
|
+
// callback is written only when both sides are logically pending, and
|
|
337
|
+
// the response always reflects the projected awaited promise.
|
|
338
|
+
const awaitedProjected = this.projectedPromise(now, awaitedPromise);
|
|
339
|
+
const awaiterProjected = this.projectedPromise(now, awaiterPromise);
|
|
340
|
+
if (awaitedProjected.state === "pending" && awaiterProjected.state === "pending") {
|
|
341
|
+
awaitedPromise.callbacks.add(req.data.awaiter);
|
|
342
|
+
changes.push(this.setPromise(awaitedPromise));
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
response: this.response("promise.register_callback", 200, {
|
|
346
|
+
promise: this.toPromiseRecord(awaitedProjected),
|
|
347
|
+
}),
|
|
348
|
+
changes,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
promiseRegisterListener(now, req) {
|
|
352
|
+
const promise = this.promises.get(req.data.awaited);
|
|
353
|
+
if (!promise) {
|
|
354
|
+
return { response: this.response("promise.register_listener", 404, "Promise not found"), changes: [] };
|
|
355
|
+
}
|
|
356
|
+
const changes = [];
|
|
357
|
+
// Project for the logically-pending check and for the response — the
|
|
358
|
+
// listener is registered only when the promise is logically pending,
|
|
359
|
+
// and the response always reflects the projected awaited promise.
|
|
360
|
+
const projected = this.projectedPromise(now, promise);
|
|
361
|
+
if (projected.state === "pending") {
|
|
362
|
+
promise.listeners.add(req.data.address);
|
|
363
|
+
changes.push(this.setPromise(promise));
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
response: this.response("promise.register_listener", 200, {
|
|
367
|
+
promise: this.toPromiseRecord(projected),
|
|
368
|
+
}),
|
|
369
|
+
changes,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// ===========================================================================
|
|
373
|
+
// TASK OPERATIONS
|
|
374
|
+
// ===========================================================================
|
|
375
|
+
taskGet(now, req) {
|
|
376
|
+
const task = this.tasks.get(req.data.id);
|
|
377
|
+
if (!task) {
|
|
378
|
+
return { response: this.response("task.get", 404, "Task not found"), changes: [] };
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
response: this.response("task.get", 200, {
|
|
382
|
+
task: this.toTaskRecord(this.projectedTask(now, task)),
|
|
383
|
+
}),
|
|
384
|
+
changes: [],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
taskCreate(now, req) {
|
|
388
|
+
const existingTask = this.tasks.get(req.data.action.data.id);
|
|
389
|
+
if (existingTask) {
|
|
390
|
+
const promise = this.getPromiseOrThrow(existingTask.id);
|
|
391
|
+
// Pending → Acquired (acquire and return 200)
|
|
392
|
+
if (existingTask.state === "pending") {
|
|
393
|
+
const newVersion = existingTask.version + 1;
|
|
394
|
+
const changes = [];
|
|
395
|
+
changes.push(this.setTask({
|
|
396
|
+
...existingTask,
|
|
397
|
+
state: "acquired",
|
|
398
|
+
version: newVersion,
|
|
399
|
+
pid: req.data.pid,
|
|
400
|
+
ttl: req.data.ttl,
|
|
401
|
+
resumes: new Set(),
|
|
402
|
+
}));
|
|
403
|
+
changes.push(this.setTTimeout({ id: existingTask.id, type: 1, timeout: now + req.data.ttl }));
|
|
404
|
+
return {
|
|
405
|
+
response: this.response("task.create", 200, {
|
|
406
|
+
task: this.toTaskRecord({
|
|
407
|
+
...existingTask,
|
|
408
|
+
state: "acquired",
|
|
409
|
+
version: newVersion,
|
|
410
|
+
pid: req.data.pid,
|
|
411
|
+
ttl: req.data.ttl,
|
|
412
|
+
resumes: new Set(),
|
|
413
|
+
}),
|
|
414
|
+
promise: this.toPromiseRecord(promise),
|
|
415
|
+
preload: this.preload(existingTask.id),
|
|
416
|
+
}),
|
|
417
|
+
changes,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// Fulfilled → Fulfilled (idempotent, return 200)
|
|
421
|
+
if (existingTask.state === "fulfilled") {
|
|
422
|
+
return {
|
|
423
|
+
response: this.response("task.create", 200, {
|
|
424
|
+
task: this.toTaskRecord(existingTask),
|
|
425
|
+
promise: this.toPromiseRecord(promise),
|
|
426
|
+
preload: this.preload(existingTask.id),
|
|
427
|
+
}),
|
|
428
|
+
changes: [],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { response: this.response("task.create", 409, "Task already exists"), changes: [] };
|
|
432
|
+
}
|
|
433
|
+
const changes = [];
|
|
434
|
+
const actionData = req.data.action.data;
|
|
435
|
+
// Guard: promise already exists
|
|
436
|
+
const existingPromise = this.promises.get(actionData.id);
|
|
437
|
+
if (existingPromise) {
|
|
438
|
+
if (!existingPromise.tags["resonate:target"]) {
|
|
439
|
+
return { response: this.response("task.create", 422, "Promise has no address"), changes: [] };
|
|
440
|
+
}
|
|
441
|
+
return { response: this.response("task.create", 409, "Promise already exists"), changes: [] };
|
|
442
|
+
}
|
|
443
|
+
// Guard: promise already timed out
|
|
444
|
+
if (now >= actionData.timeoutAt) {
|
|
445
|
+
const promise = {
|
|
446
|
+
id: actionData.id,
|
|
447
|
+
state: this.timeoutState(actionData.tags),
|
|
448
|
+
param: actionData.param,
|
|
449
|
+
value: {},
|
|
450
|
+
tags: actionData.tags,
|
|
451
|
+
createdAt: actionData.timeoutAt,
|
|
452
|
+
settledAt: actionData.timeoutAt,
|
|
453
|
+
timeoutAt: actionData.timeoutAt,
|
|
454
|
+
callbacks: new Set(),
|
|
455
|
+
listeners: new Set(),
|
|
456
|
+
};
|
|
457
|
+
changes.push(this.setPromise(promise));
|
|
458
|
+
const task = {
|
|
459
|
+
id: actionData.id,
|
|
460
|
+
state: "fulfilled",
|
|
461
|
+
version: 0,
|
|
462
|
+
pid: undefined,
|
|
463
|
+
ttl: undefined,
|
|
464
|
+
resumes: new Set(),
|
|
465
|
+
};
|
|
466
|
+
changes.push(this.setTask(task));
|
|
467
|
+
return {
|
|
468
|
+
response: this.response("task.create", 200, {
|
|
469
|
+
task: this.toTaskRecord(task),
|
|
470
|
+
promise: this.toPromiseRecord(promise),
|
|
471
|
+
preload: [],
|
|
472
|
+
}),
|
|
473
|
+
changes,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
// Create promise in pending state
|
|
477
|
+
const promise = {
|
|
478
|
+
id: actionData.id,
|
|
479
|
+
state: "pending",
|
|
480
|
+
param: actionData.param,
|
|
481
|
+
value: {},
|
|
482
|
+
tags: actionData.tags,
|
|
483
|
+
createdAt: now,
|
|
484
|
+
settledAt: null,
|
|
485
|
+
timeoutAt: actionData.timeoutAt,
|
|
486
|
+
callbacks: new Set(),
|
|
487
|
+
listeners: new Set(),
|
|
488
|
+
};
|
|
489
|
+
changes.push(this.setPromise(promise));
|
|
490
|
+
changes.push(this.setPTimeout({ id: actionData.id, timeout: actionData.timeoutAt }));
|
|
491
|
+
// Step 2: Create task in acquired state
|
|
492
|
+
const task = {
|
|
493
|
+
id: actionData.id,
|
|
494
|
+
state: "acquired",
|
|
495
|
+
version: 1,
|
|
496
|
+
pid: req.data.pid,
|
|
497
|
+
ttl: req.data.ttl,
|
|
498
|
+
resumes: new Set(),
|
|
499
|
+
};
|
|
500
|
+
changes.push(this.setTask(task));
|
|
501
|
+
// Step 3: Set type=1 (lease) timeout
|
|
502
|
+
changes.push(this.setTTimeout({ id: actionData.id, type: 1, timeout: now + req.data.ttl }));
|
|
503
|
+
return {
|
|
504
|
+
response: this.response("task.create", 200, {
|
|
505
|
+
task: this.toTaskRecord(task),
|
|
506
|
+
promise: this.toPromiseRecord(promise),
|
|
507
|
+
preload: this.preload(actionData.id),
|
|
508
|
+
}),
|
|
509
|
+
changes,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
taskAcquire(now, req) {
|
|
513
|
+
const task = this.tasks.get(req.data.id);
|
|
514
|
+
if (!task) {
|
|
515
|
+
return { response: this.response("task.acquire", 404, "Task not found"), changes: [] };
|
|
516
|
+
}
|
|
517
|
+
if (task.state !== "pending") {
|
|
518
|
+
return { response: this.response("task.acquire", 409, "Task not in pending state"), changes: [] };
|
|
519
|
+
}
|
|
520
|
+
// Logical-state check: if the task's promise is logically settled,
|
|
521
|
+
// the task is logically fulfilled and cannot be acquired. This catches
|
|
522
|
+
// the projection-mode case where the physical task state has not yet
|
|
523
|
+
// converged.
|
|
524
|
+
const promise = this.getPromiseOrThrow(req.data.id);
|
|
525
|
+
if (this.projectedPromise(now, promise).state !== "pending") {
|
|
526
|
+
return { response: this.response("task.acquire", 409, "Task logically fulfilled"), changes: [] };
|
|
527
|
+
}
|
|
528
|
+
if (task.version !== req.data.version) {
|
|
529
|
+
return { response: this.response("task.acquire", 409, "Version mismatch"), changes: [] };
|
|
530
|
+
}
|
|
531
|
+
const newVersion = task.version + 1;
|
|
532
|
+
const changes = [];
|
|
533
|
+
changes.push(this.setTask({
|
|
534
|
+
...task,
|
|
535
|
+
state: "acquired",
|
|
536
|
+
version: newVersion,
|
|
537
|
+
pid: req.data.pid,
|
|
538
|
+
ttl: req.data.ttl,
|
|
539
|
+
resumes: new Set(),
|
|
540
|
+
}));
|
|
541
|
+
changes.push(this.setTTimeout({ id: req.data.id, type: 1, timeout: now + req.data.ttl }));
|
|
542
|
+
return {
|
|
543
|
+
response: this.response("task.acquire", 200, {
|
|
544
|
+
task: this.toTaskRecord({
|
|
545
|
+
...task,
|
|
546
|
+
state: "acquired",
|
|
547
|
+
version: newVersion,
|
|
548
|
+
pid: req.data.pid,
|
|
549
|
+
ttl: req.data.ttl,
|
|
550
|
+
resumes: new Set(),
|
|
551
|
+
}),
|
|
552
|
+
promise: this.toPromiseRecord(promise),
|
|
553
|
+
preload: this.preload(req.data.id),
|
|
554
|
+
}),
|
|
555
|
+
changes,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
taskRelease(now, req) {
|
|
559
|
+
const task = this.tasks.get(req.data.id);
|
|
560
|
+
if (!task) {
|
|
561
|
+
return { response: this.response("task.release", 404, "Task not found"), changes: [] };
|
|
562
|
+
}
|
|
563
|
+
if (task.state !== "acquired") {
|
|
564
|
+
return { response: this.response("task.release", 409, "Task not acquired"), changes: [] };
|
|
565
|
+
}
|
|
566
|
+
// Logical-state check: if the task's promise is logically settled,
|
|
567
|
+
// the task is logically fulfilled and cannot be released.
|
|
568
|
+
const taskPromise = this.getPromiseOrThrow(req.data.id);
|
|
569
|
+
if (this.projectedPromise(now, taskPromise).state !== "pending") {
|
|
570
|
+
return { response: this.response("task.release", 409, "Task not acquired"), changes: [] };
|
|
571
|
+
}
|
|
572
|
+
if (task.version !== req.data.version) {
|
|
573
|
+
return { response: this.response("task.release", 409, "Version mismatch"), changes: [] };
|
|
574
|
+
}
|
|
575
|
+
const changes = [];
|
|
576
|
+
changes.push(this.setTask({ ...task, state: "pending", pid: undefined, ttl: undefined }));
|
|
577
|
+
changes.push(this.setTTimeout({ id: req.data.id, type: 0, timeout: now + PENDING_RETRY_TTL }));
|
|
578
|
+
const promise = this.getPromiseOrThrow(req.data.id);
|
|
579
|
+
const address = this.getAddressOrThrow(promise);
|
|
580
|
+
changes.push(this.sendMessage(address, {
|
|
581
|
+
kind: "execute",
|
|
582
|
+
head: {},
|
|
583
|
+
data: { task: { id: req.data.id, version: task.version } },
|
|
584
|
+
}));
|
|
585
|
+
return { response: this.response("task.release", 200, {}), changes };
|
|
586
|
+
}
|
|
587
|
+
taskFulfill(now, req) {
|
|
588
|
+
const task = this.tasks.get(req.data.id);
|
|
589
|
+
if (!task) {
|
|
590
|
+
return { response: this.response("task.fulfill", 404, "Task not found"), changes: [] };
|
|
591
|
+
}
|
|
592
|
+
if (task.state !== "acquired") {
|
|
593
|
+
return { response: this.response("task.fulfill", 409, "Task not acquired"), changes: [] };
|
|
594
|
+
}
|
|
595
|
+
// Logical-state check: if the task's promise is logically settled, the
|
|
596
|
+
// task is logically fulfilled and cannot be fulfilled again.
|
|
597
|
+
const taskPromise = this.getPromiseOrThrow(req.data.id);
|
|
598
|
+
if (this.projectedPromise(now, taskPromise).state !== "pending") {
|
|
599
|
+
return { response: this.response("task.fulfill", 409, "Task not acquired"), changes: [] };
|
|
600
|
+
}
|
|
601
|
+
if (task.version !== req.data.version) {
|
|
602
|
+
return { response: this.response("task.fulfill", 409, "Version mismatch"), changes: [] };
|
|
603
|
+
}
|
|
604
|
+
const promise = this.getPromiseOrThrow(req.data.action.data.id);
|
|
605
|
+
const changes = [];
|
|
606
|
+
// Check if promise is already settled (possibly by auto-timeout above)
|
|
607
|
+
if (promise.state !== "pending") {
|
|
608
|
+
// Still fulfill the task but indicate the promise was already settled
|
|
609
|
+
changes.push(...this.triggerFulfilled(req.data.id));
|
|
610
|
+
return { response: this.response("task.fulfill", 200, { promise: this.toPromiseRecord(promise) }), changes };
|
|
611
|
+
}
|
|
612
|
+
// Settle the promise
|
|
613
|
+
const settledPromise = {
|
|
614
|
+
...promise,
|
|
615
|
+
state: req.data.action.data.state,
|
|
616
|
+
value: req.data.action.data.value ?? {},
|
|
617
|
+
settledAt: now,
|
|
618
|
+
};
|
|
619
|
+
changes.push(this.setPromise(settledPromise));
|
|
620
|
+
changes.push(this.delPTimeout(req.data.action.data.id));
|
|
621
|
+
changes.push(...this.triggerSettlement(req.data.id, now));
|
|
622
|
+
return { response: this.response("task.fulfill", 200, { promise: this.toPromiseRecord(settledPromise) }), changes };
|
|
623
|
+
}
|
|
624
|
+
taskSuspend(now, req) {
|
|
625
|
+
const task = this.tasks.get(req.data.id);
|
|
626
|
+
if (!task) {
|
|
627
|
+
return { response: this.response("task.suspend", 404, "Task not found"), changes: [] };
|
|
628
|
+
}
|
|
629
|
+
if (task.state !== "acquired") {
|
|
630
|
+
return { response: this.response("task.suspend", 409, "Task not acquired"), changes: [] };
|
|
631
|
+
}
|
|
632
|
+
// Logical-state check: if the task's promise is logically settled, the
|
|
633
|
+
// task is logically fulfilled and cannot be suspended.
|
|
634
|
+
const taskPromise = this.getPromiseOrThrow(req.data.id);
|
|
635
|
+
if (this.projectedPromise(now, taskPromise).state !== "pending") {
|
|
636
|
+
return { response: this.response("task.suspend", 409, "Task not acquired"), changes: [] };
|
|
637
|
+
}
|
|
638
|
+
if (task.version !== req.data.version) {
|
|
639
|
+
return { response: this.response("task.suspend", 409, "Version mismatch"), changes: [] };
|
|
640
|
+
}
|
|
641
|
+
const changes = [];
|
|
642
|
+
// First pass: validate all promises exist and classify as pending or settled.
|
|
643
|
+
// Callbacks are only registered when ALL promises are pending (spec transition #40).
|
|
644
|
+
// If any promise is already settled, return 300 immediately with no callbacks (spec transition #39).
|
|
645
|
+
const pendingPromises = [];
|
|
646
|
+
let hasSettled = false;
|
|
647
|
+
for (const action of req.data.actions) {
|
|
648
|
+
const awaitedPromise = this.promises.get(action.data.awaited);
|
|
649
|
+
if (!awaitedPromise) {
|
|
650
|
+
return { response: this.response("task.suspend", 422, {}), changes: [] };
|
|
651
|
+
}
|
|
652
|
+
if (awaitedPromise.state === "pending") {
|
|
653
|
+
pendingPromises.push(awaitedPromise);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
hasSettled = true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Immediate resume — stay acquired, clear all resumes, no callbacks registered
|
|
660
|
+
if (hasSettled) {
|
|
661
|
+
changes.push(this.setTask({ ...task, resumes: new Set() }));
|
|
662
|
+
return { response: this.response("task.suspend", 300, { preload: this.preload(req.data.id) }), changes };
|
|
663
|
+
}
|
|
664
|
+
// All pending: register callbacks then suspend
|
|
665
|
+
for (const awaitedPromise of pendingPromises) {
|
|
666
|
+
awaitedPromise.callbacks.add(req.data.id);
|
|
667
|
+
changes.push(this.setPromise(awaitedPromise));
|
|
668
|
+
}
|
|
669
|
+
changes.push(this.setTask({ ...task, state: "suspended", pid: undefined, ttl: undefined, resumes: new Set() }));
|
|
670
|
+
changes.push(this.delTTimeout(req.data.id));
|
|
671
|
+
return { response: this.response("task.suspend", 200, {}), changes };
|
|
672
|
+
}
|
|
673
|
+
taskHalt(now, req) {
|
|
674
|
+
const task = this.tasks.get(req.data.id);
|
|
675
|
+
if (!task) {
|
|
676
|
+
return { response: this.response("task.halt", 404, "Task not found"), changes: [] };
|
|
677
|
+
}
|
|
678
|
+
if (task.state === "fulfilled") {
|
|
679
|
+
return { response: this.response("task.halt", 409, "Task is fulfilled"), changes: [] };
|
|
680
|
+
}
|
|
681
|
+
const changes = [];
|
|
682
|
+
if (task.state !== "halted") {
|
|
683
|
+
changes.push(this.setTask({ ...task, state: "halted", pid: undefined, ttl: undefined }));
|
|
684
|
+
changes.push(this.delTTimeout(req.data.id));
|
|
685
|
+
}
|
|
686
|
+
return { response: this.response("task.halt", 200, {}), changes };
|
|
687
|
+
}
|
|
688
|
+
taskContinue(now, req) {
|
|
689
|
+
const task = this.tasks.get(req.data.id);
|
|
690
|
+
if (!task) {
|
|
691
|
+
return { response: this.response("task.continue", 404, "Task not found"), changes: [] };
|
|
692
|
+
}
|
|
693
|
+
if (task.state !== "halted") {
|
|
694
|
+
return { response: this.response("task.continue", 409, "Task is not halted"), changes: [] };
|
|
695
|
+
}
|
|
696
|
+
const changes = [];
|
|
697
|
+
changes.push(this.setTask({ ...task, state: "pending" }));
|
|
698
|
+
changes.push(this.setTTimeout({ id: req.data.id, type: 0, timeout: now + PENDING_RETRY_TTL }));
|
|
699
|
+
const promise = this.getPromiseOrThrow(req.data.id);
|
|
700
|
+
const address = this.getAddressOrThrow(promise);
|
|
701
|
+
changes.push(this.sendMessage(address, {
|
|
702
|
+
kind: "execute",
|
|
703
|
+
head: {},
|
|
704
|
+
data: { task: { id: req.data.id, version: task.version } },
|
|
705
|
+
}));
|
|
706
|
+
return { response: this.response("task.continue", 200, {}), changes };
|
|
707
|
+
}
|
|
708
|
+
taskFence(now, req) {
|
|
709
|
+
const task = this.tasks.get(req.data.id);
|
|
710
|
+
if (!task) {
|
|
711
|
+
return { response: this.response("task.fence", 404, "Task not found"), changes: [] };
|
|
712
|
+
}
|
|
713
|
+
if (task.state !== "acquired") {
|
|
714
|
+
return { response: this.response("task.fence", 409, "Fence check failed"), changes: [] };
|
|
715
|
+
}
|
|
716
|
+
// Logical-state check: if the task's promise is logically settled,
|
|
717
|
+
// the task is logically fulfilled and the fence fails.
|
|
718
|
+
const taskPromise = this.getPromiseOrThrow(req.data.id);
|
|
719
|
+
if (this.projectedPromise(now, taskPromise).state !== "pending") {
|
|
720
|
+
return { response: this.response("task.fence", 409, "Fence check failed"), changes: [] };
|
|
721
|
+
}
|
|
722
|
+
if (task.version !== req.data.version) {
|
|
723
|
+
return { response: this.response("task.fence", 409, "Fence check failed"), changes: [] };
|
|
724
|
+
}
|
|
725
|
+
const action = req.data.action;
|
|
726
|
+
let inner;
|
|
727
|
+
if (action.kind === "promise.create") {
|
|
728
|
+
inner = this.promiseCreate(now, action);
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
inner = this.promiseSettle(now, action);
|
|
732
|
+
}
|
|
733
|
+
const actionResponse = {
|
|
734
|
+
...inner.response,
|
|
735
|
+
head: { corrId: action.head.corrId, status: inner.response.head.status, version: action.head.version },
|
|
736
|
+
};
|
|
737
|
+
return {
|
|
738
|
+
response: this.response("task.fence", 200, { action: actionResponse, preload: [] }),
|
|
739
|
+
changes: inner.changes,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
taskHeartbeat(now, req) {
|
|
743
|
+
const changes = [];
|
|
744
|
+
for (const ref of req.data.tasks) {
|
|
745
|
+
const task = this.tasks.get(ref.id);
|
|
746
|
+
if (!task || task.state !== "acquired" || task.version !== ref.version || task.pid !== req.data.pid) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
// Logical-state check: if the task's promise is logically settled,
|
|
750
|
+
// the task is logically fulfilled and its lease cannot be refreshed.
|
|
751
|
+
const promise = this.getPromiseOrThrow(ref.id);
|
|
752
|
+
if (this.projectedPromise(now, promise).state !== "pending") {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const ttl = task.ttl ?? 30000;
|
|
756
|
+
changes.push(this.setTTimeout({ id: ref.id, type: 1, timeout: now + ttl }));
|
|
757
|
+
}
|
|
758
|
+
return { response: this.response("task.heartbeat", 200, {}), changes };
|
|
759
|
+
}
|
|
760
|
+
// ===========================================================================
|
|
761
|
+
// SCHEDULE OPERATIONS
|
|
762
|
+
// ===========================================================================
|
|
763
|
+
scheduleGet(req) {
|
|
764
|
+
const schedule = this.schedules.get(req.data.id);
|
|
765
|
+
if (!schedule) {
|
|
766
|
+
return { response: this.response("schedule.get", 404, "Schedule not found"), changes: [] };
|
|
767
|
+
}
|
|
768
|
+
return { response: this.response("schedule.get", 200, { schedule: this.toScheduleRecord(schedule) }), changes: [] };
|
|
769
|
+
}
|
|
770
|
+
scheduleCreate(now, req) {
|
|
771
|
+
const existingSchedule = this.schedules.get(req.data.id);
|
|
772
|
+
if (existingSchedule) {
|
|
773
|
+
return {
|
|
774
|
+
response: this.response("schedule.create", 200, { schedule: this.toScheduleRecord(existingSchedule) }),
|
|
775
|
+
changes: [],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
let nextRunAt;
|
|
779
|
+
try {
|
|
780
|
+
const interval = CronExpressionParser.parse(req.data.cron, { currentDate: new Date(now) });
|
|
781
|
+
nextRunAt = interval.next().getTime();
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return { response: this.response("schedule.create", 400, "Invalid cron expression"), changes: [] };
|
|
785
|
+
}
|
|
786
|
+
const changes = [];
|
|
787
|
+
const schedule = {
|
|
788
|
+
id: req.data.id,
|
|
789
|
+
cron: req.data.cron,
|
|
790
|
+
promiseId: req.data.promiseId,
|
|
791
|
+
promiseTimeout: req.data.promiseTimeout,
|
|
792
|
+
promiseParam: req.data.promiseParam,
|
|
793
|
+
promiseTags: req.data.promiseTags,
|
|
794
|
+
createdAt: now,
|
|
795
|
+
};
|
|
796
|
+
changes.push(this.setSTimeout({ id: schedule.id, timeout: nextRunAt }));
|
|
797
|
+
changes.push(this.setSchedule(schedule));
|
|
798
|
+
return { response: this.response("schedule.create", 200, { schedule: this.toScheduleRecord(schedule) }), changes };
|
|
799
|
+
}
|
|
800
|
+
scheduleDelete(req) {
|
|
801
|
+
const schedule = this.schedules.get(req.data.id);
|
|
802
|
+
if (!schedule) {
|
|
803
|
+
return { response: this.response("schedule.delete", 404, "Schedule not found"), changes: [] };
|
|
804
|
+
}
|
|
805
|
+
const changes = [];
|
|
806
|
+
changes.push(this.delSTimeout(req.data.id));
|
|
807
|
+
changes.push(this.delSchedule(req.data.id));
|
|
808
|
+
return { response: this.response("schedule.delete", 200, {}), changes };
|
|
809
|
+
}
|
|
810
|
+
// ===========================================================================
|
|
811
|
+
// DEBUG OPERATIONS
|
|
812
|
+
// ===========================================================================
|
|
813
|
+
debugStart() {
|
|
814
|
+
return { response: this.response("debug.start", 200, {}), changes: [] };
|
|
815
|
+
}
|
|
816
|
+
debugReset() {
|
|
817
|
+
this.promises.clear();
|
|
818
|
+
this.tasks.clear();
|
|
819
|
+
this.schedules.clear();
|
|
820
|
+
this.outgoing = [];
|
|
821
|
+
this.pTimeouts = [];
|
|
822
|
+
this.tTimeouts = [];
|
|
823
|
+
this.sTimeouts = [];
|
|
824
|
+
return { response: this.response("debug.reset", 200, {}), changes: [] };
|
|
825
|
+
}
|
|
826
|
+
debugSnap() {
|
|
827
|
+
return {
|
|
828
|
+
response: this.response("debug.snap", 200, {
|
|
829
|
+
promises: Array.from(this.promises.values()).map((p) => this.toPromiseRecord(p)),
|
|
830
|
+
promiseTimeouts: this.pTimeouts,
|
|
831
|
+
callbacks: Array.from(this.promises.values()).flatMap((p) => [...p.callbacks].map((awaiter) => ({ awaiter, awaited: p.id }))),
|
|
832
|
+
listeners: Array.from(this.promises.values()).flatMap((p) => [...p.listeners].map((address) => ({ id: p.id, address }))),
|
|
833
|
+
tasks: Array.from(this.tasks.values()).map((t) => this.toTaskRecord(t)),
|
|
834
|
+
taskTimeouts: this.tTimeouts,
|
|
835
|
+
messages: this.outgoing,
|
|
836
|
+
}),
|
|
837
|
+
changes: [],
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
debugTick(req) {
|
|
841
|
+
const now = req.data.time;
|
|
842
|
+
const changes = [];
|
|
843
|
+
const actions = [];
|
|
844
|
+
// Promise timeouts -> settle as rejected_timedout (or resolved for timer promises)
|
|
845
|
+
for (const pt of this.pTimeouts) {
|
|
846
|
+
if (now >= pt.timeout) {
|
|
847
|
+
const promise = this.getPromiseOrThrow(pt.id);
|
|
848
|
+
if (promise.state === "pending") {
|
|
849
|
+
const state = this.timeoutState(promise.tags);
|
|
850
|
+
actions.push({
|
|
851
|
+
kind: "promise.settle",
|
|
852
|
+
data: { id: pt.id, state },
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// Task timeouts -> release (lease) or retry (pending)
|
|
858
|
+
for (const tt of this.tTimeouts) {
|
|
859
|
+
if (now < tt.timeout)
|
|
860
|
+
continue;
|
|
861
|
+
if (tt.type === 1) {
|
|
862
|
+
const task = this.getTaskOrThrow(tt.id);
|
|
863
|
+
if (task.state === "acquired") {
|
|
864
|
+
actions.push({
|
|
865
|
+
kind: "task.release",
|
|
866
|
+
data: { id: tt.id, version: task.version },
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else if (tt.type === 0) {
|
|
871
|
+
const task = this.getTaskOrThrow(tt.id);
|
|
872
|
+
if (task.state === "pending") {
|
|
873
|
+
actions.push({
|
|
874
|
+
kind: "task.retry",
|
|
875
|
+
data: { id: tt.id, version: task.version },
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
// Apply actions to own state. Promise settlements are split into three
|
|
881
|
+
// phases to make the tick atomic — the result must not depend on the
|
|
882
|
+
// order promises appear in pTimeouts.
|
|
883
|
+
//
|
|
884
|
+
// Phase 1: Settle all expired promises (state change only).
|
|
885
|
+
// Phase 2: Fulfill tasks whose own promise settled (triggerFulfilled).
|
|
886
|
+
// Phase 3: Resume suspended callbacks of settled promises (triggerCallbacks).
|
|
887
|
+
//
|
|
888
|
+
// Phase 2 before phase 3 ensures that a task whose own promise settled
|
|
889
|
+
// is fulfilled before triggerCallbacks runs. This prevents a spurious
|
|
890
|
+
// suspended → pending (version++) → fulfilled path for tasks that
|
|
891
|
+
// should go directly suspended → fulfilled.
|
|
892
|
+
const settledIds = [];
|
|
893
|
+
// Phase 1: Settle promises
|
|
894
|
+
for (const action of actions) {
|
|
895
|
+
if (action.kind === "promise.settle") {
|
|
896
|
+
const promise = this.getPromiseOrThrow(action.data.id);
|
|
897
|
+
if (promise.state !== "pending")
|
|
898
|
+
continue;
|
|
899
|
+
changes.push(this.setPromise({
|
|
900
|
+
...promise,
|
|
901
|
+
state: action.data.state,
|
|
902
|
+
value: {},
|
|
903
|
+
settledAt: promise.timeoutAt,
|
|
904
|
+
}));
|
|
905
|
+
changes.push(this.delPTimeout(action.data.id));
|
|
906
|
+
settledIds.push(action.data.id);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// Phase 2: Fulfill tasks whose own promise settled
|
|
910
|
+
for (const id of settledIds) {
|
|
911
|
+
changes.push(...this.triggerFulfilled(id));
|
|
912
|
+
}
|
|
913
|
+
// Phase 3: Resume suspended callbacks and notify listeners
|
|
914
|
+
for (const id of settledIds) {
|
|
915
|
+
changes.push(...this.triggerCallbacks(id, now));
|
|
916
|
+
changes.push(...this.triggerListeners(id));
|
|
917
|
+
}
|
|
918
|
+
for (const action of actions) {
|
|
919
|
+
if (action.kind === "task.release") {
|
|
920
|
+
const task = this.getTaskOrThrow(action.data.id);
|
|
921
|
+
if (task.state === "acquired" && task.version === action.data.version) {
|
|
922
|
+
changes.push(this.setTask({ ...task, state: "pending", pid: undefined, ttl: undefined }));
|
|
923
|
+
changes.push(this.setTTimeout({ id: action.data.id, type: 0, timeout: now + PENDING_RETRY_TTL }));
|
|
924
|
+
const promise = this.getPromiseOrThrow(action.data.id);
|
|
925
|
+
const address = this.getAddressOrThrow(promise);
|
|
926
|
+
changes.push(this.sendMessage(address, {
|
|
927
|
+
kind: "execute",
|
|
928
|
+
head: {},
|
|
929
|
+
data: { task: { id: action.data.id, version: task.version } },
|
|
930
|
+
}));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else if (action.kind === "task.retry") {
|
|
934
|
+
const task = this.getTaskOrThrow(action.data.id);
|
|
935
|
+
if (task.state === "pending") {
|
|
936
|
+
changes.push(this.setTTimeout({ id: action.data.id, type: 0, timeout: now + PENDING_RETRY_TTL }));
|
|
937
|
+
const promise = this.getPromiseOrThrow(action.data.id);
|
|
938
|
+
const address = this.getAddressOrThrow(promise);
|
|
939
|
+
changes.push(this.sendMessage(address, {
|
|
940
|
+
kind: "execute",
|
|
941
|
+
head: {},
|
|
942
|
+
data: { task: { id: action.data.id, version: task.version } },
|
|
943
|
+
}));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// Schedule timeouts -> create promises for due schedules
|
|
948
|
+
for (const st of this.sTimeouts) {
|
|
949
|
+
if (now < st.timeout)
|
|
950
|
+
continue;
|
|
951
|
+
const schedule = this.getScheduleOrThrow(st.id);
|
|
952
|
+
const promiseId = schedule.promiseId
|
|
953
|
+
.replaceAll("{{.id}}", schedule.id)
|
|
954
|
+
.replaceAll("{{.timestamp}}", String(st.timeout));
|
|
955
|
+
const { changes: createChanges } = this.promiseCreate(now, {
|
|
956
|
+
kind: "promise.create",
|
|
957
|
+
head: { corrId: "", version: "" },
|
|
958
|
+
data: {
|
|
959
|
+
id: promiseId,
|
|
960
|
+
timeoutAt: st.timeout + schedule.promiseTimeout,
|
|
961
|
+
param: schedule.promiseParam,
|
|
962
|
+
tags: { ...schedule.promiseTags, "resonate:schedule": schedule.id },
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
changes.push(...createChanges);
|
|
966
|
+
// Advance to next run
|
|
967
|
+
const interval = CronExpressionParser.parse(schedule.cron, {
|
|
968
|
+
currentDate: new Date(st.timeout),
|
|
969
|
+
});
|
|
970
|
+
const nextRunAt = interval.next().getTime();
|
|
971
|
+
schedule.lastRunAt = st.timeout;
|
|
972
|
+
changes.push(this.setSTimeout({ id: schedule.id, timeout: nextRunAt }));
|
|
973
|
+
changes.push(this.setSchedule(schedule));
|
|
974
|
+
}
|
|
975
|
+
return { response: this.response("debug.tick", 200, []), changes };
|
|
976
|
+
}
|
|
977
|
+
debugStop() {
|
|
978
|
+
return { response: this.response("debug.stop", 200, {}), changes: [] };
|
|
979
|
+
}
|
|
980
|
+
// ===========================================================================
|
|
981
|
+
// CONVERTERS
|
|
982
|
+
// ===========================================================================
|
|
983
|
+
toPromiseRecord(p) {
|
|
984
|
+
const { callbacks, listeners, settledAt, ...rest } = p;
|
|
985
|
+
return settledAt != null ? { ...rest, settledAt } : rest;
|
|
986
|
+
}
|
|
987
|
+
toTaskRecord(t) {
|
|
988
|
+
const record = { id: t.id, version: t.version, state: t.state, resumes: Array.from(t.resumes) };
|
|
989
|
+
if (t.pid !== undefined)
|
|
990
|
+
record.pid = t.pid;
|
|
991
|
+
if (t.ttl !== undefined)
|
|
992
|
+
record.ttl = t.ttl;
|
|
993
|
+
return record;
|
|
994
|
+
}
|
|
995
|
+
toScheduleRecord(s) {
|
|
996
|
+
const st = this.sTimeouts.find((e) => e.id === s.id);
|
|
997
|
+
const record = {
|
|
998
|
+
id: s.id,
|
|
999
|
+
cron: s.cron,
|
|
1000
|
+
promiseId: s.promiseId,
|
|
1001
|
+
promiseTimeout: s.promiseTimeout,
|
|
1002
|
+
promiseParam: s.promiseParam,
|
|
1003
|
+
promiseTags: s.promiseTags,
|
|
1004
|
+
createdAt: s.createdAt,
|
|
1005
|
+
nextRunAt: st.timeout,
|
|
1006
|
+
};
|
|
1007
|
+
if (s.lastRunAt != null) {
|
|
1008
|
+
record.lastRunAt = s.lastRunAt;
|
|
1009
|
+
}
|
|
1010
|
+
return record;
|
|
1011
|
+
}
|
|
1012
|
+
// ===========================================================================
|
|
1013
|
+
// HELPERS
|
|
1014
|
+
// ===========================================================================
|
|
1015
|
+
tryEagerTimeout(now, id) {
|
|
1016
|
+
if (this.timeoutMode !== "eager")
|
|
1017
|
+
return [];
|
|
1018
|
+
const promise = this.promises.get(id);
|
|
1019
|
+
if (!promise || promise.state !== "pending" || now < promise.timeoutAt) {
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
const changes = [];
|
|
1023
|
+
const state = this.timeoutState(promise.tags);
|
|
1024
|
+
changes.push(this.setPromise({ ...promise, state, settledAt: promise.timeoutAt }));
|
|
1025
|
+
changes.push(this.delPTimeout(id));
|
|
1026
|
+
changes.push(...this.triggerSettlement(id, now));
|
|
1027
|
+
return changes;
|
|
1028
|
+
}
|
|
1029
|
+
triggerSettlement(id, now) {
|
|
1030
|
+
return [...this.triggerFulfilled(id), ...this.triggerCallbacks(id, now), ...this.triggerListeners(id)];
|
|
1031
|
+
}
|
|
1032
|
+
triggerFulfilled(promiseId) {
|
|
1033
|
+
const task = this.tasks.get(promiseId);
|
|
1034
|
+
if (!task)
|
|
1035
|
+
return [];
|
|
1036
|
+
if (task.state === "fulfilled")
|
|
1037
|
+
return [];
|
|
1038
|
+
const changes = [];
|
|
1039
|
+
changes.push(this.setTask({ ...task, state: "fulfilled", pid: undefined, ttl: undefined, resumes: new Set() }));
|
|
1040
|
+
changes.push(this.delTTimeout(promiseId));
|
|
1041
|
+
// Remove this task from all promise callbacks (delete callbacks where awaiter_id = task_id)
|
|
1042
|
+
for (const [, promise] of this.promises) {
|
|
1043
|
+
if (promise.callbacks.delete(promiseId)) {
|
|
1044
|
+
changes.push(this.setPromise(promise));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return changes;
|
|
1048
|
+
}
|
|
1049
|
+
triggerCallbacks(promiseId, now) {
|
|
1050
|
+
const settledPromise = this.getPromiseOrThrow(promiseId);
|
|
1051
|
+
const changes = [];
|
|
1052
|
+
// Resume or buffer for all tasks that were awaiting this promise
|
|
1053
|
+
for (const awaiterId of settledPromise.callbacks) {
|
|
1054
|
+
const task = this.getTaskOrThrow(awaiterId);
|
|
1055
|
+
if (task.state === "suspended") {
|
|
1056
|
+
changes.push(this.setTask({ ...task, state: "pending", resumes: new Set([promiseId]) }));
|
|
1057
|
+
// Add task timeout entry back (type=0 for pending retry)
|
|
1058
|
+
changes.push(this.setTTimeout({
|
|
1059
|
+
id: awaiterId,
|
|
1060
|
+
type: 0,
|
|
1061
|
+
timeout: now + PENDING_RETRY_TTL,
|
|
1062
|
+
}));
|
|
1063
|
+
const awaiterPromise = this.getPromiseOrThrow(awaiterId);
|
|
1064
|
+
const address = this.getAddressOrThrow(awaiterPromise);
|
|
1065
|
+
changes.push(this.sendMessage(address, {
|
|
1066
|
+
kind: "execute",
|
|
1067
|
+
head: {},
|
|
1068
|
+
data: { task: { id: awaiterId, version: task.version } },
|
|
1069
|
+
}));
|
|
1070
|
+
}
|
|
1071
|
+
else if (task.state === "pending" || task.state === "acquired" || task.state === "halted") {
|
|
1072
|
+
// Buffer the resume — will be checked when task suspends or continues
|
|
1073
|
+
task.resumes.add(promiseId);
|
|
1074
|
+
changes.push(this.setTask(task));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// Clear callbacks after processing
|
|
1078
|
+
settledPromise.callbacks.clear();
|
|
1079
|
+
changes.push(this.setPromise(settledPromise));
|
|
1080
|
+
return changes;
|
|
1081
|
+
}
|
|
1082
|
+
triggerListeners(promiseId) {
|
|
1083
|
+
const promise = this.getPromiseOrThrow(promiseId);
|
|
1084
|
+
if (promise.listeners.size === 0)
|
|
1085
|
+
return [];
|
|
1086
|
+
const changes = [];
|
|
1087
|
+
for (const address of promise.listeners) {
|
|
1088
|
+
changes.push(this.sendMessage(address, {
|
|
1089
|
+
kind: "unblock",
|
|
1090
|
+
head: {},
|
|
1091
|
+
data: { promise: this.toPromiseRecord(promise) },
|
|
1092
|
+
}));
|
|
1093
|
+
}
|
|
1094
|
+
promise.listeners.clear();
|
|
1095
|
+
changes.push(this.setPromise(promise));
|
|
1096
|
+
return changes;
|
|
1097
|
+
}
|
|
1098
|
+
preload(promiseId) {
|
|
1099
|
+
const promise = this.promises.get(promiseId);
|
|
1100
|
+
if (!promise)
|
|
1101
|
+
return [];
|
|
1102
|
+
const branch = promise.tags["resonate:branch"];
|
|
1103
|
+
if (!branch)
|
|
1104
|
+
return [];
|
|
1105
|
+
const results = [];
|
|
1106
|
+
for (const [, p] of this.promises) {
|
|
1107
|
+
if (p.id !== promiseId && p.tags["resonate:branch"] === branch) {
|
|
1108
|
+
results.push(this.toPromiseRecord(p));
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return results;
|
|
1112
|
+
}
|
|
1113
|
+
timeoutState(tags) {
|
|
1114
|
+
return tags["resonate:timer"] === "true" ? "resolved" : "rejected_timedout";
|
|
1115
|
+
}
|
|
1116
|
+
// Projection: return the LOGICAL view of a promise without mutating
|
|
1117
|
+
// storage. When physical state is `pending` and `now >= timeoutAt`, the
|
|
1118
|
+
// logical state is the terminal state implied by the timer tag. Used at
|
|
1119
|
+
// response-construction sites in read paths so callers always observe
|
|
1120
|
+
// logical state regardless of whether convergence has happened yet.
|
|
1121
|
+
//
|
|
1122
|
+
// Unguarded by timeoutMode: when timeoutMode === "eager", the physical
|
|
1123
|
+
// state already matches the logical state by the time this runs (because
|
|
1124
|
+
// tryEagerTimeout fired at the start of the action), so the projection
|
|
1125
|
+
// condition is never met and this is a no-op.
|
|
1126
|
+
projectedPromise(now, promise) {
|
|
1127
|
+
if (promise.state !== "pending" || now < promise.timeoutAt) {
|
|
1128
|
+
return promise;
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
...promise,
|
|
1132
|
+
state: this.timeoutState(promise.tags),
|
|
1133
|
+
settledAt: promise.timeoutAt,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
// Projection: return the LOGICAL view of a task without mutating storage.
|
|
1137
|
+
// A task's logical state is bound to its promise's logical state — when
|
|
1138
|
+
// the promise is logically settled, the task is logically fulfilled.
|
|
1139
|
+
//
|
|
1140
|
+
// Unguarded for the same reason as projectedPromise.
|
|
1141
|
+
projectedTask(now, task) {
|
|
1142
|
+
const promise = this.promises.get(task.id);
|
|
1143
|
+
if (!promise)
|
|
1144
|
+
return task;
|
|
1145
|
+
const projected = this.projectedPromise(now, promise);
|
|
1146
|
+
if (projected.state === "pending")
|
|
1147
|
+
return task;
|
|
1148
|
+
if (task.state === "fulfilled")
|
|
1149
|
+
return task;
|
|
1150
|
+
return { ...task, state: "fulfilled", pid: undefined, ttl: undefined };
|
|
1151
|
+
}
|
|
1152
|
+
getPromiseOrThrow(id) {
|
|
1153
|
+
const promise = this.promises.get(id);
|
|
1154
|
+
if (!promise) {
|
|
1155
|
+
throw new Error(`Invariant violation: promise ${id} not found`);
|
|
1156
|
+
}
|
|
1157
|
+
return promise;
|
|
1158
|
+
}
|
|
1159
|
+
getAddressOrThrow(promise) {
|
|
1160
|
+
const address = promise.tags["resonate:target"];
|
|
1161
|
+
if (!address) {
|
|
1162
|
+
throw new Error(`Invariant violation: promise ${promise.id} has no resonate:target tag`);
|
|
1163
|
+
}
|
|
1164
|
+
return address;
|
|
1165
|
+
}
|
|
1166
|
+
getTaskOrThrow(id) {
|
|
1167
|
+
const task = this.tasks.get(id);
|
|
1168
|
+
if (!task) {
|
|
1169
|
+
throw new Error(`Invariant violation: task ${id} not found`);
|
|
1170
|
+
}
|
|
1171
|
+
return task;
|
|
1172
|
+
}
|
|
1173
|
+
getScheduleOrThrow(id) {
|
|
1174
|
+
const schedule = this.schedules.get(id);
|
|
1175
|
+
if (!schedule) {
|
|
1176
|
+
throw new Error(`Invariant violation: schedule ${id} not found`);
|
|
1177
|
+
}
|
|
1178
|
+
return schedule;
|
|
1179
|
+
}
|
|
1180
|
+
// ===========================================================================
|
|
1181
|
+
// ACCESSORS (change-tracking)
|
|
1182
|
+
// ===========================================================================
|
|
1183
|
+
setPromise(p) {
|
|
1184
|
+
this.promises.set(p.id, p);
|
|
1185
|
+
return { kind: "promise.set", promise: this.toPromiseRecord(p) };
|
|
1186
|
+
}
|
|
1187
|
+
setTask(t) {
|
|
1188
|
+
this.tasks.set(t.id, t);
|
|
1189
|
+
return { kind: "task.set", task: this.toTaskRecord(t) };
|
|
1190
|
+
}
|
|
1191
|
+
setSchedule(s) {
|
|
1192
|
+
this.schedules.set(s.id, s);
|
|
1193
|
+
return { kind: "schedule.set", schedule: this.toScheduleRecord(s) };
|
|
1194
|
+
}
|
|
1195
|
+
delSchedule(id) {
|
|
1196
|
+
this.schedules.delete(id);
|
|
1197
|
+
return { kind: "schedule.del", id };
|
|
1198
|
+
}
|
|
1199
|
+
setPTimeout(pt) {
|
|
1200
|
+
const idx = this.pTimeouts.findIndex((e) => e.id === pt.id);
|
|
1201
|
+
if (idx !== -1) {
|
|
1202
|
+
this.pTimeouts[idx] = pt;
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
this.pTimeouts.push(pt);
|
|
1206
|
+
}
|
|
1207
|
+
return { kind: "ptimeout.set", timeout: { id: pt.id, timeout: pt.timeout } };
|
|
1208
|
+
}
|
|
1209
|
+
delPTimeout(id) {
|
|
1210
|
+
const idx = this.pTimeouts.findIndex((e) => e.id === id);
|
|
1211
|
+
if (idx !== -1) {
|
|
1212
|
+
this.pTimeouts.splice(idx, 1);
|
|
1213
|
+
}
|
|
1214
|
+
return { kind: "ptimeout.del", id };
|
|
1215
|
+
}
|
|
1216
|
+
setTTimeout(tt) {
|
|
1217
|
+
const idx = this.tTimeouts.findIndex((e) => e.id === tt.id);
|
|
1218
|
+
if (idx !== -1) {
|
|
1219
|
+
this.tTimeouts[idx] = tt;
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
this.tTimeouts.push(tt);
|
|
1223
|
+
}
|
|
1224
|
+
return { kind: "ttimeout.set", timeout: { id: tt.id, type: tt.type, timeout: tt.timeout } };
|
|
1225
|
+
}
|
|
1226
|
+
delTTimeout(id) {
|
|
1227
|
+
const idx = this.tTimeouts.findIndex((e) => e.id === id);
|
|
1228
|
+
if (idx !== -1) {
|
|
1229
|
+
this.tTimeouts.splice(idx, 1);
|
|
1230
|
+
}
|
|
1231
|
+
return { kind: "ttimeout.del", id };
|
|
1232
|
+
}
|
|
1233
|
+
setSTimeout(st) {
|
|
1234
|
+
const idx = this.sTimeouts.findIndex((e) => e.id === st.id);
|
|
1235
|
+
if (idx !== -1) {
|
|
1236
|
+
this.sTimeouts[idx] = st;
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
this.sTimeouts.push(st);
|
|
1240
|
+
}
|
|
1241
|
+
return { kind: "stimeout.set", timeout: { id: st.id, timeout: st.timeout } };
|
|
1242
|
+
}
|
|
1243
|
+
delSTimeout(id) {
|
|
1244
|
+
const idx = this.sTimeouts.findIndex((e) => e.id === id);
|
|
1245
|
+
if (idx !== -1) {
|
|
1246
|
+
this.sTimeouts.splice(idx, 1);
|
|
1247
|
+
}
|
|
1248
|
+
return { kind: "stimeout.del", id };
|
|
1249
|
+
}
|
|
1250
|
+
// FIX: Change from accumulate (push) to upsert by task ID.
|
|
1251
|
+
// A task can only be claimed by one worker at one version, so when a task
|
|
1252
|
+
// is resumed (version incremented), the previous message is obsolete.
|
|
1253
|
+
// Upsert keeps only the latest message per task ID, matching the HTTP's
|
|
1254
|
+
// outgoing table behavior (ON CONFLICT (id) DO UPDATE).
|
|
1255
|
+
sendMessage(address, msg) {
|
|
1256
|
+
if (msg.kind === "execute") {
|
|
1257
|
+
const taskId = msg.data.task.id;
|
|
1258
|
+
const idx = this.outgoing.findIndex((m) => m.message.kind === "execute" && m.message.data.task.id === taskId);
|
|
1259
|
+
if (idx >= 0) {
|
|
1260
|
+
this.outgoing[idx] = { address, message: msg };
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
this.outgoing.push({ address, message: msg });
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
this.outgoing.push({ address, message: msg });
|
|
1268
|
+
}
|
|
1269
|
+
return { kind: "message.send", address, message: msg };
|
|
1270
|
+
}
|
|
1271
|
+
// OLD: accumulate (push) — causes divergence with HTTP snapshots
|
|
1272
|
+
// private sendMessage(address: string, msg: Message): Change {
|
|
1273
|
+
// this.outgoing.push({ address, message: msg });
|
|
1274
|
+
// return { kind: "message.send", address, message: msg };
|
|
1275
|
+
// }
|
|
1276
|
+
response(kind, status, data) {
|
|
1277
|
+
return { kind, head: { status }, data };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// =============================================================================
|
|
1281
|
+
// LOCAL NETWORK
|
|
1282
|
+
// =============================================================================
|
|
1283
|
+
export class LocalNetwork {
|
|
1284
|
+
unicast;
|
|
1285
|
+
anycast;
|
|
1286
|
+
started;
|
|
1287
|
+
server;
|
|
1288
|
+
subscribers = [];
|
|
1289
|
+
tickInterval;
|
|
1290
|
+
constructor({ pid = crypto.randomUUID().replace(/-/g, ""), group = "default", } = {}) {
|
|
1291
|
+
this.started = false;
|
|
1292
|
+
this.server = new Server();
|
|
1293
|
+
this.unicast = `local://uni@${group}/${pid}`;
|
|
1294
|
+
this.anycast = `local://any@${group}/${pid}`;
|
|
1295
|
+
}
|
|
1296
|
+
// -- Network ---------------------------------------------------------------
|
|
1297
|
+
match(target) {
|
|
1298
|
+
return `local://any@${target}`;
|
|
1299
|
+
}
|
|
1300
|
+
async init() {
|
|
1301
|
+
if (this.started)
|
|
1302
|
+
return;
|
|
1303
|
+
this.tickInterval = setInterval(() => {
|
|
1304
|
+
const now = Date.now();
|
|
1305
|
+
const result = this.server.apply(now, {
|
|
1306
|
+
kind: "debug.tick",
|
|
1307
|
+
head: { corrId: randomUUID(), version: VERSION },
|
|
1308
|
+
data: { time: now },
|
|
1309
|
+
});
|
|
1310
|
+
this.dispatchMessages(result);
|
|
1311
|
+
}, 1000);
|
|
1312
|
+
this.started = true;
|
|
1313
|
+
}
|
|
1314
|
+
async stop() {
|
|
1315
|
+
if (this.tickInterval) {
|
|
1316
|
+
clearInterval(this.tickInterval);
|
|
1317
|
+
}
|
|
1318
|
+
this.subscribers = [];
|
|
1319
|
+
this.started = false;
|
|
1320
|
+
}
|
|
1321
|
+
send = (req) => {
|
|
1322
|
+
const { corrId, version } = req.head;
|
|
1323
|
+
const now = Date.now();
|
|
1324
|
+
const result = this.server.apply(now, req);
|
|
1325
|
+
const response = result.response;
|
|
1326
|
+
const res = {
|
|
1327
|
+
kind: response.kind,
|
|
1328
|
+
head: { corrId, status: response.head.status, version },
|
|
1329
|
+
data: response.data,
|
|
1330
|
+
};
|
|
1331
|
+
assert(isResponse(res));
|
|
1332
|
+
setTimeout(() => this.dispatchMessages(result), 0);
|
|
1333
|
+
return Promise.resolve(res);
|
|
1334
|
+
};
|
|
1335
|
+
recv(callback) {
|
|
1336
|
+
this.subscribers.push(callback);
|
|
1337
|
+
}
|
|
1338
|
+
// -- internal: message dispatch --------------------------------------------
|
|
1339
|
+
dispatchMessages(result) {
|
|
1340
|
+
for (const change of result.changes) {
|
|
1341
|
+
if (change.kind !== "message.send")
|
|
1342
|
+
continue;
|
|
1343
|
+
for (const cb of this.subscribers) {
|
|
1344
|
+
cb(change.message);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
//# sourceMappingURL=local.js.map
|