@danypops/vehicle-core 0.18.1 → 0.18.3
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/dist/cli-safety/assert-no-leading-flag-char.d.ts +21 -0
- package/dist/cli-safety/assert-no-leading-flag-char.js +31 -0
- package/dist/cli-safety/index.d.ts +1 -0
- package/dist/cli-safety/index.js +1 -0
- package/dist/concurrency/debounced-scheduler.d.ts +46 -0
- package/dist/concurrency/debounced-scheduler.js +92 -0
- package/dist/concurrency/index.d.ts +1 -0
- package/dist/concurrency/index.js +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/package.json +1 -1
- package/src/cli-safety/assert-no-leading-flag-char.ts +32 -0
- package/src/cli-safety/index.ts +1 -0
- package/src/concurrency/debounced-scheduler.ts +106 -0
- package/src/concurrency/index.ts +1 -0
- package/src/index.ts +7 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards against the argv-injection class behind a well-known family of CLI-wrapper CVEs
|
|
3
|
+
* (simple-git's own history includes several): a caller-influenced string that starts with `-`
|
|
4
|
+
* can be parsed by the target CLI as a *flag* (`--upload-pack`, `--exec`, `--template`, `-c`
|
|
5
|
+
* config override) instead of the literal ref/path/pattern value the caller intended. Any Vehicle
|
|
6
|
+
* operation that hands a caller-supplied string to a shelled-out CLI's argv should run it through
|
|
7
|
+
* this check first, at the exact position it reaches that argv.
|
|
8
|
+
*
|
|
9
|
+
* This is a hard rejection with no exceptions -- it has no opinion about *why* a value starting
|
|
10
|
+
* with `-` might be needed and does not attempt to allow-list specific flags. A caller whose CLI
|
|
11
|
+
* genuinely accepts caller-influenced flag-shaped arguments needs a purpose-built allow-list of
|
|
12
|
+
* its own; this primitive only ever covers the common case of a value that should always be a
|
|
13
|
+
* literal.
|
|
14
|
+
*/
|
|
15
|
+
export declare class UnsafeCliArgument extends Error {
|
|
16
|
+
readonly value: string;
|
|
17
|
+
readonly fieldName?: string | undefined;
|
|
18
|
+
constructor(value: string, fieldName?: string | undefined);
|
|
19
|
+
}
|
|
20
|
+
/** Throws UnsafeCliArgument if `value` starts with `-`. `fieldName`, when given, names the field in the thrown error's own message for a caller with several distinct argv positions to check. */
|
|
21
|
+
export declare function assertNoLeadingFlagChar(value: string, fieldName?: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards against the argv-injection class behind a well-known family of CLI-wrapper CVEs
|
|
3
|
+
* (simple-git's own history includes several): a caller-influenced string that starts with `-`
|
|
4
|
+
* can be parsed by the target CLI as a *flag* (`--upload-pack`, `--exec`, `--template`, `-c`
|
|
5
|
+
* config override) instead of the literal ref/path/pattern value the caller intended. Any Vehicle
|
|
6
|
+
* operation that hands a caller-supplied string to a shelled-out CLI's argv should run it through
|
|
7
|
+
* this check first, at the exact position it reaches that argv.
|
|
8
|
+
*
|
|
9
|
+
* This is a hard rejection with no exceptions -- it has no opinion about *why* a value starting
|
|
10
|
+
* with `-` might be needed and does not attempt to allow-list specific flags. A caller whose CLI
|
|
11
|
+
* genuinely accepts caller-influenced flag-shaped arguments needs a purpose-built allow-list of
|
|
12
|
+
* its own; this primitive only ever covers the common case of a value that should always be a
|
|
13
|
+
* literal.
|
|
14
|
+
*/
|
|
15
|
+
export class UnsafeCliArgument extends Error {
|
|
16
|
+
value;
|
|
17
|
+
fieldName;
|
|
18
|
+
constructor(value, fieldName) {
|
|
19
|
+
super(fieldName
|
|
20
|
+
? `"${value}" cannot be used as ${fieldName} -- it would be interpreted as a CLI flag, not a literal value`
|
|
21
|
+
: `"${value}" cannot be used as a CLI argument -- it would be interpreted as a flag, not a literal value`);
|
|
22
|
+
this.value = value;
|
|
23
|
+
this.fieldName = fieldName;
|
|
24
|
+
this.name = "UnsafeCliArgument";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Throws UnsafeCliArgument if `value` starts with `-`. `fieldName`, when given, names the field in the thrown error's own message for a caller with several distinct argv positions to check. */
|
|
28
|
+
export function assertNoLeadingFlagChar(value, fieldName) {
|
|
29
|
+
if (value.startsWith("-"))
|
|
30
|
+
throw new UnsafeCliArgument(value, fieldName);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./assert-no-leading-flag-char.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./assert-no-leading-flag-char.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesces a burst of calls for the same key into exactly one callback fire,
|
|
3
|
+
* delayMs after the last call for that key -- the classic debounce shape,
|
|
4
|
+
* useful anywhere a flurry of raw upstream events for one logical unit of
|
|
5
|
+
* work (a filesystem save that fires more than once via temp-file write +
|
|
6
|
+
* atomic rename, a burst of webhook deliveries for the same resource) needs
|
|
7
|
+
* to collapse into a single downstream action. Different keys are fully
|
|
8
|
+
* independent. Pure timer bookkeeping, no I/O -- the callback itself does
|
|
9
|
+
* whatever real work is needed.
|
|
10
|
+
*/
|
|
11
|
+
/** The minimal logging surface this module needs -- any real Logger (e.g. Vehicle's own daemon Logger) satisfies this structurally, no adapter required. */
|
|
12
|
+
export interface MinimalLogger {
|
|
13
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
14
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class DebounceCapacityExceeded extends Error {
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly max: number;
|
|
19
|
+
constructor(key: string, max: number);
|
|
20
|
+
}
|
|
21
|
+
export interface DebouncedSchedulerOptions {
|
|
22
|
+
/** Maximum distinct keys with a pending fire at once. Default 4096. */
|
|
23
|
+
readonly maxKeys?: number;
|
|
24
|
+
readonly logger?: MinimalLogger;
|
|
25
|
+
}
|
|
26
|
+
export declare class DebouncedScheduler {
|
|
27
|
+
private readonly timers;
|
|
28
|
+
private readonly delayMs;
|
|
29
|
+
private readonly maxKeys;
|
|
30
|
+
private readonly logger;
|
|
31
|
+
constructor(delayMs: number, options?: DebouncedSchedulerOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Schedules `callback` to run delayMs after this call, resetting any pending fire already
|
|
34
|
+
* scheduled for `key`. A callback that throws or rejects is caught and dropped -- there is
|
|
35
|
+
* no request awaiting this fire to report the error, and an unhandled timer failure would
|
|
36
|
+
* otherwise crash the whole process rather than just this one key's work.
|
|
37
|
+
* A caller that cares about its own errors should catch and log inside `callback` itself.
|
|
38
|
+
*/
|
|
39
|
+
schedule(key: string, callback: () => unknown): void;
|
|
40
|
+
/** Cancels `key`'s pending fire, if any. Idempotent -- an unknown or already-fired key is a safe no-op. */
|
|
41
|
+
cancel(key: string): void;
|
|
42
|
+
/** True while `key` has a fire pending. */
|
|
43
|
+
has(key: string): boolean;
|
|
44
|
+
/** Cancels every pending key at once -- for clean shutdown. */
|
|
45
|
+
clear(): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesces a burst of calls for the same key into exactly one callback fire,
|
|
3
|
+
* delayMs after the last call for that key -- the classic debounce shape,
|
|
4
|
+
* useful anywhere a flurry of raw upstream events for one logical unit of
|
|
5
|
+
* work (a filesystem save that fires more than once via temp-file write +
|
|
6
|
+
* atomic rename, a burst of webhook deliveries for the same resource) needs
|
|
7
|
+
* to collapse into a single downstream action. Different keys are fully
|
|
8
|
+
* independent. Pure timer bookkeeping, no I/O -- the callback itself does
|
|
9
|
+
* whatever real work is needed.
|
|
10
|
+
*/
|
|
11
|
+
export class DebounceCapacityExceeded extends Error {
|
|
12
|
+
key;
|
|
13
|
+
max;
|
|
14
|
+
constructor(key, max) {
|
|
15
|
+
super(`debounced scheduler distinct-key bound exceeded (${max}) scheduling key "${key}"`);
|
|
16
|
+
this.key = key;
|
|
17
|
+
this.max = max;
|
|
18
|
+
this.name = "DebounceCapacityExceeded";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_MAX_KEYS = 4096;
|
|
22
|
+
const NOOP_LOGGER = { debug() { }, warn() { } };
|
|
23
|
+
export class DebouncedScheduler {
|
|
24
|
+
timers = new Map();
|
|
25
|
+
delayMs;
|
|
26
|
+
maxKeys;
|
|
27
|
+
logger;
|
|
28
|
+
constructor(delayMs, options = {}) {
|
|
29
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 0)
|
|
30
|
+
throw new TypeError("delayMs must be a non-negative safe integer");
|
|
31
|
+
this.delayMs = delayMs;
|
|
32
|
+
this.maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS;
|
|
33
|
+
this.logger = options.logger ?? NOOP_LOGGER;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Schedules `callback` to run delayMs after this call, resetting any pending fire already
|
|
37
|
+
* scheduled for `key`. A callback that throws or rejects is caught and dropped -- there is
|
|
38
|
+
* no request awaiting this fire to report the error, and an unhandled timer failure would
|
|
39
|
+
* otherwise crash the whole process rather than just this one key's work.
|
|
40
|
+
* A caller that cares about its own errors should catch and log inside `callback` itself.
|
|
41
|
+
*/
|
|
42
|
+
schedule(key, callback) {
|
|
43
|
+
const existing = this.timers.get(key);
|
|
44
|
+
if (existing) {
|
|
45
|
+
clearTimeout(existing);
|
|
46
|
+
this.logger.debug("debounced schedule coalesced", { component: "debounced-scheduler", operation: "schedule" });
|
|
47
|
+
}
|
|
48
|
+
else if (this.timers.size >= this.maxKeys) {
|
|
49
|
+
this.logger.warn("debounced schedule rejected", {
|
|
50
|
+
component: "debounced-scheduler",
|
|
51
|
+
operation: "schedule",
|
|
52
|
+
code: "DebounceCapacityExceeded",
|
|
53
|
+
});
|
|
54
|
+
throw new DebounceCapacityExceeded(key, this.maxKeys);
|
|
55
|
+
}
|
|
56
|
+
const reportFailure = (error) => {
|
|
57
|
+
this.logger.warn("debounced callback failed", {
|
|
58
|
+
component: "debounced-scheduler",
|
|
59
|
+
operation: "fire",
|
|
60
|
+
code: error instanceof Error ? error.name || "Error" : "Error",
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
this.timers.delete(key);
|
|
65
|
+
try {
|
|
66
|
+
Promise.resolve(callback()).catch(reportFailure);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
reportFailure(error);
|
|
70
|
+
}
|
|
71
|
+
}, this.delayMs);
|
|
72
|
+
this.timers.set(key, timer);
|
|
73
|
+
}
|
|
74
|
+
/** Cancels `key`'s pending fire, if any. Idempotent -- an unknown or already-fired key is a safe no-op. */
|
|
75
|
+
cancel(key) {
|
|
76
|
+
const existing = this.timers.get(key);
|
|
77
|
+
if (!existing)
|
|
78
|
+
return;
|
|
79
|
+
clearTimeout(existing);
|
|
80
|
+
this.timers.delete(key);
|
|
81
|
+
}
|
|
82
|
+
/** True while `key` has a fire pending. */
|
|
83
|
+
has(key) {
|
|
84
|
+
return this.timers.has(key);
|
|
85
|
+
}
|
|
86
|
+
/** Cancels every pending key at once -- for clean shutdown. */
|
|
87
|
+
clear() {
|
|
88
|
+
for (const timer of this.timers.values())
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
this.timers.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./debounced-scheduler.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./debounced-scheduler.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* (model-facing narrative blocks), operations (descriptors, effect
|
|
6
6
|
* classification, invocation context), events, manifest, client (the port a
|
|
7
7
|
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
|
-
* (Vehicle Jobs' pure pieces), schedules, watches,
|
|
9
|
-
* JSON
|
|
10
|
-
*
|
|
8
|
+
* (Vehicle Jobs' pure pieces), schedules, watches, persistence (atomic
|
|
9
|
+
* JSON), concurrency (timer-based scheduling primitives), and cli-safety
|
|
10
|
+
* (argv-injection guards for any operation shelling out to a CLI) -- the
|
|
11
|
+
* latter three are technical utilities, not Vehicle protocol capabilities,
|
|
12
|
+
* kept distinct for that reason. Every symbol below is re-exported unchanged
|
|
11
13
|
* from its historical flat-file home, so root-level `import { X } from
|
|
12
14
|
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
13
15
|
*/
|
|
14
16
|
export * from "./approvals/index.js";
|
|
17
|
+
export * from "./cli-safety/index.js";
|
|
15
18
|
export * from "./client/index.js";
|
|
19
|
+
export * from "./concurrency/index.js";
|
|
16
20
|
export * from "./content/index.js";
|
|
17
21
|
export * from "./errors/index.js";
|
|
18
22
|
export * from "./events/index.js";
|
package/dist/index.js
CHANGED
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* (model-facing narrative blocks), operations (descriptors, effect
|
|
6
6
|
* classification, invocation context), events, manifest, client (the port a
|
|
7
7
|
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
|
-
* (Vehicle Jobs' pure pieces), schedules, watches,
|
|
9
|
-
* JSON
|
|
10
|
-
*
|
|
8
|
+
* (Vehicle Jobs' pure pieces), schedules, watches, persistence (atomic
|
|
9
|
+
* JSON), concurrency (timer-based scheduling primitives), and cli-safety
|
|
10
|
+
* (argv-injection guards for any operation shelling out to a CLI) -- the
|
|
11
|
+
* latter three are technical utilities, not Vehicle protocol capabilities,
|
|
12
|
+
* kept distinct for that reason. Every symbol below is re-exported unchanged
|
|
11
13
|
* from its historical flat-file home, so root-level `import { X } from
|
|
12
14
|
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
13
15
|
*/
|
|
14
16
|
export * from "./approvals/index.js";
|
|
17
|
+
export * from "./cli-safety/index.js";
|
|
15
18
|
export * from "./client/index.js";
|
|
19
|
+
export * from "./concurrency/index.js";
|
|
16
20
|
export * from "./content/index.js";
|
|
17
21
|
export * from "./errors/index.js";
|
|
18
22
|
export * from "./events/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "Vehicle's runtime-neutral wire contract: operation descriptors, schema codecs, failure shapes. Zero runtime dependencies, zero Bun-specific code -- the one thing every Vehicle client and server package depends on.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards against the argv-injection class behind a well-known family of CLI-wrapper CVEs
|
|
3
|
+
* (simple-git's own history includes several): a caller-influenced string that starts with `-`
|
|
4
|
+
* can be parsed by the target CLI as a *flag* (`--upload-pack`, `--exec`, `--template`, `-c`
|
|
5
|
+
* config override) instead of the literal ref/path/pattern value the caller intended. Any Vehicle
|
|
6
|
+
* operation that hands a caller-supplied string to a shelled-out CLI's argv should run it through
|
|
7
|
+
* this check first, at the exact position it reaches that argv.
|
|
8
|
+
*
|
|
9
|
+
* This is a hard rejection with no exceptions -- it has no opinion about *why* a value starting
|
|
10
|
+
* with `-` might be needed and does not attempt to allow-list specific flags. A caller whose CLI
|
|
11
|
+
* genuinely accepts caller-influenced flag-shaped arguments needs a purpose-built allow-list of
|
|
12
|
+
* its own; this primitive only ever covers the common case of a value that should always be a
|
|
13
|
+
* literal.
|
|
14
|
+
*/
|
|
15
|
+
export class UnsafeCliArgument extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly value: string,
|
|
18
|
+
readonly fieldName?: string,
|
|
19
|
+
) {
|
|
20
|
+
super(
|
|
21
|
+
fieldName
|
|
22
|
+
? `"${value}" cannot be used as ${fieldName} -- it would be interpreted as a CLI flag, not a literal value`
|
|
23
|
+
: `"${value}" cannot be used as a CLI argument -- it would be interpreted as a flag, not a literal value`,
|
|
24
|
+
);
|
|
25
|
+
this.name = "UnsafeCliArgument";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Throws UnsafeCliArgument if `value` starts with `-`. `fieldName`, when given, names the field in the thrown error's own message for a caller with several distinct argv positions to check. */
|
|
30
|
+
export function assertNoLeadingFlagChar(value: string, fieldName?: string): void {
|
|
31
|
+
if (value.startsWith("-")) throw new UnsafeCliArgument(value, fieldName);
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./assert-no-leading-flag-char.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesces a burst of calls for the same key into exactly one callback fire,
|
|
3
|
+
* delayMs after the last call for that key -- the classic debounce shape,
|
|
4
|
+
* useful anywhere a flurry of raw upstream events for one logical unit of
|
|
5
|
+
* work (a filesystem save that fires more than once via temp-file write +
|
|
6
|
+
* atomic rename, a burst of webhook deliveries for the same resource) needs
|
|
7
|
+
* to collapse into a single downstream action. Different keys are fully
|
|
8
|
+
* independent. Pure timer bookkeeping, no I/O -- the callback itself does
|
|
9
|
+
* whatever real work is needed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The minimal logging surface this module needs -- any real Logger (e.g. Vehicle's own daemon Logger) satisfies this structurally, no adapter required. */
|
|
13
|
+
export interface MinimalLogger {
|
|
14
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
15
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DebounceCapacityExceeded extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
readonly key: string,
|
|
21
|
+
readonly max: number,
|
|
22
|
+
) {
|
|
23
|
+
super(`debounced scheduler distinct-key bound exceeded (${max}) scheduling key "${key}"`);
|
|
24
|
+
this.name = "DebounceCapacityExceeded";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DebouncedSchedulerOptions {
|
|
29
|
+
/** Maximum distinct keys with a pending fire at once. Default 4096. */
|
|
30
|
+
readonly maxKeys?: number;
|
|
31
|
+
readonly logger?: MinimalLogger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_MAX_KEYS = 4096;
|
|
35
|
+
const NOOP_LOGGER: MinimalLogger = { debug() {}, warn() {} };
|
|
36
|
+
|
|
37
|
+
export class DebouncedScheduler {
|
|
38
|
+
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
39
|
+
private readonly delayMs: number;
|
|
40
|
+
private readonly maxKeys: number;
|
|
41
|
+
private readonly logger: MinimalLogger;
|
|
42
|
+
|
|
43
|
+
constructor(delayMs: number, options: DebouncedSchedulerOptions = {}) {
|
|
44
|
+
if (!Number.isSafeInteger(delayMs) || delayMs < 0) throw new TypeError("delayMs must be a non-negative safe integer");
|
|
45
|
+
this.delayMs = delayMs;
|
|
46
|
+
this.maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS;
|
|
47
|
+
this.logger = options.logger ?? NOOP_LOGGER;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Schedules `callback` to run delayMs after this call, resetting any pending fire already
|
|
52
|
+
* scheduled for `key`. A callback that throws or rejects is caught and dropped -- there is
|
|
53
|
+
* no request awaiting this fire to report the error, and an unhandled timer failure would
|
|
54
|
+
* otherwise crash the whole process rather than just this one key's work.
|
|
55
|
+
* A caller that cares about its own errors should catch and log inside `callback` itself.
|
|
56
|
+
*/
|
|
57
|
+
schedule(key: string, callback: () => unknown): void {
|
|
58
|
+
const existing = this.timers.get(key);
|
|
59
|
+
if (existing) {
|
|
60
|
+
clearTimeout(existing);
|
|
61
|
+
this.logger.debug("debounced schedule coalesced", { component: "debounced-scheduler", operation: "schedule" });
|
|
62
|
+
} else if (this.timers.size >= this.maxKeys) {
|
|
63
|
+
this.logger.warn("debounced schedule rejected", {
|
|
64
|
+
component: "debounced-scheduler",
|
|
65
|
+
operation: "schedule",
|
|
66
|
+
code: "DebounceCapacityExceeded",
|
|
67
|
+
});
|
|
68
|
+
throw new DebounceCapacityExceeded(key, this.maxKeys);
|
|
69
|
+
}
|
|
70
|
+
const reportFailure = (error: unknown): void => {
|
|
71
|
+
this.logger.warn("debounced callback failed", {
|
|
72
|
+
component: "debounced-scheduler",
|
|
73
|
+
operation: "fire",
|
|
74
|
+
code: error instanceof Error ? error.name || "Error" : "Error",
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
this.timers.delete(key);
|
|
79
|
+
try {
|
|
80
|
+
Promise.resolve(callback()).catch(reportFailure);
|
|
81
|
+
} catch (error: unknown) {
|
|
82
|
+
reportFailure(error);
|
|
83
|
+
}
|
|
84
|
+
}, this.delayMs);
|
|
85
|
+
this.timers.set(key, timer);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Cancels `key`'s pending fire, if any. Idempotent -- an unknown or already-fired key is a safe no-op. */
|
|
89
|
+
cancel(key: string): void {
|
|
90
|
+
const existing = this.timers.get(key);
|
|
91
|
+
if (!existing) return;
|
|
92
|
+
clearTimeout(existing);
|
|
93
|
+
this.timers.delete(key);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True while `key` has a fire pending. */
|
|
97
|
+
has(key: string): boolean {
|
|
98
|
+
return this.timers.has(key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Cancels every pending key at once -- for clean shutdown. */
|
|
102
|
+
clear(): void {
|
|
103
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
104
|
+
this.timers.clear();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./debounced-scheduler.js";
|
package/src/index.ts
CHANGED
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* (model-facing narrative blocks), operations (descriptors, effect
|
|
6
6
|
* classification, invocation context), events, manifest, client (the port a
|
|
7
7
|
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
|
-
* (Vehicle Jobs' pure pieces), schedules, watches,
|
|
9
|
-
* JSON
|
|
10
|
-
*
|
|
8
|
+
* (Vehicle Jobs' pure pieces), schedules, watches, persistence (atomic
|
|
9
|
+
* JSON), concurrency (timer-based scheduling primitives), and cli-safety
|
|
10
|
+
* (argv-injection guards for any operation shelling out to a CLI) -- the
|
|
11
|
+
* latter three are technical utilities, not Vehicle protocol capabilities,
|
|
12
|
+
* kept distinct for that reason. Every symbol below is re-exported unchanged
|
|
11
13
|
* from its historical flat-file home, so root-level `import { X } from
|
|
12
14
|
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
13
15
|
*/
|
|
14
16
|
export * from "./approvals/index.js";
|
|
17
|
+
export * from "./cli-safety/index.js";
|
|
15
18
|
export * from "./client/index.js";
|
|
19
|
+
export * from "./concurrency/index.js";
|
|
16
20
|
export * from "./content/index.js";
|
|
17
21
|
export * from "./errors/index.js";
|
|
18
22
|
export * from "./events/index.js";
|