@porulle/core 0.26.0 → 0.28.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.
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/kernel/jobs/runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKrE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,eAAe,CAAC;IACpB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,kBAAkB,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AA4CD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoNhD"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/kernel/jobs/runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKrE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,eAAe,CAAC;IACpB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,kBAAkB,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAuDD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA4NhD"}
@@ -5,6 +5,12 @@ import { commerceJobs } from "./schema.js";
5
5
  function isExclusiveTask(task) {
6
6
  return Boolean(task?.concurrency && task.concurrency.exclusive !== false);
7
7
  }
8
+ // Scoped slot matches supersedes (organizationId, taskSlug, concurrencyKey): the key alone
9
+ // is not global — two tasks may share an entity id without sharing an exclusive slot.
10
+ const CONCURRENCY_SLOT_SEP = "\u0000";
11
+ function scopedConcurrencySlot(taskSlug, concurrencyKey) {
12
+ return `${taskSlug}${CONCURRENCY_SLOT_SEP}${concurrencyKey}`;
13
+ }
8
14
  function compareByProcessingOrder(processingOrder) {
9
15
  if (typeof processingOrder === "function") {
10
16
  return (left, right) => processingOrder({
@@ -44,10 +50,15 @@ export async function runPendingJobs(args) {
44
50
  // Phase 1: Claim jobs atomically
45
51
  const claimed = await db.transaction(async (tx) => {
46
52
  const processing = await tx
47
- .select({ concurrencyKey: commerceJobs.concurrencyKey })
53
+ .select({
54
+ taskSlug: commerceJobs.taskSlug,
55
+ concurrencyKey: commerceJobs.concurrencyKey,
56
+ })
48
57
  .from(commerceJobs)
49
58
  .where(eq(commerceJobs.status, "processing"));
50
- const processingKeys = new Set(processing.flatMap((job) => job.concurrencyKey ? [job.concurrencyKey] : []));
59
+ const processingKeys = new Set(processing.flatMap((job) => job.concurrencyKey
60
+ ? [scopedConcurrencySlot(job.taskSlug, job.concurrencyKey)]
61
+ : []));
51
62
  const candidates = await tx
52
63
  .select()
53
64
  .from(commerceJobs)
@@ -60,7 +71,7 @@ export async function runPendingJobs(args) {
60
71
  break;
61
72
  if (candidate.concurrencyKey &&
62
73
  isExclusiveTask(tasks.get(candidate.taskSlug)) &&
63
- processingKeys.has(candidate.concurrencyKey)) {
74
+ processingKeys.has(scopedConcurrencySlot(candidate.taskSlug, candidate.concurrencyKey))) {
64
75
  continue;
65
76
  }
66
77
  const [locked] = await tx
@@ -90,13 +101,14 @@ export async function runPendingJobs(args) {
90
101
  jobsToRun.push(job);
91
102
  continue;
92
103
  }
93
- const oldest = oldestByKey.get(job.concurrencyKey);
104
+ const slot = scopedConcurrencySlot(job.taskSlug, job.concurrencyKey);
105
+ const oldest = oldestByKey.get(slot);
94
106
  if (!oldest) {
95
- oldestByKey.set(job.concurrencyKey, job);
107
+ oldestByKey.set(slot, job);
96
108
  continue;
97
109
  }
98
110
  if (job.createdAt < oldest.createdAt) {
99
- oldestByKey.set(job.concurrencyKey, job);
111
+ oldestByKey.set(slot, job);
100
112
  jobsToRelease.push(oldest);
101
113
  }
102
114
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -62,8 +62,8 @@
62
62
  "eslint": "^9.39.1",
63
63
  "typescript": "5.9.2",
64
64
  "vitest": "^3.2.4",
65
- "@porulle/eslint-config": "0.1.0",
66
- "@porulle/typescript-config": "0.1.0"
65
+ "@porulle/typescript-config": "0.1.0",
66
+ "@porulle/eslint-config": "0.1.0"
67
67
  },
68
68
  "publishConfig": {
69
69
  "access": "public"
@@ -22,6 +22,17 @@ function isExclusiveTask(task: TaskDefinition | undefined): boolean {
22
22
  return Boolean(task?.concurrency && task.concurrency.exclusive !== false);
23
23
  }
24
24
 
25
+ // Scoped slot matches supersedes (organizationId, taskSlug, concurrencyKey): the key alone
26
+ // is not global — two tasks may share an entity id without sharing an exclusive slot.
27
+ const CONCURRENCY_SLOT_SEP = "\u0000";
28
+
29
+ function scopedConcurrencySlot(
30
+ taskSlug: string,
31
+ concurrencyKey: string,
32
+ ): string {
33
+ return `${taskSlug}${CONCURRENCY_SLOT_SEP}${concurrencyKey}`;
34
+ }
35
+
25
36
  function compareByProcessingOrder(
26
37
  processingOrder: JobProcessingOrder | undefined,
27
38
  ): (left: CommerceJob, right: CommerceJob) => number {
@@ -82,12 +93,17 @@ export async function runPendingJobs(
82
93
  // Phase 1: Claim jobs atomically
83
94
  const claimed = await db.transaction(async (tx) => {
84
95
  const processing = await tx
85
- .select({ concurrencyKey: commerceJobs.concurrencyKey })
96
+ .select({
97
+ taskSlug: commerceJobs.taskSlug,
98
+ concurrencyKey: commerceJobs.concurrencyKey,
99
+ })
86
100
  .from(commerceJobs)
87
101
  .where(eq(commerceJobs.status, "processing"));
88
102
  const processingKeys = new Set(
89
103
  processing.flatMap((job) =>
90
- job.concurrencyKey ? [job.concurrencyKey] : [],
104
+ job.concurrencyKey
105
+ ? [scopedConcurrencySlot(job.taskSlug, job.concurrencyKey)]
106
+ : [],
91
107
  ),
92
108
  );
93
109
 
@@ -111,7 +127,9 @@ export async function runPendingJobs(
111
127
  if (
112
128
  candidate.concurrencyKey &&
113
129
  isExclusiveTask(tasks.get(candidate.taskSlug)) &&
114
- processingKeys.has(candidate.concurrencyKey)
130
+ processingKeys.has(
131
+ scopedConcurrencySlot(candidate.taskSlug, candidate.concurrencyKey),
132
+ )
115
133
  ) {
116
134
  continue;
117
135
  }
@@ -151,14 +169,15 @@ export async function runPendingJobs(
151
169
  continue;
152
170
  }
153
171
 
154
- const oldest = oldestByKey.get(job.concurrencyKey);
172
+ const slot = scopedConcurrencySlot(job.taskSlug, job.concurrencyKey);
173
+ const oldest = oldestByKey.get(slot);
155
174
  if (!oldest) {
156
- oldestByKey.set(job.concurrencyKey, job);
175
+ oldestByKey.set(slot, job);
157
176
  continue;
158
177
  }
159
178
 
160
179
  if (job.createdAt < oldest.createdAt) {
161
- oldestByKey.set(job.concurrencyKey, job);
180
+ oldestByKey.set(slot, job);
162
181
  jobsToRelease.push(oldest);
163
182
  } else {
164
183
  jobsToRelease.push(job);