@porulle/jobs-cloudflare 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coordinator.d.ts +22 -6
- package/dist/coordinator.js +39 -9
- package/package.json +2 -2
- package/src/coordinator.ts +69 -10
package/dist/coordinator.d.ts
CHANGED
|
@@ -11,15 +11,31 @@ export interface CoordinatorStorage {
|
|
|
11
11
|
export declare class JobCoordinatorLogic {
|
|
12
12
|
private readonly storage;
|
|
13
13
|
constructor(storage: CoordinatorStorage);
|
|
14
|
-
/** Registers `instanceId` as pending for `key` before the
|
|
15
|
-
* a later supersede can see it even if it has not started running yet
|
|
16
|
-
* `supersedes`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
14
|
+
/** First enqueue phase: storage only. Registers `instanceId` as pending for `key` before the
|
|
15
|
+
* caller creates it, so a later supersede can see it even if it has not started running yet;
|
|
16
|
+
* under `supersedes` the previously pending ids are cleared and returned for the caller to
|
|
17
|
+
* terminate. Never touches the currently running instance — matching the drizzle adapter,
|
|
18
|
+
* supersede only drops jobs that have not started. When a pending instance carries the same input its id
|
|
19
|
+
* comes back as a CANDIDATE rather than a decision, so the Durable Object can check outside
|
|
20
|
+
* the gate whether that instance still exists. */
|
|
21
|
+
enqueueRead(key: string, supersedes: boolean, instanceId: string, inputHash?: string): Promise<{
|
|
22
|
+
terminated: string[];
|
|
23
|
+
} | {
|
|
24
|
+
coalesceCandidate: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Re-enter after the candidate was confirmed LIVE outside the gate. Coalesces only if the
|
|
27
|
+
* candidate is STILL pending under the same hash — if it started running meanwhile it has
|
|
28
|
+
* already read its input and the caller needs its own instance. */
|
|
29
|
+
enqueueAfterLiveCandidate(key: string, candidate: string, supersedes: boolean, instanceId: string, inputHash?: string): Promise<{
|
|
20
30
|
terminated: string[];
|
|
21
31
|
coalescedInto?: string;
|
|
22
32
|
}>;
|
|
33
|
+
/** Re-enter after the candidate was found STALE outside the gate: enqueue normally, which
|
|
34
|
+
* under `supersedes` drops every pending id including the dead candidate. */
|
|
35
|
+
enqueueAfterStaleCandidate(key: string, supersedes: boolean, instanceId: string, inputHash?: string): Promise<{
|
|
36
|
+
terminated: string[];
|
|
37
|
+
}>;
|
|
38
|
+
private commitEnqueue;
|
|
23
39
|
/** First gate phase: storage only. Returns `needsStaleCheck` when another instance
|
|
24
40
|
* holds the key so the Durable Object can ask the Workflow binding outside the gate. */
|
|
25
41
|
acquireRead(key: string, instanceId: string): Promise<"granted" | {
|
package/dist/coordinator.js
CHANGED
|
@@ -26,20 +26,44 @@ export class JobCoordinatorLogic {
|
|
|
26
26
|
constructor(storage) {
|
|
27
27
|
this.storage = storage;
|
|
28
28
|
}
|
|
29
|
-
/** Registers `instanceId` as pending for `key` before the
|
|
30
|
-
* a later supersede can see it even if it has not started running yet
|
|
31
|
-
* `supersedes`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
29
|
+
/** First enqueue phase: storage only. Registers `instanceId` as pending for `key` before the
|
|
30
|
+
* caller creates it, so a later supersede can see it even if it has not started running yet;
|
|
31
|
+
* under `supersedes` the previously pending ids are cleared and returned for the caller to
|
|
32
|
+
* terminate. Never touches the currently running instance — matching the drizzle adapter,
|
|
33
|
+
* supersede only drops jobs that have not started. When a pending instance carries the same input its id
|
|
34
|
+
* comes back as a CANDIDATE rather than a decision, so the Durable Object can check outside
|
|
35
|
+
* the gate whether that instance still exists. */
|
|
36
|
+
async enqueueRead(key, supersedes, instanceId, inputHash) {
|
|
35
37
|
const state = await this.getState(key);
|
|
36
38
|
if (supersedes && inputHash !== undefined) {
|
|
37
39
|
for (const pendingId of state.pending) {
|
|
38
40
|
if (state.pendingHashes[pendingId] === inputHash) {
|
|
39
|
-
return {
|
|
41
|
+
return { coalesceCandidate: pendingId };
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
return this.commitEnqueue(key, supersedes, instanceId, inputHash, state);
|
|
46
|
+
}
|
|
47
|
+
/** Re-enter after the candidate was confirmed LIVE outside the gate. Coalesces only if the
|
|
48
|
+
* candidate is STILL pending under the same hash — if it started running meanwhile it has
|
|
49
|
+
* already read its input and the caller needs its own instance. */
|
|
50
|
+
async enqueueAfterLiveCandidate(key, candidate, supersedes, instanceId, inputHash) {
|
|
51
|
+
const state = await this.getState(key);
|
|
52
|
+
if (supersedes &&
|
|
53
|
+
inputHash !== undefined &&
|
|
54
|
+
state.pending.includes(candidate) &&
|
|
55
|
+
state.pendingHashes[candidate] === inputHash) {
|
|
56
|
+
return { terminated: [], coalescedInto: candidate };
|
|
57
|
+
}
|
|
58
|
+
return this.commitEnqueue(key, supersedes, instanceId, inputHash, state);
|
|
59
|
+
}
|
|
60
|
+
/** Re-enter after the candidate was found STALE outside the gate: enqueue normally, which
|
|
61
|
+
* under `supersedes` drops every pending id including the dead candidate. */
|
|
62
|
+
async enqueueAfterStaleCandidate(key, supersedes, instanceId, inputHash) {
|
|
63
|
+
const state = await this.getState(key);
|
|
64
|
+
return this.commitEnqueue(key, supersedes, instanceId, inputHash, state);
|
|
65
|
+
}
|
|
66
|
+
async commitEnqueue(key, supersedes, instanceId, inputHash, state) {
|
|
43
67
|
const terminated = supersedes ? state.pending.filter((id) => id !== instanceId) : [];
|
|
44
68
|
const kept = supersedes ? [] : state.pending.filter((id) => id !== instanceId);
|
|
45
69
|
const pendingHashes = { ...state.pendingHashes };
|
|
@@ -184,8 +208,14 @@ export function porulleJobCoordinator(Base) {
|
|
|
184
208
|
return true;
|
|
185
209
|
}
|
|
186
210
|
}
|
|
187
|
-
enqueue(key, supersedes, instanceId, inputHash) {
|
|
188
|
-
|
|
211
|
+
async enqueue(key, supersedes, instanceId, inputHash) {
|
|
212
|
+
const first = await this.#state.blockConcurrencyWhile(() => this.#logic.enqueueRead(key, supersedes, instanceId, inputHash));
|
|
213
|
+
if (!("coalesceCandidate" in first))
|
|
214
|
+
return first;
|
|
215
|
+
const stale = await this.#isStale(first.coalesceCandidate);
|
|
216
|
+
return this.#state.blockConcurrencyWhile(() => stale
|
|
217
|
+
? this.#logic.enqueueAfterStaleCandidate(key, supersedes, instanceId, inputHash)
|
|
218
|
+
: this.#logic.enqueueAfterLiveCandidate(key, first.coalesceCandidate, supersedes, instanceId, inputHash));
|
|
189
219
|
}
|
|
190
220
|
async acquire(key, instanceId) {
|
|
191
221
|
const first = await this.#state.blockConcurrencyWhile(() => this.#logic.acquireRead(key, instanceId));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/jobs-cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@porulle/core": "0.
|
|
14
|
+
"@porulle/core": "0.32.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/node": "^24.5.2",
|
package/src/coordinator.ts
CHANGED
|
@@ -46,25 +46,71 @@ interface CoordinatorKeyState {
|
|
|
46
46
|
export class JobCoordinatorLogic {
|
|
47
47
|
constructor(private readonly storage: CoordinatorStorage) {}
|
|
48
48
|
|
|
49
|
-
/** Registers `instanceId` as pending for `key` before the
|
|
50
|
-
* a later supersede can see it even if it has not started running yet
|
|
51
|
-
* `supersedes`
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
49
|
+
/** First enqueue phase: storage only. Registers `instanceId` as pending for `key` before the
|
|
50
|
+
* caller creates it, so a later supersede can see it even if it has not started running yet;
|
|
51
|
+
* under `supersedes` the previously pending ids are cleared and returned for the caller to
|
|
52
|
+
* terminate. Never touches the currently running instance — matching the drizzle adapter,
|
|
53
|
+
* supersede only drops jobs that have not started. When a pending instance carries the same input its id
|
|
54
|
+
* comes back as a CANDIDATE rather than a decision, so the Durable Object can check outside
|
|
55
|
+
* the gate whether that instance still exists. */
|
|
56
|
+
async enqueueRead(
|
|
55
57
|
key: string,
|
|
56
58
|
supersedes: boolean,
|
|
57
59
|
instanceId: string,
|
|
58
60
|
inputHash?: string,
|
|
59
|
-
): Promise<{ terminated: string[]
|
|
61
|
+
): Promise<{ terminated: string[] } | { coalesceCandidate: string }> {
|
|
60
62
|
const state = await this.getState(key);
|
|
61
63
|
if (supersedes && inputHash !== undefined) {
|
|
62
64
|
for (const pendingId of state.pending) {
|
|
63
65
|
if (state.pendingHashes[pendingId] === inputHash) {
|
|
64
|
-
return {
|
|
66
|
+
return { coalesceCandidate: pendingId };
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
}
|
|
70
|
+
return this.commitEnqueue(key, supersedes, instanceId, inputHash, state);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Re-enter after the candidate was confirmed LIVE outside the gate. Coalesces only if the
|
|
74
|
+
* candidate is STILL pending under the same hash — if it started running meanwhile it has
|
|
75
|
+
* already read its input and the caller needs its own instance. */
|
|
76
|
+
async enqueueAfterLiveCandidate(
|
|
77
|
+
key: string,
|
|
78
|
+
candidate: string,
|
|
79
|
+
supersedes: boolean,
|
|
80
|
+
instanceId: string,
|
|
81
|
+
inputHash?: string,
|
|
82
|
+
): Promise<{ terminated: string[]; coalescedInto?: string }> {
|
|
83
|
+
const state = await this.getState(key);
|
|
84
|
+
if (
|
|
85
|
+
supersedes &&
|
|
86
|
+
inputHash !== undefined &&
|
|
87
|
+
state.pending.includes(candidate) &&
|
|
88
|
+
state.pendingHashes[candidate] === inputHash
|
|
89
|
+
) {
|
|
90
|
+
return { terminated: [], coalescedInto: candidate };
|
|
91
|
+
}
|
|
92
|
+
return this.commitEnqueue(key, supersedes, instanceId, inputHash, state);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Re-enter after the candidate was found STALE outside the gate: enqueue normally, which
|
|
96
|
+
* under `supersedes` drops every pending id including the dead candidate. */
|
|
97
|
+
async enqueueAfterStaleCandidate(
|
|
98
|
+
key: string,
|
|
99
|
+
supersedes: boolean,
|
|
100
|
+
instanceId: string,
|
|
101
|
+
inputHash?: string,
|
|
102
|
+
): Promise<{ terminated: string[] }> {
|
|
103
|
+
const state = await this.getState(key);
|
|
104
|
+
return this.commitEnqueue(key, supersedes, instanceId, inputHash, state);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async commitEnqueue(
|
|
108
|
+
key: string,
|
|
109
|
+
supersedes: boolean,
|
|
110
|
+
instanceId: string,
|
|
111
|
+
inputHash: string | undefined,
|
|
112
|
+
state: CoordinatorKeyState,
|
|
113
|
+
): Promise<{ terminated: string[] }> {
|
|
68
114
|
const terminated = supersedes ? state.pending.filter((id) => id !== instanceId) : [];
|
|
69
115
|
const kept = supersedes ? [] : state.pending.filter((id) => id !== instanceId);
|
|
70
116
|
const pendingHashes = { ...state.pendingHashes };
|
|
@@ -257,14 +303,27 @@ export function porulleJobCoordinator<TBase extends DurableObjectConstructor>(
|
|
|
257
303
|
}
|
|
258
304
|
}
|
|
259
305
|
|
|
260
|
-
enqueue(
|
|
306
|
+
async enqueue(
|
|
261
307
|
key: string,
|
|
262
308
|
supersedes: boolean,
|
|
263
309
|
instanceId: string,
|
|
264
310
|
inputHash?: string,
|
|
265
311
|
): Promise<{ terminated: string[]; coalescedInto?: string }> {
|
|
312
|
+
const first = await this.#state.blockConcurrencyWhile(() =>
|
|
313
|
+
this.#logic.enqueueRead(key, supersedes, instanceId, inputHash),
|
|
314
|
+
);
|
|
315
|
+
if (!("coalesceCandidate" in first)) return first;
|
|
316
|
+
const stale = await this.#isStale(first.coalesceCandidate);
|
|
266
317
|
return this.#state.blockConcurrencyWhile(() =>
|
|
267
|
-
|
|
318
|
+
stale
|
|
319
|
+
? this.#logic.enqueueAfterStaleCandidate(key, supersedes, instanceId, inputHash)
|
|
320
|
+
: this.#logic.enqueueAfterLiveCandidate(
|
|
321
|
+
key,
|
|
322
|
+
first.coalesceCandidate,
|
|
323
|
+
supersedes,
|
|
324
|
+
instanceId,
|
|
325
|
+
inputHash,
|
|
326
|
+
),
|
|
268
327
|
);
|
|
269
328
|
}
|
|
270
329
|
|