deepline 0.1.19 → 0.1.21
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/cli/index.js +391 -129
- package/dist/cli/index.mjs +391 -129
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +53 -7
- package/dist/index.mjs +53 -7
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +999 -257
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +604 -75
- package/dist/repo/apps/play-runner-workers/src/entry.ts +442 -357
- package/dist/repo/sdk/src/client.ts +46 -4
- package/dist/repo/sdk/src/http.ts +38 -4
- package/dist/repo/sdk/src/plays/harness-stub.ts +12 -0
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +3 -6
- package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
- package/package.json +1 -1
|
@@ -48,17 +48,26 @@ type AwaitRequest = {
|
|
|
48
48
|
timeoutMs: number;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
type WorkflowPoolEntryState = 'warming' | 'ready';
|
|
52
|
+
|
|
51
53
|
type WorkflowPoolEntry = {
|
|
52
54
|
id: string;
|
|
53
55
|
version: string;
|
|
56
|
+
state: WorkflowPoolEntryState;
|
|
54
57
|
createdAt: number;
|
|
55
58
|
readyAt: number | null;
|
|
56
59
|
expiresAt: number;
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
type WorkflowRunMappingState = 'claimed' | 'started' | 'blocked';
|
|
63
|
+
|
|
59
64
|
type WorkflowRunMapping = {
|
|
60
65
|
runId: string;
|
|
61
66
|
instanceId: string;
|
|
67
|
+
state: WorkflowRunMappingState;
|
|
68
|
+
blockedInstanceId?: string | null;
|
|
69
|
+
claimedAt?: number | null;
|
|
70
|
+
startedAt?: number | null;
|
|
62
71
|
version: string;
|
|
63
72
|
createdAt: number;
|
|
64
73
|
expiresAt: number;
|
|
@@ -74,19 +83,83 @@ type CoordinatorTraceEntry = {
|
|
|
74
83
|
[key: string]: unknown;
|
|
75
84
|
};
|
|
76
85
|
|
|
86
|
+
type CoordinatorTerminalState = {
|
|
87
|
+
runId: string;
|
|
88
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
89
|
+
result?: unknown;
|
|
90
|
+
error?: string | null;
|
|
91
|
+
totalRows?: unknown;
|
|
92
|
+
durationMs?: unknown;
|
|
93
|
+
playName?: string | null;
|
|
94
|
+
completedAt: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type CoordinatorRunEvent =
|
|
98
|
+
| {
|
|
99
|
+
seq: number;
|
|
100
|
+
runId: string;
|
|
101
|
+
type: 'status';
|
|
102
|
+
status: string;
|
|
103
|
+
ts: number;
|
|
104
|
+
logs?: string[];
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
seq: number;
|
|
108
|
+
runId: string;
|
|
109
|
+
type: 'log';
|
|
110
|
+
line: string;
|
|
111
|
+
ts: number;
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
seq: number;
|
|
115
|
+
runId: string;
|
|
116
|
+
type: 'progress';
|
|
117
|
+
status: string;
|
|
118
|
+
ts: number;
|
|
119
|
+
logs?: string[];
|
|
120
|
+
activeNodeId?: string | null;
|
|
121
|
+
activeArtifactTableNamespace?: string | null;
|
|
122
|
+
updatedAt?: number | null;
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
seq: number;
|
|
126
|
+
runId: string;
|
|
127
|
+
type: 'terminal';
|
|
128
|
+
status: 'completed' | 'failed' | 'cancelled';
|
|
129
|
+
ts: number;
|
|
130
|
+
result?: unknown;
|
|
131
|
+
error?: string | null;
|
|
132
|
+
totalRows?: unknown;
|
|
133
|
+
durationMs?: unknown;
|
|
134
|
+
playName?: string | null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type OmitRunEventSequence<T> = T extends unknown ? Omit<T, 'seq'> : never;
|
|
138
|
+
type CoordinatorRunEventInput = OmitRunEventSequence<CoordinatorRunEvent>;
|
|
139
|
+
|
|
77
140
|
type ReadyWorkflowPoolEntryRecord = {
|
|
78
141
|
key: string;
|
|
79
|
-
entry: WorkflowPoolEntry & { readyAt: number };
|
|
142
|
+
entry: WorkflowPoolEntry & { state: 'ready'; readyAt: number };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type WorkflowPoolCounts = {
|
|
146
|
+
available: number;
|
|
147
|
+
warming: number;
|
|
80
148
|
};
|
|
81
149
|
|
|
82
150
|
const DEDUP_KEY_PREFIX = 'd:';
|
|
83
151
|
const WORKFLOW_POOL_KEY_PREFIX = 'p:';
|
|
84
152
|
const WORKFLOW_POOL_RUN_KEY_PREFIX = 'm:';
|
|
85
153
|
const COORDINATOR_TRACE_KEY_PREFIX = 't:';
|
|
154
|
+
const COORDINATOR_RUN_EVENT_KEY_PREFIX = 'e:';
|
|
155
|
+
const COORDINATOR_TERMINAL_KEY = 'terminal';
|
|
156
|
+
const COORDINATOR_RUN_EVENT_SEQUENCE_KEY = 'event-seq';
|
|
86
157
|
const COORDINATOR_TRACE_MAX_ENTRIES = 200;
|
|
158
|
+
const COORDINATOR_RUN_EVENT_MAX_ENTRIES = 500;
|
|
87
159
|
const FINISH_ALARM_DELAY_MS = 60_000; // self-evict 1 min after finish() called
|
|
88
160
|
const WORKFLOW_POOL_DEFAULT_TTL_MS = 8 * 60 * 1000;
|
|
89
161
|
const WORKFLOW_POOL_RUN_MAPPING_TTL_MS = 60 * 60 * 1000;
|
|
162
|
+
const WORKFLOW_POOL_READY_MAX_AGE_MS = 7 * 60_000;
|
|
90
163
|
|
|
91
164
|
interface DedupEnv {
|
|
92
165
|
PLAY_DEDUP: DurableObjectNamespace;
|
|
@@ -96,6 +169,8 @@ export class PlayDedup implements DurableObject {
|
|
|
96
169
|
// Promises waiting on /await endpoints. Keyed by `${toolId}:${inputHash}`.
|
|
97
170
|
// Resolved by /publish; rejected on timeout.
|
|
98
171
|
private waiters: Map<string, Set<(value: unknown) => void>> = new Map();
|
|
172
|
+
private runEventWaiters: Set<(value: CoordinatorRunEvent) => void> =
|
|
173
|
+
new Set();
|
|
99
174
|
|
|
100
175
|
constructor(
|
|
101
176
|
private readonly state: DurableObjectState,
|
|
@@ -110,6 +185,10 @@ export class PlayDedup implements DurableObject {
|
|
|
110
185
|
return `${COORDINATOR_TRACE_KEY_PREFIX}${String(entry.ts).padStart(13, '0')}:${crypto.randomUUID().slice(0, 8)}`;
|
|
111
186
|
}
|
|
112
187
|
|
|
188
|
+
private runEventKey(event: CoordinatorRunEvent): string {
|
|
189
|
+
return `${COORDINATOR_RUN_EVENT_KEY_PREFIX}${String(event.seq).padStart(13, '0')}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
113
192
|
private waiterKey(toolId: string, inputHash: string): string {
|
|
114
193
|
return `${toolId}:${inputHash}`;
|
|
115
194
|
}
|
|
@@ -130,18 +209,22 @@ export class PlayDedup implements DurableObject {
|
|
|
130
209
|
return await this.handleDebug(req);
|
|
131
210
|
case '/pool-add':
|
|
132
211
|
return await this.handlePoolAdd(req);
|
|
133
|
-
case '/pool-
|
|
134
|
-
return await this.
|
|
212
|
+
case '/pool-claim':
|
|
213
|
+
return await this.handlePoolClaim(req);
|
|
135
214
|
case '/pool-count':
|
|
136
215
|
return await this.handlePoolCount(req);
|
|
137
216
|
case '/pool-list':
|
|
138
217
|
return await this.handlePoolList(req);
|
|
139
218
|
case '/pool-promote':
|
|
140
219
|
return await this.handlePoolPromote(req);
|
|
220
|
+
case '/pool-ready':
|
|
221
|
+
return await this.handlePoolReady(req);
|
|
141
222
|
case '/pool-delete':
|
|
142
223
|
return await this.handlePoolDelete(req);
|
|
143
224
|
case '/pool-map-run':
|
|
144
225
|
return await this.handlePoolMapRun(req);
|
|
226
|
+
case '/pool-block-run':
|
|
227
|
+
return await this.handlePoolBlockRun(req);
|
|
145
228
|
case '/pool-resolve-run':
|
|
146
229
|
return await this.handlePoolResolveRun(req);
|
|
147
230
|
case '/pool-clear':
|
|
@@ -150,6 +233,14 @@ export class PlayDedup implements DurableObject {
|
|
|
150
233
|
return await this.handleTraceAdd(req);
|
|
151
234
|
case '/trace-list':
|
|
152
235
|
return await this.handleTraceList();
|
|
236
|
+
case '/event-add':
|
|
237
|
+
return await this.handleRunEventAdd(req);
|
|
238
|
+
case '/event-list':
|
|
239
|
+
return await this.handleRunEventList(req);
|
|
240
|
+
case '/terminal-set':
|
|
241
|
+
return await this.handleTerminalSet(req);
|
|
242
|
+
case '/terminal-get':
|
|
243
|
+
return await this.handleTerminalGet();
|
|
153
244
|
default:
|
|
154
245
|
return new Response('not found', { status: 404 });
|
|
155
246
|
}
|
|
@@ -167,6 +258,7 @@ export class PlayDedup implements DurableObject {
|
|
|
167
258
|
// be garbage-collected by CF's hot pool.
|
|
168
259
|
await this.state.storage.deleteAll();
|
|
169
260
|
this.waiters.clear();
|
|
261
|
+
this.runEventWaiters.clear();
|
|
170
262
|
}
|
|
171
263
|
|
|
172
264
|
private async handleLookupOrClaim(req: Request): Promise<Response> {
|
|
@@ -248,7 +340,9 @@ export class PlayDedup implements DurableObject {
|
|
|
248
340
|
}
|
|
249
341
|
}
|
|
250
342
|
|
|
251
|
-
return new Response('{}', {
|
|
343
|
+
return new Response('{}', {
|
|
344
|
+
headers: { 'content-type': 'application/json' },
|
|
345
|
+
});
|
|
252
346
|
}
|
|
253
347
|
|
|
254
348
|
private async handleAwait(req: Request): Promise<Response> {
|
|
@@ -313,7 +407,9 @@ export class PlayDedup implements DurableObject {
|
|
|
313
407
|
// Schedule alarm to evict after a grace period (any straggler awaits
|
|
314
408
|
// get a chance to resolve). Grace = 60s.
|
|
315
409
|
await this.state.storage.setAlarm(Date.now() + FINISH_ALARM_DELAY_MS);
|
|
316
|
-
return new Response('{}', {
|
|
410
|
+
return new Response('{}', {
|
|
411
|
+
headers: { 'content-type': 'application/json' },
|
|
412
|
+
});
|
|
317
413
|
}
|
|
318
414
|
|
|
319
415
|
private async handleDebug(_req: Request): Promise<Response> {
|
|
@@ -321,7 +417,11 @@ export class PlayDedup implements DurableObject {
|
|
|
321
417
|
const dump: Record<string, DedupEntry> = {};
|
|
322
418
|
for (const [k, v] of all) dump[k] = v as DedupEntry;
|
|
323
419
|
return new Response(
|
|
324
|
-
JSON.stringify({
|
|
420
|
+
JSON.stringify({
|
|
421
|
+
size: all.size,
|
|
422
|
+
entries: dump,
|
|
423
|
+
waiters: this.waiters.size,
|
|
424
|
+
}),
|
|
325
425
|
{ headers: { 'content-type': 'application/json' } },
|
|
326
426
|
);
|
|
327
427
|
}
|
|
@@ -330,16 +430,28 @@ export class PlayDedup implements DurableObject {
|
|
|
330
430
|
return new URL(req.url).searchParams.get('version')?.trim() ?? '';
|
|
331
431
|
}
|
|
332
432
|
|
|
433
|
+
private workflowPoolMinReadyAgeMs(req: Request): number {
|
|
434
|
+
const raw = Number(new URL(req.url).searchParams.get('minReadyAgeMs') ?? 0);
|
|
435
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
return Math.min(Math.floor(raw), WORKFLOW_POOL_DEFAULT_TTL_MS);
|
|
439
|
+
}
|
|
440
|
+
|
|
333
441
|
private isReadyWorkflowPoolEntry(
|
|
334
442
|
value: { key: string; entry: WorkflowPoolEntry | undefined },
|
|
335
443
|
version: string,
|
|
336
444
|
now: number,
|
|
445
|
+
minReadyAgeMs = 0,
|
|
337
446
|
): value is ReadyWorkflowPoolEntryRecord {
|
|
338
447
|
return (
|
|
339
448
|
value.entry !== undefined &&
|
|
340
449
|
value.entry.version === version &&
|
|
341
450
|
value.entry.expiresAt > now &&
|
|
342
|
-
value.entry.
|
|
451
|
+
value.entry.state === 'ready' &&
|
|
452
|
+
value.entry.readyAt !== null &&
|
|
453
|
+
now - value.entry.readyAt <= WORKFLOW_POOL_READY_MAX_AGE_MS &&
|
|
454
|
+
now - value.entry.readyAt >= minReadyAgeMs
|
|
343
455
|
);
|
|
344
456
|
}
|
|
345
457
|
|
|
@@ -360,6 +472,9 @@ export class PlayDedup implements DurableObject {
|
|
|
360
472
|
if (
|
|
361
473
|
!entry ||
|
|
362
474
|
entry.expiresAt <= now ||
|
|
475
|
+
(entry.state === 'ready' &&
|
|
476
|
+
entry.readyAt !== null &&
|
|
477
|
+
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS) ||
|
|
363
478
|
(version && entry.version !== version)
|
|
364
479
|
) {
|
|
365
480
|
expiredKeys.push(key);
|
|
@@ -380,16 +495,15 @@ export class PlayDedup implements DurableObject {
|
|
|
380
495
|
}
|
|
381
496
|
|
|
382
497
|
private async handlePoolAdd(req: Request): Promise<Response> {
|
|
383
|
-
const body = (await req.json().catch(() => null)) as
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
498
|
+
const body = (await req.json().catch(() => null)) as {
|
|
499
|
+
ids?: unknown;
|
|
500
|
+
ttlMs?: unknown;
|
|
501
|
+
version?: unknown;
|
|
502
|
+
readyAt?: unknown;
|
|
503
|
+
ready?: unknown;
|
|
504
|
+
} | null;
|
|
505
|
+
const version =
|
|
506
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
393
507
|
if (!version) {
|
|
394
508
|
return new Response('version is required', { status: 400 });
|
|
395
509
|
}
|
|
@@ -409,18 +523,34 @@ export class PlayDedup implements DurableObject {
|
|
|
409
523
|
body.ttlMs > 0
|
|
410
524
|
? Math.min(body.ttlMs, WORKFLOW_POOL_DEFAULT_TTL_MS)
|
|
411
525
|
: WORKFLOW_POOL_DEFAULT_TTL_MS;
|
|
412
|
-
const writes: Record<string, WorkflowPoolEntry> = {};
|
|
413
|
-
for (const id of ids) {
|
|
414
|
-
writes[`${WORKFLOW_POOL_KEY_PREFIX}${id}`] = {
|
|
415
|
-
id,
|
|
416
|
-
version,
|
|
417
|
-
createdAt: now,
|
|
418
|
-
readyAt: ready ? readyAt : null,
|
|
419
|
-
expiresAt: now + ttlMs,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
526
|
await this.state.blockConcurrencyWhile(async () => {
|
|
423
527
|
await this.gcWorkflowPool(now, version);
|
|
528
|
+
const writes: Record<string, WorkflowPoolEntry> = {};
|
|
529
|
+
const keys = ids.map((id) => `${WORKFLOW_POOL_KEY_PREFIX}${id}`);
|
|
530
|
+
const existing = (await this.state.storage.get<WorkflowPoolEntry>(
|
|
531
|
+
keys,
|
|
532
|
+
)) as Map<string, WorkflowPoolEntry>;
|
|
533
|
+
for (const id of ids) {
|
|
534
|
+
const key = `${WORKFLOW_POOL_KEY_PREFIX}${id}`;
|
|
535
|
+
const existingEntry = existing.get(key);
|
|
536
|
+
const existingReadyAt =
|
|
537
|
+
existingEntry?.version === version &&
|
|
538
|
+
existingEntry.expiresAt > now &&
|
|
539
|
+
existingEntry.state === 'ready' &&
|
|
540
|
+
existingEntry.readyAt !== null
|
|
541
|
+
? existingEntry.readyAt
|
|
542
|
+
: null;
|
|
543
|
+
const nextReadyAt = ready ? readyAt : existingReadyAt;
|
|
544
|
+
writes[key] = {
|
|
545
|
+
id,
|
|
546
|
+
version,
|
|
547
|
+
state: nextReadyAt !== null ? 'ready' : 'warming',
|
|
548
|
+
createdAt:
|
|
549
|
+
existingEntry?.version === version ? existingEntry.createdAt : now,
|
|
550
|
+
readyAt: nextReadyAt,
|
|
551
|
+
expiresAt: now + ttlMs,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
424
554
|
if (Object.keys(writes).length > 0) {
|
|
425
555
|
await this.state.storage.put(writes);
|
|
426
556
|
}
|
|
@@ -430,28 +560,72 @@ export class PlayDedup implements DurableObject {
|
|
|
430
560
|
});
|
|
431
561
|
}
|
|
432
562
|
|
|
433
|
-
private async
|
|
563
|
+
private async handlePoolClaim(req: Request): Promise<Response> {
|
|
434
564
|
const version = this.workflowPoolVersion(req);
|
|
435
565
|
if (!version) {
|
|
436
566
|
return new Response('version is required', { status: 400 });
|
|
437
567
|
}
|
|
438
|
-
|
|
568
|
+
const body = (await req.json().catch(() => null)) as {
|
|
569
|
+
runId?: unknown;
|
|
570
|
+
} | null;
|
|
571
|
+
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
572
|
+
if (!runId) {
|
|
573
|
+
return new Response('runId is required', { status: 400 });
|
|
574
|
+
}
|
|
575
|
+
const minReadyAgeMs = this.workflowPoolMinReadyAgeMs(req);
|
|
576
|
+
let claimedId: string | null = null;
|
|
577
|
+
let counts: WorkflowPoolCounts = { available: 0, warming: 0 };
|
|
439
578
|
await this.state.blockConcurrencyWhile(async () => {
|
|
440
579
|
const now = Date.now();
|
|
441
580
|
await this.gcWorkflowPool(now, version);
|
|
581
|
+
const mappingKey = `${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`;
|
|
582
|
+
const existingMapping =
|
|
583
|
+
await this.state.storage.get<WorkflowRunMapping>(mappingKey);
|
|
584
|
+
if (existingMapping?.version === version) {
|
|
585
|
+
if (
|
|
586
|
+
existingMapping.state === 'claimed' ||
|
|
587
|
+
existingMapping.state === 'started'
|
|
588
|
+
) {
|
|
589
|
+
claimedId = existingMapping.instanceId || null;
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
442
593
|
const entries = await this.state.storage.list<WorkflowPoolEntry>({
|
|
443
594
|
prefix: WORKFLOW_POOL_KEY_PREFIX,
|
|
444
595
|
});
|
|
596
|
+
counts = this.countWorkflowPoolEntries(
|
|
597
|
+
entries,
|
|
598
|
+
version,
|
|
599
|
+
now,
|
|
600
|
+
minReadyAgeMs,
|
|
601
|
+
);
|
|
445
602
|
const sorted = [...entries.entries()]
|
|
446
603
|
.map(([key, entry]) => ({ key, entry }))
|
|
447
|
-
.filter((entry) =>
|
|
448
|
-
|
|
604
|
+
.filter((entry) =>
|
|
605
|
+
this.isReadyWorkflowPoolEntry(entry, version, now, minReadyAgeMs),
|
|
606
|
+
)
|
|
607
|
+
.sort((a, b) => b.entry.readyAt - a.entry.readyAt);
|
|
449
608
|
const selected = sorted[0];
|
|
450
609
|
if (!selected) return;
|
|
451
|
-
|
|
610
|
+
claimedId = selected.entry.id;
|
|
452
611
|
await this.state.storage.delete(selected.key);
|
|
612
|
+
await this.state.storage.put(mappingKey, {
|
|
613
|
+
runId,
|
|
614
|
+
instanceId: claimedId,
|
|
615
|
+
state: 'claimed',
|
|
616
|
+
blockedInstanceId: null,
|
|
617
|
+
claimedAt: now,
|
|
618
|
+
startedAt: null,
|
|
619
|
+
version,
|
|
620
|
+
createdAt: now,
|
|
621
|
+
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
622
|
+
} satisfies WorkflowRunMapping);
|
|
623
|
+
counts = {
|
|
624
|
+
available: Math.max(0, counts.available - 1),
|
|
625
|
+
warming: counts.warming,
|
|
626
|
+
};
|
|
453
627
|
});
|
|
454
|
-
return new Response(JSON.stringify({ id:
|
|
628
|
+
return new Response(JSON.stringify({ id: claimedId, ...counts }), {
|
|
455
629
|
headers: { 'content-type': 'application/json' },
|
|
456
630
|
});
|
|
457
631
|
}
|
|
@@ -461,20 +635,45 @@ export class PlayDedup implements DurableObject {
|
|
|
461
635
|
if (!version) {
|
|
462
636
|
return new Response('version is required', { status: 400 });
|
|
463
637
|
}
|
|
464
|
-
|
|
638
|
+
const minReadyAgeMs = this.workflowPoolMinReadyAgeMs(req);
|
|
639
|
+
const now = Date.now();
|
|
640
|
+
await this.gcWorkflowPool(now, version);
|
|
465
641
|
const entries = await this.state.storage.list<WorkflowPoolEntry>({
|
|
466
642
|
prefix: WORKFLOW_POOL_KEY_PREFIX,
|
|
467
643
|
});
|
|
644
|
+
const counts = this.countWorkflowPoolEntries(
|
|
645
|
+
entries,
|
|
646
|
+
version,
|
|
647
|
+
now,
|
|
648
|
+
minReadyAgeMs,
|
|
649
|
+
);
|
|
650
|
+
return new Response(JSON.stringify(counts), {
|
|
651
|
+
headers: { 'content-type': 'application/json' },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private countWorkflowPoolEntries(
|
|
656
|
+
entries: Map<string, WorkflowPoolEntry>,
|
|
657
|
+
version: string,
|
|
658
|
+
now: number,
|
|
659
|
+
minReadyAgeMs: number,
|
|
660
|
+
): WorkflowPoolCounts {
|
|
468
661
|
let available = 0;
|
|
469
662
|
let warming = 0;
|
|
470
663
|
for (const entry of entries.values()) {
|
|
471
664
|
if (entry.version !== version) continue;
|
|
472
|
-
if (
|
|
473
|
-
|
|
665
|
+
if (
|
|
666
|
+
entry.state !== 'ready' ||
|
|
667
|
+
entry.readyAt === null ||
|
|
668
|
+
now - entry.readyAt > WORKFLOW_POOL_READY_MAX_AGE_MS ||
|
|
669
|
+
now - entry.readyAt < minReadyAgeMs
|
|
670
|
+
) {
|
|
671
|
+
warming += 1;
|
|
672
|
+
} else {
|
|
673
|
+
available += 1;
|
|
674
|
+
}
|
|
474
675
|
}
|
|
475
|
-
return
|
|
476
|
-
headers: { 'content-type': 'application/json' },
|
|
477
|
-
});
|
|
676
|
+
return { available, warming };
|
|
478
677
|
}
|
|
479
678
|
|
|
480
679
|
private async handlePoolList(req: Request): Promise<Response> {
|
|
@@ -492,6 +691,7 @@ export class PlayDedup implements DurableObject {
|
|
|
492
691
|
.filter((entry) => entry.version === version)
|
|
493
692
|
.map((entry) => ({
|
|
494
693
|
id: entry.id,
|
|
694
|
+
state: entry.state,
|
|
495
695
|
createdAt: entry.createdAt,
|
|
496
696
|
readyAt: entry.readyAt,
|
|
497
697
|
expiresAt: entry.expiresAt,
|
|
@@ -502,13 +702,12 @@ export class PlayDedup implements DurableObject {
|
|
|
502
702
|
}
|
|
503
703
|
|
|
504
704
|
private async handlePoolPromote(req: Request): Promise<Response> {
|
|
505
|
-
const body = (await req.json().catch(() => null)) as
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
705
|
+
const body = (await req.json().catch(() => null)) as {
|
|
706
|
+
ids?: unknown;
|
|
707
|
+
version?: unknown;
|
|
708
|
+
} | null;
|
|
709
|
+
const version =
|
|
710
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
512
711
|
if (!version) {
|
|
513
712
|
return new Response('version is required', { status: 400 });
|
|
514
713
|
}
|
|
@@ -529,6 +728,7 @@ export class PlayDedup implements DurableObject {
|
|
|
529
728
|
}
|
|
530
729
|
writes[key] = {
|
|
531
730
|
...entry,
|
|
731
|
+
state: 'ready',
|
|
532
732
|
readyAt: now,
|
|
533
733
|
};
|
|
534
734
|
}
|
|
@@ -536,19 +736,53 @@ export class PlayDedup implements DurableObject {
|
|
|
536
736
|
await this.state.storage.put(writes);
|
|
537
737
|
}
|
|
538
738
|
});
|
|
539
|
-
return new Response(
|
|
739
|
+
return new Response(
|
|
740
|
+
JSON.stringify({ promoted: Object.keys(writes).length }),
|
|
741
|
+
{
|
|
742
|
+
headers: { 'content-type': 'application/json' },
|
|
743
|
+
},
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private async handlePoolReady(req: Request): Promise<Response> {
|
|
748
|
+
const body = (await req.json().catch(() => null)) as {
|
|
749
|
+
poolId?: unknown;
|
|
750
|
+
version?: unknown;
|
|
751
|
+
} | null;
|
|
752
|
+
const poolId = typeof body?.poolId === 'string' ? body.poolId : '';
|
|
753
|
+
const version =
|
|
754
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
755
|
+
if (!poolId || !version) {
|
|
756
|
+
return new Response('poolId and version are required', { status: 400 });
|
|
757
|
+
}
|
|
758
|
+
const now = Date.now();
|
|
759
|
+
let ready = false;
|
|
760
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
761
|
+
await this.gcWorkflowPool(now, version);
|
|
762
|
+
const key = `${WORKFLOW_POOL_KEY_PREFIX}${poolId}`;
|
|
763
|
+
const entry = await this.state.storage.get<WorkflowPoolEntry>(key);
|
|
764
|
+
if (!entry || entry.version !== version || entry.expiresAt <= now) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
await this.state.storage.put(key, {
|
|
768
|
+
...entry,
|
|
769
|
+
state: 'ready',
|
|
770
|
+
readyAt: entry.readyAt ?? now,
|
|
771
|
+
});
|
|
772
|
+
ready = true;
|
|
773
|
+
});
|
|
774
|
+
return new Response(JSON.stringify({ ready }), {
|
|
540
775
|
headers: { 'content-type': 'application/json' },
|
|
541
776
|
});
|
|
542
777
|
}
|
|
543
778
|
|
|
544
779
|
private async handlePoolDelete(req: Request): Promise<Response> {
|
|
545
|
-
const body = (await req.json().catch(() => null)) as
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const version = typeof body?.version === 'string' ? body.version.trim() : '';
|
|
780
|
+
const body = (await req.json().catch(() => null)) as {
|
|
781
|
+
ids?: unknown;
|
|
782
|
+
version?: unknown;
|
|
783
|
+
} | null;
|
|
784
|
+
const version =
|
|
785
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
552
786
|
if (!version) {
|
|
553
787
|
return new Response('version is required', { status: 400 });
|
|
554
788
|
}
|
|
@@ -575,34 +809,118 @@ export class PlayDedup implements DurableObject {
|
|
|
575
809
|
}
|
|
576
810
|
|
|
577
811
|
private async handlePoolMapRun(req: Request): Promise<Response> {
|
|
578
|
-
const body = (await req.json().catch(() => null)) as
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
| null;
|
|
812
|
+
const body = (await req.json().catch(() => null)) as {
|
|
813
|
+
runId?: unknown;
|
|
814
|
+
instanceId?: unknown;
|
|
815
|
+
version?: unknown;
|
|
816
|
+
started?: unknown;
|
|
817
|
+
} | null;
|
|
585
818
|
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
586
819
|
const instanceId =
|
|
587
820
|
typeof body?.instanceId === 'string' ? body.instanceId : '';
|
|
588
|
-
const version =
|
|
821
|
+
const version =
|
|
822
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
823
|
+
const started = body?.started === true;
|
|
589
824
|
if (!runId || !instanceId || !version) {
|
|
590
825
|
return new Response('runId, instanceId, and version are required', {
|
|
591
826
|
status: 400,
|
|
592
827
|
});
|
|
593
828
|
}
|
|
594
829
|
const now = Date.now();
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
830
|
+
let mapped = true;
|
|
831
|
+
const key = `${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`;
|
|
832
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
833
|
+
const existing = await this.state.storage.get<WorkflowRunMapping>(key);
|
|
834
|
+
if (
|
|
835
|
+
existing?.version === version &&
|
|
836
|
+
existing.state === 'blocked' &&
|
|
837
|
+
existing.blockedInstanceId === instanceId
|
|
838
|
+
) {
|
|
839
|
+
mapped = false;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const alreadyStarted =
|
|
843
|
+
existing?.version === version &&
|
|
844
|
+
existing.instanceId === instanceId &&
|
|
845
|
+
existing.state === 'started' &&
|
|
846
|
+
typeof existing.startedAt === 'number';
|
|
847
|
+
const startedAt = started || alreadyStarted ? now : null;
|
|
848
|
+
await this.state.storage.put(key, {
|
|
598
849
|
runId,
|
|
599
850
|
instanceId,
|
|
851
|
+
state: startedAt !== null ? 'started' : 'claimed',
|
|
852
|
+
blockedInstanceId: null,
|
|
853
|
+
claimedAt:
|
|
854
|
+
existing?.version === version && typeof existing.claimedAt === 'number'
|
|
855
|
+
? existing.claimedAt
|
|
856
|
+
: now,
|
|
857
|
+
startedAt,
|
|
858
|
+
version,
|
|
859
|
+
createdAt:
|
|
860
|
+
existing?.version === version && existing.createdAt
|
|
861
|
+
? existing.createdAt
|
|
862
|
+
: now,
|
|
863
|
+
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
864
|
+
} satisfies WorkflowRunMapping);
|
|
865
|
+
});
|
|
866
|
+
return new Response(JSON.stringify({ mapped }), {
|
|
867
|
+
headers: { 'content-type': 'application/json' },
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private async handlePoolBlockRun(req: Request): Promise<Response> {
|
|
872
|
+
const body = (await req.json().catch(() => null)) as {
|
|
873
|
+
runId?: unknown;
|
|
874
|
+
instanceId?: unknown;
|
|
875
|
+
version?: unknown;
|
|
876
|
+
} | null;
|
|
877
|
+
const runId = typeof body?.runId === 'string' ? body.runId : '';
|
|
878
|
+
const instanceId =
|
|
879
|
+
typeof body?.instanceId === 'string' ? body.instanceId : '';
|
|
880
|
+
const version =
|
|
881
|
+
typeof body?.version === 'string' ? body.version.trim() : '';
|
|
882
|
+
if (!runId || !instanceId || !version) {
|
|
883
|
+
return new Response('runId, instanceId, and version are required', {
|
|
884
|
+
status: 400,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const now = Date.now();
|
|
888
|
+
const key = `${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`;
|
|
889
|
+
let started = false;
|
|
890
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
891
|
+
const existing = await this.state.storage.get<WorkflowRunMapping>(key);
|
|
892
|
+
if (
|
|
893
|
+
existing?.version === version &&
|
|
894
|
+
existing.instanceId === instanceId &&
|
|
895
|
+
existing.state === 'started' &&
|
|
896
|
+
typeof existing.startedAt === 'number'
|
|
897
|
+
) {
|
|
898
|
+
started = true;
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
await this.state.storage.put(key, {
|
|
902
|
+
runId,
|
|
903
|
+
instanceId: '',
|
|
904
|
+
state: 'blocked',
|
|
905
|
+
blockedInstanceId: instanceId,
|
|
906
|
+
claimedAt:
|
|
907
|
+
existing?.version === version && typeof existing.claimedAt === 'number'
|
|
908
|
+
? existing.claimedAt
|
|
909
|
+
: now,
|
|
910
|
+
startedAt: null,
|
|
600
911
|
version,
|
|
601
912
|
createdAt: now,
|
|
602
913
|
expiresAt: now + WORKFLOW_POOL_RUN_MAPPING_TTL_MS,
|
|
603
|
-
} satisfies WorkflowRunMapping
|
|
604
|
-
);
|
|
605
|
-
|
|
914
|
+
} satisfies WorkflowRunMapping);
|
|
915
|
+
});
|
|
916
|
+
if (started) {
|
|
917
|
+
return new Response(JSON.stringify({ blocked: false, started: true }), {
|
|
918
|
+
headers: { 'content-type': 'application/json' },
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return new Response(JSON.stringify({ blocked: true, started: false }), {
|
|
922
|
+
headers: { 'content-type': 'application/json' },
|
|
923
|
+
});
|
|
606
924
|
}
|
|
607
925
|
|
|
608
926
|
private async handlePoolResolveRun(req: Request): Promise<Response> {
|
|
@@ -620,13 +938,18 @@ export class PlayDedup implements DurableObject {
|
|
|
620
938
|
`${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`,
|
|
621
939
|
);
|
|
622
940
|
if (mapping && mapping.version !== version) {
|
|
623
|
-
await this.state.storage.delete(
|
|
941
|
+
await this.state.storage.delete(
|
|
942
|
+
`${WORKFLOW_POOL_RUN_KEY_PREFIX}${runId}`,
|
|
943
|
+
);
|
|
624
944
|
return new Response(JSON.stringify({ instanceId: null }), {
|
|
625
945
|
headers: { 'content-type': 'application/json' },
|
|
626
946
|
});
|
|
627
947
|
}
|
|
628
948
|
return new Response(
|
|
629
|
-
JSON.stringify({
|
|
949
|
+
JSON.stringify({
|
|
950
|
+
instanceId: mapping?.instanceId || null,
|
|
951
|
+
startedAt: mapping?.startedAt ?? null,
|
|
952
|
+
}),
|
|
630
953
|
{ headers: { 'content-type': 'application/json' } },
|
|
631
954
|
);
|
|
632
955
|
}
|
|
@@ -658,9 +981,9 @@ export class PlayDedup implements DurableObject {
|
|
|
658
981
|
}
|
|
659
982
|
|
|
660
983
|
private async handleTraceAdd(req: Request): Promise<Response> {
|
|
661
|
-
const body = (await req
|
|
662
|
-
|
|
663
|
-
| null;
|
|
984
|
+
const body = (await req
|
|
985
|
+
.json()
|
|
986
|
+
.catch(() => null)) as Partial<CoordinatorTraceEntry> | null;
|
|
664
987
|
if (
|
|
665
988
|
!body ||
|
|
666
989
|
(body.source !== 'coordinator' && body.source !== 'dynamic_worker') ||
|
|
@@ -693,7 +1016,9 @@ export class PlayDedup implements DurableObject {
|
|
|
693
1016
|
await this.state.storage.delete([...entries.keys()].slice(0, overflow));
|
|
694
1017
|
}
|
|
695
1018
|
});
|
|
696
|
-
return new Response('{}', {
|
|
1019
|
+
return new Response('{}', {
|
|
1020
|
+
headers: { 'content-type': 'application/json' },
|
|
1021
|
+
});
|
|
697
1022
|
}
|
|
698
1023
|
|
|
699
1024
|
private async handleTraceList(): Promise<Response> {
|
|
@@ -702,9 +1027,213 @@ export class PlayDedup implements DurableObject {
|
|
|
702
1027
|
});
|
|
703
1028
|
return new Response(
|
|
704
1029
|
JSON.stringify({
|
|
705
|
-
entries: [...entries.values()].sort(
|
|
1030
|
+
entries: [...entries.values()].sort(
|
|
1031
|
+
(left, right) => left.ts - right.ts,
|
|
1032
|
+
),
|
|
706
1033
|
}),
|
|
707
1034
|
{ headers: { 'content-type': 'application/json' } },
|
|
708
1035
|
);
|
|
709
1036
|
}
|
|
1037
|
+
|
|
1038
|
+
private async handleTerminalSet(req: Request): Promise<Response> {
|
|
1039
|
+
const body = (await req
|
|
1040
|
+
.json()
|
|
1041
|
+
.catch(() => null)) as Partial<CoordinatorTerminalState> | null;
|
|
1042
|
+
if (
|
|
1043
|
+
!body ||
|
|
1044
|
+
typeof body.runId !== 'string' ||
|
|
1045
|
+
(body.status !== 'completed' &&
|
|
1046
|
+
body.status !== 'failed' &&
|
|
1047
|
+
body.status !== 'cancelled')
|
|
1048
|
+
) {
|
|
1049
|
+
return new Response('invalid terminal state', { status: 400 });
|
|
1050
|
+
}
|
|
1051
|
+
const state: CoordinatorTerminalState = {
|
|
1052
|
+
runId: body.runId,
|
|
1053
|
+
status: body.status,
|
|
1054
|
+
result: body.result,
|
|
1055
|
+
error: typeof body.error === 'string' ? body.error : null,
|
|
1056
|
+
totalRows: body.totalRows,
|
|
1057
|
+
durationMs: body.durationMs,
|
|
1058
|
+
playName: typeof body.playName === 'string' ? body.playName : null,
|
|
1059
|
+
completedAt:
|
|
1060
|
+
typeof body.completedAt === 'number' ? body.completedAt : Date.now(),
|
|
1061
|
+
};
|
|
1062
|
+
await this.state.storage.put(COORDINATOR_TERMINAL_KEY, state);
|
|
1063
|
+
const event = await this.storeRunEvent({
|
|
1064
|
+
runId: state.runId,
|
|
1065
|
+
type: 'terminal',
|
|
1066
|
+
status: state.status,
|
|
1067
|
+
result: state.result,
|
|
1068
|
+
error: state.error,
|
|
1069
|
+
totalRows: state.totalRows,
|
|
1070
|
+
durationMs: state.durationMs,
|
|
1071
|
+
playName: state.playName,
|
|
1072
|
+
ts: state.completedAt,
|
|
1073
|
+
});
|
|
1074
|
+
this.wakeRunEventWaiters(event);
|
|
1075
|
+
return new Response('{}', {
|
|
1076
|
+
headers: { 'content-type': 'application/json' },
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private async handleTerminalGet(): Promise<Response> {
|
|
1081
|
+
const state = await this.state.storage.get<CoordinatorTerminalState>(
|
|
1082
|
+
COORDINATOR_TERMINAL_KEY,
|
|
1083
|
+
);
|
|
1084
|
+
return new Response(JSON.stringify({ state: state ?? null }), {
|
|
1085
|
+
headers: { 'content-type': 'application/json' },
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private async storeRunEvent(
|
|
1090
|
+
input: CoordinatorRunEventInput,
|
|
1091
|
+
): Promise<CoordinatorRunEvent> {
|
|
1092
|
+
let event: CoordinatorRunEvent | null = null;
|
|
1093
|
+
await this.state.blockConcurrencyWhile(async () => {
|
|
1094
|
+
const current =
|
|
1095
|
+
(await this.state.storage.get<number>(
|
|
1096
|
+
COORDINATOR_RUN_EVENT_SEQUENCE_KEY,
|
|
1097
|
+
)) ?? 0;
|
|
1098
|
+
const seq = current + 1;
|
|
1099
|
+
event = { ...input, seq } as CoordinatorRunEvent;
|
|
1100
|
+
await this.state.storage.put({
|
|
1101
|
+
[COORDINATOR_RUN_EVENT_SEQUENCE_KEY]: seq,
|
|
1102
|
+
[this.runEventKey(event)]: event,
|
|
1103
|
+
});
|
|
1104
|
+
const entries = await this.state.storage.list<CoordinatorRunEvent>({
|
|
1105
|
+
prefix: COORDINATOR_RUN_EVENT_KEY_PREFIX,
|
|
1106
|
+
});
|
|
1107
|
+
const overflow = entries.size - COORDINATOR_RUN_EVENT_MAX_ENTRIES;
|
|
1108
|
+
if (overflow > 0) {
|
|
1109
|
+
await this.state.storage.delete([...entries.keys()].slice(0, overflow));
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
if (!event) {
|
|
1113
|
+
throw new Error('failed to store run event');
|
|
1114
|
+
}
|
|
1115
|
+
return event;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private wakeRunEventWaiters(event: CoordinatorRunEvent): void {
|
|
1119
|
+
for (const resolve of this.runEventWaiters) {
|
|
1120
|
+
resolve(event);
|
|
1121
|
+
}
|
|
1122
|
+
this.runEventWaiters.clear();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private async handleRunEventAdd(req: Request): Promise<Response> {
|
|
1126
|
+
const body = (await req.json().catch(() => null)) as
|
|
1127
|
+
| Partial<CoordinatorRunEvent>
|
|
1128
|
+
| null;
|
|
1129
|
+
if (!body || typeof body.runId !== 'string') {
|
|
1130
|
+
return new Response('invalid run event', { status: 400 });
|
|
1131
|
+
}
|
|
1132
|
+
let eventInput: CoordinatorRunEventInput | null = null;
|
|
1133
|
+
const ts = typeof body.ts === 'number' ? body.ts : Date.now();
|
|
1134
|
+
if (body.type === 'log' && typeof body.line === 'string') {
|
|
1135
|
+
eventInput = {
|
|
1136
|
+
runId: body.runId,
|
|
1137
|
+
type: 'log',
|
|
1138
|
+
line: body.line,
|
|
1139
|
+
ts,
|
|
1140
|
+
};
|
|
1141
|
+
} else if (body.type === 'status' && typeof body.status === 'string') {
|
|
1142
|
+
eventInput = {
|
|
1143
|
+
runId: body.runId,
|
|
1144
|
+
type: 'status',
|
|
1145
|
+
status: body.status,
|
|
1146
|
+
ts,
|
|
1147
|
+
logs: Array.isArray(body.logs)
|
|
1148
|
+
? body.logs.filter((line): line is string => typeof line === 'string')
|
|
1149
|
+
: undefined,
|
|
1150
|
+
};
|
|
1151
|
+
} else if (body.type === 'progress' && typeof body.status === 'string') {
|
|
1152
|
+
eventInput = {
|
|
1153
|
+
runId: body.runId,
|
|
1154
|
+
type: 'progress',
|
|
1155
|
+
status: body.status,
|
|
1156
|
+
ts,
|
|
1157
|
+
logs: Array.isArray(body.logs)
|
|
1158
|
+
? body.logs.filter((line): line is string => typeof line === 'string')
|
|
1159
|
+
: undefined,
|
|
1160
|
+
activeNodeId:
|
|
1161
|
+
typeof body.activeNodeId === 'string' ? body.activeNodeId : null,
|
|
1162
|
+
activeArtifactTableNamespace:
|
|
1163
|
+
typeof body.activeArtifactTableNamespace === 'string'
|
|
1164
|
+
? body.activeArtifactTableNamespace
|
|
1165
|
+
: null,
|
|
1166
|
+
updatedAt: typeof body.updatedAt === 'number' ? body.updatedAt : null,
|
|
1167
|
+
};
|
|
1168
|
+
} else if (
|
|
1169
|
+
body.type === 'terminal' &&
|
|
1170
|
+
(body.status === 'completed' ||
|
|
1171
|
+
body.status === 'failed' ||
|
|
1172
|
+
body.status === 'cancelled')
|
|
1173
|
+
) {
|
|
1174
|
+
eventInput = {
|
|
1175
|
+
runId: body.runId,
|
|
1176
|
+
type: 'terminal',
|
|
1177
|
+
status: body.status,
|
|
1178
|
+
ts,
|
|
1179
|
+
result: body.result,
|
|
1180
|
+
error: typeof body.error === 'string' ? body.error : null,
|
|
1181
|
+
totalRows: body.totalRows,
|
|
1182
|
+
durationMs: body.durationMs,
|
|
1183
|
+
playName: typeof body.playName === 'string' ? body.playName : null,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (!eventInput) {
|
|
1187
|
+
return new Response('invalid run event', { status: 400 });
|
|
1188
|
+
}
|
|
1189
|
+
const event = await this.storeRunEvent(eventInput);
|
|
1190
|
+
this.wakeRunEventWaiters(event);
|
|
1191
|
+
return new Response(JSON.stringify({ event }), {
|
|
1192
|
+
headers: { 'content-type': 'application/json' },
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private async handleRunEventList(req: Request): Promise<Response> {
|
|
1197
|
+
const url = new URL(req.url);
|
|
1198
|
+
const afterSeq = Math.max(
|
|
1199
|
+
0,
|
|
1200
|
+
Math.floor(Number(url.searchParams.get('afterSeq') ?? '0')),
|
|
1201
|
+
);
|
|
1202
|
+
const timeoutMs = Math.min(
|
|
1203
|
+
Math.max(Number(url.searchParams.get('timeoutMs') ?? '0'), 0),
|
|
1204
|
+
30_000,
|
|
1205
|
+
);
|
|
1206
|
+
const readEvents = async () => {
|
|
1207
|
+
const entries = await this.state.storage.list<CoordinatorRunEvent>({
|
|
1208
|
+
prefix: COORDINATOR_RUN_EVENT_KEY_PREFIX,
|
|
1209
|
+
});
|
|
1210
|
+
return [...entries.values()]
|
|
1211
|
+
.filter((event) => event.seq > afterSeq)
|
|
1212
|
+
.sort((left, right) => left.seq - right.seq);
|
|
1213
|
+
};
|
|
1214
|
+
let events = await readEvents();
|
|
1215
|
+
if (events.length === 0 && timeoutMs > 0) {
|
|
1216
|
+
await new Promise<void>((resolve) => {
|
|
1217
|
+
let settled = false;
|
|
1218
|
+
const finish = () => {
|
|
1219
|
+
if (settled) return;
|
|
1220
|
+
settled = true;
|
|
1221
|
+
this.runEventWaiters.delete(onEvent);
|
|
1222
|
+
clearTimeout(timeout);
|
|
1223
|
+
resolve();
|
|
1224
|
+
};
|
|
1225
|
+
const onEvent = () => finish();
|
|
1226
|
+
const timeout = setTimeout(finish, timeoutMs);
|
|
1227
|
+
this.runEventWaiters.add(onEvent);
|
|
1228
|
+
});
|
|
1229
|
+
events = await readEvents();
|
|
1230
|
+
}
|
|
1231
|
+
const latestSeq =
|
|
1232
|
+
(await this.state.storage.get<number>(
|
|
1233
|
+
COORDINATOR_RUN_EVENT_SEQUENCE_KEY,
|
|
1234
|
+
)) ?? afterSeq;
|
|
1235
|
+
return new Response(JSON.stringify({ events, latestSeq }), {
|
|
1236
|
+
headers: { 'content-type': 'application/json' },
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
710
1239
|
}
|