@absurd-sqlite/bun-worker 0.2.2-alpha.0 → 0.2.2-alpha.2
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/bun.lock +7 -4
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +826 -30
- package/package.json +4 -3
- package/src/index.ts +102 -9
- package/test/basic.test.ts +52 -0
- package/test/cli.test.ts +83 -0
- package/test/run.test.ts +2 -2
package/bun.lock
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@absurd-sqlite/bun-worker",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@absurd-sqlite/sdk": "
|
|
9
|
-
"absurd-sdk": "
|
|
8
|
+
"@absurd-sqlite/sdk": "next",
|
|
9
|
+
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz",
|
|
10
|
+
"cac": "^6.7.14",
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
13
|
"bun-types": "^1.3.5",
|
|
@@ -15,11 +16,11 @@
|
|
|
15
16
|
},
|
|
16
17
|
},
|
|
17
18
|
"packages": {
|
|
18
|
-
"@absurd-sqlite/sdk": ["@absurd-sqlite/sdk@0.2.
|
|
19
|
+
"@absurd-sqlite/sdk": ["@absurd-sqlite/sdk@0.2.1-alpha.2", "", { "dependencies": { "absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz" }, "peerDependencies": { "better-sqlite3": "^12.5.0" } }, "sha512-+8qBOtV/aoHX7BbzxIkzGpuG/o8RQ73Njyxt8Og04NvevceSeUtQ1IlF1Syil5v8EVMsAa2sshuBU0LzZKW/5A=="],
|
|
19
20
|
|
|
20
21
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
21
22
|
|
|
22
|
-
"absurd-sdk": ["absurd-sdk@0.0.7",
|
|
23
|
+
"absurd-sdk": ["absurd-sdk@https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz", { "peerDependencies": { "pg": "^8.0.0" } }],
|
|
23
24
|
|
|
24
25
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
|
25
26
|
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
|
|
34
35
|
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
35
36
|
|
|
37
|
+
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
|
38
|
+
|
|
36
39
|
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
|
37
40
|
|
|
38
41
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
2
2
|
export type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
3
|
+
export type { WorkerOptions } from "absurd-sdk";
|
|
4
|
+
export { downloadExtension, type DownloadExtensionOptions, } from "@absurd-sqlite/sdk";
|
|
3
5
|
/**
|
|
4
6
|
* Register tasks and perform any one-time setup before the worker starts.
|
|
5
7
|
*/
|
|
@@ -7,9 +9,10 @@ export type SetupFunction = (absurd: AbsurdClient) => void | Promise<void>;
|
|
|
7
9
|
/**
|
|
8
10
|
* Boots a worker using Bun's SQLite driver and Absurd's task engine.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
12
|
+
* CLI flags:
|
|
13
|
+
* - --concurrency, -c: Number of tasks to process concurrently (default: 10)
|
|
14
|
+
* - --database-path: SQLite database file path (overrides ABSURD_DATABASE_PATH)
|
|
15
|
+
* - --extension-path: Absurd-SQLite extension path (overrides ABSURD_DATABASE_EXTENSION_PATH)
|
|
13
16
|
*/
|
|
14
17
|
export default function run(setupFunction: SetupFunction): Promise<void>;
|
|
15
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKvD,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,GAC9B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AA0D3E;;;;;;;GAOG;AACH,wBAA8B,GAAG,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6D7E"}
|
package/dist/index.js
CHANGED
|
@@ -4772,6 +4772,47 @@ class TimeoutError extends Error {
|
|
|
4772
4772
|
}
|
|
4773
4773
|
}
|
|
4774
4774
|
|
|
4775
|
+
class LeaseTimerManager {
|
|
4776
|
+
log;
|
|
4777
|
+
taskLabel;
|
|
4778
|
+
fatalOnLeaseTimeout;
|
|
4779
|
+
warnTimer = null;
|
|
4780
|
+
fatalTimer = null;
|
|
4781
|
+
constructor(log, taskLabel, fatalOnLeaseTimeout) {
|
|
4782
|
+
this.log = log;
|
|
4783
|
+
this.taskLabel = taskLabel;
|
|
4784
|
+
this.fatalOnLeaseTimeout = fatalOnLeaseTimeout;
|
|
4785
|
+
}
|
|
4786
|
+
update(leaseSeconds) {
|
|
4787
|
+
this.clear();
|
|
4788
|
+
if (leaseSeconds <= 0) {
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
this.warnTimer = setTimeout(() => {
|
|
4792
|
+
this.log.warn(`task ${this.taskLabel} exceeded claim timeout of ${leaseSeconds}s`);
|
|
4793
|
+
}, leaseSeconds * 1000);
|
|
4794
|
+
if (this.fatalOnLeaseTimeout) {
|
|
4795
|
+
this.fatalTimer = setTimeout(() => {
|
|
4796
|
+
this.log.error(`task ${this.taskLabel} exceeded claim timeout of ${leaseSeconds}s by more than 100%; terminating process`);
|
|
4797
|
+
process.exit(1);
|
|
4798
|
+
}, leaseSeconds * 1000 * 2);
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
stop() {
|
|
4802
|
+
this.clear();
|
|
4803
|
+
}
|
|
4804
|
+
clear() {
|
|
4805
|
+
if (this.warnTimer) {
|
|
4806
|
+
clearTimeout(this.warnTimer);
|
|
4807
|
+
this.warnTimer = null;
|
|
4808
|
+
}
|
|
4809
|
+
if (this.fatalTimer) {
|
|
4810
|
+
clearTimeout(this.fatalTimer);
|
|
4811
|
+
this.fatalTimer = null;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4775
4816
|
class TaskContext {
|
|
4776
4817
|
log;
|
|
4777
4818
|
taskID;
|
|
@@ -4780,8 +4821,9 @@ class TaskContext {
|
|
|
4780
4821
|
task;
|
|
4781
4822
|
checkpointCache;
|
|
4782
4823
|
claimTimeout;
|
|
4824
|
+
leaseTimer;
|
|
4783
4825
|
stepNameCounter = new Map;
|
|
4784
|
-
constructor(log, taskID, con, queueName, task, checkpointCache, claimTimeout) {
|
|
4826
|
+
constructor(log, taskID, con, queueName, task, checkpointCache, claimTimeout, leaseTimer) {
|
|
4785
4827
|
this.log = log;
|
|
4786
4828
|
this.taskID = taskID;
|
|
4787
4829
|
this.con = con;
|
|
@@ -4789,19 +4831,21 @@ class TaskContext {
|
|
|
4789
4831
|
this.task = task;
|
|
4790
4832
|
this.checkpointCache = checkpointCache;
|
|
4791
4833
|
this.claimTimeout = claimTimeout;
|
|
4834
|
+
this.leaseTimer = leaseTimer;
|
|
4792
4835
|
}
|
|
4793
4836
|
get headers() {
|
|
4794
4837
|
return this.task.headers ?? {};
|
|
4795
4838
|
}
|
|
4796
4839
|
static async create(args) {
|
|
4797
|
-
const { log, taskID, con, queueName, task, claimTimeout } = args;
|
|
4840
|
+
const { log, taskID, con, queueName, task, claimTimeout, leaseTimer } = args;
|
|
4798
4841
|
const result = await con.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
|
|
4799
4842
|
FROM absurd.get_task_checkpoint_states($1, $2, $3)`, [queueName, task.task_id, task.run_id]);
|
|
4800
4843
|
const cache = new Map;
|
|
4801
4844
|
for (const row of result.rows) {
|
|
4802
4845
|
cache.set(row.checkpoint_name, row.state);
|
|
4803
4846
|
}
|
|
4804
|
-
|
|
4847
|
+
const ctx = new TaskContext(log, taskID, con, queueName, task, cache, claimTimeout, leaseTimer);
|
|
4848
|
+
return ctx;
|
|
4805
4849
|
}
|
|
4806
4850
|
async queryWithCancelCheck(sql, params) {
|
|
4807
4851
|
try {
|
|
@@ -4868,6 +4912,7 @@ class TaskContext {
|
|
|
4868
4912
|
this.claimTimeout
|
|
4869
4913
|
]);
|
|
4870
4914
|
this.checkpointCache.set(checkpointName, value);
|
|
4915
|
+
this.recordLeaseExtension(this.claimTimeout);
|
|
4871
4916
|
}
|
|
4872
4917
|
async scheduleRun(wakeAt) {
|
|
4873
4918
|
await this.con.query(`SELECT absurd.schedule_run($1, $2, $3)`, [
|
|
@@ -4913,11 +4958,16 @@ class TaskContext {
|
|
|
4913
4958
|
throw new SuspendTask;
|
|
4914
4959
|
}
|
|
4915
4960
|
async heartbeat(seconds) {
|
|
4961
|
+
const leaseSeconds = seconds ?? this.claimTimeout;
|
|
4916
4962
|
await this.queryWithCancelCheck(`SELECT absurd.extend_claim($1, $2, $3)`, [
|
|
4917
4963
|
this.queueName,
|
|
4918
4964
|
this.task.run_id,
|
|
4919
|
-
|
|
4965
|
+
leaseSeconds
|
|
4920
4966
|
]);
|
|
4967
|
+
this.recordLeaseExtension(leaseSeconds);
|
|
4968
|
+
}
|
|
4969
|
+
recordLeaseExtension(leaseSeconds) {
|
|
4970
|
+
this.leaseTimer.update(leaseSeconds);
|
|
4921
4971
|
}
|
|
4922
4972
|
async emitEvent(eventName, payload) {
|
|
4923
4973
|
if (!eventName) {
|
|
@@ -5171,30 +5221,20 @@ class Absurd {
|
|
|
5171
5221
|
}
|
|
5172
5222
|
}
|
|
5173
5223
|
async executeTask(task, claimTimeout, options) {
|
|
5174
|
-
let warnTimer;
|
|
5175
|
-
let fatalTimer;
|
|
5176
5224
|
const registration = this.registry.get(task.task_name);
|
|
5225
|
+
const taskLabel = `${task.task_name} (${task.task_id})`;
|
|
5226
|
+
const leaseTimer = new LeaseTimerManager(this.log, taskLabel, options?.fatalOnLeaseTimeout ?? false);
|
|
5177
5227
|
const ctx = await TaskContext.create({
|
|
5178
5228
|
log: this.log,
|
|
5179
5229
|
taskID: task.task_id,
|
|
5180
5230
|
con: this.con,
|
|
5181
5231
|
queueName: registration?.queue ?? "unknown",
|
|
5182
5232
|
task,
|
|
5183
|
-
claimTimeout
|
|
5233
|
+
claimTimeout,
|
|
5234
|
+
leaseTimer
|
|
5184
5235
|
});
|
|
5236
|
+
leaseTimer.update(claimTimeout);
|
|
5185
5237
|
try {
|
|
5186
|
-
if (claimTimeout > 0) {
|
|
5187
|
-
const taskLabel = `${task.task_name} (${task.task_id})`;
|
|
5188
|
-
warnTimer = setTimeout(() => {
|
|
5189
|
-
this.log.warn(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s`);
|
|
5190
|
-
}, claimTimeout * 1000);
|
|
5191
|
-
if (options?.fatalOnLeaseTimeout) {
|
|
5192
|
-
fatalTimer = setTimeout(() => {
|
|
5193
|
-
this.log.error(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s by more than 100%; terminating process`);
|
|
5194
|
-
process.exit(1);
|
|
5195
|
-
}, claimTimeout * 1000 * 2);
|
|
5196
|
-
}
|
|
5197
|
-
}
|
|
5198
5238
|
if (!registration) {
|
|
5199
5239
|
throw new Error("Unknown task");
|
|
5200
5240
|
} else if (registration.queue !== this.queueName) {
|
|
@@ -5216,12 +5256,7 @@ class Absurd {
|
|
|
5216
5256
|
this.log.error("[absurd] task execution failed:", err);
|
|
5217
5257
|
await failTaskRun(this.con, this.queueName, task.run_id, err);
|
|
5218
5258
|
} finally {
|
|
5219
|
-
|
|
5220
|
-
clearTimeout(warnTimer);
|
|
5221
|
-
}
|
|
5222
|
-
if (fatalTimer) {
|
|
5223
|
-
clearTimeout(fatalTimer);
|
|
5224
|
-
}
|
|
5259
|
+
leaseTimer.stop();
|
|
5225
5260
|
}
|
|
5226
5261
|
}
|
|
5227
5262
|
}
|
|
@@ -5305,6 +5340,607 @@ function normalizeCancellation(policy) {
|
|
|
5305
5340
|
// src/index.ts
|
|
5306
5341
|
import { Database } from "bun:sqlite";
|
|
5307
5342
|
|
|
5343
|
+
// node_modules/cac/dist/index.mjs
|
|
5344
|
+
import { EventEmitter } from "events";
|
|
5345
|
+
function toArr(any) {
|
|
5346
|
+
return any == null ? [] : Array.isArray(any) ? any : [any];
|
|
5347
|
+
}
|
|
5348
|
+
function toVal(out, key, val, opts) {
|
|
5349
|
+
var x, old = out[key], nxt = ~opts.string.indexOf(key) ? val == null || val === true ? "" : String(val) : typeof val === "boolean" ? val : ~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
|
|
5350
|
+
out[key] = old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
|
|
5351
|
+
}
|
|
5352
|
+
function mri2(args, opts) {
|
|
5353
|
+
args = args || [];
|
|
5354
|
+
opts = opts || {};
|
|
5355
|
+
var k, arr, arg, name, val, out = { _: [] };
|
|
5356
|
+
var i = 0, j = 0, idx = 0, len = args.length;
|
|
5357
|
+
const alibi = opts.alias !== undefined;
|
|
5358
|
+
const strict = opts.unknown !== undefined;
|
|
5359
|
+
const defaults2 = opts.default !== undefined;
|
|
5360
|
+
opts.alias = opts.alias || {};
|
|
5361
|
+
opts.string = toArr(opts.string);
|
|
5362
|
+
opts.boolean = toArr(opts.boolean);
|
|
5363
|
+
if (alibi) {
|
|
5364
|
+
for (k in opts.alias) {
|
|
5365
|
+
arr = opts.alias[k] = toArr(opts.alias[k]);
|
|
5366
|
+
for (i = 0;i < arr.length; i++) {
|
|
5367
|
+
(opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
for (i = opts.boolean.length;i-- > 0; ) {
|
|
5372
|
+
arr = opts.alias[opts.boolean[i]] || [];
|
|
5373
|
+
for (j = arr.length;j-- > 0; )
|
|
5374
|
+
opts.boolean.push(arr[j]);
|
|
5375
|
+
}
|
|
5376
|
+
for (i = opts.string.length;i-- > 0; ) {
|
|
5377
|
+
arr = opts.alias[opts.string[i]] || [];
|
|
5378
|
+
for (j = arr.length;j-- > 0; )
|
|
5379
|
+
opts.string.push(arr[j]);
|
|
5380
|
+
}
|
|
5381
|
+
if (defaults2) {
|
|
5382
|
+
for (k in opts.default) {
|
|
5383
|
+
name = typeof opts.default[k];
|
|
5384
|
+
arr = opts.alias[k] = opts.alias[k] || [];
|
|
5385
|
+
if (opts[name] !== undefined) {
|
|
5386
|
+
opts[name].push(k);
|
|
5387
|
+
for (i = 0;i < arr.length; i++) {
|
|
5388
|
+
opts[name].push(arr[i]);
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
const keys = strict ? Object.keys(opts.alias) : [];
|
|
5394
|
+
for (i = 0;i < len; i++) {
|
|
5395
|
+
arg = args[i];
|
|
5396
|
+
if (arg === "--") {
|
|
5397
|
+
out._ = out._.concat(args.slice(++i));
|
|
5398
|
+
break;
|
|
5399
|
+
}
|
|
5400
|
+
for (j = 0;j < arg.length; j++) {
|
|
5401
|
+
if (arg.charCodeAt(j) !== 45)
|
|
5402
|
+
break;
|
|
5403
|
+
}
|
|
5404
|
+
if (j === 0) {
|
|
5405
|
+
out._.push(arg);
|
|
5406
|
+
} else if (arg.substring(j, j + 3) === "no-") {
|
|
5407
|
+
name = arg.substring(j + 3);
|
|
5408
|
+
if (strict && !~keys.indexOf(name)) {
|
|
5409
|
+
return opts.unknown(arg);
|
|
5410
|
+
}
|
|
5411
|
+
out[name] = false;
|
|
5412
|
+
} else {
|
|
5413
|
+
for (idx = j + 1;idx < arg.length; idx++) {
|
|
5414
|
+
if (arg.charCodeAt(idx) === 61)
|
|
5415
|
+
break;
|
|
5416
|
+
}
|
|
5417
|
+
name = arg.substring(j, idx);
|
|
5418
|
+
val = arg.substring(++idx) || (i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i]);
|
|
5419
|
+
arr = j === 2 ? [name] : name;
|
|
5420
|
+
for (idx = 0;idx < arr.length; idx++) {
|
|
5421
|
+
name = arr[idx];
|
|
5422
|
+
if (strict && !~keys.indexOf(name))
|
|
5423
|
+
return opts.unknown("-".repeat(j) + name);
|
|
5424
|
+
toVal(out, name, idx + 1 < arr.length || val, opts);
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
if (defaults2) {
|
|
5429
|
+
for (k in opts.default) {
|
|
5430
|
+
if (out[k] === undefined) {
|
|
5431
|
+
out[k] = opts.default[k];
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
if (alibi) {
|
|
5436
|
+
for (k in out) {
|
|
5437
|
+
arr = opts.alias[k] || [];
|
|
5438
|
+
while (arr.length > 0) {
|
|
5439
|
+
out[arr.shift()] = out[k];
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
return out;
|
|
5444
|
+
}
|
|
5445
|
+
var removeBrackets = (v) => v.replace(/[<[].+/, "").trim();
|
|
5446
|
+
var findAllBrackets = (v) => {
|
|
5447
|
+
const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
|
|
5448
|
+
const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
|
|
5449
|
+
const res = [];
|
|
5450
|
+
const parse = (match) => {
|
|
5451
|
+
let variadic = false;
|
|
5452
|
+
let value = match[1];
|
|
5453
|
+
if (value.startsWith("...")) {
|
|
5454
|
+
value = value.slice(3);
|
|
5455
|
+
variadic = true;
|
|
5456
|
+
}
|
|
5457
|
+
return {
|
|
5458
|
+
required: match[0].startsWith("<"),
|
|
5459
|
+
value,
|
|
5460
|
+
variadic
|
|
5461
|
+
};
|
|
5462
|
+
};
|
|
5463
|
+
let angledMatch;
|
|
5464
|
+
while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {
|
|
5465
|
+
res.push(parse(angledMatch));
|
|
5466
|
+
}
|
|
5467
|
+
let squareMatch;
|
|
5468
|
+
while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {
|
|
5469
|
+
res.push(parse(squareMatch));
|
|
5470
|
+
}
|
|
5471
|
+
return res;
|
|
5472
|
+
};
|
|
5473
|
+
var getMriOptions = (options) => {
|
|
5474
|
+
const result = { alias: {}, boolean: [] };
|
|
5475
|
+
for (const [index, option] of options.entries()) {
|
|
5476
|
+
if (option.names.length > 1) {
|
|
5477
|
+
result.alias[option.names[0]] = option.names.slice(1);
|
|
5478
|
+
}
|
|
5479
|
+
if (option.isBoolean) {
|
|
5480
|
+
if (option.negated) {
|
|
5481
|
+
const hasStringTypeOption = options.some((o, i) => {
|
|
5482
|
+
return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === "boolean";
|
|
5483
|
+
});
|
|
5484
|
+
if (!hasStringTypeOption) {
|
|
5485
|
+
result.boolean.push(option.names[0]);
|
|
5486
|
+
}
|
|
5487
|
+
} else {
|
|
5488
|
+
result.boolean.push(option.names[0]);
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
return result;
|
|
5493
|
+
};
|
|
5494
|
+
var findLongest = (arr) => {
|
|
5495
|
+
return arr.sort((a, b) => {
|
|
5496
|
+
return a.length > b.length ? -1 : 1;
|
|
5497
|
+
})[0];
|
|
5498
|
+
};
|
|
5499
|
+
var padRight = (str, length) => {
|
|
5500
|
+
return str.length >= length ? str : `${str}${" ".repeat(length - str.length)}`;
|
|
5501
|
+
};
|
|
5502
|
+
var camelcase = (input) => {
|
|
5503
|
+
return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
|
|
5504
|
+
return p1 + p2.toUpperCase();
|
|
5505
|
+
});
|
|
5506
|
+
};
|
|
5507
|
+
var setDotProp = (obj, keys, val) => {
|
|
5508
|
+
let i = 0;
|
|
5509
|
+
let length = keys.length;
|
|
5510
|
+
let t = obj;
|
|
5511
|
+
let x;
|
|
5512
|
+
for (;i < length; ++i) {
|
|
5513
|
+
x = t[keys[i]];
|
|
5514
|
+
t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(".") || !(+keys[i + 1] > -1) ? {} : [];
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5517
|
+
var setByType = (obj, transforms) => {
|
|
5518
|
+
for (const key of Object.keys(transforms)) {
|
|
5519
|
+
const transform = transforms[key];
|
|
5520
|
+
if (transform.shouldTransform) {
|
|
5521
|
+
obj[key] = Array.prototype.concat.call([], obj[key]);
|
|
5522
|
+
if (typeof transform.transformFunction === "function") {
|
|
5523
|
+
obj[key] = obj[key].map(transform.transformFunction);
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
};
|
|
5528
|
+
var getFileName = (input) => {
|
|
5529
|
+
const m = /([^\\\/]+)$/.exec(input);
|
|
5530
|
+
return m ? m[1] : "";
|
|
5531
|
+
};
|
|
5532
|
+
var camelcaseOptionName = (name) => {
|
|
5533
|
+
return name.split(".").map((v, i) => {
|
|
5534
|
+
return i === 0 ? camelcase(v) : v;
|
|
5535
|
+
}).join(".");
|
|
5536
|
+
};
|
|
5537
|
+
|
|
5538
|
+
class CACError extends Error {
|
|
5539
|
+
constructor(message) {
|
|
5540
|
+
super(message);
|
|
5541
|
+
this.name = this.constructor.name;
|
|
5542
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
5543
|
+
Error.captureStackTrace(this, this.constructor);
|
|
5544
|
+
} else {
|
|
5545
|
+
this.stack = new Error(message).stack;
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
class Option {
|
|
5551
|
+
constructor(rawName, description, config) {
|
|
5552
|
+
this.rawName = rawName;
|
|
5553
|
+
this.description = description;
|
|
5554
|
+
this.config = Object.assign({}, config);
|
|
5555
|
+
rawName = rawName.replace(/\.\*/g, "");
|
|
5556
|
+
this.negated = false;
|
|
5557
|
+
this.names = removeBrackets(rawName).split(",").map((v) => {
|
|
5558
|
+
let name = v.trim().replace(/^-{1,2}/, "");
|
|
5559
|
+
if (name.startsWith("no-")) {
|
|
5560
|
+
this.negated = true;
|
|
5561
|
+
name = name.replace(/^no-/, "");
|
|
5562
|
+
}
|
|
5563
|
+
return camelcaseOptionName(name);
|
|
5564
|
+
}).sort((a, b) => a.length > b.length ? 1 : -1);
|
|
5565
|
+
this.name = this.names[this.names.length - 1];
|
|
5566
|
+
if (this.negated && this.config.default == null) {
|
|
5567
|
+
this.config.default = true;
|
|
5568
|
+
}
|
|
5569
|
+
if (rawName.includes("<")) {
|
|
5570
|
+
this.required = true;
|
|
5571
|
+
} else if (rawName.includes("[")) {
|
|
5572
|
+
this.required = false;
|
|
5573
|
+
} else {
|
|
5574
|
+
this.isBoolean = true;
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
var processArgs = process.argv;
|
|
5579
|
+
var platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
|
|
5580
|
+
|
|
5581
|
+
class Command {
|
|
5582
|
+
constructor(rawName, description, config = {}, cli) {
|
|
5583
|
+
this.rawName = rawName;
|
|
5584
|
+
this.description = description;
|
|
5585
|
+
this.config = config;
|
|
5586
|
+
this.cli = cli;
|
|
5587
|
+
this.options = [];
|
|
5588
|
+
this.aliasNames = [];
|
|
5589
|
+
this.name = removeBrackets(rawName);
|
|
5590
|
+
this.args = findAllBrackets(rawName);
|
|
5591
|
+
this.examples = [];
|
|
5592
|
+
}
|
|
5593
|
+
usage(text) {
|
|
5594
|
+
this.usageText = text;
|
|
5595
|
+
return this;
|
|
5596
|
+
}
|
|
5597
|
+
allowUnknownOptions() {
|
|
5598
|
+
this.config.allowUnknownOptions = true;
|
|
5599
|
+
return this;
|
|
5600
|
+
}
|
|
5601
|
+
ignoreOptionDefaultValue() {
|
|
5602
|
+
this.config.ignoreOptionDefaultValue = true;
|
|
5603
|
+
return this;
|
|
5604
|
+
}
|
|
5605
|
+
version(version, customFlags = "-v, --version") {
|
|
5606
|
+
this.versionNumber = version;
|
|
5607
|
+
this.option(customFlags, "Display version number");
|
|
5608
|
+
return this;
|
|
5609
|
+
}
|
|
5610
|
+
example(example) {
|
|
5611
|
+
this.examples.push(example);
|
|
5612
|
+
return this;
|
|
5613
|
+
}
|
|
5614
|
+
option(rawName, description, config) {
|
|
5615
|
+
const option = new Option(rawName, description, config);
|
|
5616
|
+
this.options.push(option);
|
|
5617
|
+
return this;
|
|
5618
|
+
}
|
|
5619
|
+
alias(name) {
|
|
5620
|
+
this.aliasNames.push(name);
|
|
5621
|
+
return this;
|
|
5622
|
+
}
|
|
5623
|
+
action(callback) {
|
|
5624
|
+
this.commandAction = callback;
|
|
5625
|
+
return this;
|
|
5626
|
+
}
|
|
5627
|
+
isMatched(name) {
|
|
5628
|
+
return this.name === name || this.aliasNames.includes(name);
|
|
5629
|
+
}
|
|
5630
|
+
get isDefaultCommand() {
|
|
5631
|
+
return this.name === "" || this.aliasNames.includes("!");
|
|
5632
|
+
}
|
|
5633
|
+
get isGlobalCommand() {
|
|
5634
|
+
return this instanceof GlobalCommand;
|
|
5635
|
+
}
|
|
5636
|
+
hasOption(name) {
|
|
5637
|
+
name = name.split(".")[0];
|
|
5638
|
+
return this.options.find((option) => {
|
|
5639
|
+
return option.names.includes(name);
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
outputHelp() {
|
|
5643
|
+
const { name, commands } = this.cli;
|
|
5644
|
+
const {
|
|
5645
|
+
versionNumber,
|
|
5646
|
+
options: globalOptions,
|
|
5647
|
+
helpCallback
|
|
5648
|
+
} = this.cli.globalCommand;
|
|
5649
|
+
let sections = [
|
|
5650
|
+
{
|
|
5651
|
+
body: `${name}${versionNumber ? `/${versionNumber}` : ""}`
|
|
5652
|
+
}
|
|
5653
|
+
];
|
|
5654
|
+
sections.push({
|
|
5655
|
+
title: "Usage",
|
|
5656
|
+
body: ` $ ${name} ${this.usageText || this.rawName}`
|
|
5657
|
+
});
|
|
5658
|
+
const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
|
|
5659
|
+
if (showCommands) {
|
|
5660
|
+
const longestCommandName = findLongest(commands.map((command) => command.rawName));
|
|
5661
|
+
sections.push({
|
|
5662
|
+
title: "Commands",
|
|
5663
|
+
body: commands.map((command) => {
|
|
5664
|
+
return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
|
|
5665
|
+
}).join(`
|
|
5666
|
+
`)
|
|
5667
|
+
});
|
|
5668
|
+
sections.push({
|
|
5669
|
+
title: `For more info, run any command with the \`--help\` flag`,
|
|
5670
|
+
body: commands.map((command) => ` $ ${name}${command.name === "" ? "" : ` ${command.name}`} --help`).join(`
|
|
5671
|
+
`)
|
|
5672
|
+
});
|
|
5673
|
+
}
|
|
5674
|
+
let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
|
|
5675
|
+
if (!this.isGlobalCommand && !this.isDefaultCommand) {
|
|
5676
|
+
options = options.filter((option) => option.name !== "version");
|
|
5677
|
+
}
|
|
5678
|
+
if (options.length > 0) {
|
|
5679
|
+
const longestOptionName = findLongest(options.map((option) => option.rawName));
|
|
5680
|
+
sections.push({
|
|
5681
|
+
title: "Options",
|
|
5682
|
+
body: options.map((option) => {
|
|
5683
|
+
return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? "" : `(default: ${option.config.default})`}`;
|
|
5684
|
+
}).join(`
|
|
5685
|
+
`)
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5688
|
+
if (this.examples.length > 0) {
|
|
5689
|
+
sections.push({
|
|
5690
|
+
title: "Examples",
|
|
5691
|
+
body: this.examples.map((example) => {
|
|
5692
|
+
if (typeof example === "function") {
|
|
5693
|
+
return example(name);
|
|
5694
|
+
}
|
|
5695
|
+
return example;
|
|
5696
|
+
}).join(`
|
|
5697
|
+
`)
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
if (helpCallback) {
|
|
5701
|
+
sections = helpCallback(sections) || sections;
|
|
5702
|
+
}
|
|
5703
|
+
console.log(sections.map((section) => {
|
|
5704
|
+
return section.title ? `${section.title}:
|
|
5705
|
+
${section.body}` : section.body;
|
|
5706
|
+
}).join(`
|
|
5707
|
+
|
|
5708
|
+
`));
|
|
5709
|
+
}
|
|
5710
|
+
outputVersion() {
|
|
5711
|
+
const { name } = this.cli;
|
|
5712
|
+
const { versionNumber } = this.cli.globalCommand;
|
|
5713
|
+
if (versionNumber) {
|
|
5714
|
+
console.log(`${name}/${versionNumber} ${platformInfo}`);
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
checkRequiredArgs() {
|
|
5718
|
+
const minimalArgsCount = this.args.filter((arg) => arg.required).length;
|
|
5719
|
+
if (this.cli.args.length < minimalArgsCount) {
|
|
5720
|
+
throw new CACError(`missing required args for command \`${this.rawName}\``);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
checkUnknownOptions() {
|
|
5724
|
+
const { options, globalCommand } = this.cli;
|
|
5725
|
+
if (!this.config.allowUnknownOptions) {
|
|
5726
|
+
for (const name of Object.keys(options)) {
|
|
5727
|
+
if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
|
|
5728
|
+
throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
checkOptionValue() {
|
|
5734
|
+
const { options: parsedOptions, globalCommand } = this.cli;
|
|
5735
|
+
const options = [...globalCommand.options, ...this.options];
|
|
5736
|
+
for (const option of options) {
|
|
5737
|
+
const value = parsedOptions[option.name.split(".")[0]];
|
|
5738
|
+
if (option.required) {
|
|
5739
|
+
const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
|
|
5740
|
+
if (value === true || value === false && !hasNegated) {
|
|
5741
|
+
throw new CACError(`option \`${option.rawName}\` value is missing`);
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5748
|
+
class GlobalCommand extends Command {
|
|
5749
|
+
constructor(cli) {
|
|
5750
|
+
super("@@global@@", "", {}, cli);
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
var __assign = Object.assign;
|
|
5754
|
+
|
|
5755
|
+
class CAC extends EventEmitter {
|
|
5756
|
+
constructor(name = "") {
|
|
5757
|
+
super();
|
|
5758
|
+
this.name = name;
|
|
5759
|
+
this.commands = [];
|
|
5760
|
+
this.rawArgs = [];
|
|
5761
|
+
this.args = [];
|
|
5762
|
+
this.options = {};
|
|
5763
|
+
this.globalCommand = new GlobalCommand(this);
|
|
5764
|
+
this.globalCommand.usage("<command> [options]");
|
|
5765
|
+
}
|
|
5766
|
+
usage(text) {
|
|
5767
|
+
this.globalCommand.usage(text);
|
|
5768
|
+
return this;
|
|
5769
|
+
}
|
|
5770
|
+
command(rawName, description, config) {
|
|
5771
|
+
const command = new Command(rawName, description || "", config, this);
|
|
5772
|
+
command.globalCommand = this.globalCommand;
|
|
5773
|
+
this.commands.push(command);
|
|
5774
|
+
return command;
|
|
5775
|
+
}
|
|
5776
|
+
option(rawName, description, config) {
|
|
5777
|
+
this.globalCommand.option(rawName, description, config);
|
|
5778
|
+
return this;
|
|
5779
|
+
}
|
|
5780
|
+
help(callback) {
|
|
5781
|
+
this.globalCommand.option("-h, --help", "Display this message");
|
|
5782
|
+
this.globalCommand.helpCallback = callback;
|
|
5783
|
+
this.showHelpOnExit = true;
|
|
5784
|
+
return this;
|
|
5785
|
+
}
|
|
5786
|
+
version(version, customFlags = "-v, --version") {
|
|
5787
|
+
this.globalCommand.version(version, customFlags);
|
|
5788
|
+
this.showVersionOnExit = true;
|
|
5789
|
+
return this;
|
|
5790
|
+
}
|
|
5791
|
+
example(example) {
|
|
5792
|
+
this.globalCommand.example(example);
|
|
5793
|
+
return this;
|
|
5794
|
+
}
|
|
5795
|
+
outputHelp() {
|
|
5796
|
+
if (this.matchedCommand) {
|
|
5797
|
+
this.matchedCommand.outputHelp();
|
|
5798
|
+
} else {
|
|
5799
|
+
this.globalCommand.outputHelp();
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
outputVersion() {
|
|
5803
|
+
this.globalCommand.outputVersion();
|
|
5804
|
+
}
|
|
5805
|
+
setParsedInfo({ args, options }, matchedCommand, matchedCommandName) {
|
|
5806
|
+
this.args = args;
|
|
5807
|
+
this.options = options;
|
|
5808
|
+
if (matchedCommand) {
|
|
5809
|
+
this.matchedCommand = matchedCommand;
|
|
5810
|
+
}
|
|
5811
|
+
if (matchedCommandName) {
|
|
5812
|
+
this.matchedCommandName = matchedCommandName;
|
|
5813
|
+
}
|
|
5814
|
+
return this;
|
|
5815
|
+
}
|
|
5816
|
+
unsetMatchedCommand() {
|
|
5817
|
+
this.matchedCommand = undefined;
|
|
5818
|
+
this.matchedCommandName = undefined;
|
|
5819
|
+
}
|
|
5820
|
+
parse(argv = processArgs, {
|
|
5821
|
+
run = true
|
|
5822
|
+
} = {}) {
|
|
5823
|
+
this.rawArgs = argv;
|
|
5824
|
+
if (!this.name) {
|
|
5825
|
+
this.name = argv[1] ? getFileName(argv[1]) : "cli";
|
|
5826
|
+
}
|
|
5827
|
+
let shouldParse = true;
|
|
5828
|
+
for (const command of this.commands) {
|
|
5829
|
+
const parsed = this.mri(argv.slice(2), command);
|
|
5830
|
+
const commandName = parsed.args[0];
|
|
5831
|
+
if (command.isMatched(commandName)) {
|
|
5832
|
+
shouldParse = false;
|
|
5833
|
+
const parsedInfo = __assign(__assign({}, parsed), {
|
|
5834
|
+
args: parsed.args.slice(1)
|
|
5835
|
+
});
|
|
5836
|
+
this.setParsedInfo(parsedInfo, command, commandName);
|
|
5837
|
+
this.emit(`command:${commandName}`, command);
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
if (shouldParse) {
|
|
5841
|
+
for (const command of this.commands) {
|
|
5842
|
+
if (command.name === "") {
|
|
5843
|
+
shouldParse = false;
|
|
5844
|
+
const parsed = this.mri(argv.slice(2), command);
|
|
5845
|
+
this.setParsedInfo(parsed, command);
|
|
5846
|
+
this.emit(`command:!`, command);
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
if (shouldParse) {
|
|
5851
|
+
const parsed = this.mri(argv.slice(2));
|
|
5852
|
+
this.setParsedInfo(parsed);
|
|
5853
|
+
}
|
|
5854
|
+
if (this.options.help && this.showHelpOnExit) {
|
|
5855
|
+
this.outputHelp();
|
|
5856
|
+
run = false;
|
|
5857
|
+
this.unsetMatchedCommand();
|
|
5858
|
+
}
|
|
5859
|
+
if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
|
|
5860
|
+
this.outputVersion();
|
|
5861
|
+
run = false;
|
|
5862
|
+
this.unsetMatchedCommand();
|
|
5863
|
+
}
|
|
5864
|
+
const parsedArgv = { args: this.args, options: this.options };
|
|
5865
|
+
if (run) {
|
|
5866
|
+
this.runMatchedCommand();
|
|
5867
|
+
}
|
|
5868
|
+
if (!this.matchedCommand && this.args[0]) {
|
|
5869
|
+
this.emit("command:*");
|
|
5870
|
+
}
|
|
5871
|
+
return parsedArgv;
|
|
5872
|
+
}
|
|
5873
|
+
mri(argv, command) {
|
|
5874
|
+
const cliOptions = [
|
|
5875
|
+
...this.globalCommand.options,
|
|
5876
|
+
...command ? command.options : []
|
|
5877
|
+
];
|
|
5878
|
+
const mriOptions = getMriOptions(cliOptions);
|
|
5879
|
+
let argsAfterDoubleDashes = [];
|
|
5880
|
+
const doubleDashesIndex = argv.indexOf("--");
|
|
5881
|
+
if (doubleDashesIndex > -1) {
|
|
5882
|
+
argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
|
|
5883
|
+
argv = argv.slice(0, doubleDashesIndex);
|
|
5884
|
+
}
|
|
5885
|
+
let parsed = mri2(argv, mriOptions);
|
|
5886
|
+
parsed = Object.keys(parsed).reduce((res, name) => {
|
|
5887
|
+
return __assign(__assign({}, res), {
|
|
5888
|
+
[camelcaseOptionName(name)]: parsed[name]
|
|
5889
|
+
});
|
|
5890
|
+
}, { _: [] });
|
|
5891
|
+
const args = parsed._;
|
|
5892
|
+
const options = {
|
|
5893
|
+
"--": argsAfterDoubleDashes
|
|
5894
|
+
};
|
|
5895
|
+
const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
|
|
5896
|
+
let transforms = Object.create(null);
|
|
5897
|
+
for (const cliOption of cliOptions) {
|
|
5898
|
+
if (!ignoreDefault && cliOption.config.default !== undefined) {
|
|
5899
|
+
for (const name of cliOption.names) {
|
|
5900
|
+
options[name] = cliOption.config.default;
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
if (Array.isArray(cliOption.config.type)) {
|
|
5904
|
+
if (transforms[cliOption.name] === undefined) {
|
|
5905
|
+
transforms[cliOption.name] = Object.create(null);
|
|
5906
|
+
transforms[cliOption.name]["shouldTransform"] = true;
|
|
5907
|
+
transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
for (const key of Object.keys(parsed)) {
|
|
5912
|
+
if (key !== "_") {
|
|
5913
|
+
const keys = key.split(".");
|
|
5914
|
+
setDotProp(options, keys, parsed[key]);
|
|
5915
|
+
setByType(options, transforms);
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
return {
|
|
5919
|
+
args,
|
|
5920
|
+
options
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5923
|
+
runMatchedCommand() {
|
|
5924
|
+
const { args, options, matchedCommand: command } = this;
|
|
5925
|
+
if (!command || !command.commandAction)
|
|
5926
|
+
return;
|
|
5927
|
+
command.checkUnknownOptions();
|
|
5928
|
+
command.checkOptionValue();
|
|
5929
|
+
command.checkRequiredArgs();
|
|
5930
|
+
const actionArgs = [];
|
|
5931
|
+
command.args.forEach((arg, index) => {
|
|
5932
|
+
if (arg.variadic) {
|
|
5933
|
+
actionArgs.push(args.slice(index));
|
|
5934
|
+
} else {
|
|
5935
|
+
actionArgs.push(args[index]);
|
|
5936
|
+
}
|
|
5937
|
+
});
|
|
5938
|
+
actionArgs.push(options);
|
|
5939
|
+
return command.commandAction.apply(this, actionArgs);
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
var cac = (name = "") => new CAC(name);
|
|
5943
|
+
|
|
5308
5944
|
// src/sqlite.ts
|
|
5309
5945
|
class BunSqliteConnection {
|
|
5310
5946
|
db;
|
|
@@ -5456,22 +6092,174 @@ function delay(ms) {
|
|
|
5456
6092
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5457
6093
|
}
|
|
5458
6094
|
|
|
6095
|
+
// node_modules/@absurd-sqlite/sdk/dist/sqlite.js
|
|
6096
|
+
var sqliteRetryableErrorCodes2 = new Set(["SQLITE_BUSY", "SQLITE_LOCKED"]);
|
|
6097
|
+
var sqliteRetryableErrnos2 = new Set([5, 6]);
|
|
6098
|
+
|
|
6099
|
+
// node_modules/@absurd-sqlite/sdk/dist/extension-downloader.js
|
|
6100
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from "fs";
|
|
6101
|
+
import { join } from "path";
|
|
6102
|
+
import { homedir } from "os";
|
|
6103
|
+
import { createHash } from "crypto";
|
|
6104
|
+
function getPlatformInfo() {
|
|
6105
|
+
const platform = process.platform;
|
|
6106
|
+
const arch = process.arch;
|
|
6107
|
+
let os2;
|
|
6108
|
+
let ext;
|
|
6109
|
+
switch (platform) {
|
|
6110
|
+
case "darwin":
|
|
6111
|
+
os2 = "macOS";
|
|
6112
|
+
ext = "dylib";
|
|
6113
|
+
break;
|
|
6114
|
+
case "linux":
|
|
6115
|
+
os2 = "Linux";
|
|
6116
|
+
ext = "so";
|
|
6117
|
+
break;
|
|
6118
|
+
case "win32":
|
|
6119
|
+
os2 = "Windows";
|
|
6120
|
+
ext = "dll";
|
|
6121
|
+
break;
|
|
6122
|
+
default:
|
|
6123
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
6124
|
+
}
|
|
6125
|
+
let archStr;
|
|
6126
|
+
switch (arch) {
|
|
6127
|
+
case "x64":
|
|
6128
|
+
archStr = "X64";
|
|
6129
|
+
break;
|
|
6130
|
+
case "arm64":
|
|
6131
|
+
archStr = "ARM64";
|
|
6132
|
+
break;
|
|
6133
|
+
default:
|
|
6134
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
6135
|
+
}
|
|
6136
|
+
return { os: os2, arch: archStr, ext };
|
|
6137
|
+
}
|
|
6138
|
+
function getDefaultCacheDir() {
|
|
6139
|
+
return join(homedir(), ".cache", "absurd-sqlite", "extensions");
|
|
6140
|
+
}
|
|
6141
|
+
function getAssetName(version, platform) {
|
|
6142
|
+
return `absurd-absurd-sqlite-extension-${version}-${platform.os}-${platform.arch}.${platform.ext}`;
|
|
6143
|
+
}
|
|
6144
|
+
function getTag(version) {
|
|
6145
|
+
return `absurd-sqlite-extension/${version}`;
|
|
6146
|
+
}
|
|
6147
|
+
async function fetchLatestVersion(owner, repo) {
|
|
6148
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/releases`;
|
|
6149
|
+
const response = await fetch(url);
|
|
6150
|
+
if (!response.ok) {
|
|
6151
|
+
throw new Error(`Failed to fetch releases: ${response.status} ${response.statusText}`);
|
|
6152
|
+
}
|
|
6153
|
+
const releases = await response.json();
|
|
6154
|
+
for (const release of releases) {
|
|
6155
|
+
if (!release.draft && release.tag_name.startsWith("absurd-sqlite-extension/")) {
|
|
6156
|
+
return release.tag_name.replace("absurd-sqlite-extension/", "");
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
throw new Error("No extension releases found");
|
|
6160
|
+
}
|
|
6161
|
+
async function downloadAsset(owner, repo, tag, assetName, destPath, expectedChecksum) {
|
|
6162
|
+
const url = `https://github.com/${owner}/${repo}/releases/download/${tag}/${assetName}`;
|
|
6163
|
+
const response = await fetch(url);
|
|
6164
|
+
if (!response.ok) {
|
|
6165
|
+
throw new Error(`Failed to download extension: ${response.status} ${response.statusText} from ${url}`);
|
|
6166
|
+
}
|
|
6167
|
+
const buffer = await response.arrayBuffer();
|
|
6168
|
+
writeFileSync(destPath, Buffer.from(buffer));
|
|
6169
|
+
if (expectedChecksum) {
|
|
6170
|
+
const actualChecksum = calculateChecksum(destPath);
|
|
6171
|
+
if (actualChecksum !== expectedChecksum.toLowerCase()) {
|
|
6172
|
+
throw new Error(`Checksum verification failed. Expected: ${expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`);
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
if (process.platform !== "win32") {
|
|
6176
|
+
chmodSync(destPath, 493);
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
function calculateChecksum(filePath) {
|
|
6180
|
+
const fileBuffer = readFileSync(filePath);
|
|
6181
|
+
const hash = createHash("sha256");
|
|
6182
|
+
hash.update(fileBuffer);
|
|
6183
|
+
return hash.digest("hex");
|
|
6184
|
+
}
|
|
6185
|
+
function getCachedPath(cacheDir, version, platform) {
|
|
6186
|
+
const ext = platform.ext;
|
|
6187
|
+
return join(cacheDir, version, `libabsurd.${ext}`);
|
|
6188
|
+
}
|
|
6189
|
+
async function downloadExtension(options = {}) {
|
|
6190
|
+
const owner = options.owner ?? "b4fun";
|
|
6191
|
+
const repo = options.repo ?? "absurd-sqlite";
|
|
6192
|
+
const cacheDir = options.cacheDir ?? getDefaultCacheDir();
|
|
6193
|
+
const force = options.force ?? false;
|
|
6194
|
+
let version = options.version ?? "latest";
|
|
6195
|
+
if (version === "latest") {
|
|
6196
|
+
version = await fetchLatestVersion(owner, repo);
|
|
6197
|
+
}
|
|
6198
|
+
const platform = getPlatformInfo();
|
|
6199
|
+
const assetName = getAssetName(version, platform);
|
|
6200
|
+
const tag = getTag(version);
|
|
6201
|
+
const cachedPath = getCachedPath(cacheDir, version, platform);
|
|
6202
|
+
if (!force && existsSync(cachedPath)) {
|
|
6203
|
+
if (options.expectedChecksum) {
|
|
6204
|
+
const actualChecksum = calculateChecksum(cachedPath);
|
|
6205
|
+
if (actualChecksum !== options.expectedChecksum.toLowerCase()) {
|
|
6206
|
+
throw new Error(`Cached file checksum verification failed. Expected: ${options.expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`);
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
return cachedPath;
|
|
6210
|
+
}
|
|
6211
|
+
const versionDir = join(cacheDir, version);
|
|
6212
|
+
mkdirSync(versionDir, { recursive: true });
|
|
6213
|
+
await downloadAsset(owner, repo, tag, assetName, cachedPath, options.expectedChecksum);
|
|
6214
|
+
return cachedPath;
|
|
6215
|
+
}
|
|
5459
6216
|
// src/index.ts
|
|
6217
|
+
function parseCliOptions() {
|
|
6218
|
+
const cli = cac("bun-worker");
|
|
6219
|
+
cli.option("-c, --concurrency <number>", "Number of tasks to process concurrently", {
|
|
6220
|
+
default: 10
|
|
6221
|
+
}).option("--database-path <path>", "SQLite database file path").option("--extension-path <path>", "Absurd-SQLite extension path").help();
|
|
6222
|
+
const parsed = cli.parse(process.argv, { run: false });
|
|
6223
|
+
if (parsed.options.help) {
|
|
6224
|
+
process.exit(0);
|
|
6225
|
+
}
|
|
6226
|
+
const options = {};
|
|
6227
|
+
if (parsed.options.databasePath) {
|
|
6228
|
+
options.dbPath = parsed.options.databasePath;
|
|
6229
|
+
}
|
|
6230
|
+
if (parsed.options.extensionPath) {
|
|
6231
|
+
options.extensionPath = parsed.options.extensionPath;
|
|
6232
|
+
}
|
|
6233
|
+
if (parsed.options.concurrency) {
|
|
6234
|
+
const value = parseInt(parsed.options.concurrency, 10);
|
|
6235
|
+
if (!isNaN(value) && value > 0) {
|
|
6236
|
+
options.concurrency = value;
|
|
6237
|
+
} else {
|
|
6238
|
+
console.warn(`Invalid value for --concurrency: "${parsed.options.concurrency}" (must be a positive integer)`);
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
return options;
|
|
6242
|
+
}
|
|
5460
6243
|
async function run(setupFunction) {
|
|
5461
|
-
const
|
|
5462
|
-
const
|
|
6244
|
+
const cliOptions = parseCliOptions();
|
|
6245
|
+
const dbPath = cliOptions.dbPath || process.env.ABSURD_DATABASE_PATH;
|
|
6246
|
+
const extensionPath = cliOptions.extensionPath || process.env.ABSURD_DATABASE_EXTENSION_PATH;
|
|
5463
6247
|
if (!dbPath) {
|
|
5464
|
-
throw new Error("
|
|
6248
|
+
throw new Error("Database path is required. Set ABSURD_DATABASE_PATH environment variable or use --database-path flag.");
|
|
5465
6249
|
}
|
|
5466
6250
|
if (!extensionPath) {
|
|
5467
|
-
throw new Error("
|
|
6251
|
+
throw new Error("Extension path is required. Set ABSURD_DATABASE_EXTENSION_PATH environment variable or use --extension-path flag.");
|
|
5468
6252
|
}
|
|
5469
6253
|
const db = new Database(dbPath);
|
|
5470
6254
|
db.loadExtension(extensionPath);
|
|
5471
6255
|
const conn = new BunSqliteConnection(db);
|
|
5472
6256
|
const absurd = new Absurd({ db: conn });
|
|
5473
6257
|
await setupFunction(absurd);
|
|
5474
|
-
const
|
|
6258
|
+
const workerOptions = {
|
|
6259
|
+
...getDefaultWorkerOptions(),
|
|
6260
|
+
...cliOptions.concurrency !== undefined ? { concurrency: cliOptions.concurrency } : {}
|
|
6261
|
+
};
|
|
6262
|
+
const worker = await absurd.startWorker(workerOptions);
|
|
5475
6263
|
let shuttingDown = false;
|
|
5476
6264
|
const shutdown = async (signal) => {
|
|
5477
6265
|
if (shuttingDown) {
|
|
@@ -5493,6 +6281,14 @@ async function run(setupFunction) {
|
|
|
5493
6281
|
shutdown("SIGTERM");
|
|
5494
6282
|
});
|
|
5495
6283
|
}
|
|
6284
|
+
function getDefaultWorkerOptions() {
|
|
6285
|
+
return {
|
|
6286
|
+
concurrency: 10,
|
|
6287
|
+
pollInterval: 5,
|
|
6288
|
+
claimTimeout: 60
|
|
6289
|
+
};
|
|
6290
|
+
}
|
|
5496
6291
|
export {
|
|
6292
|
+
downloadExtension,
|
|
5497
6293
|
run as default
|
|
5498
6294
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absurd-sqlite/bun-worker",
|
|
3
|
-
"version": "0.2.2-alpha.
|
|
3
|
+
"version": "0.2.2-alpha.2",
|
|
4
4
|
"description": "Bun worker utilities for Absurd-SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"homepage": "https://github.com/b4fun/absurd-sqlite#readme",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@absurd-sqlite/sdk": "
|
|
42
|
-
"absurd-sdk": "
|
|
41
|
+
"@absurd-sqlite/sdk": "next",
|
|
42
|
+
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz",
|
|
43
|
+
"cac": "^6.7.14"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"bun-types": "^1.3.5",
|
package/src/index.ts
CHANGED
|
@@ -1,32 +1,103 @@
|
|
|
1
|
-
import { Absurd } from "absurd-sdk";
|
|
1
|
+
import { Absurd, type WorkerOptions } from "absurd-sdk";
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
4
|
+
import { cac } from "cac";
|
|
4
5
|
|
|
5
6
|
import { BunSqliteConnection } from "./sqlite";
|
|
6
7
|
|
|
7
8
|
export type { AbsurdClient } from "@absurd-sqlite/sdk";
|
|
9
|
+
export type { WorkerOptions } from "absurd-sdk";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
downloadExtension,
|
|
13
|
+
type DownloadExtensionOptions,
|
|
14
|
+
} from "@absurd-sqlite/sdk";
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* Register tasks and perform any one-time setup before the worker starts.
|
|
11
18
|
*/
|
|
12
19
|
export type SetupFunction = (absurd: AbsurdClient) => void | Promise<void>;
|
|
13
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Parsed CLI options.
|
|
23
|
+
*/
|
|
24
|
+
interface ParsedOptions {
|
|
25
|
+
dbPath?: string;
|
|
26
|
+
extensionPath?: string;
|
|
27
|
+
concurrency?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parses CLI arguments and returns parsed options.
|
|
32
|
+
*/
|
|
33
|
+
function parseCliOptions(): ParsedOptions {
|
|
34
|
+
const cli = cac("bun-worker");
|
|
35
|
+
|
|
36
|
+
cli
|
|
37
|
+
.option(
|
|
38
|
+
"-c, --concurrency <number>",
|
|
39
|
+
"Number of tasks to process concurrently",
|
|
40
|
+
{
|
|
41
|
+
default: 10,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
.option("--database-path <path>", "SQLite database file path")
|
|
45
|
+
.option("--extension-path <path>", "Absurd-SQLite extension path")
|
|
46
|
+
.help();
|
|
47
|
+
|
|
48
|
+
const parsed = cli.parse(process.argv, { run: false });
|
|
49
|
+
|
|
50
|
+
// If help was requested, cac will output it and we should exit gracefully
|
|
51
|
+
if (parsed.options.help) {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const options: ParsedOptions = {};
|
|
56
|
+
|
|
57
|
+
if (parsed.options.databasePath) {
|
|
58
|
+
options.dbPath = parsed.options.databasePath;
|
|
59
|
+
}
|
|
60
|
+
if (parsed.options.extensionPath) {
|
|
61
|
+
options.extensionPath = parsed.options.extensionPath;
|
|
62
|
+
}
|
|
63
|
+
if (parsed.options.concurrency) {
|
|
64
|
+
const value = parseInt(parsed.options.concurrency, 10);
|
|
65
|
+
if (!isNaN(value) && value > 0) {
|
|
66
|
+
options.concurrency = value;
|
|
67
|
+
} else {
|
|
68
|
+
console.warn(
|
|
69
|
+
`Invalid value for --concurrency: "${parsed.options.concurrency}" (must be a positive integer)`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return options;
|
|
75
|
+
}
|
|
76
|
+
|
|
14
77
|
/**
|
|
15
78
|
* Boots a worker using Bun's SQLite driver and Absurd's task engine.
|
|
16
79
|
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
80
|
+
* CLI flags:
|
|
81
|
+
* - --concurrency, -c: Number of tasks to process concurrently (default: 10)
|
|
82
|
+
* - --database-path: SQLite database file path (overrides ABSURD_DATABASE_PATH)
|
|
83
|
+
* - --extension-path: Absurd-SQLite extension path (overrides ABSURD_DATABASE_EXTENSION_PATH)
|
|
20
84
|
*/
|
|
21
85
|
export default async function run(setupFunction: SetupFunction): Promise<void> {
|
|
22
|
-
const
|
|
23
|
-
|
|
86
|
+
const cliOptions = parseCliOptions();
|
|
87
|
+
|
|
88
|
+
const dbPath = cliOptions.dbPath || process.env.ABSURD_DATABASE_PATH;
|
|
89
|
+
const extensionPath =
|
|
90
|
+
cliOptions.extensionPath || process.env.ABSURD_DATABASE_EXTENSION_PATH;
|
|
24
91
|
|
|
25
92
|
if (!dbPath) {
|
|
26
|
-
throw new Error(
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Database path is required. Set ABSURD_DATABASE_PATH environment variable or use --database-path flag."
|
|
95
|
+
);
|
|
27
96
|
}
|
|
28
97
|
if (!extensionPath) {
|
|
29
|
-
throw new Error(
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Extension path is required. Set ABSURD_DATABASE_EXTENSION_PATH environment variable or use --extension-path flag."
|
|
100
|
+
);
|
|
30
101
|
}
|
|
31
102
|
|
|
32
103
|
const db = new Database(dbPath);
|
|
@@ -38,7 +109,18 @@ export default async function run(setupFunction: SetupFunction): Promise<void> {
|
|
|
38
109
|
const absurd = new Absurd({ db: conn });
|
|
39
110
|
|
|
40
111
|
await setupFunction(absurd);
|
|
41
|
-
|
|
112
|
+
|
|
113
|
+
// Merge worker options from multiple sources:
|
|
114
|
+
// 1. Default options
|
|
115
|
+
// 2. CLI flags
|
|
116
|
+
const workerOptions: WorkerOptions = {
|
|
117
|
+
...getDefaultWorkerOptions(),
|
|
118
|
+
...(cliOptions.concurrency !== undefined
|
|
119
|
+
? { concurrency: cliOptions.concurrency }
|
|
120
|
+
: {}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const worker = await absurd.startWorker(workerOptions);
|
|
42
124
|
|
|
43
125
|
let shuttingDown = false;
|
|
44
126
|
const shutdown = async (signal: string) => {
|
|
@@ -62,3 +144,14 @@ export default async function run(setupFunction: SetupFunction): Promise<void> {
|
|
|
62
144
|
void shutdown("SIGTERM");
|
|
63
145
|
});
|
|
64
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns default worker options.
|
|
150
|
+
*/
|
|
151
|
+
function getDefaultWorkerOptions(): WorkerOptions {
|
|
152
|
+
return {
|
|
153
|
+
concurrency: 10,
|
|
154
|
+
pollInterval: 5,
|
|
155
|
+
claimTimeout: 60,
|
|
156
|
+
};
|
|
157
|
+
}
|
package/test/basic.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
expect,
|
|
5
5
|
beforeAll,
|
|
6
6
|
afterEach,
|
|
7
|
+
jest,
|
|
7
8
|
} from "bun:test";
|
|
8
9
|
import assert from "node:assert/strict";
|
|
9
10
|
import type { Absurd } from "absurd-sdk";
|
|
@@ -501,5 +502,56 @@ describe("Basic SDK Operations", () => {
|
|
|
501
502
|
);
|
|
502
503
|
});
|
|
503
504
|
});
|
|
505
|
+
|
|
506
|
+
test("heartbeat keeps task alive past original claim timeout", async () => {
|
|
507
|
+
const claimTimeout = 1;
|
|
508
|
+
const extension = 10;
|
|
509
|
+
const longWorkMs = claimTimeout * 2000 + 100;
|
|
510
|
+
let heartbeatFired = false;
|
|
511
|
+
|
|
512
|
+
absurd.registerTask(
|
|
513
|
+
{ name: "heartbeat-long-task" },
|
|
514
|
+
async (_params, taskCtx) => {
|
|
515
|
+
await taskCtx.heartbeat(extension);
|
|
516
|
+
heartbeatFired = true;
|
|
517
|
+
await ctx.sleep(longWorkMs);
|
|
518
|
+
return { ok: true };
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const { taskID } = await absurd.spawn("heartbeat-long-task", {});
|
|
523
|
+
|
|
524
|
+
const exitSpy = jest
|
|
525
|
+
.spyOn(process, "exit")
|
|
526
|
+
.mockImplementation(() => undefined as never);
|
|
527
|
+
|
|
528
|
+
const worker = await absurd.startWorker({
|
|
529
|
+
claimTimeout,
|
|
530
|
+
concurrency: 1,
|
|
531
|
+
pollInterval: 0.01,
|
|
532
|
+
fatalOnLeaseTimeout: true,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
await waitFor(() => expect(heartbeatFired).toBe(true), {
|
|
537
|
+
timeout: 500,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await waitFor(async () => {
|
|
541
|
+
const task = await ctx.getTask(taskID);
|
|
542
|
+
expect(task?.state).toBe("completed");
|
|
543
|
+
}, {
|
|
544
|
+
timeout: longWorkMs + 2000,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const runs = await ctx.getRuns(taskID);
|
|
548
|
+
expect(runs).toHaveLength(1);
|
|
549
|
+
expect(runs[0]?.state).toBe("completed");
|
|
550
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
551
|
+
} finally {
|
|
552
|
+
await worker.close();
|
|
553
|
+
exitSpy.mockRestore();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
504
556
|
});
|
|
505
557
|
});
|
package/test/cli.test.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const testDir = fileURLToPath(new URL(".", import.meta.url));
|
|
9
|
+
const repoRoot = join(testDir, "../../..");
|
|
10
|
+
const extensionBase = join(repoRoot, "target/release/libabsurd");
|
|
11
|
+
|
|
12
|
+
function resolveExtensionPath(base: string): string {
|
|
13
|
+
const platformExt =
|
|
14
|
+
process.platform === "win32"
|
|
15
|
+
? ".dll"
|
|
16
|
+
: process.platform === "darwin"
|
|
17
|
+
? ".dylib"
|
|
18
|
+
: ".so";
|
|
19
|
+
const candidates = [base, `${base}${platformExt}`];
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
if (existsSync(candidate)) {
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new Error(
|
|
26
|
+
`SQLite extension not found at ${base} (expected ${platformExt})`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const extensionPath = resolveExtensionPath(extensionBase);
|
|
31
|
+
|
|
32
|
+
describe("Worker configuration", () => {
|
|
33
|
+
let tempDir: string;
|
|
34
|
+
let dbPath: string;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
tempDir = mkdtempSync(join(tmpdir(), "absurd-cli-test-"));
|
|
38
|
+
dbPath = join(tempDir, "test.db");
|
|
39
|
+
|
|
40
|
+
// Initialize database with migrations
|
|
41
|
+
const db = new Database(dbPath);
|
|
42
|
+
(db as unknown as { loadExtension(path: string): void }).loadExtension(
|
|
43
|
+
extensionPath
|
|
44
|
+
);
|
|
45
|
+
db.query("select absurd_apply_migrations()").get();
|
|
46
|
+
db.close();
|
|
47
|
+
|
|
48
|
+
process.env.ABSURD_DATABASE_PATH = dbPath;
|
|
49
|
+
process.env.ABSURD_DATABASE_EXTENSION_PATH = extensionPath;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
if (tempDir) {
|
|
54
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
delete process.env.ABSURD_DATABASE_PATH;
|
|
57
|
+
delete process.env.ABSURD_DATABASE_EXTENSION_PATH;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("works with default options when none provided", async () => {
|
|
61
|
+
const { default: run } = await import("../src/index");
|
|
62
|
+
|
|
63
|
+
let workerStarted = false;
|
|
64
|
+
|
|
65
|
+
const promise = run(async (absurd) => {
|
|
66
|
+
await absurd.createQueue("default");
|
|
67
|
+
absurd.registerTask({ name: "test" }, async () => {
|
|
68
|
+
return { ok: true };
|
|
69
|
+
});
|
|
70
|
+
workerStarted = true;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Give the worker time to start
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
75
|
+
|
|
76
|
+
expect(workerStarted).toBe(true);
|
|
77
|
+
|
|
78
|
+
// Clean up by sending SIGINT
|
|
79
|
+
process.emit("SIGINT");
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
package/test/run.test.ts
CHANGED
|
@@ -62,7 +62,7 @@ describe("run", () => {
|
|
|
62
62
|
const { default: run } = await import("../src/index");
|
|
63
63
|
|
|
64
64
|
await expect(run(() => {})).rejects.toThrow(
|
|
65
|
-
"
|
|
65
|
+
"Database path is required"
|
|
66
66
|
);
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -71,7 +71,7 @@ describe("run", () => {
|
|
|
71
71
|
const { default: run } = await import("../src/index");
|
|
72
72
|
|
|
73
73
|
await expect(run(() => {})).rejects.toThrow(
|
|
74
|
-
"
|
|
74
|
+
"Extension path is required"
|
|
75
75
|
);
|
|
76
76
|
});
|
|
77
77
|
|