@resonatehq/dev 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/LICENSE +201 -0
- package/README.md +251 -0
- package/dist/api.d.ts +163 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/dist/entities.d.ts +74 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +2 -0
- package/dist/entities.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +107 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +878 -0
- package/dist/server.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/package.json +42 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import CronExpressionParser from "cron-parser";
|
|
2
|
+
import { assert, assertDefined } from "./utils";
|
|
3
|
+
class ServerError extends Error {
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
class DefaultRouter {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.tag = "resonate:invoke";
|
|
12
|
+
}
|
|
13
|
+
route(promise) {
|
|
14
|
+
return promise.tags[this.tag];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class Server {
|
|
18
|
+
constructor(taskExpiryMs = 5000) {
|
|
19
|
+
this.taskExpiryMs = taskExpiryMs;
|
|
20
|
+
this.version = "2025-01-15";
|
|
21
|
+
this.promises = {};
|
|
22
|
+
this.tasks = {};
|
|
23
|
+
this.schedules = {};
|
|
24
|
+
this.router = new DefaultRouter();
|
|
25
|
+
this.targets = {
|
|
26
|
+
default: "local://any@default",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
getState() {
|
|
30
|
+
return {
|
|
31
|
+
promises: this.promises,
|
|
32
|
+
tasks: this.tasks,
|
|
33
|
+
schedules: this.schedules,
|
|
34
|
+
stats: {
|
|
35
|
+
promiseCount: Object.keys(this.promises).length,
|
|
36
|
+
taskCount: Object.keys(this.tasks).length,
|
|
37
|
+
scheduleCount: Object.keys(this.schedules).length,
|
|
38
|
+
pendingPromises: Object.values(this.promises).filter((p) => p.state === "pending").length,
|
|
39
|
+
resolvedPromises: Object.values(this.promises).filter((p) => p.state === "resolved").length,
|
|
40
|
+
rejectedPromises: Object.values(this.promises).filter((p) => p.state === "rejected").length,
|
|
41
|
+
canceledPromises: Object.values(this.promises).filter((p) => p.state === "rejected_canceled").length,
|
|
42
|
+
timedoutPromises: Object.values(this.promises).filter((p) => p.state === "rejected_timedout").length,
|
|
43
|
+
initTasks: Object.values(this.tasks).filter((t) => t.state === "init")
|
|
44
|
+
.length,
|
|
45
|
+
enqueuedTasks: Object.values(this.tasks).filter((t) => t.state === "enqueued").length,
|
|
46
|
+
claimedTasks: Object.values(this.tasks).filter((t) => t.state === "claimed").length,
|
|
47
|
+
completedTasks: Object.values(this.tasks).filter((t) => t.state === "completed").length,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
next({ at }) {
|
|
52
|
+
let timeout = Infinity;
|
|
53
|
+
for (const promise of Object.values(this.promises)) {
|
|
54
|
+
if (promise.state === "pending") {
|
|
55
|
+
timeout = Math.min(promise.timeoutAt, timeout);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const schedule of Object.values(this.schedules)) {
|
|
59
|
+
timeout = Math.min(schedule.nextRunAt, timeout);
|
|
60
|
+
}
|
|
61
|
+
for (const task of Object.values(this.tasks)) {
|
|
62
|
+
if (isNotCompleted(task.state)) {
|
|
63
|
+
timeout = Math.min(task.expiry, timeout);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return timeout === Infinity
|
|
67
|
+
? undefined
|
|
68
|
+
: Math.min(Math.max(0, timeout - at), 2147483647);
|
|
69
|
+
}
|
|
70
|
+
step({ at }) {
|
|
71
|
+
const schedulesSnapshot = Object.values(this.schedules);
|
|
72
|
+
for (const schedule of schedulesSnapshot) {
|
|
73
|
+
if (at < schedule.nextRunAt) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
this.promiseCreate({
|
|
78
|
+
at,
|
|
79
|
+
req: {
|
|
80
|
+
kind: "promise.create",
|
|
81
|
+
head: { corrId: "", version: this.version },
|
|
82
|
+
data: {
|
|
83
|
+
id: schedule.promiseId.replace("{{.timestamp}}", at.toString()),
|
|
84
|
+
param: schedule.promiseParam,
|
|
85
|
+
tags: schedule.promiseTags,
|
|
86
|
+
timeoutAt: at + schedule.promiseTimeout,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
const { applied } = this.transitionSchedule({
|
|
93
|
+
at,
|
|
94
|
+
id: schedule.id,
|
|
95
|
+
to: "created",
|
|
96
|
+
updating: true,
|
|
97
|
+
});
|
|
98
|
+
assert(applied);
|
|
99
|
+
}
|
|
100
|
+
const promisesSnapshot = Object.values(this.promises);
|
|
101
|
+
for (const promise of promisesSnapshot) {
|
|
102
|
+
if (promise.state === "pending" && at >= promise.timeoutAt) {
|
|
103
|
+
const { applied } = this.transitionPromise({
|
|
104
|
+
at,
|
|
105
|
+
id: promise.id,
|
|
106
|
+
to: "rejected_timedout",
|
|
107
|
+
});
|
|
108
|
+
assert(applied);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const tasksSnapshot = Object.values(this.tasks);
|
|
112
|
+
for (const task of tasksSnapshot) {
|
|
113
|
+
if (isActiveState(task.state)) {
|
|
114
|
+
if (at >= task.expiry) {
|
|
115
|
+
const { applied } = this.transitionTask({
|
|
116
|
+
at,
|
|
117
|
+
id: task.id,
|
|
118
|
+
to: "init",
|
|
119
|
+
force: true,
|
|
120
|
+
});
|
|
121
|
+
assert(applied);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const inFlightAwaiters = new Set();
|
|
126
|
+
for (const task of Object.values(this.tasks)) {
|
|
127
|
+
if (isActiveState(task.state)) {
|
|
128
|
+
inFlightAwaiters.add(task.awaiter);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const mesgs = [];
|
|
132
|
+
for (const task of Object.values(this.tasks)) {
|
|
133
|
+
if (task.state !== "init" ||
|
|
134
|
+
task.expiry > at ||
|
|
135
|
+
inFlightAwaiters.has(task.awaiter)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
let mesg;
|
|
139
|
+
switch (task.type) {
|
|
140
|
+
case "notify": {
|
|
141
|
+
mesg = {
|
|
142
|
+
mesg: {
|
|
143
|
+
kind: task.type,
|
|
144
|
+
head: {},
|
|
145
|
+
data: { promise: this.getPromiseRecord(task.awaiter) },
|
|
146
|
+
},
|
|
147
|
+
recv: task.recv,
|
|
148
|
+
};
|
|
149
|
+
const { applied } = this.transitionTask({
|
|
150
|
+
at,
|
|
151
|
+
id: task.id,
|
|
152
|
+
to: "completed",
|
|
153
|
+
});
|
|
154
|
+
assert(applied);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
mesg = {
|
|
159
|
+
mesg: {
|
|
160
|
+
kind: task.type,
|
|
161
|
+
head: {},
|
|
162
|
+
data: {
|
|
163
|
+
task,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
recv: task.recv,
|
|
167
|
+
};
|
|
168
|
+
const { applied } = this.transitionTask({
|
|
169
|
+
at,
|
|
170
|
+
id: task.id,
|
|
171
|
+
to: "enqueued",
|
|
172
|
+
});
|
|
173
|
+
assert(applied);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
mesgs.push(mesg);
|
|
178
|
+
inFlightAwaiters.add(task.awaiter);
|
|
179
|
+
}
|
|
180
|
+
return mesgs;
|
|
181
|
+
}
|
|
182
|
+
process({ at, req }) {
|
|
183
|
+
this.ensureVersion(req);
|
|
184
|
+
try {
|
|
185
|
+
if (req.kind === "task.fence") {
|
|
186
|
+
return this.processFence({ at, req });
|
|
187
|
+
}
|
|
188
|
+
return this.processRequest({ at, req });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof ServerError) {
|
|
192
|
+
return this.buildErrorRes(req, err);
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
processFence({ at, req, }) {
|
|
198
|
+
const task = this.getTaskRecord(req.data.id);
|
|
199
|
+
if (task.state !== "claimed" || task.version !== req.data.version) {
|
|
200
|
+
throw new ServerError(409, "version mismatch.");
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
kind: "task.fence",
|
|
204
|
+
head: { corrId: req.head.corrId, status: 200, version: this.version },
|
|
205
|
+
data: {
|
|
206
|
+
action: req.data.action.kind === "promise.create"
|
|
207
|
+
? this.buildOkRes({
|
|
208
|
+
kind: req.data.action.kind,
|
|
209
|
+
corrId: req.head.corrId,
|
|
210
|
+
...this.promiseCreate({ at, req: req.data.action }),
|
|
211
|
+
})
|
|
212
|
+
: this.buildOkRes({
|
|
213
|
+
kind: req.data.action.kind,
|
|
214
|
+
corrId: req.head.corrId,
|
|
215
|
+
...this.promiseSettle({ at, req: req.data.action }),
|
|
216
|
+
}),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
processRequest({ at, req }) {
|
|
221
|
+
const corrId = req.head.corrId;
|
|
222
|
+
switch (req.kind) {
|
|
223
|
+
case "promise.create":
|
|
224
|
+
return this.buildOkRes({
|
|
225
|
+
kind: req.kind,
|
|
226
|
+
corrId,
|
|
227
|
+
...this.promiseCreate({ at, req }),
|
|
228
|
+
});
|
|
229
|
+
case "promise.get":
|
|
230
|
+
return this.buildOkRes({
|
|
231
|
+
kind: req.kind,
|
|
232
|
+
corrId,
|
|
233
|
+
...this.promiseGet({ req }),
|
|
234
|
+
});
|
|
235
|
+
case "promise.register":
|
|
236
|
+
return this.buildOkRes({
|
|
237
|
+
kind: req.kind,
|
|
238
|
+
corrId,
|
|
239
|
+
...this.promiseRegister({ at, req }),
|
|
240
|
+
});
|
|
241
|
+
case "promise.settle":
|
|
242
|
+
return this.buildOkRes({
|
|
243
|
+
kind: req.kind,
|
|
244
|
+
corrId,
|
|
245
|
+
...this.promiseSettle({ at, req }),
|
|
246
|
+
});
|
|
247
|
+
case "promise.subscribe":
|
|
248
|
+
return this.buildOkRes({
|
|
249
|
+
kind: req.kind,
|
|
250
|
+
corrId,
|
|
251
|
+
...this.promiseSubscribe({ at, req }),
|
|
252
|
+
});
|
|
253
|
+
case "schedule.create":
|
|
254
|
+
return this.buildOkRes({
|
|
255
|
+
kind: req.kind,
|
|
256
|
+
corrId,
|
|
257
|
+
...this.scheduleCreate({ at, req }),
|
|
258
|
+
});
|
|
259
|
+
case "schedule.delete":
|
|
260
|
+
return this.buildOkRes({
|
|
261
|
+
kind: req.kind,
|
|
262
|
+
corrId,
|
|
263
|
+
...this.scheduleDelete({ at, req }),
|
|
264
|
+
});
|
|
265
|
+
case "schedule.get":
|
|
266
|
+
return this.buildOkRes({
|
|
267
|
+
kind: req.kind,
|
|
268
|
+
corrId,
|
|
269
|
+
...this.scheduleGet({ req }),
|
|
270
|
+
});
|
|
271
|
+
case "task.acquire":
|
|
272
|
+
return this.buildOkRes({
|
|
273
|
+
kind: req.kind,
|
|
274
|
+
corrId,
|
|
275
|
+
...this.taskAcquire({ at, req }),
|
|
276
|
+
});
|
|
277
|
+
case "task.create":
|
|
278
|
+
return this.buildOkRes({
|
|
279
|
+
kind: req.kind,
|
|
280
|
+
corrId,
|
|
281
|
+
...this.taskCreate({ at, req }),
|
|
282
|
+
});
|
|
283
|
+
case "task.fulfill":
|
|
284
|
+
return this.buildOkRes({
|
|
285
|
+
kind: req.kind,
|
|
286
|
+
corrId,
|
|
287
|
+
...this.taskFulfill({ at, req }),
|
|
288
|
+
});
|
|
289
|
+
case "task.get":
|
|
290
|
+
return this.buildOkRes({
|
|
291
|
+
kind: req.kind,
|
|
292
|
+
corrId,
|
|
293
|
+
...this.taskGet({ req }),
|
|
294
|
+
});
|
|
295
|
+
case "task.heartbeat":
|
|
296
|
+
return this.buildOkRes({
|
|
297
|
+
kind: req.kind,
|
|
298
|
+
corrId,
|
|
299
|
+
...this.taskHeartbeat({ at, req }),
|
|
300
|
+
});
|
|
301
|
+
case "task.release":
|
|
302
|
+
return this.buildOkRes({
|
|
303
|
+
kind: req.kind,
|
|
304
|
+
corrId,
|
|
305
|
+
...this.taskRelease({ at, req }),
|
|
306
|
+
});
|
|
307
|
+
case "task.suspend":
|
|
308
|
+
return this.buildOkRes({
|
|
309
|
+
kind: req.kind,
|
|
310
|
+
corrId,
|
|
311
|
+
...this.taskSuspend({ at, req }),
|
|
312
|
+
});
|
|
313
|
+
case "task.fence":
|
|
314
|
+
throw new ServerError(500, "unexpected task.fence in processRequest");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
promiseCreate({ at, req, }) {
|
|
318
|
+
const { promise, task, applied } = this.transitionPromiseAndTask({
|
|
319
|
+
at,
|
|
320
|
+
id: req.data.id,
|
|
321
|
+
to: "pending",
|
|
322
|
+
payload: req.data.param,
|
|
323
|
+
tags: req.data.tags,
|
|
324
|
+
timeoutAt: req.data.timeoutAt,
|
|
325
|
+
});
|
|
326
|
+
if (!applied) {
|
|
327
|
+
assert(task === undefined);
|
|
328
|
+
return { status: 200, data: { promise }, task };
|
|
329
|
+
}
|
|
330
|
+
assert(promise.createdAt <= at);
|
|
331
|
+
return { status: 200, data: { promise }, task };
|
|
332
|
+
}
|
|
333
|
+
promiseGet({ req, }) {
|
|
334
|
+
return {
|
|
335
|
+
status: 200,
|
|
336
|
+
data: { promise: this.getPromiseRecord(req.data.id) },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
promiseRegister({ at, req, }) {
|
|
340
|
+
// Get both promises involved in the dependency relationship:
|
|
341
|
+
// - awaiter: the promise that is waiting (the parent/caller)
|
|
342
|
+
// - awaited: the promise being waited on (the child/dependency)
|
|
343
|
+
const promiseAwaiter = this.getPromiseRecord(req.data.awaiter);
|
|
344
|
+
const promiseAwaited = this.getPromiseRecord(req.data.awaited);
|
|
345
|
+
// Create a unique callback ID for this dependency relationship
|
|
346
|
+
const cbId = `__resume:${req.data.awaiter}:${req.data.awaited}`;
|
|
347
|
+
// Early return if callback registration is not needed:
|
|
348
|
+
// 1. Awaited promise is already settled (no need to register callback on completed promises)
|
|
349
|
+
// 2. Callback already exists (avoid duplicate registrations)
|
|
350
|
+
if (promiseAwaited.state !== "pending" ||
|
|
351
|
+
promiseAwaited.callbacks[cbId] !== undefined) {
|
|
352
|
+
return { status: 200, data: { promise: promiseAwaited }, created: false };
|
|
353
|
+
}
|
|
354
|
+
// Determine the target receiver/handler for the awaiter promise
|
|
355
|
+
// This is where the resume task will be sent when the awaited promise completes
|
|
356
|
+
const recv = this.router.route(promiseAwaiter);
|
|
357
|
+
if (!recv) {
|
|
358
|
+
throw new ServerError(500, "recv must be set");
|
|
359
|
+
}
|
|
360
|
+
// Register the callback on the awaited promise (not the awaiter!)
|
|
361
|
+
// When the awaited promise settles, this callback will be initialized as a resume task
|
|
362
|
+
// in transitionPromiseAndTask, which will continue execution of the awaiter promise
|
|
363
|
+
promiseAwaited.callbacks[cbId] = {
|
|
364
|
+
id: cbId,
|
|
365
|
+
type: "resume",
|
|
366
|
+
awaited: req.data.awaited,
|
|
367
|
+
awaiter: req.data.awaiter,
|
|
368
|
+
recv,
|
|
369
|
+
timeoutAt: promiseAwaiter.timeoutAt,
|
|
370
|
+
createdAt: at,
|
|
371
|
+
};
|
|
372
|
+
return { status: 200, data: { promise: promiseAwaited }, created: true };
|
|
373
|
+
}
|
|
374
|
+
promiseSettle({ at, req, }) {
|
|
375
|
+
const { promise, task } = this.transitionPromiseAndTask({
|
|
376
|
+
at,
|
|
377
|
+
id: req.data.id,
|
|
378
|
+
to: req.data.state,
|
|
379
|
+
payload: req.data.value,
|
|
380
|
+
});
|
|
381
|
+
assert(task === undefined);
|
|
382
|
+
assert(promise.state !== "pending");
|
|
383
|
+
return { status: 200, data: { promise } };
|
|
384
|
+
}
|
|
385
|
+
promiseSubscribe({ at, req, }) {
|
|
386
|
+
// Get the promise that the subscriber wants to be notified about
|
|
387
|
+
const promise = this.getPromiseRecord(req.data.awaited);
|
|
388
|
+
// Create a unique callback ID based on the promise and destination address
|
|
389
|
+
// This ensures one callback per address per promise
|
|
390
|
+
const cbId = `__notify:${req.data.awaited}:${req.data.address}`;
|
|
391
|
+
// Early return if subscription is not needed:
|
|
392
|
+
// 1. Promise is already settled (no need to subscribe to completed promises)
|
|
393
|
+
// 2. Callback already exists (avoid duplicate subscriptions)
|
|
394
|
+
if (promise.state !== "pending" || promise.callbacks[cbId] !== undefined) {
|
|
395
|
+
return { status: 200, data: { promise } };
|
|
396
|
+
}
|
|
397
|
+
// Determine the target receiver/handler for this promise's notifications
|
|
398
|
+
const recv = this.router.route(promise);
|
|
399
|
+
if (!recv) {
|
|
400
|
+
throw new ServerError(500, "recv must be set");
|
|
401
|
+
}
|
|
402
|
+
// Register the callback on the promise
|
|
403
|
+
// When the promise settles, this callback will be initialized as a notify task
|
|
404
|
+
// in transitionPromiseAndTask, which will send the notification
|
|
405
|
+
promise.callbacks[cbId] = {
|
|
406
|
+
id: cbId,
|
|
407
|
+
type: "notify",
|
|
408
|
+
awaited: req.data.awaited,
|
|
409
|
+
awaiter: req.data.awaited,
|
|
410
|
+
recv,
|
|
411
|
+
timeoutAt: promise.timeoutAt,
|
|
412
|
+
createdAt: at,
|
|
413
|
+
};
|
|
414
|
+
return { status: 200, data: { promise } };
|
|
415
|
+
}
|
|
416
|
+
scheduleCreate({ at, req, }) {
|
|
417
|
+
return {
|
|
418
|
+
status: 200,
|
|
419
|
+
data: {
|
|
420
|
+
schedule: this.transitionSchedule({
|
|
421
|
+
at,
|
|
422
|
+
id: req.data.id,
|
|
423
|
+
to: "created",
|
|
424
|
+
cron: req.data.cron,
|
|
425
|
+
promiseId: req.data.promiseId,
|
|
426
|
+
promiseTimeout: req.data.promiseTimeout,
|
|
427
|
+
promiseParam: req.data.promiseParam,
|
|
428
|
+
promiseTags: req.data.promiseTags,
|
|
429
|
+
}).schedule,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
scheduleDelete({ at, req, }) {
|
|
434
|
+
const { applied } = this.transitionSchedule({
|
|
435
|
+
at,
|
|
436
|
+
id: req.data.id,
|
|
437
|
+
to: "deleted",
|
|
438
|
+
});
|
|
439
|
+
assert(applied);
|
|
440
|
+
return { status: 200, data: undefined };
|
|
441
|
+
}
|
|
442
|
+
scheduleGet({ req, }) {
|
|
443
|
+
return {
|
|
444
|
+
status: 200,
|
|
445
|
+
data: { schedule: this.getScheduleRecord(req.data.id) },
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
taskAcquire({ at, req, }) {
|
|
449
|
+
const { task, applied } = this.transitionTask({
|
|
450
|
+
at,
|
|
451
|
+
id: req.data.id,
|
|
452
|
+
to: "claimed",
|
|
453
|
+
version: req.data.version,
|
|
454
|
+
pid: req.data.pid,
|
|
455
|
+
ttl: req.data.ttl,
|
|
456
|
+
});
|
|
457
|
+
assert(applied);
|
|
458
|
+
assert(task.type !== "notify");
|
|
459
|
+
return {
|
|
460
|
+
status: 200,
|
|
461
|
+
data: {
|
|
462
|
+
kind: task.type,
|
|
463
|
+
data: {
|
|
464
|
+
promise: this.getPromiseRecord(task.awaiter),
|
|
465
|
+
preload: Object.values(this.promises).filter((p) => p.state !== "pending"),
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
taskCreate({ at, req, }) {
|
|
471
|
+
const { data: { promise }, task, } = this.promiseCreate({ at, req: req.data.action });
|
|
472
|
+
if (task !== undefined && task.state !== "claimed") {
|
|
473
|
+
const { task: claimed, applied } = this.transitionTask({
|
|
474
|
+
at,
|
|
475
|
+
id: task.id,
|
|
476
|
+
to: "claimed",
|
|
477
|
+
version: 1,
|
|
478
|
+
pid: req.data.pid,
|
|
479
|
+
ttl: req.data.ttl,
|
|
480
|
+
});
|
|
481
|
+
assert(applied);
|
|
482
|
+
return { status: 200, data: { task: claimed, promise } };
|
|
483
|
+
}
|
|
484
|
+
return { status: 200, data: { promise } };
|
|
485
|
+
}
|
|
486
|
+
taskFulfill({ at, req, }) {
|
|
487
|
+
const { data: { promise }, } = this.promiseSettle({ at, req: req.data.action });
|
|
488
|
+
assert(promise.state !== "pending");
|
|
489
|
+
assertDefined(promise.settledAt);
|
|
490
|
+
if (promise.settledAt === at) {
|
|
491
|
+
const { task } = this.transitionTask({
|
|
492
|
+
at,
|
|
493
|
+
id: req.data.id,
|
|
494
|
+
to: "completed",
|
|
495
|
+
version: req.data.version,
|
|
496
|
+
});
|
|
497
|
+
assert(task.state === "completed");
|
|
498
|
+
assertDefined(task.completedAt);
|
|
499
|
+
assert(task.completedAt <= at);
|
|
500
|
+
}
|
|
501
|
+
return { status: 200, data: { promise } };
|
|
502
|
+
}
|
|
503
|
+
taskGet({ req }) {
|
|
504
|
+
return { status: 200, data: { task: this.getTaskRecord(req.data.id) } };
|
|
505
|
+
}
|
|
506
|
+
taskHeartbeat({ at, req, }) {
|
|
507
|
+
for (const task of Object.values(this.tasks)) {
|
|
508
|
+
if (task.state !== "claimed" || task.pid !== req.data.pid) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const { applied } = this.transitionTask({
|
|
512
|
+
at,
|
|
513
|
+
id: task.id,
|
|
514
|
+
to: "claimed",
|
|
515
|
+
force: true,
|
|
516
|
+
});
|
|
517
|
+
assert(applied);
|
|
518
|
+
}
|
|
519
|
+
return { status: 200, data: undefined };
|
|
520
|
+
}
|
|
521
|
+
taskRelease({ at, req, }) {
|
|
522
|
+
const { applied } = this.transitionTask({
|
|
523
|
+
at,
|
|
524
|
+
id: req.data.id,
|
|
525
|
+
to: "init",
|
|
526
|
+
version: req.data.version,
|
|
527
|
+
});
|
|
528
|
+
assert(applied);
|
|
529
|
+
return { status: 200, data: undefined };
|
|
530
|
+
}
|
|
531
|
+
taskSuspend({ at, req, }) {
|
|
532
|
+
let status = 200;
|
|
533
|
+
for (const action of req.data.actions) {
|
|
534
|
+
const { created } = this.promiseRegister({ at, req: action });
|
|
535
|
+
if (!created) {
|
|
536
|
+
status = 300;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const { applied } = this.transitionTask({
|
|
541
|
+
at,
|
|
542
|
+
id: req.data.id,
|
|
543
|
+
to: "completed",
|
|
544
|
+
version: req.data.version,
|
|
545
|
+
});
|
|
546
|
+
if (applied) {
|
|
547
|
+
// this is expected to be an invariant once we move the tasks to waiting
|
|
548
|
+
}
|
|
549
|
+
return { status, data: undefined };
|
|
550
|
+
}
|
|
551
|
+
transitionPromise({ at, id, to, timeoutAt, payload, tags, }) {
|
|
552
|
+
let promise = this.promises[id];
|
|
553
|
+
if (promise === undefined && to === "pending") {
|
|
554
|
+
assertDefined(timeoutAt);
|
|
555
|
+
promise = {
|
|
556
|
+
id,
|
|
557
|
+
state: to,
|
|
558
|
+
timeoutAt,
|
|
559
|
+
param: payload ?? { headers: {}, data: "" },
|
|
560
|
+
value: { headers: {}, data: "" },
|
|
561
|
+
tags: tags ?? {},
|
|
562
|
+
createdAt: at,
|
|
563
|
+
callbacks: {},
|
|
564
|
+
};
|
|
565
|
+
this.promises[id] = promise;
|
|
566
|
+
return { promise, applied: true };
|
|
567
|
+
}
|
|
568
|
+
if (promise === undefined && isTerminalState(to)) {
|
|
569
|
+
throw new ServerError(404, "promise not found");
|
|
570
|
+
}
|
|
571
|
+
if (promise.state === "pending" && to === "pending") {
|
|
572
|
+
if (at < promise.timeoutAt) {
|
|
573
|
+
return { promise, applied: false };
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
return this.transitionPromise({ at, id, to: "rejected_timedout" });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (promise.state === "pending" && isTerminalState(to)) {
|
|
580
|
+
if (at < promise.timeoutAt) {
|
|
581
|
+
promise = {
|
|
582
|
+
...promise,
|
|
583
|
+
state: to,
|
|
584
|
+
value: payload ?? { headers: {}, data: "" },
|
|
585
|
+
settledAt: at,
|
|
586
|
+
};
|
|
587
|
+
this.promises[id] = promise;
|
|
588
|
+
return { promise, applied: true };
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
return this.transitionPromise({ at, id, to: "rejected_timedout" });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (promise.state === "pending" && to === "rejected_timedout") {
|
|
595
|
+
assert(at >= promise.timeoutAt);
|
|
596
|
+
promise = {
|
|
597
|
+
...promise,
|
|
598
|
+
state: promise.tags["resonate:timeout"] === "true" ? "resolved" : to,
|
|
599
|
+
settledAt: at,
|
|
600
|
+
};
|
|
601
|
+
this.promises[id] = promise;
|
|
602
|
+
return { promise, applied: true };
|
|
603
|
+
}
|
|
604
|
+
if (promise.state !== "pending" && to === "pending") {
|
|
605
|
+
return { promise, applied: false };
|
|
606
|
+
}
|
|
607
|
+
if (isTerminalState(promise.state) && isTerminalState(to)) {
|
|
608
|
+
return { promise, applied: false };
|
|
609
|
+
}
|
|
610
|
+
if (promise.state === "rejected_timedout" && isTerminalState(to)) {
|
|
611
|
+
return { promise, applied: false };
|
|
612
|
+
}
|
|
613
|
+
assert(false, "unexpected promise transition");
|
|
614
|
+
}
|
|
615
|
+
transitionTask({ at, id, to, type, recv, awaiter, awaited, timeoutAt, version, pid, ttl, force, }) {
|
|
616
|
+
let task = this.tasks[id];
|
|
617
|
+
if (task === undefined && to === "init") {
|
|
618
|
+
assertDefined(type);
|
|
619
|
+
assertDefined(recv);
|
|
620
|
+
assertDefined(awaited);
|
|
621
|
+
assertDefined(awaiter);
|
|
622
|
+
assertDefined(timeoutAt);
|
|
623
|
+
task = {
|
|
624
|
+
id,
|
|
625
|
+
version: 1,
|
|
626
|
+
state: to,
|
|
627
|
+
type,
|
|
628
|
+
recv,
|
|
629
|
+
awaited,
|
|
630
|
+
awaiter,
|
|
631
|
+
timeoutAt,
|
|
632
|
+
pid,
|
|
633
|
+
ttl,
|
|
634
|
+
expiry: 0,
|
|
635
|
+
createdAt: at,
|
|
636
|
+
};
|
|
637
|
+
this.tasks[id] = task;
|
|
638
|
+
return { task, applied: true };
|
|
639
|
+
}
|
|
640
|
+
if (task.state === "init" && to === "enqueued") {
|
|
641
|
+
task = { ...task, state: to, expiry: at + this.taskExpiryMs };
|
|
642
|
+
this.tasks[id] = task;
|
|
643
|
+
return { task, applied: true };
|
|
644
|
+
}
|
|
645
|
+
if (isClaimableState(task.state) &&
|
|
646
|
+
to === "claimed" &&
|
|
647
|
+
task.version === version) {
|
|
648
|
+
assertDefined(pid);
|
|
649
|
+
assertDefined(ttl);
|
|
650
|
+
task = { ...task, state: to, pid, ttl, expiry: at + ttl };
|
|
651
|
+
this.tasks[id] = task;
|
|
652
|
+
return { task, applied: true };
|
|
653
|
+
}
|
|
654
|
+
if (isClaimableState(task.state) &&
|
|
655
|
+
task.type === "notify" &&
|
|
656
|
+
to === "completed") {
|
|
657
|
+
task = { ...task, state: to, completedAt: at };
|
|
658
|
+
this.tasks[id] = task;
|
|
659
|
+
return { task, applied: true };
|
|
660
|
+
}
|
|
661
|
+
if ((task.state === "claimed" && task.version === version && to === "init") ||
|
|
662
|
+
(isActiveState(task.state) && to === "init" && force)) {
|
|
663
|
+
task = {
|
|
664
|
+
...task,
|
|
665
|
+
version: task.version + 1,
|
|
666
|
+
state: "init",
|
|
667
|
+
pid: undefined,
|
|
668
|
+
ttl: undefined,
|
|
669
|
+
expiry: 0,
|
|
670
|
+
};
|
|
671
|
+
this.tasks[id] = task;
|
|
672
|
+
return { task, applied: true };
|
|
673
|
+
}
|
|
674
|
+
if (task.state === "claimed" && to === "claimed" && force) {
|
|
675
|
+
assertDefined(task.ttl);
|
|
676
|
+
task = { ...task, expiry: at + task.ttl };
|
|
677
|
+
this.tasks[id] = task;
|
|
678
|
+
return { task, applied: true };
|
|
679
|
+
}
|
|
680
|
+
if (task.state === "claimed" &&
|
|
681
|
+
to === "completed" &&
|
|
682
|
+
task.version === version &&
|
|
683
|
+
task.expiry >= at) {
|
|
684
|
+
task = { ...task, state: to, completedAt: at };
|
|
685
|
+
this.tasks[id] = task;
|
|
686
|
+
return { task, applied: true };
|
|
687
|
+
}
|
|
688
|
+
if (isNotCompleted(task.state) && to === "completed" && force) {
|
|
689
|
+
task = { ...task, state: to, completedAt: at };
|
|
690
|
+
this.tasks[id] = task;
|
|
691
|
+
return { task, applied: true };
|
|
692
|
+
}
|
|
693
|
+
if (task.state === "completed" && to === "completed") {
|
|
694
|
+
return { task, applied: false };
|
|
695
|
+
}
|
|
696
|
+
throw new ServerError(500, "invalid task transition");
|
|
697
|
+
}
|
|
698
|
+
transitionPromiseAndTask({ at, id, to, timeoutAt, payload, tags, }) {
|
|
699
|
+
// Attempt to transition the promise to the new state
|
|
700
|
+
const { promise, applied } = this.transitionPromise({
|
|
701
|
+
at,
|
|
702
|
+
id,
|
|
703
|
+
to,
|
|
704
|
+
timeoutAt,
|
|
705
|
+
payload,
|
|
706
|
+
tags,
|
|
707
|
+
});
|
|
708
|
+
// If the transition wasn't applied (e.g., invalid state change), return early
|
|
709
|
+
if (!applied) {
|
|
710
|
+
return { promise, applied };
|
|
711
|
+
}
|
|
712
|
+
// Handle newly created pending promises by creating an associated invoke task
|
|
713
|
+
if (promise.state === "pending") {
|
|
714
|
+
const recv = this.router.route(promise);
|
|
715
|
+
if (recv !== undefined) {
|
|
716
|
+
// Create an invoke task to execute the promise
|
|
717
|
+
const { task, applied } = this.transitionTask({
|
|
718
|
+
at,
|
|
719
|
+
id: invokeId(promise.id),
|
|
720
|
+
to: "init",
|
|
721
|
+
type: "invoke",
|
|
722
|
+
recv: this.targets[recv] ?? recv,
|
|
723
|
+
awaited: promise.id,
|
|
724
|
+
awaiter: promise.id,
|
|
725
|
+
timeoutAt: promise.timeoutAt,
|
|
726
|
+
});
|
|
727
|
+
assert(applied);
|
|
728
|
+
assert(task.awaited === promise.id);
|
|
729
|
+
assert(task.awaiter === promise.id);
|
|
730
|
+
return { promise, task, applied };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Handle completed promises (resolved, rejected, canceled, or timed out)
|
|
734
|
+
if (promise.state !== "pending") {
|
|
735
|
+
assertDefined(promise.settledAt);
|
|
736
|
+
assert(promise.settledAt >= promise.createdAt);
|
|
737
|
+
// Complete all tasks that were awaiting this promise
|
|
738
|
+
for (const task of Object.values(this.tasks)) {
|
|
739
|
+
if (task.awaiter === promise.id && isNotCompleted(task.state)) {
|
|
740
|
+
const { applied } = this.transitionTask({
|
|
741
|
+
at,
|
|
742
|
+
id: task.id,
|
|
743
|
+
to: "completed",
|
|
744
|
+
force: true,
|
|
745
|
+
});
|
|
746
|
+
assert(applied);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Initialize all registered callbacks for this promise
|
|
750
|
+
for (const callback of Object.values(promise.callbacks)) {
|
|
751
|
+
const { applied } = this.transitionTask({
|
|
752
|
+
...callback,
|
|
753
|
+
at,
|
|
754
|
+
to: "init",
|
|
755
|
+
awaited: callback.awaited,
|
|
756
|
+
});
|
|
757
|
+
assert(applied);
|
|
758
|
+
}
|
|
759
|
+
// Clear callbacks after initializing them
|
|
760
|
+
promise.callbacks = {};
|
|
761
|
+
}
|
|
762
|
+
return { promise, applied };
|
|
763
|
+
}
|
|
764
|
+
transitionSchedule({ at, id, to, cron, promiseId, promiseTimeout, promiseParam, promiseTags, updating, }) {
|
|
765
|
+
let schedule = this.schedules[id];
|
|
766
|
+
if (schedule === undefined && to === "created") {
|
|
767
|
+
assertDefined(cron);
|
|
768
|
+
assertDefined(promiseId);
|
|
769
|
+
assertDefined(promiseTimeout);
|
|
770
|
+
assertDefined(promiseParam);
|
|
771
|
+
assertDefined(promiseTags);
|
|
772
|
+
schedule = {
|
|
773
|
+
id,
|
|
774
|
+
cron,
|
|
775
|
+
promiseId,
|
|
776
|
+
promiseTimeout,
|
|
777
|
+
promiseParam,
|
|
778
|
+
promiseTags,
|
|
779
|
+
createdAt: at,
|
|
780
|
+
nextRunAt: CronExpressionParser.parse(cron, { currentDate: at })
|
|
781
|
+
.next()
|
|
782
|
+
.getTime(),
|
|
783
|
+
};
|
|
784
|
+
this.schedules[id] = schedule;
|
|
785
|
+
return { schedule, applied: true };
|
|
786
|
+
}
|
|
787
|
+
if (schedule !== undefined && to === "created" && updating) {
|
|
788
|
+
schedule = {
|
|
789
|
+
...schedule,
|
|
790
|
+
lastRunAt: schedule.nextRunAt,
|
|
791
|
+
nextRunAt: CronExpressionParser.parse(schedule.cron, {
|
|
792
|
+
currentDate: at,
|
|
793
|
+
})
|
|
794
|
+
.next()
|
|
795
|
+
.getTime(),
|
|
796
|
+
};
|
|
797
|
+
this.schedules[id] = schedule;
|
|
798
|
+
return { schedule, applied: true };
|
|
799
|
+
}
|
|
800
|
+
if (schedule !== undefined && to === "created") {
|
|
801
|
+
return { schedule, applied: false };
|
|
802
|
+
}
|
|
803
|
+
if (schedule === undefined && to === "deleted") {
|
|
804
|
+
throw new ServerError(404, "schedule not found");
|
|
805
|
+
}
|
|
806
|
+
if (schedule !== undefined && to === "deleted") {
|
|
807
|
+
delete this.schedules[id];
|
|
808
|
+
return { schedule, applied: true };
|
|
809
|
+
}
|
|
810
|
+
throw new ServerError(500, "invalid schedule transition");
|
|
811
|
+
}
|
|
812
|
+
getPromiseRecord(id) {
|
|
813
|
+
const promise = this.promises[id];
|
|
814
|
+
if (!promise) {
|
|
815
|
+
throw new ServerError(404, "promise not found");
|
|
816
|
+
}
|
|
817
|
+
return promise;
|
|
818
|
+
}
|
|
819
|
+
getTaskRecord(id) {
|
|
820
|
+
const task = this.tasks[id];
|
|
821
|
+
if (!task) {
|
|
822
|
+
throw new ServerError(404, "task not found");
|
|
823
|
+
}
|
|
824
|
+
return task;
|
|
825
|
+
}
|
|
826
|
+
getScheduleRecord(id) {
|
|
827
|
+
const schedule = this.schedules[id];
|
|
828
|
+
if (!schedule) {
|
|
829
|
+
throw new ServerError(404, "schedule not found");
|
|
830
|
+
}
|
|
831
|
+
return schedule;
|
|
832
|
+
}
|
|
833
|
+
buildOkRes({ kind, corrId, status, data, }) {
|
|
834
|
+
return {
|
|
835
|
+
kind,
|
|
836
|
+
head: {
|
|
837
|
+
corrId,
|
|
838
|
+
status,
|
|
839
|
+
version: this.version,
|
|
840
|
+
},
|
|
841
|
+
data,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
buildErrorRes(req, err) {
|
|
845
|
+
return {
|
|
846
|
+
kind: "error",
|
|
847
|
+
head: {
|
|
848
|
+
corrId: req.head.corrId,
|
|
849
|
+
status: err.code,
|
|
850
|
+
version: this.version,
|
|
851
|
+
},
|
|
852
|
+
data: err.message,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
ensureVersion(req) {
|
|
856
|
+
if (req.head.version !== this.version) {
|
|
857
|
+
throw new ServerError(409, "version mismatch");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function isTerminalState(state) {
|
|
862
|
+
return (state === "resolved" ||
|
|
863
|
+
state === "rejected" ||
|
|
864
|
+
state === "rejected_canceled");
|
|
865
|
+
}
|
|
866
|
+
function isClaimableState(state) {
|
|
867
|
+
return state === "init" || state === "enqueued";
|
|
868
|
+
}
|
|
869
|
+
function isActiveState(state) {
|
|
870
|
+
return state === "enqueued" || state === "claimed";
|
|
871
|
+
}
|
|
872
|
+
function isNotCompleted(state) {
|
|
873
|
+
return state === "init" || state === "enqueued" || state === "claimed";
|
|
874
|
+
}
|
|
875
|
+
function invokeId(id) {
|
|
876
|
+
return `__invoke:${id}`;
|
|
877
|
+
}
|
|
878
|
+
//# sourceMappingURL=server.js.map
|