@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.
@@ -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 caller creates it, so
15
- * a later supersede can see it even if it has not started running yet. When
16
- * `supersedes` is set, the previously pending ids are cleared and returned for
17
- * the caller to terminate. Never touches the currently running instance —
18
- * matching the drizzle adapter, supersede only drops jobs that have not started. */
19
- enqueue(key: string, supersedes: boolean, instanceId: string, inputHash?: string): Promise<{
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" | {
@@ -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 caller creates it, so
30
- * a later supersede can see it even if it has not started running yet. When
31
- * `supersedes` is set, the previously pending ids are cleared and returned for
32
- * the caller to terminate. Never touches the currently running instance —
33
- * matching the drizzle adapter, supersede only drops jobs that have not started. */
34
- async enqueue(key, supersedes, instanceId, inputHash) {
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 { terminated: [], coalescedInto: pendingId };
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
- return this.#state.blockConcurrencyWhile(() => this.#logic.enqueue(key, supersedes, instanceId, inputHash));
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.31.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.31.0"
14
+ "@porulle/core": "0.32.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/node": "^24.5.2",
@@ -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 caller creates it, so
50
- * a later supersede can see it even if it has not started running yet. When
51
- * `supersedes` is set, the previously pending ids are cleared and returned for
52
- * the caller to terminate. Never touches the currently running instance —
53
- * matching the drizzle adapter, supersede only drops jobs that have not started. */
54
- async enqueue(
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[]; coalescedInto?: 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 { terminated: [], coalescedInto: pendingId };
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
- this.#logic.enqueue(key, supersedes, instanceId, inputHash),
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