bare-agent 0.10.4 → 0.12.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.
Files changed (65) hide show
  1. package/bin/cli.d.ts +4 -0
  2. package/bin/cli.js +70 -12
  3. package/bin/test-provider.d.ts +2 -0
  4. package/bin/test-provider.js +5 -1
  5. package/index.d.ts +20 -0
  6. package/package.json +44 -10
  7. package/src/bareguard-adapter.d.ts +118 -0
  8. package/src/bareguard-adapter.js +75 -3
  9. package/src/checkpoint.d.ts +61 -0
  10. package/src/checkpoint.js +17 -8
  11. package/src/circuit-breaker.d.ts +70 -0
  12. package/src/circuit-breaker.js +20 -4
  13. package/src/errors.d.ts +106 -0
  14. package/src/errors.js +50 -1
  15. package/src/loop.d.ts +135 -0
  16. package/src/loop.js +80 -18
  17. package/src/mcp-bridge.d.ts +133 -0
  18. package/src/mcp-bridge.js +199 -26
  19. package/src/mcp.d.ts +4 -0
  20. package/src/memory.d.ts +50 -0
  21. package/src/memory.js +22 -2
  22. package/src/planner.d.ts +62 -0
  23. package/src/planner.js +26 -7
  24. package/src/provider-anthropic.d.ts +55 -0
  25. package/src/provider-anthropic.js +34 -10
  26. package/src/provider-clipipe.d.ts +86 -0
  27. package/src/provider-clipipe.js +28 -18
  28. package/src/provider-fallback.d.ts +44 -0
  29. package/src/provider-fallback.js +18 -8
  30. package/src/provider-ollama.d.ts +41 -0
  31. package/src/provider-ollama.js +29 -7
  32. package/src/provider-openai.d.ts +57 -0
  33. package/src/provider-openai.js +34 -7
  34. package/src/providers.d.ts +6 -0
  35. package/src/retry.d.ts +44 -0
  36. package/src/retry.js +15 -1
  37. package/src/run-plan.d.ts +126 -0
  38. package/src/run-plan.js +46 -13
  39. package/src/scheduler.d.ts +102 -0
  40. package/src/scheduler.js +32 -4
  41. package/src/state.d.ts +45 -0
  42. package/src/state.js +18 -2
  43. package/src/store-jsonfile.d.ts +85 -0
  44. package/src/store-jsonfile.js +50 -8
  45. package/src/store-sqlite.d.ts +90 -0
  46. package/src/store-sqlite.js +31 -7
  47. package/src/stores.d.ts +3 -0
  48. package/src/stream.d.ts +79 -0
  49. package/src/stream.js +32 -0
  50. package/src/tools.d.ts +8 -0
  51. package/src/transport-jsonl.d.ts +30 -0
  52. package/src/transport-jsonl.js +13 -0
  53. package/src/transports.d.ts +2 -0
  54. package/tools/browse.d.ts +10 -0
  55. package/tools/browse.js +2 -0
  56. package/tools/defer.d.ts +33 -0
  57. package/tools/defer.js +12 -3
  58. package/tools/mobile.d.ts +34 -0
  59. package/tools/mobile.js +28 -15
  60. package/tools/shell.d.ts +31 -0
  61. package/tools/shell.js +83 -6
  62. package/tools/spawn.d.ts +107 -0
  63. package/tools/spawn.js +24 -5
  64. package/types/index.d.ts +66 -0
  65. package/types/shims.d.ts +16 -0
@@ -4,22 +4,44 @@ const https = require('https');
4
4
  const http = require('http');
5
5
  const { ProviderError } = require('./errors');
6
6
 
