@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;
|
|
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({
|
|
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
|
|
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
|
|
104
|
+
const slot = scopedConcurrencySlot(job.taskSlug, job.concurrencyKey);
|
|
105
|
+
const oldest = oldestByKey.get(slot);
|
|
94
106
|
if (!oldest) {
|
|
95
|
-
oldestByKey.set(
|
|
107
|
+
oldestByKey.set(slot, job);
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
98
110
|
if (job.createdAt < oldest.createdAt) {
|
|
99
|
-
oldestByKey.set(
|
|
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.
|
|
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/
|
|
66
|
-
"@porulle/
|
|
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({
|
|
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
|
|
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(
|
|
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
|
|
172
|
+
const slot = scopedConcurrencySlot(job.taskSlug, job.concurrencyKey);
|
|
173
|
+
const oldest = oldestByKey.get(slot);
|
|
155
174
|
if (!oldest) {
|
|
156
|
-
oldestByKey.set(
|
|
175
|
+
oldestByKey.set(slot, job);
|
|
157
176
|
continue;
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
if (job.createdAt < oldest.createdAt) {
|
|
161
|
-
oldestByKey.set(
|
|
180
|
+
oldestByKey.set(slot, job);
|
|
162
181
|
jobsToRelease.push(oldest);
|
|
163
182
|
} else {
|
|
164
183
|
jobsToRelease.push(job);
|