@librechat/agents 3.7.1 → 3.7.3
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/dist/cjs/eventActor/EventActorExecutor.cjs +797 -0
- package/dist/cjs/eventActor/EventActorExecutor.cjs.map +1 -0
- package/dist/cjs/eventActor/index.cjs +1 -0
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +6 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/eventActor/EventActorExecutor.mjs +796 -0
- package/dist/esm/eventActor/EventActorExecutor.mjs.map +1 -0
- package/dist/esm/eventActor/index.mjs +2 -0
- package/dist/esm/main.mjs +3 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/eventActor/EventActorExecutor.d.ts +18 -0
- package/dist/types/eventActor/index.d.ts +2 -0
- package/dist/types/eventActor/types.d.ts +193 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +68 -79
- package/src/eventActor/EventActorExecutor.ts +1499 -0
- package/src/eventActor/index.ts +32 -0
- package/src/eventActor/types.ts +250 -0
- package/src/index.ts +3 -0
- package/src/tools/BashProgrammaticToolCalling.ts +11 -1
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
import { isGraphInterrupt, isParentCommand } from '@langchain/langgraph';
|
|
2
|
+
import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
|
|
3
|
+
import {
|
|
4
|
+
createHash,
|
|
5
|
+
createHmac,
|
|
6
|
+
randomBytes,
|
|
7
|
+
randomUUID,
|
|
8
|
+
timingSafeEqual,
|
|
9
|
+
} from 'node:crypto';
|
|
10
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
11
|
+
import type {
|
|
12
|
+
EventActorAdapterPrepareRequest,
|
|
13
|
+
EventActorAdapterPreparation,
|
|
14
|
+
EventActorAppliedResult,
|
|
15
|
+
EventActorCheckpointFork,
|
|
16
|
+
EventActorCheckpointReference,
|
|
17
|
+
EventActorDiscardReason,
|
|
18
|
+
EventActorEvent,
|
|
19
|
+
EventActorExecutionRequest,
|
|
20
|
+
EventActorExecutionResult,
|
|
21
|
+
EventActorExecutorOptions,
|
|
22
|
+
EventActorHead,
|
|
23
|
+
EventActorHostAdapter,
|
|
24
|
+
EventActorIndeterminateResult,
|
|
25
|
+
EventActorInvocation,
|
|
26
|
+
EventActorInvocationResult,
|
|
27
|
+
EventActorInvocationReference,
|
|
28
|
+
EventActorPreparedInvocation,
|
|
29
|
+
EventActorPrepareRequest,
|
|
30
|
+
EventActorPreparation,
|
|
31
|
+
EventActorSettlementResult,
|
|
32
|
+
EventActorTerminalResult,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MAX_DEPTH = 1;
|
|
36
|
+
const DEFAULT_DORMANT_CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
37
|
+
|
|
38
|
+
function createInvocationCheckpointNs(
|
|
39
|
+
request: EventActorPrepareRequest<EventActorEvent>,
|
|
40
|
+
attemptId = randomUUID()
|
|
41
|
+
): string {
|
|
42
|
+
return `event-actor/${createHash('sha256')
|
|
43
|
+
.update(request.actorThreadId)
|
|
44
|
+
.update('\0')
|
|
45
|
+
.update(request.invocationId)
|
|
46
|
+
.update('\0')
|
|
47
|
+
.update(attemptId)
|
|
48
|
+
.digest('hex')
|
|
49
|
+
.slice(0, 32)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function snapshotEvent<TEvent extends EventActorEvent>(event: TEvent): TEvent {
|
|
53
|
+
const ancestors = new WeakSet<object>();
|
|
54
|
+
const clone = (value: unknown): EventActorEvent => {
|
|
55
|
+
if (
|
|
56
|
+
value === null ||
|
|
57
|
+
typeof value === 'string' ||
|
|
58
|
+
typeof value === 'boolean'
|
|
59
|
+
) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
if (!Number.isFinite(value)) {
|
|
64
|
+
throw new Error('Event actor event numbers must be finite');
|
|
65
|
+
}
|
|
66
|
+
return Object.is(value, -0) ? 0 : value;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value !== 'object') {
|
|
69
|
+
throw new Error('Event actor events must contain only JSON values');
|
|
70
|
+
}
|
|
71
|
+
if (ancestors.has(value)) {
|
|
72
|
+
throw new Error('Event actor events must not contain cycles');
|
|
73
|
+
}
|
|
74
|
+
ancestors.add(value);
|
|
75
|
+
try {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
78
|
+
throw new Error('Event actor event arrays must not contain symbols');
|
|
79
|
+
}
|
|
80
|
+
const snapshot: EventActorEvent[] = [];
|
|
81
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
82
|
+
if (!Object.hasOwn(value, index)) {
|
|
83
|
+
throw new Error('Event actor event arrays must not contain holes');
|
|
84
|
+
}
|
|
85
|
+
snapshot.push(clone(value[index]));
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(value).length !== value.length) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'Event actor event arrays must not contain named properties'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze(snapshot);
|
|
93
|
+
}
|
|
94
|
+
const prototype = Object.getPrototypeOf(value);
|
|
95
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
96
|
+
throw new Error('Event actor events must contain only JSON objects');
|
|
97
|
+
}
|
|
98
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
99
|
+
throw new Error('Event actor events must not contain symbol keys');
|
|
100
|
+
}
|
|
101
|
+
const snapshot: Record<string, EventActorEvent> = {};
|
|
102
|
+
for (const key of Object.keys(value).sort()) {
|
|
103
|
+
const item = value[key as keyof typeof value];
|
|
104
|
+
Object.defineProperty(snapshot, key, {
|
|
105
|
+
configurable: false,
|
|
106
|
+
enumerable: true,
|
|
107
|
+
writable: false,
|
|
108
|
+
value: clone(item),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze(snapshot);
|
|
112
|
+
} finally {
|
|
113
|
+
ancestors.delete(value);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return clone(event) as TEvent;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function snapshotCheckpointReference(
|
|
120
|
+
checkpoint: EventActorCheckpointReference
|
|
121
|
+
): EventActorCheckpointReference {
|
|
122
|
+
return {
|
|
123
|
+
threadId: checkpoint.threadId,
|
|
124
|
+
...(checkpoint.checkpointId == null
|
|
125
|
+
? {}
|
|
126
|
+
: { checkpointId: checkpoint.checkpointId }),
|
|
127
|
+
checkpointNs: checkpoint.checkpointNs,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function snapshotCheckpointFork(
|
|
132
|
+
checkpoint: EventActorCheckpointFork
|
|
133
|
+
): EventActorCheckpointFork {
|
|
134
|
+
return {
|
|
135
|
+
...snapshotCheckpointReference(checkpoint),
|
|
136
|
+
invocationId: checkpoint.invocationId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function snapshotHead(head: EventActorHead): EventActorHead {
|
|
141
|
+
return {
|
|
142
|
+
actorThreadId: head.actorThreadId,
|
|
143
|
+
generation: Object.is(head.generation, -0) ? 0 : head.generation,
|
|
144
|
+
...(head.checkpoint == null
|
|
145
|
+
? {}
|
|
146
|
+
: { checkpoint: snapshotCheckpointReference(head.checkpoint) }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function snapshotInvocationReference(
|
|
151
|
+
invocation: EventActorInvocationReference
|
|
152
|
+
): EventActorInvocationReference {
|
|
153
|
+
return {
|
|
154
|
+
actorThreadId: invocation.actorThreadId,
|
|
155
|
+
invocationId: invocation.invocationId,
|
|
156
|
+
depth: invocation.depth,
|
|
157
|
+
continuation: invocation.continuation,
|
|
158
|
+
base: snapshotHead(invocation.base),
|
|
159
|
+
fork: snapshotCheckpointFork(invocation.fork),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function snapshotInvocation<TEvent extends EventActorEvent>(
|
|
164
|
+
invocation: EventActorInvocation<TEvent>
|
|
165
|
+
): EventActorInvocation<TEvent> {
|
|
166
|
+
return {
|
|
167
|
+
...snapshotInvocationReference(invocation),
|
|
168
|
+
event: snapshotEvent(invocation.event),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function snapshotPrepareRequest<TEvent extends EventActorEvent>(
|
|
173
|
+
request: EventActorPrepareRequest<TEvent>
|
|
174
|
+
): EventActorPrepareRequest<TEvent> {
|
|
175
|
+
return {
|
|
176
|
+
actorThreadId: request.actorThreadId,
|
|
177
|
+
invocationId: request.invocationId,
|
|
178
|
+
depth: request.depth,
|
|
179
|
+
event: snapshotEvent(request.event),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function freezeInvocationReference(
|
|
184
|
+
invocation: EventActorInvocationReference
|
|
185
|
+
): EventActorInvocationReference {
|
|
186
|
+
const snapshot = snapshotInvocationReference(invocation);
|
|
187
|
+
if (snapshot.base.checkpoint != null) {
|
|
188
|
+
Object.freeze(snapshot.base.checkpoint);
|
|
189
|
+
}
|
|
190
|
+
Object.freeze(snapshot.base);
|
|
191
|
+
Object.freeze(snapshot.fork);
|
|
192
|
+
return Object.freeze(snapshot);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function freezeInvocation<TEvent extends EventActorEvent>(
|
|
196
|
+
invocation: EventActorInvocation<TEvent>
|
|
197
|
+
): EventActorInvocation<TEvent> {
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
...freezeInvocationReference(invocation),
|
|
200
|
+
event: snapshotEvent(invocation.event),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function snapshotPreparedInvocation<TEvent extends EventActorEvent>(
|
|
205
|
+
invocation: EventActorPreparedInvocation<TEvent>
|
|
206
|
+
): EventActorPreparedInvocation<TEvent> {
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
...freezeInvocation(invocation),
|
|
209
|
+
preparationDigest: invocation.preparationDigest,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function canonicalHead(head: EventActorHead): object {
|
|
214
|
+
if (head.checkpoint == null) {
|
|
215
|
+
return {
|
|
216
|
+
actorThreadId: head.actorThreadId,
|
|
217
|
+
generation: head.generation,
|
|
218
|
+
checkpoint: null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
actorThreadId: head.actorThreadId,
|
|
223
|
+
generation: head.generation,
|
|
224
|
+
checkpoint: {
|
|
225
|
+
threadId: head.checkpoint.threadId,
|
|
226
|
+
checkpointId: head.checkpoint.checkpointId ?? null,
|
|
227
|
+
checkpointNs: head.checkpoint.checkpointNs,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function serializeInvocationPreparation<TEvent extends EventActorEvent>(
|
|
233
|
+
invocation: EventActorInvocation<TEvent>
|
|
234
|
+
): string {
|
|
235
|
+
return JSON.stringify({
|
|
236
|
+
kind: 'invocation',
|
|
237
|
+
actorThreadId: invocation.actorThreadId,
|
|
238
|
+
invocationId: invocation.invocationId,
|
|
239
|
+
depth: invocation.depth,
|
|
240
|
+
continuation: invocation.continuation,
|
|
241
|
+
base: canonicalHead(invocation.base),
|
|
242
|
+
fork: {
|
|
243
|
+
invocationId: invocation.fork.invocationId,
|
|
244
|
+
threadId: invocation.fork.threadId,
|
|
245
|
+
checkpointId: invocation.fork.checkpointId ?? null,
|
|
246
|
+
checkpointNs: invocation.fork.checkpointNs,
|
|
247
|
+
},
|
|
248
|
+
event: snapshotEvent(invocation.event),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function serializeUnavailablePreparation<TEvent extends EventActorEvent>(
|
|
253
|
+
request: EventActorPrepareRequest<TEvent>,
|
|
254
|
+
head: EventActorHead
|
|
255
|
+
): string {
|
|
256
|
+
return JSON.stringify({
|
|
257
|
+
kind: 'checkpoint_unavailable',
|
|
258
|
+
request: {
|
|
259
|
+
actorThreadId: request.actorThreadId,
|
|
260
|
+
invocationId: request.invocationId,
|
|
261
|
+
depth: request.depth,
|
|
262
|
+
event: snapshotEvent(request.event),
|
|
263
|
+
},
|
|
264
|
+
head: canonicalHead(head),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function freezePrepareRequest<TEvent extends EventActorEvent>(
|
|
269
|
+
request: EventActorPrepareRequest<TEvent>
|
|
270
|
+
): EventActorPrepareRequest<TEvent> {
|
|
271
|
+
return Object.freeze(snapshotPrepareRequest(request));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function freezeHead(head: EventActorHead): EventActorHead {
|
|
275
|
+
const snapshot = snapshotHead(head);
|
|
276
|
+
if (snapshot.checkpoint != null) {
|
|
277
|
+
Object.freeze(snapshot.checkpoint);
|
|
278
|
+
}
|
|
279
|
+
return Object.freeze(snapshot);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function snapshotAmbientConfig(
|
|
283
|
+
config: RunnableConfig | undefined
|
|
284
|
+
): RunnableConfig | undefined {
|
|
285
|
+
if (config == null) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
...config,
|
|
290
|
+
...(config.tags == null ? {} : { tags: [...config.tags] }),
|
|
291
|
+
...(config.metadata == null ? {} : { metadata: { ...config.metadata } }),
|
|
292
|
+
...(config.configurable == null
|
|
293
|
+
? {}
|
|
294
|
+
: { configurable: { ...config.configurable } }),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function requireNonEmpty(value: string, name: string): void {
|
|
299
|
+
if (value.trim() === '') {
|
|
300
|
+
throw new Error(`${name} must not be empty`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function validateHead(
|
|
305
|
+
head: EventActorHead,
|
|
306
|
+
actorThreadId: string,
|
|
307
|
+
checkpointRequired = false
|
|
308
|
+
): void {
|
|
309
|
+
if (
|
|
310
|
+
head.actorThreadId !== actorThreadId ||
|
|
311
|
+
!Number.isSafeInteger(head.generation) ||
|
|
312
|
+
head.generation < 0
|
|
313
|
+
) {
|
|
314
|
+
throw new Error('Event actor head is invalid');
|
|
315
|
+
}
|
|
316
|
+
if (head.checkpoint == null) {
|
|
317
|
+
if (checkpointRequired) {
|
|
318
|
+
throw new Error('Committed event actor head has no checkpoint');
|
|
319
|
+
}
|
|
320
|
+
if (head.generation > 0) {
|
|
321
|
+
throw new Error('Advanced event actor head has no checkpoint');
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
requireNonEmpty(head.checkpoint.threadId, 'head.checkpoint.threadId');
|
|
326
|
+
if (typeof head.checkpoint.checkpointNs !== 'string') {
|
|
327
|
+
throw new Error('head.checkpoint.checkpointNs must be a string');
|
|
328
|
+
}
|
|
329
|
+
requireNonEmpty(
|
|
330
|
+
head.checkpoint.checkpointId ?? '',
|
|
331
|
+
'head.checkpoint.checkpointId'
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function validateInvocation<TEvent extends EventActorEvent>(
|
|
336
|
+
request: EventActorPrepareRequest<TEvent>,
|
|
337
|
+
invocation: EventActorInvocation<TEvent>,
|
|
338
|
+
continuation: 'warm' | 'cold',
|
|
339
|
+
checkpointNs: string,
|
|
340
|
+
maxDepth: number,
|
|
341
|
+
expectedHead?: EventActorInvocation<TEvent>['base']
|
|
342
|
+
): void {
|
|
343
|
+
validateInvocationReference(invocation, maxDepth);
|
|
344
|
+
if (
|
|
345
|
+
invocation.actorThreadId !== request.actorThreadId ||
|
|
346
|
+
invocation.invocationId !== request.invocationId ||
|
|
347
|
+
invocation.depth !== request.depth ||
|
|
348
|
+
invocation.continuation !== continuation
|
|
349
|
+
) {
|
|
350
|
+
throw new Error('Event actor preparation returned a mismatched invocation');
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
invocation.base.actorThreadId !== request.actorThreadId ||
|
|
354
|
+
invocation.fork.invocationId !== request.invocationId ||
|
|
355
|
+
invocation.fork.checkpointNs !== checkpointNs
|
|
356
|
+
) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
'Event actor preparation returned mismatched checkpoint ownership'
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (
|
|
362
|
+
expectedHead != null &&
|
|
363
|
+
(expectedHead.actorThreadId !== request.actorThreadId ||
|
|
364
|
+
invocation.base.generation !== expectedHead.generation ||
|
|
365
|
+
invocation.base.checkpoint?.threadId !==
|
|
366
|
+
expectedHead.checkpoint?.threadId ||
|
|
367
|
+
invocation.base.checkpoint?.checkpointId !==
|
|
368
|
+
expectedHead.checkpoint?.checkpointId ||
|
|
369
|
+
invocation.base.checkpoint?.checkpointNs !==
|
|
370
|
+
expectedHead.checkpoint?.checkpointNs)
|
|
371
|
+
) {
|
|
372
|
+
throw new Error('Cold continuation did not use the prepared actor head');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function validateInvocationReference(
|
|
377
|
+
invocation: EventActorInvocationReference,
|
|
378
|
+
maxDepth?: number
|
|
379
|
+
): void {
|
|
380
|
+
requireNonEmpty(invocation.actorThreadId, 'actorThreadId');
|
|
381
|
+
requireNonEmpty(invocation.invocationId, 'invocationId');
|
|
382
|
+
if (!Number.isSafeInteger(invocation.depth) || invocation.depth < 1) {
|
|
383
|
+
throw new Error('Event actor invocation depth is invalid');
|
|
384
|
+
}
|
|
385
|
+
if (maxDepth != null && invocation.depth > maxDepth) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Event actor depth ${invocation.depth} exceeds maximum ${maxDepth}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const continuation: unknown = invocation.continuation;
|
|
391
|
+
if (continuation !== 'warm' && continuation !== 'cold') {
|
|
392
|
+
throw new Error('Event actor invocation continuation is invalid');
|
|
393
|
+
}
|
|
394
|
+
validateHead(invocation.base, invocation.actorThreadId);
|
|
395
|
+
if (invocation.fork.invocationId !== invocation.invocationId) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
'Event actor invocation has mismatched checkpoint ownership'
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
requireNonEmpty(invocation.fork.threadId, 'fork.threadId');
|
|
401
|
+
requireNonEmpty(invocation.fork.checkpointNs, 'fork.checkpointNs');
|
|
402
|
+
if (invocation.base.checkpoint != null) {
|
|
403
|
+
if (invocation.fork.threadId !== invocation.base.checkpoint.threadId) {
|
|
404
|
+
throw new Error('Event actor fork changed its logical checkpoint thread');
|
|
405
|
+
}
|
|
406
|
+
requireNonEmpty(
|
|
407
|
+
invocation.fork.checkpointId ?? '',
|
|
408
|
+
'fork.checkpointId for resumed actor'
|
|
409
|
+
);
|
|
410
|
+
if (
|
|
411
|
+
invocation.continuation === 'warm' &&
|
|
412
|
+
invocation.fork.checkpointId !== invocation.base.checkpoint.checkpointId
|
|
413
|
+
) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
'Warm event actor fork did not start from the committed checkpoint'
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function checkpointIdsMatch(
|
|
422
|
+
left: EventActorCheckpointFork | EventActorHead['checkpoint'],
|
|
423
|
+
right: EventActorCheckpointFork | EventActorHead['checkpoint']
|
|
424
|
+
): boolean {
|
|
425
|
+
return (
|
|
426
|
+
left != null && right != null && left.checkpointId === right.checkpointId
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function checkpointsMatch(
|
|
431
|
+
left: EventActorCheckpointFork | EventActorHead['checkpoint'],
|
|
432
|
+
right: EventActorCheckpointFork | EventActorHead['checkpoint']
|
|
433
|
+
): boolean {
|
|
434
|
+
return (
|
|
435
|
+
checkpointIdsMatch(left, right) &&
|
|
436
|
+
left?.threadId === right?.threadId &&
|
|
437
|
+
left?.checkpointNs === right?.checkpointNs
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function validateTerminalCheckpoint(
|
|
442
|
+
invocation: EventActorInvocationReference,
|
|
443
|
+
checkpoint: EventActorCheckpointFork
|
|
444
|
+
): void {
|
|
445
|
+
if (
|
|
446
|
+
checkpoint.invocationId !== invocation.invocationId ||
|
|
447
|
+
checkpoint.threadId !== invocation.fork.threadId ||
|
|
448
|
+
checkpoint.checkpointNs !== invocation.fork.checkpointNs ||
|
|
449
|
+
checkpoint.checkpointId == null ||
|
|
450
|
+
checkpoint.checkpointId.trim() === '' ||
|
|
451
|
+
checkpoint.checkpointId === invocation.fork.checkpointId ||
|
|
452
|
+
checkpointIdsMatch(checkpoint, invocation.base.checkpoint)
|
|
453
|
+
) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
'Event actor result escaped its invocation checkpoint fork'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function createRunnableConfig(
|
|
461
|
+
invocation: EventActorInvocationReference,
|
|
462
|
+
signal: AbortSignal,
|
|
463
|
+
ambient?: RunnableConfig
|
|
464
|
+
): RunnableConfig {
|
|
465
|
+
const {
|
|
466
|
+
signal: _ambientSignal,
|
|
467
|
+
runId: _ambientRunId,
|
|
468
|
+
runName: _ambientRunName,
|
|
469
|
+
callbacks: ambientCallbacks,
|
|
470
|
+
tags: ambientTags,
|
|
471
|
+
metadata: ambientMetadata,
|
|
472
|
+
configurable: ambientConfigurable,
|
|
473
|
+
...ambientRuntime
|
|
474
|
+
} = ambient ?? {};
|
|
475
|
+
const configurable = Object.fromEntries(
|
|
476
|
+
Object.entries(ambientConfigurable ?? {}).filter(
|
|
477
|
+
([key]) =>
|
|
478
|
+
!key.startsWith('__pregel_') &&
|
|
479
|
+
!key.startsWith('__librechat_') &&
|
|
480
|
+
key !== 'lc_run_breaker_scope'
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
delete configurable.run_id;
|
|
484
|
+
delete configurable.thread_id;
|
|
485
|
+
delete configurable.checkpoint_ns;
|
|
486
|
+
delete configurable.checkpoint_id;
|
|
487
|
+
delete configurable.checkpoint_map;
|
|
488
|
+
delete configurable.event_actor_thread_id;
|
|
489
|
+
delete configurable.event_actor_invocation_id;
|
|
490
|
+
delete configurable.event_actor_generation;
|
|
491
|
+
delete configurable.event_actor_depth;
|
|
492
|
+
delete configurable.event_actor_continuation;
|
|
493
|
+
const metadata = Object.fromEntries(
|
|
494
|
+
Object.entries(ambientMetadata ?? {}).filter(
|
|
495
|
+
([key]) =>
|
|
496
|
+
!key.startsWith('langgraph_') &&
|
|
497
|
+
!key.startsWith('__pregel_') &&
|
|
498
|
+
key !== 'run_id' &&
|
|
499
|
+
key !== 'thread_id' &&
|
|
500
|
+
key !== 'checkpoint_ns' &&
|
|
501
|
+
key !== 'checkpoint_id' &&
|
|
502
|
+
key !== 'checkpoint_map'
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
return {
|
|
506
|
+
...ambientRuntime,
|
|
507
|
+
signal,
|
|
508
|
+
...(ambientCallbacks == null ? {} : { callbacks: ambientCallbacks }),
|
|
509
|
+
...(ambientTags == null ? {} : { tags: ambientTags }),
|
|
510
|
+
metadata: {
|
|
511
|
+
...metadata,
|
|
512
|
+
thread_id: invocation.fork.threadId,
|
|
513
|
+
checkpoint_ns: invocation.fork.checkpointNs,
|
|
514
|
+
eventActorThreadId: invocation.actorThreadId,
|
|
515
|
+
eventActorInvocationId: invocation.invocationId,
|
|
516
|
+
eventActorGeneration: invocation.base.generation,
|
|
517
|
+
eventActorDepth: invocation.depth,
|
|
518
|
+
eventActorContinuation: invocation.continuation,
|
|
519
|
+
},
|
|
520
|
+
configurable: {
|
|
521
|
+
...configurable,
|
|
522
|
+
thread_id: invocation.fork.threadId,
|
|
523
|
+
checkpoint_ns: invocation.fork.checkpointNs,
|
|
524
|
+
...(invocation.fork.checkpointId == null
|
|
525
|
+
? {}
|
|
526
|
+
: { checkpoint_id: invocation.fork.checkpointId }),
|
|
527
|
+
event_actor_thread_id: invocation.actorThreadId,
|
|
528
|
+
event_actor_invocation_id: invocation.invocationId,
|
|
529
|
+
event_actor_generation: invocation.base.generation,
|
|
530
|
+
event_actor_depth: invocation.depth,
|
|
531
|
+
event_actor_continuation: invocation.continuation,
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function asError(error: unknown): Error {
|
|
537
|
+
try {
|
|
538
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
539
|
+
} catch {
|
|
540
|
+
return new Error('Unknown event actor error');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
type EventActorAppliedSnapshot<TResult extends EventActorEvent> = {
|
|
545
|
+
status: 'snapshot_ready';
|
|
546
|
+
result: TResult;
|
|
547
|
+
checkpoint: EventActorCheckpointFork;
|
|
548
|
+
invocation: EventActorInvocationReference;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
type EventActorPreparationPhase =
|
|
552
|
+
| { status: 'invoking' | 'discarding' }
|
|
553
|
+
| {
|
|
554
|
+
status: 'discardable' | 'retained' | 'discarded';
|
|
555
|
+
expiresAt: number;
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
function createIndeterminateResult<TResult extends EventActorEvent>(
|
|
559
|
+
invocation: EventActorInvocationReference,
|
|
560
|
+
error: unknown,
|
|
561
|
+
result?: TResult
|
|
562
|
+
): EventActorIndeterminateResult<TResult> {
|
|
563
|
+
return Object.freeze({
|
|
564
|
+
status: 'commit_indeterminate',
|
|
565
|
+
...(result === undefined ? {} : { result }),
|
|
566
|
+
checkpoint: Object.freeze({
|
|
567
|
+
invocationId: invocation.fork.invocationId,
|
|
568
|
+
threadId: invocation.fork.threadId,
|
|
569
|
+
checkpointNs: invocation.fork.checkpointNs,
|
|
570
|
+
}),
|
|
571
|
+
error: asError(error),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function createSettlementIndeterminateResult<TResult extends EventActorEvent>(
|
|
576
|
+
settlement: EventActorAppliedResult<TResult>,
|
|
577
|
+
error: unknown
|
|
578
|
+
): EventActorIndeterminateResult<TResult> {
|
|
579
|
+
return Object.freeze({
|
|
580
|
+
status: 'commit_indeterminate',
|
|
581
|
+
result: settlement.result,
|
|
582
|
+
checkpoint: Object.freeze(snapshotCheckpointFork(settlement.checkpoint)),
|
|
583
|
+
error: asError(error),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function snapshotAppliedTerminal<TResult extends EventActorEvent>(
|
|
588
|
+
invocation: EventActorInvocationReference,
|
|
589
|
+
terminal: Extract<EventActorTerminalResult<TResult>, { status: 'applied' }>
|
|
590
|
+
): EventActorAppliedSnapshot<TResult> | EventActorIndeterminateResult<TResult> {
|
|
591
|
+
let result: TResult | undefined;
|
|
592
|
+
try {
|
|
593
|
+
result = snapshotEvent(terminal.result);
|
|
594
|
+
const snapshot: EventActorAppliedSnapshot<TResult> = {
|
|
595
|
+
status: 'snapshot_ready',
|
|
596
|
+
result,
|
|
597
|
+
checkpoint: snapshotCheckpointFork(terminal.checkpoint),
|
|
598
|
+
invocation: freezeInvocationReference(invocation),
|
|
599
|
+
};
|
|
600
|
+
validateTerminalCheckpoint(snapshot.invocation, snapshot.checkpoint);
|
|
601
|
+
return snapshot;
|
|
602
|
+
} catch (error) {
|
|
603
|
+
return createIndeterminateResult(invocation, error, result);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function isAborted(signal?: AbortSignal): boolean {
|
|
608
|
+
return signal?.aborted === true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
class EventActorPreparationCancelledError extends Error {
|
|
612
|
+
constructor(
|
|
613
|
+
readonly continuation: 'warm' | 'cold',
|
|
614
|
+
reason: unknown
|
|
615
|
+
) {
|
|
616
|
+
super(`Event actor ${continuation} preparation was cancelled`, {
|
|
617
|
+
cause: reason,
|
|
618
|
+
});
|
|
619
|
+
this.name = 'EventActorPreparationCancelledError';
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function resolveExecutionDepth(
|
|
624
|
+
requestedDepth: number | undefined,
|
|
625
|
+
ambientConfig: RunnableConfig | undefined
|
|
626
|
+
): number {
|
|
627
|
+
const ambientDepth = ambientConfig?.configurable?.event_actor_depth;
|
|
628
|
+
if (ambientDepth == null) {
|
|
629
|
+
return requestedDepth ?? 1;
|
|
630
|
+
}
|
|
631
|
+
if (!Number.isSafeInteger(ambientDepth) || Number(ambientDepth) < 1) {
|
|
632
|
+
throw new Error('Ambient event actor depth is invalid');
|
|
633
|
+
}
|
|
634
|
+
const nestedDepth = Number(ambientDepth) + 1;
|
|
635
|
+
if (requestedDepth != null && requestedDepth !== nestedDepth) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`Nested event actor depth ${requestedDepth} must advance parent depth ${ambientDepth}`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return nestedDepth;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function validateCommittedHead(
|
|
644
|
+
invocation: EventActorInvocationReference,
|
|
645
|
+
checkpoint: EventActorCheckpointFork,
|
|
646
|
+
head: EventActorInvocationReference['base']
|
|
647
|
+
): void {
|
|
648
|
+
validateHead(head, invocation.actorThreadId, true);
|
|
649
|
+
if (
|
|
650
|
+
head.generation !== invocation.base.generation + 1 ||
|
|
651
|
+
head.checkpoint?.threadId !== checkpoint.threadId ||
|
|
652
|
+
head.checkpoint.checkpointNs !== checkpoint.checkpointNs ||
|
|
653
|
+
head.checkpoint.checkpointId !== checkpoint.checkpointId
|
|
654
|
+
) {
|
|
655
|
+
throw new Error('Event actor commit returned an invalid logical head');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Runs one event against an isolated checkpoint fork and advances the stable
|
|
661
|
+
* actor head only through the host's atomic commit interface.
|
|
662
|
+
*/
|
|
663
|
+
export class EventActorExecutor<
|
|
664
|
+
TEvent extends EventActorEvent,
|
|
665
|
+
TResult extends EventActorEvent,
|
|
666
|
+
> {
|
|
667
|
+
readonly #adapter: EventActorHostAdapter<TEvent, TResult>;
|
|
668
|
+
readonly #maxDepth: number;
|
|
669
|
+
readonly #dormantCheckpointTtlMs: number;
|
|
670
|
+
readonly #preparationSigningKey: Uint8Array;
|
|
671
|
+
readonly #issuedSettlements = new WeakSet<object>();
|
|
672
|
+
readonly #preparationPhases = new Map<string, EventActorPreparationPhase>();
|
|
673
|
+
#nextPhaseExpiry = Number.POSITIVE_INFINITY;
|
|
674
|
+
|
|
675
|
+
constructor(
|
|
676
|
+
adapter: EventActorHostAdapter<TEvent, TResult>,
|
|
677
|
+
options: EventActorExecutorOptions = {}
|
|
678
|
+
) {
|
|
679
|
+
this.#adapter = adapter;
|
|
680
|
+
this.#maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
681
|
+
this.#dormantCheckpointTtlMs =
|
|
682
|
+
options.dormantCheckpointTtlMs ?? DEFAULT_DORMANT_CHECKPOINT_TTL_MS;
|
|
683
|
+
const signingKey = Buffer.from(
|
|
684
|
+
options.preparationSigningKey ?? randomBytes(32)
|
|
685
|
+
);
|
|
686
|
+
if (signingKey.byteLength < 32) {
|
|
687
|
+
throw new Error('preparationSigningKey must contain at least 32 bytes');
|
|
688
|
+
}
|
|
689
|
+
this.#preparationSigningKey = signingKey;
|
|
690
|
+
if (!Number.isSafeInteger(this.#maxDepth) || this.#maxDepth < 1) {
|
|
691
|
+
throw new Error('maxDepth must be a positive safe integer');
|
|
692
|
+
}
|
|
693
|
+
if (
|
|
694
|
+
!Number.isSafeInteger(this.#dormantCheckpointTtlMs) ||
|
|
695
|
+
this.#dormantCheckpointTtlMs < 1
|
|
696
|
+
) {
|
|
697
|
+
throw new Error('dormantCheckpointTtlMs must be a positive safe integer');
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
#signPreparation(payload: string): string {
|
|
702
|
+
return createHmac('sha256', this.#preparationSigningKey)
|
|
703
|
+
.update(payload)
|
|
704
|
+
.digest('hex');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#preparationSignatureMatches(signature: string, payload: string): boolean {
|
|
708
|
+
if (!/^[a-f0-9]{64}$/.test(signature)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
return timingSafeEqual(
|
|
712
|
+
Buffer.from(signature, 'hex'),
|
|
713
|
+
Buffer.from(this.#signPreparation(payload), 'hex')
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
#createPreparedInvocation(
|
|
718
|
+
invocation: EventActorInvocation<TEvent>
|
|
719
|
+
): EventActorPreparedInvocation<TEvent> {
|
|
720
|
+
const trustedInvocation = freezeInvocation(invocation);
|
|
721
|
+
const payload = serializeInvocationPreparation(trustedInvocation);
|
|
722
|
+
return Object.freeze({
|
|
723
|
+
...trustedInvocation,
|
|
724
|
+
preparationDigest: this.#createTimedPreparationDigest(payload),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#createTimedPreparationDigest(payload: string): string {
|
|
729
|
+
const expiresAt = Math.min(
|
|
730
|
+
Number.MAX_SAFE_INTEGER,
|
|
731
|
+
Date.now() + this.#dormantCheckpointTtlMs
|
|
732
|
+
);
|
|
733
|
+
return `${expiresAt}.${this.#signPreparation(`${expiresAt}\0${payload}`)}`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
#validateTimedPreparationDigest(
|
|
737
|
+
preparationDigest: string,
|
|
738
|
+
payload: string,
|
|
739
|
+
subject: 'prepared invocation' | 'unavailable preparation',
|
|
740
|
+
allowExpired = false
|
|
741
|
+
): number {
|
|
742
|
+
requireNonEmpty(preparationDigest, 'preparationDigest');
|
|
743
|
+
const match = /^(\d+)\.([a-f0-9]{64})$/.exec(preparationDigest);
|
|
744
|
+
const expiresAt = Number(match?.[1]);
|
|
745
|
+
if (
|
|
746
|
+
match == null ||
|
|
747
|
+
!Number.isSafeInteger(expiresAt) ||
|
|
748
|
+
expiresAt < 1 ||
|
|
749
|
+
!this.#preparationSignatureMatches(match[2], `${expiresAt}\0${payload}`)
|
|
750
|
+
) {
|
|
751
|
+
throw new Error(`Event actor ${subject} binding is invalid`);
|
|
752
|
+
}
|
|
753
|
+
if (!allowExpired && expiresAt <= Date.now()) {
|
|
754
|
+
throw new Error(`Event actor ${subject} binding has expired`);
|
|
755
|
+
}
|
|
756
|
+
return expiresAt;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#validatePreparedInvocation(
|
|
760
|
+
invocation: EventActorPreparedInvocation<TEvent>,
|
|
761
|
+
allowExpired = false
|
|
762
|
+
): number {
|
|
763
|
+
return this.#validateTimedPreparationDigest(
|
|
764
|
+
invocation.preparationDigest,
|
|
765
|
+
serializeInvocationPreparation(invocation),
|
|
766
|
+
'prepared invocation',
|
|
767
|
+
allowExpired
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
#prunePreparationPhases(now = Date.now()): void {
|
|
772
|
+
this.#nextPhaseExpiry = Number.POSITIVE_INFINITY;
|
|
773
|
+
for (const [digest, phase] of this.#preparationPhases) {
|
|
774
|
+
if ('expiresAt' in phase && phase.expiresAt <= now) {
|
|
775
|
+
this.#preparationPhases.delete(digest);
|
|
776
|
+
} else if ('expiresAt' in phase) {
|
|
777
|
+
this.#nextPhaseExpiry = Math.min(
|
|
778
|
+
this.#nextPhaseExpiry,
|
|
779
|
+
phase.expiresAt
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
#getPreparationPhase(
|
|
786
|
+
preparationDigest: string
|
|
787
|
+
): EventActorPreparationPhase | undefined {
|
|
788
|
+
if (Date.now() >= this.#nextPhaseExpiry) {
|
|
789
|
+
this.#prunePreparationPhases();
|
|
790
|
+
}
|
|
791
|
+
return this.#preparationPhases.get(preparationDigest);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
#setTerminalPreparationPhase(
|
|
795
|
+
preparationDigest: string,
|
|
796
|
+
status: 'discardable' | 'retained' | 'discarded',
|
|
797
|
+
authorityExpiresAt: number
|
|
798
|
+
): void {
|
|
799
|
+
const now = Date.now();
|
|
800
|
+
if (now >= this.#nextPhaseExpiry) {
|
|
801
|
+
this.#prunePreparationPhases(now);
|
|
802
|
+
}
|
|
803
|
+
const phase = {
|
|
804
|
+
status,
|
|
805
|
+
expiresAt: Math.max(
|
|
806
|
+
authorityExpiresAt,
|
|
807
|
+
Math.min(Number.MAX_SAFE_INTEGER, now + this.#dormantCheckpointTtlMs)
|
|
808
|
+
),
|
|
809
|
+
} as const;
|
|
810
|
+
this.#preparationPhases.set(preparationDigest, phase);
|
|
811
|
+
this.#nextPhaseExpiry = Math.min(this.#nextPhaseExpiry, phase.expiresAt);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async prepare(
|
|
815
|
+
request: EventActorPrepareRequest<TEvent>,
|
|
816
|
+
signal?: AbortSignal
|
|
817
|
+
): Promise<EventActorPreparation<TEvent>> {
|
|
818
|
+
const trustedRequest = snapshotPrepareRequest(request);
|
|
819
|
+
resolveExecutionDepth(
|
|
820
|
+
trustedRequest.depth,
|
|
821
|
+
AsyncLocalStorageProviderSingleton.getRunnableConfig()
|
|
822
|
+
);
|
|
823
|
+
this.#validatePrepareRequest(trustedRequest);
|
|
824
|
+
const checkpointNs = createInvocationCheckpointNs(trustedRequest);
|
|
825
|
+
const adapterRequest: EventActorAdapterPrepareRequest<TEvent> = {
|
|
826
|
+
...snapshotPrepareRequest(trustedRequest),
|
|
827
|
+
checkpointNs,
|
|
828
|
+
};
|
|
829
|
+
const controller = new AbortController();
|
|
830
|
+
const abort = (): void => controller.abort(signal?.reason);
|
|
831
|
+
if (isAborted(signal)) {
|
|
832
|
+
abort();
|
|
833
|
+
} else {
|
|
834
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
835
|
+
}
|
|
836
|
+
if (isAborted(controller.signal)) {
|
|
837
|
+
signal?.removeEventListener('abort', abort);
|
|
838
|
+
throw new EventActorPreparationCancelledError(
|
|
839
|
+
'warm',
|
|
840
|
+
controller.signal.reason
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
let preparation;
|
|
844
|
+
try {
|
|
845
|
+
preparation = await this.#adapter.prepare(
|
|
846
|
+
{ ...adapterRequest },
|
|
847
|
+
{ signal: controller.signal }
|
|
848
|
+
);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (isAborted(controller.signal) && error === controller.signal.reason) {
|
|
851
|
+
throw new EventActorPreparationCancelledError('warm', error);
|
|
852
|
+
}
|
|
853
|
+
throw error;
|
|
854
|
+
} finally {
|
|
855
|
+
signal?.removeEventListener('abort', abort);
|
|
856
|
+
}
|
|
857
|
+
const preparationStatus: unknown = preparation.status;
|
|
858
|
+
if (preparationStatus === 'ready') {
|
|
859
|
+
const readyPreparation = preparation as Extract<
|
|
860
|
+
EventActorAdapterPreparation<TEvent>,
|
|
861
|
+
{ status: 'ready' }
|
|
862
|
+
>;
|
|
863
|
+
const adapterInvocation = snapshotInvocation(readyPreparation.invocation);
|
|
864
|
+
validateInvocation(
|
|
865
|
+
trustedRequest,
|
|
866
|
+
adapterInvocation,
|
|
867
|
+
'warm',
|
|
868
|
+
checkpointNs,
|
|
869
|
+
this.#maxDepth
|
|
870
|
+
);
|
|
871
|
+
const preparedInvocation = this.#createPreparedInvocation({
|
|
872
|
+
...adapterInvocation,
|
|
873
|
+
event: snapshotEvent(trustedRequest.event),
|
|
874
|
+
});
|
|
875
|
+
if (isAborted(controller.signal)) {
|
|
876
|
+
await this.#discardInvocationReference(
|
|
877
|
+
snapshotInvocationReference(preparedInvocation),
|
|
878
|
+
'cancelled'
|
|
879
|
+
);
|
|
880
|
+
throw new EventActorPreparationCancelledError(
|
|
881
|
+
'warm',
|
|
882
|
+
controller.signal.reason
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return Object.freeze({
|
|
886
|
+
status: 'ready',
|
|
887
|
+
invocation: preparedInvocation,
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
if (preparationStatus !== 'checkpoint_unavailable') {
|
|
891
|
+
throw new Error('Event actor preparation returned an invalid status');
|
|
892
|
+
}
|
|
893
|
+
const unavailablePreparation = preparation as Extract<
|
|
894
|
+
EventActorAdapterPreparation<TEvent>,
|
|
895
|
+
{ status: 'checkpoint_unavailable' }
|
|
896
|
+
>;
|
|
897
|
+
const preparedHead = freezeHead(unavailablePreparation.head);
|
|
898
|
+
validateHead(preparedHead, trustedRequest.actorThreadId);
|
|
899
|
+
if (isAborted(controller.signal)) {
|
|
900
|
+
throw new EventActorPreparationCancelledError(
|
|
901
|
+
'warm',
|
|
902
|
+
controller.signal.reason
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
const preparedRequest = freezePrepareRequest(trustedRequest);
|
|
906
|
+
return Object.freeze({
|
|
907
|
+
status: 'checkpoint_unavailable',
|
|
908
|
+
request: preparedRequest,
|
|
909
|
+
head: preparedHead,
|
|
910
|
+
preparationDigest: this.#createTimedPreparationDigest(
|
|
911
|
+
serializeUnavailablePreparation(preparedRequest, preparedHead)
|
|
912
|
+
),
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async coldContinue(
|
|
918
|
+
preparation: Extract<
|
|
919
|
+
EventActorPreparation<TEvent>,
|
|
920
|
+
{ status: 'checkpoint_unavailable' }
|
|
921
|
+
>,
|
|
922
|
+
signal?: AbortSignal
|
|
923
|
+
): Promise<EventActorPreparedInvocation<TEvent>> {
|
|
924
|
+
const request = snapshotPrepareRequest(preparation.request);
|
|
925
|
+
const trustedHead = snapshotHead(preparation.head);
|
|
926
|
+
const preparationDigest = preparation.preparationDigest;
|
|
927
|
+
const authorityExpiresAt = this.#validateTimedPreparationDigest(
|
|
928
|
+
preparationDigest,
|
|
929
|
+
serializeUnavailablePreparation(request, trustedHead),
|
|
930
|
+
'unavailable preparation'
|
|
931
|
+
);
|
|
932
|
+
resolveExecutionDepth(
|
|
933
|
+
request.depth,
|
|
934
|
+
AsyncLocalStorageProviderSingleton.getRunnableConfig()
|
|
935
|
+
);
|
|
936
|
+
this.#validatePrepareRequest(request);
|
|
937
|
+
validateHead(trustedHead, request.actorThreadId);
|
|
938
|
+
const checkpointNs = createInvocationCheckpointNs(request);
|
|
939
|
+
const adapterRequest: EventActorAdapterPrepareRequest<TEvent> = {
|
|
940
|
+
...snapshotPrepareRequest(request),
|
|
941
|
+
checkpointNs,
|
|
942
|
+
};
|
|
943
|
+
const controller = new AbortController();
|
|
944
|
+
const abort = (): void => controller.abort(signal?.reason);
|
|
945
|
+
if (isAborted(signal)) {
|
|
946
|
+
abort();
|
|
947
|
+
} else {
|
|
948
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
949
|
+
}
|
|
950
|
+
if (isAborted(controller.signal)) {
|
|
951
|
+
signal?.removeEventListener('abort', abort);
|
|
952
|
+
throw new EventActorPreparationCancelledError(
|
|
953
|
+
'cold',
|
|
954
|
+
controller.signal.reason
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (this.#getPreparationPhase(preparationDigest) != null) {
|
|
958
|
+
signal?.removeEventListener('abort', abort);
|
|
959
|
+
throw new Error(
|
|
960
|
+
'Event actor unavailable preparation was already consumed'
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
this.#preparationPhases.set(preparationDigest, { status: 'invoking' });
|
|
964
|
+
let invocation;
|
|
965
|
+
try {
|
|
966
|
+
invocation = await this.#adapter.coldContinue(
|
|
967
|
+
{ ...adapterRequest },
|
|
968
|
+
snapshotHead(trustedHead),
|
|
969
|
+
{ signal: controller.signal }
|
|
970
|
+
);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
this.#setTerminalPreparationPhase(
|
|
973
|
+
preparationDigest,
|
|
974
|
+
'discarded',
|
|
975
|
+
authorityExpiresAt
|
|
976
|
+
);
|
|
977
|
+
if (isAborted(controller.signal) && error === controller.signal.reason) {
|
|
978
|
+
throw new EventActorPreparationCancelledError('cold', error);
|
|
979
|
+
}
|
|
980
|
+
throw error;
|
|
981
|
+
} finally {
|
|
982
|
+
signal?.removeEventListener('abort', abort);
|
|
983
|
+
}
|
|
984
|
+
this.#setTerminalPreparationPhase(
|
|
985
|
+
preparationDigest,
|
|
986
|
+
'retained',
|
|
987
|
+
authorityExpiresAt
|
|
988
|
+
);
|
|
989
|
+
const adapterInvocation = snapshotInvocation(invocation);
|
|
990
|
+
validateInvocation(
|
|
991
|
+
request,
|
|
992
|
+
adapterInvocation,
|
|
993
|
+
'cold',
|
|
994
|
+
checkpointNs,
|
|
995
|
+
this.#maxDepth,
|
|
996
|
+
trustedHead
|
|
997
|
+
);
|
|
998
|
+
const trustedInvocation: EventActorInvocation<TEvent> = {
|
|
999
|
+
...adapterInvocation,
|
|
1000
|
+
event: snapshotEvent(request.event),
|
|
1001
|
+
};
|
|
1002
|
+
if (isAborted(controller.signal)) {
|
|
1003
|
+
await this.#discardInvocationReference(
|
|
1004
|
+
snapshotInvocationReference(trustedInvocation),
|
|
1005
|
+
'cancelled'
|
|
1006
|
+
);
|
|
1007
|
+
this.#setTerminalPreparationPhase(
|
|
1008
|
+
preparationDigest,
|
|
1009
|
+
'discarded',
|
|
1010
|
+
authorityExpiresAt
|
|
1011
|
+
);
|
|
1012
|
+
throw new EventActorPreparationCancelledError(
|
|
1013
|
+
'cold',
|
|
1014
|
+
controller.signal.reason
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
const preparedInvocation =
|
|
1018
|
+
this.#createPreparedInvocation(trustedInvocation);
|
|
1019
|
+
this.#setTerminalPreparationPhase(
|
|
1020
|
+
preparationDigest,
|
|
1021
|
+
'discarded',
|
|
1022
|
+
authorityExpiresAt
|
|
1023
|
+
);
|
|
1024
|
+
return preparedInvocation;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async invoke(
|
|
1028
|
+
invocation: EventActorPreparedInvocation<TEvent>,
|
|
1029
|
+
signal?: AbortSignal
|
|
1030
|
+
): Promise<EventActorInvocationResult<TResult>> {
|
|
1031
|
+
const trustedInvocation = snapshotPreparedInvocation(invocation);
|
|
1032
|
+
const authorityExpiresAt =
|
|
1033
|
+
this.#validatePreparedInvocation(trustedInvocation);
|
|
1034
|
+
const preparationDigest = trustedInvocation.preparationDigest;
|
|
1035
|
+
if (this.#getPreparationPhase(preparationDigest) != null) {
|
|
1036
|
+
throw new Error('Event actor prepared invocation was already consumed');
|
|
1037
|
+
}
|
|
1038
|
+
this.#preparationPhases.set(preparationDigest, { status: 'invoking' });
|
|
1039
|
+
const settlementInvocation = snapshotInvocationReference(trustedInvocation);
|
|
1040
|
+
let terminal: EventActorTerminalResult<TResult>;
|
|
1041
|
+
try {
|
|
1042
|
+
terminal = await this.#invokeWithConfig(
|
|
1043
|
+
snapshotInvocation(trustedInvocation),
|
|
1044
|
+
signal,
|
|
1045
|
+
AsyncLocalStorageProviderSingleton.getRunnableConfig()
|
|
1046
|
+
);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (isGraphInterrupt(error) || isParentCommand(error)) {
|
|
1049
|
+
this.#setTerminalPreparationPhase(
|
|
1050
|
+
preparationDigest,
|
|
1051
|
+
'retained',
|
|
1052
|
+
authorityExpiresAt
|
|
1053
|
+
);
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
this.#setTerminalPreparationPhase(
|
|
1057
|
+
preparationDigest,
|
|
1058
|
+
'discardable',
|
|
1059
|
+
authorityExpiresAt
|
|
1060
|
+
);
|
|
1061
|
+
await this.discard(
|
|
1062
|
+
trustedInvocation,
|
|
1063
|
+
isAborted(signal) ? 'cancelled' : 'failed'
|
|
1064
|
+
);
|
|
1065
|
+
throw error;
|
|
1066
|
+
}
|
|
1067
|
+
this.#setTerminalPreparationPhase(
|
|
1068
|
+
preparationDigest,
|
|
1069
|
+
'retained',
|
|
1070
|
+
authorityExpiresAt
|
|
1071
|
+
);
|
|
1072
|
+
let status: unknown;
|
|
1073
|
+
try {
|
|
1074
|
+
status = terminal.status;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
return createIndeterminateResult(settlementInvocation, error);
|
|
1077
|
+
}
|
|
1078
|
+
if (status === 'applied') {
|
|
1079
|
+
const snapshot = snapshotAppliedTerminal<TResult>(
|
|
1080
|
+
settlementInvocation,
|
|
1081
|
+
terminal as Extract<
|
|
1082
|
+
EventActorTerminalResult<TResult>,
|
|
1083
|
+
{ status: 'applied' }
|
|
1084
|
+
>
|
|
1085
|
+
);
|
|
1086
|
+
return snapshot.status === 'snapshot_ready'
|
|
1087
|
+
? this.#issueSettlement(snapshot)
|
|
1088
|
+
: snapshot;
|
|
1089
|
+
}
|
|
1090
|
+
if (status !== 'completed_no_action') {
|
|
1091
|
+
return createIndeterminateResult(
|
|
1092
|
+
settlementInvocation,
|
|
1093
|
+
new Error('Event actor invocation returned an invalid status')
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
let completed: Extract<
|
|
1097
|
+
EventActorTerminalResult<TResult>,
|
|
1098
|
+
{ status: 'completed_no_action' }
|
|
1099
|
+
>;
|
|
1100
|
+
try {
|
|
1101
|
+
completed = Object.freeze({
|
|
1102
|
+
status: 'completed_no_action' as const,
|
|
1103
|
+
...(terminal.result === undefined
|
|
1104
|
+
? {}
|
|
1105
|
+
: { result: snapshotEvent(terminal.result) }),
|
|
1106
|
+
});
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
this.#setTerminalPreparationPhase(
|
|
1109
|
+
preparationDigest,
|
|
1110
|
+
'discardable',
|
|
1111
|
+
authorityExpiresAt
|
|
1112
|
+
);
|
|
1113
|
+
await this.discard(trustedInvocation, 'completed_no_action');
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
this.#setTerminalPreparationPhase(
|
|
1117
|
+
preparationDigest,
|
|
1118
|
+
'discardable',
|
|
1119
|
+
authorityExpiresAt
|
|
1120
|
+
);
|
|
1121
|
+
await this.discard(trustedInvocation, 'completed_no_action');
|
|
1122
|
+
return completed;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
#issueSettlement(
|
|
1126
|
+
snapshot: EventActorAppliedSnapshot<TResult>
|
|
1127
|
+
): EventActorAppliedResult<TResult> {
|
|
1128
|
+
const settlement = Object.freeze({
|
|
1129
|
+
status: 'applied' as const,
|
|
1130
|
+
result: snapshot.result,
|
|
1131
|
+
checkpoint: Object.freeze(snapshotCheckpointFork(snapshot.checkpoint)),
|
|
1132
|
+
invocation: freezeInvocationReference(snapshot.invocation),
|
|
1133
|
+
});
|
|
1134
|
+
this.#issuedSettlements.add(settlement);
|
|
1135
|
+
return settlement;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async #invokeWithConfig(
|
|
1139
|
+
invocation: EventActorInvocation<TEvent>,
|
|
1140
|
+
signal: AbortSignal | undefined,
|
|
1141
|
+
ambientConfig: RunnableConfig | undefined
|
|
1142
|
+
): Promise<EventActorTerminalResult<TResult>> {
|
|
1143
|
+
resolveExecutionDepth(invocation.depth, ambientConfig);
|
|
1144
|
+
validateInvocation(
|
|
1145
|
+
{
|
|
1146
|
+
actorThreadId: invocation.actorThreadId,
|
|
1147
|
+
invocationId: invocation.invocationId,
|
|
1148
|
+
depth: invocation.depth,
|
|
1149
|
+
event: invocation.event,
|
|
1150
|
+
},
|
|
1151
|
+
invocation,
|
|
1152
|
+
invocation.continuation,
|
|
1153
|
+
invocation.fork.checkpointNs,
|
|
1154
|
+
this.#maxDepth
|
|
1155
|
+
);
|
|
1156
|
+
const controller = new AbortController();
|
|
1157
|
+
const abort = (): void => controller.abort(signal?.reason);
|
|
1158
|
+
if (isAborted(signal)) {
|
|
1159
|
+
abort();
|
|
1160
|
+
} else {
|
|
1161
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
1162
|
+
}
|
|
1163
|
+
const config = createRunnableConfig(
|
|
1164
|
+
invocation,
|
|
1165
|
+
controller.signal,
|
|
1166
|
+
ambientConfig
|
|
1167
|
+
);
|
|
1168
|
+
try {
|
|
1169
|
+
if (controller.signal.aborted) {
|
|
1170
|
+
throw asError(controller.signal.reason ?? 'Event actor cancelled');
|
|
1171
|
+
}
|
|
1172
|
+
return await AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
1173
|
+
config,
|
|
1174
|
+
() =>
|
|
1175
|
+
this.#adapter.invoke(invocation, {
|
|
1176
|
+
signal: controller.signal,
|
|
1177
|
+
config,
|
|
1178
|
+
})
|
|
1179
|
+
);
|
|
1180
|
+
} finally {
|
|
1181
|
+
signal?.removeEventListener('abort', abort);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async commit(
|
|
1186
|
+
settlement: EventActorAppliedResult<TResult>
|
|
1187
|
+
): Promise<EventActorSettlementResult<TResult>> {
|
|
1188
|
+
if (!this.#issuedSettlements.has(settlement)) {
|
|
1189
|
+
throw new Error('Event actor settlement was not issued by this executor');
|
|
1190
|
+
}
|
|
1191
|
+
const trustedInvocation = snapshotInvocationReference(
|
|
1192
|
+
settlement.invocation
|
|
1193
|
+
);
|
|
1194
|
+
validateInvocationReference(trustedInvocation, this.#maxDepth);
|
|
1195
|
+
const trustedCheckpoint = snapshotCheckpointFork(settlement.checkpoint);
|
|
1196
|
+
validateTerminalCheckpoint(trustedInvocation, trustedCheckpoint);
|
|
1197
|
+
this.#issuedSettlements.delete(settlement);
|
|
1198
|
+
try {
|
|
1199
|
+
const committed = await this.#adapter.commit({
|
|
1200
|
+
invocation: snapshotInvocationReference(trustedInvocation),
|
|
1201
|
+
expectedHead: snapshotHead(trustedInvocation.base),
|
|
1202
|
+
checkpoint: { ...trustedCheckpoint },
|
|
1203
|
+
result: settlement.result,
|
|
1204
|
+
retention: {
|
|
1205
|
+
committedCheckpoints: 2,
|
|
1206
|
+
dormantCheckpointTtlMs: this.#dormantCheckpointTtlMs,
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
const status: unknown = committed.status;
|
|
1210
|
+
if (status === 'committed') {
|
|
1211
|
+
const committedHead = snapshotHead(
|
|
1212
|
+
(committed as { status: 'committed'; head: EventActorHead }).head
|
|
1213
|
+
);
|
|
1214
|
+
validateCommittedHead(
|
|
1215
|
+
trustedInvocation,
|
|
1216
|
+
trustedCheckpoint,
|
|
1217
|
+
committedHead
|
|
1218
|
+
);
|
|
1219
|
+
return { status: 'committed', head: committedHead };
|
|
1220
|
+
}
|
|
1221
|
+
if (status !== 'stale') {
|
|
1222
|
+
throw new Error('Event actor commit returned an invalid status');
|
|
1223
|
+
}
|
|
1224
|
+
const staleHead = committed.head;
|
|
1225
|
+
if (staleHead != null) {
|
|
1226
|
+
const committedHead = snapshotHead(staleHead);
|
|
1227
|
+
validateHead(committedHead, trustedInvocation.actorThreadId);
|
|
1228
|
+
if (committedHead.generation <= trustedInvocation.base.generation) {
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
'Stale event actor head did not advance past its base'
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
if (
|
|
1234
|
+
trustedInvocation.base.checkpoint != null &&
|
|
1235
|
+
committedHead.checkpoint?.threadId !==
|
|
1236
|
+
trustedInvocation.base.checkpoint.threadId
|
|
1237
|
+
) {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
'Stale event actor head changed its checkpoint thread'
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
if (
|
|
1243
|
+
checkpointIdsMatch(
|
|
1244
|
+
committedHead.checkpoint,
|
|
1245
|
+
trustedInvocation.base.checkpoint
|
|
1246
|
+
) ||
|
|
1247
|
+
checkpointIdsMatch(
|
|
1248
|
+
committedHead.checkpoint,
|
|
1249
|
+
trustedInvocation.fork
|
|
1250
|
+
) ||
|
|
1251
|
+
checkpointsMatch(committedHead.checkpoint, trustedCheckpoint)
|
|
1252
|
+
) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
'Stale event actor head does not identify a competing checkpoint'
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
return { status: 'stale', head: committedHead };
|
|
1258
|
+
}
|
|
1259
|
+
return { status: 'stale' };
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
return createSettlementIndeterminateResult(settlement, error);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
async discard(
|
|
1266
|
+
invocation: EventActorPreparedInvocation<TEvent>,
|
|
1267
|
+
reason: EventActorDiscardReason
|
|
1268
|
+
): Promise<void> {
|
|
1269
|
+
const discardReason: unknown = reason;
|
|
1270
|
+
if (
|
|
1271
|
+
discardReason !== 'cancelled' &&
|
|
1272
|
+
discardReason !== 'completed_no_action' &&
|
|
1273
|
+
discardReason !== 'failed'
|
|
1274
|
+
) {
|
|
1275
|
+
throw new Error('Event actor discard reason is invalid');
|
|
1276
|
+
}
|
|
1277
|
+
const trustedInvocation = snapshotPreparedInvocation(invocation);
|
|
1278
|
+
const expiresAt = this.#validatePreparedInvocation(trustedInvocation, true);
|
|
1279
|
+
const preparationDigest = trustedInvocation.preparationDigest;
|
|
1280
|
+
const previousPhase = this.#getPreparationPhase(preparationDigest);
|
|
1281
|
+
if (expiresAt <= Date.now() && previousPhase == null) {
|
|
1282
|
+
throw new Error('Event actor prepared invocation binding has expired');
|
|
1283
|
+
}
|
|
1284
|
+
if (previousPhase != null && previousPhase.status !== 'discardable') {
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
'Event actor prepared invocation is no longer discardable'
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
this.#preparationPhases.set(preparationDigest, { status: 'discarding' });
|
|
1290
|
+
try {
|
|
1291
|
+
await this.#discardInvocationReference(trustedInvocation, reason);
|
|
1292
|
+
this.#setTerminalPreparationPhase(
|
|
1293
|
+
preparationDigest,
|
|
1294
|
+
'discarded',
|
|
1295
|
+
expiresAt
|
|
1296
|
+
);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
if (previousPhase == null) {
|
|
1299
|
+
this.#preparationPhases.delete(preparationDigest);
|
|
1300
|
+
} else {
|
|
1301
|
+
this.#preparationPhases.set(preparationDigest, previousPhase);
|
|
1302
|
+
if ('expiresAt' in previousPhase) {
|
|
1303
|
+
this.#nextPhaseExpiry = Math.min(
|
|
1304
|
+
this.#nextPhaseExpiry,
|
|
1305
|
+
previousPhase.expiresAt
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
throw error;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
#discardInvocationReference(
|
|
1314
|
+
invocation: EventActorInvocationReference,
|
|
1315
|
+
reason: EventActorDiscardReason
|
|
1316
|
+
): Promise<void> {
|
|
1317
|
+
const trustedInvocation = snapshotInvocationReference(invocation);
|
|
1318
|
+
validateInvocationReference(trustedInvocation, this.#maxDepth);
|
|
1319
|
+
return this.#adapter.discard({
|
|
1320
|
+
invocation: trustedInvocation,
|
|
1321
|
+
reason,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
#validatePrepareRequest(request: EventActorPrepareRequest<TEvent>): void {
|
|
1326
|
+
requireNonEmpty(request.actorThreadId, 'actorThreadId');
|
|
1327
|
+
requireNonEmpty(request.invocationId, 'invocationId');
|
|
1328
|
+
if (
|
|
1329
|
+
!Number.isSafeInteger(request.depth) ||
|
|
1330
|
+
request.depth < 1 ||
|
|
1331
|
+
request.depth > this.#maxDepth
|
|
1332
|
+
) {
|
|
1333
|
+
throw new Error(
|
|
1334
|
+
`Event actor depth ${request.depth} exceeds maximum ${this.#maxDepth}`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async execute(
|
|
1340
|
+
request: EventActorExecutionRequest<TEvent>
|
|
1341
|
+
): Promise<EventActorExecutionResult<TResult>> {
|
|
1342
|
+
const trustedRequest: EventActorExecutionRequest<TEvent> = {
|
|
1343
|
+
actorThreadId: request.actorThreadId,
|
|
1344
|
+
invocationId: request.invocationId,
|
|
1345
|
+
event: snapshotEvent(request.event),
|
|
1346
|
+
...(request.depth == null ? {} : { depth: request.depth }),
|
|
1347
|
+
...(request.signal == null ? {} : { signal: request.signal }),
|
|
1348
|
+
};
|
|
1349
|
+
const ambientConfig = snapshotAmbientConfig(
|
|
1350
|
+
AsyncLocalStorageProviderSingleton.getRunnableConfig()
|
|
1351
|
+
);
|
|
1352
|
+
const depth = resolveExecutionDepth(trustedRequest.depth, ambientConfig);
|
|
1353
|
+
const prepareRequest: EventActorPrepareRequest<TEvent> = {
|
|
1354
|
+
actorThreadId: trustedRequest.actorThreadId,
|
|
1355
|
+
invocationId: trustedRequest.invocationId,
|
|
1356
|
+
depth,
|
|
1357
|
+
event: trustedRequest.event,
|
|
1358
|
+
};
|
|
1359
|
+
let preparation;
|
|
1360
|
+
try {
|
|
1361
|
+
preparation = await this.prepare(prepareRequest, trustedRequest.signal);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (error instanceof EventActorPreparationCancelledError) {
|
|
1364
|
+
return { status: 'cancelled', continuation: error.continuation };
|
|
1365
|
+
}
|
|
1366
|
+
throw error;
|
|
1367
|
+
}
|
|
1368
|
+
if (
|
|
1369
|
+
preparation.status === 'checkpoint_unavailable' &&
|
|
1370
|
+
isAborted(trustedRequest.signal)
|
|
1371
|
+
) {
|
|
1372
|
+
return { status: 'cancelled', continuation: 'cold' };
|
|
1373
|
+
}
|
|
1374
|
+
let invocation;
|
|
1375
|
+
try {
|
|
1376
|
+
invocation =
|
|
1377
|
+
preparation.status === 'ready'
|
|
1378
|
+
? preparation.invocation
|
|
1379
|
+
: await this.coldContinue(preparation, trustedRequest.signal);
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
if (error instanceof EventActorPreparationCancelledError) {
|
|
1382
|
+
return { status: 'cancelled', continuation: 'cold' };
|
|
1383
|
+
}
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
const continuation = preparation.status === 'ready' ? 'warm' : 'cold';
|
|
1387
|
+
const invocationReference = snapshotInvocationReference(invocation);
|
|
1388
|
+
const invocationForAdapter = snapshotInvocation(invocation);
|
|
1389
|
+
if (isAborted(trustedRequest.signal)) {
|
|
1390
|
+
await this.#discardInvocationReference(invocationReference, 'cancelled');
|
|
1391
|
+
return { status: 'cancelled', continuation };
|
|
1392
|
+
}
|
|
1393
|
+
let terminal;
|
|
1394
|
+
try {
|
|
1395
|
+
terminal = await this.#invokeWithConfig(
|
|
1396
|
+
invocationForAdapter,
|
|
1397
|
+
trustedRequest.signal,
|
|
1398
|
+
ambientConfig
|
|
1399
|
+
);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
if (isGraphInterrupt(error) || isParentCommand(error)) {
|
|
1402
|
+
throw error;
|
|
1403
|
+
}
|
|
1404
|
+
const reason = isAborted(trustedRequest.signal) ? 'cancelled' : 'failed';
|
|
1405
|
+
await this.#discardInvocationReference(invocationReference, reason);
|
|
1406
|
+
if (reason === 'cancelled') {
|
|
1407
|
+
return { status: 'cancelled', continuation };
|
|
1408
|
+
}
|
|
1409
|
+
return { status: 'failed', error: asError(error), continuation };
|
|
1410
|
+
}
|
|
1411
|
+
let terminalStatus: unknown;
|
|
1412
|
+
try {
|
|
1413
|
+
terminalStatus = terminal.status;
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
return {
|
|
1416
|
+
...createIndeterminateResult(invocationReference, error),
|
|
1417
|
+
continuation,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
if (terminalStatus === 'completed_no_action') {
|
|
1421
|
+
let result: TResult | undefined;
|
|
1422
|
+
try {
|
|
1423
|
+
result =
|
|
1424
|
+
terminal.result === undefined
|
|
1425
|
+
? undefined
|
|
1426
|
+
: snapshotEvent(terminal.result);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
await this.#discardInvocationReference(
|
|
1429
|
+
invocationReference,
|
|
1430
|
+
'completed_no_action'
|
|
1431
|
+
);
|
|
1432
|
+
return { status: 'failed', error: asError(error), continuation };
|
|
1433
|
+
}
|
|
1434
|
+
await this.#discardInvocationReference(
|
|
1435
|
+
invocationReference,
|
|
1436
|
+
'completed_no_action'
|
|
1437
|
+
);
|
|
1438
|
+
return {
|
|
1439
|
+
status: 'completed_no_action',
|
|
1440
|
+
...(result === undefined ? {} : { result }),
|
|
1441
|
+
continuation,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
if (terminalStatus !== 'applied') {
|
|
1445
|
+
return {
|
|
1446
|
+
...createIndeterminateResult(
|
|
1447
|
+
invocationReference,
|
|
1448
|
+
new Error('Event actor invocation returned an invalid status')
|
|
1449
|
+
),
|
|
1450
|
+
continuation,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const appliedSnapshot = snapshotAppliedTerminal<TResult>(
|
|
1454
|
+
invocationReference,
|
|
1455
|
+
terminal as Extract<
|
|
1456
|
+
EventActorTerminalResult<TResult>,
|
|
1457
|
+
{ status: 'applied' }
|
|
1458
|
+
>
|
|
1459
|
+
);
|
|
1460
|
+
if (appliedSnapshot.status === 'commit_indeterminate') {
|
|
1461
|
+
return { ...appliedSnapshot, continuation };
|
|
1462
|
+
}
|
|
1463
|
+
const trustedTerminal = this.#issueSettlement(appliedSnapshot);
|
|
1464
|
+
const committed = await this.commit(trustedTerminal);
|
|
1465
|
+
if (committed.status === 'commit_indeterminate') {
|
|
1466
|
+
return {
|
|
1467
|
+
...committed,
|
|
1468
|
+
continuation,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
if (committed.status === 'stale') {
|
|
1472
|
+
return {
|
|
1473
|
+
status: 'commit_conflict',
|
|
1474
|
+
result: trustedTerminal.result,
|
|
1475
|
+
checkpoint: { ...trustedTerminal.checkpoint },
|
|
1476
|
+
...(committed.head == null
|
|
1477
|
+
? {}
|
|
1478
|
+
: { head: snapshotHead(committed.head) }),
|
|
1479
|
+
continuation,
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
status: 'applied',
|
|
1484
|
+
result: trustedTerminal.result,
|
|
1485
|
+
head: committed.head,
|
|
1486
|
+
continuation,
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
export function createEventActorExecutor<
|
|
1492
|
+
TEvent extends EventActorEvent,
|
|
1493
|
+
TResult extends EventActorEvent,
|
|
1494
|
+
>(
|
|
1495
|
+
adapter: EventActorHostAdapter<TEvent, TResult>,
|
|
1496
|
+
options: EventActorExecutorOptions = {}
|
|
1497
|
+
): EventActorExecutor<TEvent, TResult> {
|
|
1498
|
+
return new EventActorExecutor(adapter, options);
|
|
1499
|
+
}
|