@danypops/vehicle-core 0.9.0 → 0.11.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/vehicle-errors.d.ts +20 -0
- package/dist/vehicle-errors.js +13 -0
- package/dist/vehicle-scheduler.d.ts +63 -0
- package/dist/vehicle-scheduler.js +33 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/vehicle-errors.ts +30 -0
- package/src/vehicle-scheduler.ts +73 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/vehicle-errors.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface VehicleFailure {
|
|
|
14
14
|
readonly recovery?: VehicleRecovery;
|
|
15
15
|
readonly details?: JsonValue;
|
|
16
16
|
readonly operationId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The underlying cause's own message, when this failure wraps an unexpected error (e.g. a
|
|
19
|
+
* handler throwing something other than a VehicleError) -- bounded, never a full stack trace.
|
|
20
|
+
* Without this, a caller sees only a generic template like "x handler failed" and has no way
|
|
21
|
+
* to tell what actually went wrong or whether the underlying operation may have partially
|
|
22
|
+
* applied.
|
|
23
|
+
*/
|
|
24
|
+
readonly causeMessage?: string;
|
|
17
25
|
}
|
|
18
26
|
export interface VehicleErrorOptions {
|
|
19
27
|
readonly category: VehicleFailureCategory;
|
|
@@ -23,6 +31,15 @@ export interface VehicleErrorOptions {
|
|
|
23
31
|
readonly details?: JsonValue;
|
|
24
32
|
readonly operationId?: string;
|
|
25
33
|
readonly cause?: unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Secure by default (false): `cause` is always attached to the real in-process Error chain
|
|
36
|
+
* (for server-side logging/observability), but its message crosses the wire via
|
|
37
|
+
* toFailure().causeMessage only when the throw site explicitly opts in here -- an arbitrary
|
|
38
|
+
* cause's message could contain a credential, an internal path, or other detail the thrower
|
|
39
|
+
* never reviewed for wire-safety. Set true only when the cause is known-safe to show (e.g. a
|
|
40
|
+
* validation library's own message intended for the caller).
|
|
41
|
+
*/
|
|
42
|
+
readonly exposeCause?: boolean;
|
|
26
43
|
}
|
|
27
44
|
export declare class VehicleError extends Error {
|
|
28
45
|
readonly code: string;
|
|
@@ -32,7 +49,10 @@ export declare class VehicleError extends Error {
|
|
|
32
49
|
readonly recovery?: VehicleRecovery;
|
|
33
50
|
readonly details?: JsonValue;
|
|
34
51
|
readonly operationId?: string;
|
|
52
|
+
private readonly exposeCause;
|
|
35
53
|
constructor(code: string, message: string, options: VehicleErrorOptions);
|
|
36
54
|
toFailure(): VehicleFailure;
|
|
37
55
|
}
|
|
56
|
+
/** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
|
|
57
|
+
export declare function boundedCauseMessage(cause: unknown): string | undefined;
|
|
38
58
|
export declare function boundedValidationDetails(issues: readonly VehicleSchemaIssue[] | undefined): JsonValue | undefined;
|
package/dist/vehicle-errors.js
CHANGED
|
@@ -6,6 +6,7 @@ export class VehicleError extends Error {
|
|
|
6
6
|
recovery;
|
|
7
7
|
details;
|
|
8
8
|
operationId;
|
|
9
|
+
exposeCause;
|
|
9
10
|
constructor(code, message, options) {
|
|
10
11
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
11
12
|
this.code = code;
|
|
@@ -16,8 +17,10 @@ export class VehicleError extends Error {
|
|
|
16
17
|
this.recovery = options.recovery;
|
|
17
18
|
this.details = options.details;
|
|
18
19
|
this.operationId = options.operationId;
|
|
20
|
+
this.exposeCause = options.exposeCause ?? false;
|
|
19
21
|
}
|
|
20
22
|
toFailure() {
|
|
23
|
+
const causeMessage = this.exposeCause ? boundedCauseMessage(this.cause) : undefined;
|
|
21
24
|
return {
|
|
22
25
|
code: this.code,
|
|
23
26
|
category: this.category,
|
|
@@ -27,9 +30,19 @@ export class VehicleError extends Error {
|
|
|
27
30
|
...(this.recovery === undefined ? {} : { recovery: this.recovery }),
|
|
28
31
|
...(this.details === undefined ? {} : { details: this.details }),
|
|
29
32
|
...(this.operationId === undefined ? {} : { operationId: this.operationId }),
|
|
33
|
+
...(causeMessage === undefined ? {} : { causeMessage }),
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
const MAX_CAUSE_MESSAGE_LENGTH = 500;
|
|
38
|
+
/** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
|
|
39
|
+
export function boundedCauseMessage(cause) {
|
|
40
|
+
if (cause instanceof Error && cause.message.length > 0)
|
|
41
|
+
return cause.message.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
42
|
+
if (typeof cause === "string" && cause.length > 0)
|
|
43
|
+
return cause.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
33
46
|
const MAX_VALIDATION_ISSUES = 10;
|
|
34
47
|
const MAX_ISSUE_MESSAGE_LENGTH = 500;
|
|
35
48
|
const MAX_ISSUE_PATH_LENGTH = 20;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Trigger something at a future time or on a recurring interval,"
|
|
3
|
+
* independent of any specific job's own lifecycle -- a distinct shape from
|
|
4
|
+
* Vehicle Jobs (submit-then-track a unit of work already running). Modeled
|
|
5
|
+
* on ~/Workspace/alef's packages/core/foundry/src/scheduler.ts
|
|
6
|
+
* (defer/repeat/cancel/list), generalized so the fired action is a
|
|
7
|
+
* declarative Vehicle operation invocation or event emission -- never a
|
|
8
|
+
* bespoke callback closure, so it can be persisted and re-armed after a
|
|
9
|
+
* restart the way a closure never could.
|
|
10
|
+
*
|
|
11
|
+
* Pure pieces only: trigger/action shapes and the fire-time arithmetic.
|
|
12
|
+
* Real timers, persistence, and registry wiring live in vehicle-server's
|
|
13
|
+
* VehicleScheduler, the same core/server split vehicle-jobs.ts uses.
|
|
14
|
+
*/
|
|
15
|
+
import type { JsonValue } from "./vehicle-contract.js";
|
|
16
|
+
export type VehicleScheduleTrigger = {
|
|
17
|
+
readonly kind: "at";
|
|
18
|
+
readonly at: number;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: "every";
|
|
21
|
+
readonly intervalMs: number;
|
|
22
|
+
};
|
|
23
|
+
export type VehicleScheduleAction = {
|
|
24
|
+
readonly kind: "operation";
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly version: number;
|
|
27
|
+
readonly input: JsonValue;
|
|
28
|
+
readonly permissions?: readonly string[];
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "event";
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly version: number;
|
|
33
|
+
readonly payload: JsonValue;
|
|
34
|
+
};
|
|
35
|
+
export interface VehicleScheduledEntry {
|
|
36
|
+
readonly scheduleId: string;
|
|
37
|
+
readonly owner: string;
|
|
38
|
+
readonly trigger: VehicleScheduleTrigger;
|
|
39
|
+
readonly action: VehicleScheduleAction;
|
|
40
|
+
readonly createdAt: number;
|
|
41
|
+
/** For "at": consumed once it fires. For "every": advanced to the next tick after each fire. */
|
|
42
|
+
readonly nextFireAt: number;
|
|
43
|
+
}
|
|
44
|
+
/** Matches WatchRegistry's own historical default (Lector's MAX_WATCHES_PER_WORKSPACE). */
|
|
45
|
+
export declare const DEFAULT_MAX_SCHEDULES_PER_OWNER = 32;
|
|
46
|
+
/** Raised when an owner already has its configured maximum of schedules -- fails closed, the same bounded-resource discipline WatchLimitExceeded already applies to Vehicle Watchers. */
|
|
47
|
+
export declare class VehicleScheduleLimitExceeded extends Error {
|
|
48
|
+
readonly owner: string;
|
|
49
|
+
readonly max: number;
|
|
50
|
+
constructor(owner: string, max: number);
|
|
51
|
+
}
|
|
52
|
+
/** The first fire time for a freshly created schedule. */
|
|
53
|
+
export declare function initialFireAt(trigger: VehicleScheduleTrigger, now: number): number;
|
|
54
|
+
/** The next fire time after a successful fire, or undefined if the entry (a one-shot "at") should be removed instead of re-armed. */
|
|
55
|
+
export declare function nextFireAtAfterFire(trigger: VehicleScheduleTrigger, now: number): number | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Where a restored entry should be re-armed to. A one-shot "at" entry keeps
|
|
58
|
+
* its original persisted time (fires as soon as possible if overdue -- the
|
|
59
|
+
* one thing it was supposed to do must not be silently lost). A recurring
|
|
60
|
+
* "every" entry resumes its normal cadence from now if it fell behind while
|
|
61
|
+
* the daemon was down, rather than firing once per missed tick.
|
|
62
|
+
*/
|
|
63
|
+
export declare function nextFireAtAfterRestore(trigger: VehicleScheduleTrigger, persistedNextFireAt: number, now: number): number;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Matches WatchRegistry's own historical default (Lector's MAX_WATCHES_PER_WORKSPACE). */
|
|
2
|
+
export const DEFAULT_MAX_SCHEDULES_PER_OWNER = 32;
|
|
3
|
+
/** Raised when an owner already has its configured maximum of schedules -- fails closed, the same bounded-resource discipline WatchLimitExceeded already applies to Vehicle Watchers. */
|
|
4
|
+
export class VehicleScheduleLimitExceeded extends Error {
|
|
5
|
+
owner;
|
|
6
|
+
max;
|
|
7
|
+
constructor(owner, max) {
|
|
8
|
+
super(`owner "${owner}" already has ${max} active schedules -- cancel one before adding another`);
|
|
9
|
+
this.owner = owner;
|
|
10
|
+
this.max = max;
|
|
11
|
+
this.name = "VehicleScheduleLimitExceeded";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** The first fire time for a freshly created schedule. */
|
|
15
|
+
export function initialFireAt(trigger, now) {
|
|
16
|
+
return trigger.kind === "at" ? trigger.at : now + trigger.intervalMs;
|
|
17
|
+
}
|
|
18
|
+
/** The next fire time after a successful fire, or undefined if the entry (a one-shot "at") should be removed instead of re-armed. */
|
|
19
|
+
export function nextFireAtAfterFire(trigger, now) {
|
|
20
|
+
return trigger.kind === "every" ? now + trigger.intervalMs : undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Where a restored entry should be re-armed to. A one-shot "at" entry keeps
|
|
24
|
+
* its original persisted time (fires as soon as possible if overdue -- the
|
|
25
|
+
* one thing it was supposed to do must not be silently lost). A recurring
|
|
26
|
+
* "every" entry resumes its normal cadence from now if it fell behind while
|
|
27
|
+
* the daemon was down, rather than firing once per missed tick.
|
|
28
|
+
*/
|
|
29
|
+
export function nextFireAtAfterRestore(trigger, persistedNextFireAt, now) {
|
|
30
|
+
if (trigger.kind === "at")
|
|
31
|
+
return persistedNextFireAt;
|
|
32
|
+
return persistedNextFireAt > now ? persistedNextFireAt : now + trigger.intervalMs;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
package/src/index.ts
CHANGED
package/src/vehicle-errors.ts
CHANGED
|
@@ -45,6 +45,14 @@ export interface VehicleFailure {
|
|
|
45
45
|
readonly recovery?: VehicleRecovery;
|
|
46
46
|
readonly details?: JsonValue;
|
|
47
47
|
readonly operationId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The underlying cause's own message, when this failure wraps an unexpected error (e.g. a
|
|
50
|
+
* handler throwing something other than a VehicleError) -- bounded, never a full stack trace.
|
|
51
|
+
* Without this, a caller sees only a generic template like "x handler failed" and has no way
|
|
52
|
+
* to tell what actually went wrong or whether the underlying operation may have partially
|
|
53
|
+
* applied.
|
|
54
|
+
*/
|
|
55
|
+
readonly causeMessage?: string;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export interface VehicleErrorOptions {
|
|
@@ -55,6 +63,15 @@ export interface VehicleErrorOptions {
|
|
|
55
63
|
readonly details?: JsonValue;
|
|
56
64
|
readonly operationId?: string;
|
|
57
65
|
readonly cause?: unknown;
|
|
66
|
+
/**
|
|
67
|
+
* Secure by default (false): `cause` is always attached to the real in-process Error chain
|
|
68
|
+
* (for server-side logging/observability), but its message crosses the wire via
|
|
69
|
+
* toFailure().causeMessage only when the throw site explicitly opts in here -- an arbitrary
|
|
70
|
+
* cause's message could contain a credential, an internal path, or other detail the thrower
|
|
71
|
+
* never reviewed for wire-safety. Set true only when the cause is known-safe to show (e.g. a
|
|
72
|
+
* validation library's own message intended for the caller).
|
|
73
|
+
*/
|
|
74
|
+
readonly exposeCause?: boolean;
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
export class VehicleError extends Error {
|
|
@@ -64,6 +81,7 @@ export class VehicleError extends Error {
|
|
|
64
81
|
readonly recovery?: VehicleRecovery;
|
|
65
82
|
readonly details?: JsonValue;
|
|
66
83
|
readonly operationId?: string;
|
|
84
|
+
private readonly exposeCause: boolean;
|
|
67
85
|
|
|
68
86
|
constructor(
|
|
69
87
|
readonly code: string,
|
|
@@ -78,9 +96,11 @@ export class VehicleError extends Error {
|
|
|
78
96
|
this.recovery = options.recovery;
|
|
79
97
|
this.details = options.details;
|
|
80
98
|
this.operationId = options.operationId;
|
|
99
|
+
this.exposeCause = options.exposeCause ?? false;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
toFailure(): VehicleFailure {
|
|
103
|
+
const causeMessage = this.exposeCause ? boundedCauseMessage(this.cause) : undefined;
|
|
84
104
|
return {
|
|
85
105
|
code: this.code,
|
|
86
106
|
category: this.category,
|
|
@@ -90,10 +110,20 @@ export class VehicleError extends Error {
|
|
|
90
110
|
...(this.recovery === undefined ? {} : { recovery: this.recovery }),
|
|
91
111
|
...(this.details === undefined ? {} : { details: this.details }),
|
|
92
112
|
...(this.operationId === undefined ? {} : { operationId: this.operationId }),
|
|
113
|
+
...(causeMessage === undefined ? {} : { causeMessage }),
|
|
93
114
|
};
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
|
|
118
|
+
const MAX_CAUSE_MESSAGE_LENGTH = 500;
|
|
119
|
+
|
|
120
|
+
/** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
|
|
121
|
+
export function boundedCauseMessage(cause: unknown): string | undefined {
|
|
122
|
+
if (cause instanceof Error && cause.message.length > 0) return cause.message.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
123
|
+
if (typeof cause === "string" && cause.length > 0) return cause.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
const MAX_VALIDATION_ISSUES = 10;
|
|
98
128
|
const MAX_ISSUE_MESSAGE_LENGTH = 500;
|
|
99
129
|
const MAX_ISSUE_PATH_LENGTH = 20;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Trigger something at a future time or on a recurring interval,"
|
|
3
|
+
* independent of any specific job's own lifecycle -- a distinct shape from
|
|
4
|
+
* Vehicle Jobs (submit-then-track a unit of work already running). Modeled
|
|
5
|
+
* on ~/Workspace/alef's packages/core/foundry/src/scheduler.ts
|
|
6
|
+
* (defer/repeat/cancel/list), generalized so the fired action is a
|
|
7
|
+
* declarative Vehicle operation invocation or event emission -- never a
|
|
8
|
+
* bespoke callback closure, so it can be persisted and re-armed after a
|
|
9
|
+
* restart the way a closure never could.
|
|
10
|
+
*
|
|
11
|
+
* Pure pieces only: trigger/action shapes and the fire-time arithmetic.
|
|
12
|
+
* Real timers, persistence, and registry wiring live in vehicle-server's
|
|
13
|
+
* VehicleScheduler, the same core/server split vehicle-jobs.ts uses.
|
|
14
|
+
*/
|
|
15
|
+
import type { JsonValue } from "./vehicle-contract.js";
|
|
16
|
+
|
|
17
|
+
export type VehicleScheduleTrigger = { readonly kind: "at"; readonly at: number } | { readonly kind: "every"; readonly intervalMs: number };
|
|
18
|
+
|
|
19
|
+
export type VehicleScheduleAction =
|
|
20
|
+
| {
|
|
21
|
+
readonly kind: "operation";
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly version: number;
|
|
24
|
+
readonly input: JsonValue;
|
|
25
|
+
readonly permissions?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
| { readonly kind: "event"; readonly name: string; readonly version: number; readonly payload: JsonValue };
|
|
28
|
+
|
|
29
|
+
export interface VehicleScheduledEntry {
|
|
30
|
+
readonly scheduleId: string;
|
|
31
|
+
readonly owner: string;
|
|
32
|
+
readonly trigger: VehicleScheduleTrigger;
|
|
33
|
+
readonly action: VehicleScheduleAction;
|
|
34
|
+
readonly createdAt: number;
|
|
35
|
+
/** For "at": consumed once it fires. For "every": advanced to the next tick after each fire. */
|
|
36
|
+
readonly nextFireAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Matches WatchRegistry's own historical default (Lector's MAX_WATCHES_PER_WORKSPACE). */
|
|
40
|
+
export const DEFAULT_MAX_SCHEDULES_PER_OWNER = 32;
|
|
41
|
+
|
|
42
|
+
/** Raised when an owner already has its configured maximum of schedules -- fails closed, the same bounded-resource discipline WatchLimitExceeded already applies to Vehicle Watchers. */
|
|
43
|
+
export class VehicleScheduleLimitExceeded extends Error {
|
|
44
|
+
constructor(
|
|
45
|
+
readonly owner: string,
|
|
46
|
+
readonly max: number,
|
|
47
|
+
) {
|
|
48
|
+
super(`owner "${owner}" already has ${max} active schedules -- cancel one before adding another`);
|
|
49
|
+
this.name = "VehicleScheduleLimitExceeded";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The first fire time for a freshly created schedule. */
|
|
54
|
+
export function initialFireAt(trigger: VehicleScheduleTrigger, now: number): number {
|
|
55
|
+
return trigger.kind === "at" ? trigger.at : now + trigger.intervalMs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The next fire time after a successful fire, or undefined if the entry (a one-shot "at") should be removed instead of re-armed. */
|
|
59
|
+
export function nextFireAtAfterFire(trigger: VehicleScheduleTrigger, now: number): number | undefined {
|
|
60
|
+
return trigger.kind === "every" ? now + trigger.intervalMs : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where a restored entry should be re-armed to. A one-shot "at" entry keeps
|
|
65
|
+
* its original persisted time (fires as soon as possible if overdue -- the
|
|
66
|
+
* one thing it was supposed to do must not be silently lost). A recurring
|
|
67
|
+
* "every" entry resumes its normal cadence from now if it fell behind while
|
|
68
|
+
* the daemon was down, rather than firing once per missed tick.
|
|
69
|
+
*/
|
|
70
|
+
export function nextFireAtAfterRestore(trigger: VehicleScheduleTrigger, persistedNextFireAt: number, now: number): number {
|
|
71
|
+
if (trigger.kind === "at") return persistedNextFireAt;
|
|
72
|
+
return persistedNextFireAt > now ? persistedNextFireAt : now + trigger.intervalMs;
|
|
73
|
+
}
|