7
+ /** @typedef {import('../types').Message} Message */
8
+ /** @typedef {import('../types').ToolDef} ToolDef */
9
+ /** @typedef {import('../types').ToolCall} ToolCall */
10
+ /** @typedef {import('../types').GenerateResult} GenerateResult */
11
+
12
+ /**
13
+ * @typedef {object} OpenAIOptions
14
+ * @property {string} [apiKey]
15
+ * @property {string} [model='gpt-4o-mini']
16
+ * @property {string} [baseUrl='https://api.openai.com/v1']
17
+ * @property {boolean} [exposeErrorBody=false] - Attach the full upstream
18
+ * response to `err.body` on HTTP errors. Off by default so an unexpected
19
+ * field in an error payload can't leak through logs that dump the error
20
+ * object; `err.message` still carries the API's error message. Turn on for
21
+ * debugging only.
22
+ */
23
+
7
24
  class OpenAIProvider {
25
+ /**
26
+ * @param {OpenAIOptions} [options]
27
+ */
8
28
  constructor(options = {}) {
9
29
  this.apiKey = options.apiKey?.trim();
10
30
  this.model = options.model || 'gpt-4o-mini';
11
31
  this.baseUrl = options.baseUrl || 'https://api.openai.com/v1';
32
+ this.exposeErrorBody = options.exposeErrorBody === true;
12
33
  }
13
34
 
14
35
  /**
15
36
  * Generate a response from the OpenAI API.
16
- * @param {Array<object>} messages - Conversation messages.
17
- * @param {Array<object>} [tools=[]] - Tool definitions.
18
- * @param {object} [options={}] - Options (temperature, maxTokens).
19
- * @returns {Promise<{text: string, toolCalls: Array, usage: object}>}
37
+ * @param {Message[]} messages - Conversation messages.
38
+ * @param {ToolDef[]} [tools=[]] - Tool definitions.
39
+ * @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
40
+ * @returns {Promise<GenerateResult>}
20
41
  * @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
21
42
  */
22
43
  async generate(messages, tools = [], options = {}) {
44
+ /** @type {Record<string, any>} */
23
45
  const body = {
24
46
  model: this.model,
25
47
  messages,
@@ -39,7 +61,7 @@ class OpenAIProvider {
39
61
 
40
62
  return {
41
63
  text: msg.content || '',
42
- toolCalls: (msg.tool_calls || []).map(tc => ({
64
+ toolCalls: (msg.tool_calls || []).map((/** @type {any} */ tc) => ({
43
65
  id: tc.id,
44
66
  name: tc.function.name,
45
67
  arguments: JSON.parse(tc.function.arguments),
@@ -51,6 +73,11 @@ class OpenAIProvider {
51
73
  };
52
74
  }
53
75
 
76
+ /**
77
+ * @param {string} path
78
+ * @param {Record<string, any>} body
79
+ * @returns {Promise<any>}
80
+ */
54
81
  _request(path, body) {
55
82
  return new Promise((resolve, reject) => {
56
83
  const url = new URL(this.baseUrl + path);
@@ -70,10 +97,10 @@ class OpenAIProvider {
70
97
  res.on('end', () => {
71
98
  try {
72
99
  const parsed = JSON.parse(chunks);
73
- if (res.statusCode >= 400) {
100
+ if ((res.statusCode ?? 0) >= 400) {
74
101
  return reject(new ProviderError(
75
102
  `[OpenAIProvider] ${parsed.error?.message || `HTTP ${res.statusCode}`}`,
76
- { status: res.statusCode, body: parsed }
103
+ /** @type {any} */ ({ status: res.statusCode, body: this.exposeErrorBody ? parsed : undefined })
77
104
  ));
78
105
  }
79
106
  resolve(parsed);
@@ -0,0 +1,6 @@
1
+ import { OpenAIProvider } from "./provider-openai";
2
+ import { AnthropicProvider } from "./provider-anthropic";
3
+ import { OllamaProvider } from "./provider-ollama";
4
+ import { CLIPipeProvider } from "./provider-clipipe";
5
+ import { FallbackProvider } from "./provider-fallback";
6
+ export { OpenAIProvider as OpenAI, AnthropicProvider as Anthropic, OllamaProvider as Ollama, CLIPipeProvider as CLIPipe, FallbackProvider as Fallback };
package/src/retry.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ export type RetryOptions = {
2
+ /**
3
+ * - Maximum number of attempts.
4
+ */
5
+ maxAttempts?: number | undefined;
6
+ /**
7
+ * - Backoff strategy or fixed ms.
8
+ */
9
+ backoff?: number | "linear" | "exponential" | undefined;
10
+ /**
11
+ * - Per-attempt timeout in ms (0 to disable).
12
+ */
13
+ timeout?: number | undefined;
14
+ /**
15
+ * - Predicate deciding whether to retry an error.
16
+ */
17
+ retryOn?: ((err: any) => boolean) | undefined;
18
+ /**
19
+ * - Jitter strategy.
20
+ */
21
+ jitter?: number | boolean | "full" | "equal" | undefined;
22
+ };
23
+ export class Retry {
24
+ /** @param {RetryOptions} [options={}] */
25
+ constructor(options?: RetryOptions);
26
+ maxAttempts: number;
27
+ backoff: number | "linear" | "exponential";
28
+ timeout: number;
29
+ retryOn: (err: any) => boolean;
30
+ jitter: number | boolean | "full" | "equal";
31
+ /**
32
+ * Call a function with retry logic.
33
+ * @param {() => Promise<*>} fn - Async function to execute.
34
+ * @param {RetryOptions} [options={}] - Per-call overrides for maxAttempts, retryOn, timeout.
35
+ * @returns {Promise<*>} The result of fn().
36
+ * @throws {TimeoutError} When an individual attempt exceeds the timeout.
37
+ * @throws {Error} Rethrows the last error when maxAttempts is exhausted or error is not retryable.
38
+ */
39
+ call(fn: () => Promise<any>, options?: RetryOptions): Promise<any>;
40
+ /** @param {number} attempt */
41
+ _delay(attempt: number): number;
42
+ /** @param {number} base */
43
+ _applyJitter(base: number): number;
44
+ }
package/src/retry.js CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  const { TimeoutError } = require('./errors');
4
4
 
5
+ /**
6
+ * @typedef {object} RetryOptions
7
+ * @property {number} [maxAttempts=3] - Maximum number of attempts.
8
+ * @property {number|'linear'|'exponential'} [backoff='exponential'] - Backoff strategy or fixed ms.
9
+ * @property {number} [timeout=60000] - Per-attempt timeout in ms (0 to disable).
10
+ * @property {(err: any) => boolean} [retryOn] - Predicate deciding whether to retry an error.
11
+ * @property {boolean|number|'full'|'equal'} [jitter=false] - Jitter strategy.
12
+ */
13
+
14
+ /** @param {any} err */
5
15
  const DEFAULT_RETRY_ON = (err) => {
6
16
  if (err.retryable === true) return true;
7
17
  if (err.retryable === false) return false;
@@ -13,6 +23,7 @@ const DEFAULT_RETRY_ON = (err) => {
13
23
  };
14
24
 
15
25
  class Retry {
26
+ /** @param {RetryOptions} [options={}] */
16
27
  constructor(options = {}) {
17
28
  this.maxAttempts = options.maxAttempts !== undefined ? options.maxAttempts : 3;
18
29
  this.backoff = options.backoff || 'exponential';
@@ -24,7 +35,7 @@ class Retry {
24
35
  /**
25
36
  * Call a function with retry logic.
26
37
  * @param {() => Promise<*>} fn - Async function to execute.
27
- * @param {object} [options={}] - Per-call overrides for maxAttempts, retryOn, timeout.
38
+ * @param {RetryOptions} [options={}] - Per-call overrides for maxAttempts, retryOn, timeout.
28
39
  * @returns {Promise<*>} The result of fn().
29
40
  * @throws {TimeoutError} When an individual attempt exceeds the timeout.
30
41
  * @throws {Error} Rethrows the last error when maxAttempts is exhausted or error is not retryable.
@@ -35,6 +46,7 @@ class Retry {
35
46
  const timeout = options.timeout !== undefined ? options.timeout : this.timeout;
36
47
 
37
48
  for (let attempt = 1; attempt <= max; attempt++) {
49
+ /** @type {NodeJS.Timeout|undefined} */
38
50
  let timeoutId;
39
51
  try {
40
52
  const result = await (timeout
@@ -56,6 +68,7 @@ class Retry {
56
68
  }
57
69
  }
58
70
 
71
+ /** @param {number} attempt */
59
72
  _delay(attempt) {
60
73
  let base;
61
74
  if (typeof this.backoff === 'number') {
@@ -68,6 +81,7 @@ class Retry {
68
81
  return this._applyJitter(base);
69
82
  }
70
83
 
84
+ /** @param {number} base */
71
85
  _applyJitter(base) {
72
86
  if (this.jitter === false || this.jitter === 0) return base;
73
87
  if (this.jitter === 'full') {
@@ -0,0 +1,126 @@
1
+ export type StateMachine = import("./state").StateMachine;
2
+ export type Retry = import("./retry").Retry;
3
+ export type Step = {
4
+ /**
5
+ * - Unique step identifier.
6
+ */
7
+ id: string;
8
+ /**
9
+ * - Description of the step to execute.
10
+ */
11
+ action: string;
12
+ /**
13
+ * - Ids of steps that must complete first.
14
+ */
15
+ dependsOn?: string[] | undefined;
16
+ };
17
+ export type TrackingEntry = {
18
+ /**
19
+ * - The (cloned) step being tracked.
20
+ */
21
+ step: Step;
22
+ /**
23
+ * - Lifecycle status: pending/running/done/failed.
24
+ */
25
+ status: string;
26
+ /**
27
+ * - Result returned by executeFn on success.
28
+ */
29
+ result: any;
30
+ /**
31
+ * - Error message on failure.
32
+ */
33
+ error: string | undefined;
34
+ };
35
+ export type RunPlanOptions = {
36
+ /**
37
+ * - Max parallel steps per wave.
38
+ */
39
+ concurrency?: number | undefined;
40
+ /**
41
+ * - StateMachine instance for lifecycle tracking.
42
+ */
43
+ stateMachine?: import("./state").StateMachine | undefined;
44
+ /**
45
+ * - Callback fired when a step begins.
46
+ */
47
+ onStepStart?: ((step: Step) => void) | undefined;
48
+ /**
49
+ * - Callback fired on success.
50
+ */
51
+ onStepDone?: ((step: Step, result: any) => void) | undefined;
52
+ /**
53
+ * - Callback fired on failure.
54
+ */
55
+ onStepFail?: ((step: Step, error: Error) => void) | undefined;
56
+ /**
57
+ * - Callback fired before each wave executes.
58
+ */
59
+ onWaveStart?: ((waveNumber: number, steps: Step[]) => void) | undefined;
60
+ /**
61
+ * - Optional Retry instance wrapping each step execution.
62
+ */
63
+ stepRetry?: import("./retry").Retry | undefined;
64
+ };
65
+ export type StepResult = {
66
+ /**
67
+ * - Step id.
68
+ */
69
+ id: string;
70
+ /**
71
+ * - Final status.
72
+ */
73
+ status: string;
74
+ /**
75
+ * - Result value if the step succeeded.
76
+ */
77
+ result?: any;
78
+ /**
79
+ * - Error message if the step failed.
80
+ */
81
+ error?: string | undefined;
82
+ };
83
+ /** @typedef {import('./state').StateMachine} StateMachine */
84
+ /** @typedef {import('./retry').Retry} Retry */
85
+ /**
86
+ * @typedef {object} Step
87
+ * @property {string} id - Unique step identifier.
88
+ * @property {string} action - Description of the step to execute.
89
+ * @property {string[]} [dependsOn] - Ids of steps that must complete first.
90
+ */
91
+ /**
92
+ * @typedef {object} TrackingEntry
93
+ * @property {Step} step - The (cloned) step being tracked.
94
+ * @property {string} status - Lifecycle status: pending/running/done/failed.
95
+ * @property {*} result - Result returned by executeFn on success.
96
+ * @property {string|undefined} error - Error message on failure.
97
+ */
98
+ /**
99
+ * @typedef {object} RunPlanOptions
100
+ * @property {number} [concurrency=Infinity] - Max parallel steps per wave.
101
+ * @property {StateMachine} [stateMachine] - StateMachine instance for lifecycle tracking.
102
+ * @property {(step: Step) => void} [onStepStart] - Callback fired when a step begins.
103
+ * @property {(step: Step, result: any) => void} [onStepDone] - Callback fired on success.
104
+ * @property {(step: Step, error: Error) => void} [onStepFail] - Callback fired on failure.
105
+ * @property {(waveNumber: number, steps: Step[]) => void} [onWaveStart] - Callback fired before each wave executes.
106
+ * @property {Retry} [stepRetry] - Optional Retry instance wrapping each step execution.
107
+ */
108
+ /**
109
+ * @typedef {object} StepResult
110
+ * @property {string} id - Step id.
111
+ * @property {string} status - Final status.
112
+ * @property {*} [result] - Result value if the step succeeded.
113
+ * @property {string} [error] - Error message if the step failed.
114
+ */
115
+ /**
116
+ * Execute a step DAG with wave-based parallelism.
117
+ * @param {Step[]} steps - Steps from Planner.
118
+ * @param {(step: Step) => any} executeFn - Async function called for each step: (step) => result.
119
+ * @param {RunPlanOptions} [options={}]
120
+ * @returns {Promise<StepResult[]>}
121
+ * @throws {Error} `[runPlan] steps must be a non-empty array` — when steps is not a non-empty array.
122
+ * @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
123
+ * @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
124
+ * @throws {Error} `[runPlan] step "X" depends on unknown step "Y"` — when dependsOn references missing id.
125
+ */
126
+ export function runPlan(steps: Step[], executeFn: (step: Step) => any, options?: RunPlanOptions): Promise<StepResult[]>;
package/src/run-plan.js CHANGED
@@ -1,17 +1,48 @@
1
1
  'use strict';
2
2
 
3
+ /** @typedef {import('./state').StateMachine} StateMachine */
4
+ /** @typedef {import('./retry').Retry} Retry */
5
+
6
+ /**
7
+ * @typedef {object} Step
8
+ * @property {string} id - Unique step identifier.
9
+ * @property {string} action - Description of the step to execute.
10
+ * @property {string[]} [dependsOn] - Ids of steps that must complete first.
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} TrackingEntry
15
+ * @property {Step} step - The (cloned) step being tracked.
16
+ * @property {string} status - Lifecycle status: pending/running/done/failed.
17
+ * @property {*} result - Result returned by executeFn on success.
18
+ * @property {string|undefined} error - Error message on failure.
19
+ */
20
+
21
+ /**
22
+ * @typedef {object} RunPlanOptions
23
+ * @property {number} [concurrency=Infinity] - Max parallel steps per wave.
24
+ * @property {StateMachine} [stateMachine] - StateMachine instance for lifecycle tracking.
25
+ * @property {(step: Step) => void} [onStepStart] - Callback fired when a step begins.
26
+ * @property {(step: Step, result: any) => void} [onStepDone] - Callback fired on success.
27
+ * @property {(step: Step, error: Error) => void} [onStepFail] - Callback fired on failure.
28
+ * @property {(waveNumber: number, steps: Step[]) => void} [onWaveStart] - Callback fired before each wave executes.
29
+ * @property {Retry} [stepRetry] - Optional Retry instance wrapping each step execution.
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} StepResult
34
+ * @property {string} id - Step id.
35
+ * @property {string} status - Final status.
36
+ * @property {*} [result] - Result value if the step succeeded.
37
+ * @property {string} [error] - Error message if the step failed.
38
+ */
39
+
3
40
  /**
4
41
  * Execute a step DAG with wave-based parallelism.
5
- * @param {Array<{id: string, action: string, dependsOn?: string[]}>} steps - Steps from Planner.
6
- * @param {function} executeFn - Async function called for each step: (step) => result.
7
- * @param {object} [options={}]
8
- * @param {number} [options.concurrency=Infinity] - Max parallel steps per wave.
9
- * @param {object} [options.stateMachine] - StateMachine instance for lifecycle tracking.
10
- * @param {function} [options.onStepStart] - Callback(step) fired when a step begins.
11
- * @param {function} [options.onStepDone] - Callback(step, result) fired on success.
12
- * @param {function} [options.onStepFail] - Callback(step, error) fired on failure.
13
- * @param {function} [options.onWaveStart] - Callback(waveNumber, steps) fired before each wave executes.
14
- * @returns {Promise<Array<{id: string, status: string, result?: *, error?: string}>>}
42
+ * @param {Step[]} steps - Steps from Planner.
43
+ * @param {(step: Step) => any} executeFn - Async function called for each step: (step) => result.
44
+ * @param {RunPlanOptions} [options={}]
45
+ * @returns {Promise<StepResult[]>}
15
46
  * @throws {Error} `[runPlan] steps must be a non-empty array` — when steps is not a non-empty array.
16
47
  * @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
17
48
  * @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
@@ -26,6 +57,7 @@ async function runPlan(steps, executeFn, options = {}) {
26
57
  }
27
58
 
28
59
  // Build tracking map (don't mutate input)
60
+ /** @type {Map<string, TrackingEntry>} */
29
61
  const tracking = new Map();
30
62
  for (const step of steps) {
31
63
  if (tracking.has(step.id)) {
@@ -59,7 +91,7 @@ async function runPlan(steps, executeFn, options = {}) {
59
91
  if (entry.status !== 'pending') continue;
60
92
  for (const dep of (entry.step.dependsOn || [])) {
61
93
  const depEntry = tracking.get(dep);
62
- if (depEntry.status === 'failed') {
94
+ if (depEntry && depEntry.status === 'failed') {
63
95
  entry.status = 'failed';
64
96
  entry.error = `dependency '${dep}' failed`;
65
97
  stateMachine?.transition(id, 'start');
@@ -75,7 +107,7 @@ async function runPlan(steps, executeFn, options = {}) {
75
107
  for (const [id, entry] of tracking) {
76
108
  if (entry.status !== 'pending') continue;
77
109
  const deps = entry.step.dependsOn || [];
78
- const allDone = deps.every(dep => tracking.get(dep).status === 'done');
110
+ const allDone = deps.every(/** @param {string} dep */ dep => tracking.get(dep)?.status === 'done');
79
111
  if (allDone) ready.push(entry);
80
112
  }
81
113
 
@@ -113,7 +145,8 @@ async function runPlan(steps, executeFn, options = {}) {
113
145
 
114
146
  // Return results in original order
115
147
  return steps.map(s => {
116
- const entry = tracking.get(s.id);
148
+ const entry = /** @type {TrackingEntry} */ (tracking.get(s.id));
149
+ /** @type {StepResult} */
117
150
  const out = { id: s.id, status: entry.status };
118
151
  if (entry.result !== undefined) out.result = entry.result;
119
152
  if (entry.error !== undefined) out.error = entry.error;
@@ -0,0 +1,102 @@
1
+ export type Job = {
2
+ id: number;
3
+ type: string;
4
+ schedule: string;
5
+ action: any;
6
+ status: string;
7
+ nextRun: string;
8
+ createdAt?: string | undefined;
9
+ };
10
+ export type SchedulerOptions = {
11
+ /**
12
+ * - Path to JSON persistence file.
13
+ */
14
+ file?: string | null | undefined;
15
+ /**
16
+ * - Tick interval in ms.
17
+ */
18
+ interval?: number | undefined;
19
+ /**
20
+ * - Handler errors callback.
21
+ */
22
+ onError?: ((err: any, job: Job) => void) | null | undefined;
23
+ };
24
+ /**
25
+ * Time-triggered agent turns. The only way the agent acts without being messaged.
26
+ *
27
+ * Interface:
28
+ * add(job) → jobId
29
+ * remove(jobId) → void
30
+ * list() → [jobs]
31
+ * start(handler) → begin tick loop (handler receives due jobs)
32
+ * stop() → stop tick loop
33
+ */
34
+ /**
35
+ * @typedef {object} Job
36
+ * @property {number} id
37
+ * @property {string} type
38
+ * @property {string} schedule
39
+ * @property {*} action
40
+ * @property {string} status
41
+ * @property {string} nextRun
42
+ * @property {string} [createdAt]
43
+ */
44
+ /**
45
+ * @typedef {object} SchedulerOptions
46
+ * @property {string|null} [file] - Path to JSON persistence file.
47
+ * @property {number} [interval=60000] - Tick interval in ms.
48
+ * @property {((err: any, job: Job) => void)|null} [onError] - Handler errors callback.
49
+ */
50
+ export class Scheduler {
51
+ /** @param {SchedulerOptions} [options={}] */
52
+ constructor(options?: SchedulerOptions);
53
+ _file: string | null;
54
+ _interval: number;
55
+ onError: ((err: any, job: Job) => void) | null;
56
+ /** @type {Job[]} */
57
+ _jobs: Job[];
58
+ /** @type {NodeJS.Timeout|null} */
59
+ _timer: NodeJS.Timeout | null;
60
+ /** @type {Set<number>} */
61
+ _running: Set<number>;
62
+ _nextId: number;
63
+ _save(): void;
64
+ /** @param {{ type?: string, schedule: string, action: * }} job */
65
+ add(job: {
66
+ type?: string;
67
+ schedule: string;
68
+ action: any;
69
+ }): number;
70
+ /** @param {number} jobId */
71
+ remove(jobId: number): void;
72
+ list(): {
73
+ id: number;
74
+ type: string;
75
+ schedule: string;
76
+ action: any;
77
+ status: string;
78
+ nextRun: string;
79
+ createdAt?: string | undefined;
80
+ }[];
81
+ /**
82
+ * Begin the tick loop. Calls `handler(job)` for each due job every tick interval.
83
+ *
84
+ * - `handler` is called with the full job object: `{ id, type, schedule, action, status, nextRun }`.
85
+ * - Jobs that are still running (handler hasn't resolved) are skipped on subsequent ticks
86
+ * via the internal `_running` Set — this prevents overlapping executions of the same job.
87
+ * - Within a single tick, due jobs are executed sequentially (awaited one at a time).
88
+ * - If a handler throws, the error is passed to `onError(err, job)` if configured.
89
+ * The tick loop continues to the next job — handler errors never crash the scheduler.
90
+ *
91
+ * @param {(job: object) => Promise<void>} handler - Async function called for each due job.
92
+ */
93
+ start(handler: (job: object) => Promise<void>): void;
94
+ stop(): void;
95
+ /**
96
+ * @param {string} schedule - Relative ('5s','30m','2h','1d') or cron expression.
97
+ * @returns {Date} The next run time.
98
+ * @throws {Error} `[Scheduler] Cannot parse schedule` — when format is not recognized.
99
+ * @private
100
+ */
101
+ private _parseSchedule;
102
+ }
package/src/scheduler.js CHANGED
@@ -12,17 +12,41 @@ const { readFileSync, writeFileSync, existsSync } = require('node:fs');
12
12
  * start(handler) → begin tick loop (handler receives due jobs)
13
13
  * stop() → stop tick loop
14
14
  */
15
+
16
+ /**
17
+ * @typedef {object} Job
18
+ * @property {number} id
19
+ * @property {string} type
20
+ * @property {string} schedule
21
+ * @property {*} action
22
+ * @property {string} status
23
+ * @property {string} nextRun
24
+ * @property {string} [createdAt]
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} SchedulerOptions
29
+ * @property {string|null} [file] - Path to JSON persistence file.
30
+ * @property {number} [interval=60000] - Tick interval in ms.
31
+ * @property {((err: any, job: Job) => void)|null} [onError] - Handler errors callback.
32
+ */
33
+
15
34
  class Scheduler {
35
+ /** @param {SchedulerOptions} [options={}] */
16
36
  constructor(options = {}) {
17
37
  this._file = options.file || null;
18
38
  this._interval = options.interval || 60000;
19
39
  this.onError = options.onError || null;
40
+ /** @type {Job[]} */
20
41
  this._jobs = this._file && existsSync(this._file)
21
42
  ? JSON.parse(readFileSync(this._file, 'utf8'))
22
43
  : [];
44
+ /** @type {NodeJS.Timeout|null} */
23
45
  this._timer = null;
46
+ /** @type {Set<number>} */
47
+ this._running = new Set();
24
48
  this._nextId = this._jobs.length
25
- ? this._jobs.reduce((max, j) => Math.max(max, j.id), 0) + 1
49
+ ? this._jobs.reduce((/** @type {number} */ max, /** @type {Job} */ j) => Math.max(max, j.id), 0) + 1
26
50
  : 1;
27
51
  }
28
52
 
@@ -30,6 +54,7 @@ class Scheduler {
30
54
  if (this._file) writeFileSync(this._file, JSON.stringify(this._jobs, null, 2));
31
55
  }
32
56
 
57
+ /** @param {{ type?: string, schedule: string, action: * }} job */
33
58
  add(job) {
34
59
  const id = this._nextId++;
35
60
  const nextRun = this._parseSchedule(job.schedule);
@@ -46,13 +71,14 @@ class Scheduler {
46
71
  return id;
47
72
  }
48
73
 
74
+ /** @param {number} jobId */
49
75
  remove(jobId) {
50
- this._jobs = this._jobs.filter(j => j.id !== jobId);
76
+ this._jobs = this._jobs.filter((/** @type {Job} */ j) => j.id !== jobId);
51
77
  this._save();
52
78
  }
53
79
 
54
80
  list() {
55
- return this._jobs.map(j => ({ ...j }));
81
+ return this._jobs.map((/** @type {Job} */ j) => ({ ...j }));
56
82
  }
57
83
 
58
84
  /**
@@ -114,7 +140,9 @@ class Scheduler {
114
140
  const rel = schedule.match(/^(\d+)(s|m|h|d)$/);
115
141
  if (rel) {
116
142
  const [, n, unit] = rel;
117
- const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[unit];
143
+ /** @type {Record<string, number>} */
144
+ const units = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
145
+ const ms = units[unit];
118
146
  return new Date(Date.now() + Number(n) * ms);
119
147
  }
120
148
  // Cron: try cron-parser
package/src/state.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ export type Task = {
2
+ status: string;
3
+ data: any;
4
+ error: any;
5
+ updatedAt: string;
6
+ };
7
+ /**
8
+ * @typedef {object} Task
9
+ * @property {string} status
10
+ * @property {*} data
11
+ * @property {*} error
12
+ * @property {string} updatedAt
13
+ */
14
+ export class StateMachine extends EventEmitter<[never]> {
15
+ /** @param {{ file?: string|null }} [options={}] */
16
+ constructor(options?: {
17
+ file?: string | null;
18
+ });
19
+ file: string | null;
20
+ /** @type {Map<string, Task>} */
21
+ tasks: Map<string, Task>;
22
+ /**
23
+ * Transition a task to a new state.
24
+ * @param {string} taskId - Task identifier.
25
+ * @param {string} event - Transition event (start, complete, fail, pause, resume, cancel, retry).
26
+ * @param {*} [data] - Optional data to attach to the task.
27
+ * @returns {string} The new status.
28
+ * @throws {Error} `[StateMachine] Invalid transition` — when the event is not valid for the current state.
29
+ */
30
+ transition(taskId: string, event: string, data?: any): string;
31
+ /** @param {string} taskId */
32
+ getStatus(taskId: string): Task | null;
33
+ /** @param {(event: { taskId: string, from: string, to: string, event: string, data: * }) => void} callback */
34
+ onTransition(callback: (event: {
35
+ taskId: string;
36
+ from: string;
37
+ to: string;
38
+ event: string;
39
+ data: any;
40
+ }) => void): () => this;
41
+ getAll(): Record<string, Task>;
42
+ _load(): void;
43
+ _save(): void;
44
+ }
45
+ import { EventEmitter } from "events";