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