@ontrails/core 1.0.0-beta.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/.turbo/turbo-build.log +1 -0
- package/.turbo/turbo-lint.log +3 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +15 -0
- package/README.md +179 -0
- package/dist/adapters.d.ts +39 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +2 -0
- package/dist/adapters.js.map +1 -0
- package/dist/blob-ref.d.ts +20 -0
- package/dist/blob-ref.d.ts.map +1 -0
- package/dist/blob-ref.js +22 -0
- package/dist/blob-ref.js.map +1 -0
- package/dist/branded.d.ts +36 -0
- package/dist/branded.d.ts.map +1 -0
- package/dist/branded.js +89 -0
- package/dist/branded.js.map +1 -0
- package/dist/collections.d.ts +31 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +60 -0
- package/dist/collections.js.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +15 -0
- package/dist/context.js.map +1 -0
- package/dist/derive.d.ts +33 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +122 -0
- package/dist/derive.js.map +1 -0
- package/dist/errors.d.ts +83 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +142 -0
- package/dist/errors.js.map +1 -0
- package/dist/event.d.ts +45 -0
- package/dist/event.d.ts.map +1 -0
- package/dist/event.js +17 -0
- package/dist/event.js.map +1 -0
- package/dist/fetch.d.ts +15 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +102 -0
- package/dist/fetch.js.map +1 -0
- package/dist/guards.d.ts +17 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +25 -0
- package/dist/guards.js.map +1 -0
- package/dist/health.d.ts +18 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +5 -0
- package/dist/health.js.map +1 -0
- package/dist/hike.d.ts +36 -0
- package/dist/hike.d.ts.map +1 -0
- package/dist/hike.js +20 -0
- package/dist/hike.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/job.d.ts +24 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +17 -0
- package/dist/job.js.map +1 -0
- package/dist/layer.d.ts +17 -0
- package/dist/layer.d.ts.map +1 -0
- package/dist/layer.js +21 -0
- package/dist/layer.js.map +1 -0
- package/dist/path-security.d.ts +28 -0
- package/dist/path-security.d.ts.map +1 -0
- package/dist/path-security.js +63 -0
- package/dist/path-security.js.map +1 -0
- package/dist/patterns/bulk.d.ts +15 -0
- package/dist/patterns/bulk.d.ts.map +1 -0
- package/dist/patterns/bulk.js +14 -0
- package/dist/patterns/bulk.js.map +1 -0
- package/dist/patterns/change.d.ts +10 -0
- package/dist/patterns/change.d.ts.map +1 -0
- package/dist/patterns/change.js +10 -0
- package/dist/patterns/change.js.map +1 -0
- package/dist/patterns/date-range.d.ts +10 -0
- package/dist/patterns/date-range.d.ts.map +1 -0
- package/dist/patterns/date-range.js +10 -0
- package/dist/patterns/date-range.js.map +1 -0
- package/dist/patterns/index.d.ts +9 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +9 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/patterns/pagination.d.ts +18 -0
- package/dist/patterns/pagination.d.ts.map +1 -0
- package/dist/patterns/pagination.js +18 -0
- package/dist/patterns/pagination.js.map +1 -0
- package/dist/patterns/progress.d.ts +11 -0
- package/dist/patterns/progress.d.ts.map +1 -0
- package/dist/patterns/progress.js +11 -0
- package/dist/patterns/progress.js.map +1 -0
- package/dist/patterns/sorting.d.ts +13 -0
- package/dist/patterns/sorting.d.ts.map +1 -0
- package/dist/patterns/sorting.js +10 -0
- package/dist/patterns/sorting.js.map +1 -0
- package/dist/patterns/status.d.ts +15 -0
- package/dist/patterns/status.d.ts.map +1 -0
- package/dist/patterns/status.js +9 -0
- package/dist/patterns/status.js.map +1 -0
- package/dist/patterns/timestamps.d.ts +10 -0
- package/dist/patterns/timestamps.d.ts.map +1 -0
- package/dist/patterns/timestamps.js +10 -0
- package/dist/patterns/timestamps.js.map +1 -0
- package/dist/redaction/index.d.ts +4 -0
- package/dist/redaction/index.d.ts.map +1 -0
- package/dist/redaction/index.js +3 -0
- package/dist/redaction/index.js.map +1 -0
- package/dist/redaction/patterns.d.ts +9 -0
- package/dist/redaction/patterns.d.ts.map +1 -0
- package/dist/redaction/patterns.js +39 -0
- package/dist/redaction/patterns.js.map +1 -0
- package/dist/redaction/redactor.d.ts +27 -0
- package/dist/redaction/redactor.d.ts.map +1 -0
- package/dist/redaction/redactor.js +89 -0
- package/dist/redaction/redactor.js.map +1 -0
- package/dist/resilience.d.ts +34 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +164 -0
- package/dist/resilience.js.map +1 -0
- package/dist/result.d.ts +57 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +145 -0
- package/dist/result.js.map +1 -0
- package/dist/serialization.d.ts +27 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/serialization.js +115 -0
- package/dist/serialization.js.map +1 -0
- package/dist/topo.d.ts +18 -0
- package/dist/topo.d.ts.map +1 -0
- package/dist/topo.js +74 -0
- package/dist/topo.js.map +1 -0
- package/dist/trail.d.ts +83 -0
- package/dist/trail.d.ts.map +1 -0
- package/dist/trail.js +16 -0
- package/dist/trail.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-topo.d.ts +24 -0
- package/dist/validate-topo.d.ts.map +1 -0
- package/dist/validate-topo.js +108 -0
- package/dist/validate-topo.js.map +1 -0
- package/dist/validation.d.ts +27 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +134 -0
- package/dist/validation.js.map +1 -0
- package/dist/workspace.d.ts +25 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +57 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +21 -0
- package/src/__tests__/blob-ref.test.ts +103 -0
- package/src/__tests__/branded.test.ts +148 -0
- package/src/__tests__/collections.test.ts +126 -0
- package/src/__tests__/context.test.ts +66 -0
- package/src/__tests__/derive.test.ts +159 -0
- package/src/__tests__/errors.test.ts +309 -0
- package/src/__tests__/event.test.ts +82 -0
- package/src/__tests__/fetch.test.ts +217 -0
- package/src/__tests__/guards.test.ts +102 -0
- package/src/__tests__/hike.test.ts +117 -0
- package/src/__tests__/job.test.ts +98 -0
- package/src/__tests__/layer.test.ts +224 -0
- package/src/__tests__/path-security.test.ts +114 -0
- package/src/__tests__/patterns.test.ts +273 -0
- package/src/__tests__/redaction.test.ts +244 -0
- package/src/__tests__/resilience.test.ts +246 -0
- package/src/__tests__/result.test.ts +155 -0
- package/src/__tests__/serialization.test.ts +236 -0
- package/src/__tests__/topo.test.ts +184 -0
- package/src/__tests__/trail.test.ts +179 -0
- package/src/__tests__/validate-topo.test.ts +201 -0
- package/src/__tests__/validation.test.ts +283 -0
- package/src/__tests__/workspace.test.ts +183 -0
- package/src/adapters.ts +68 -0
- package/src/blob-ref.ts +39 -0
- package/src/branded.ts +135 -0
- package/src/collections.ts +99 -0
- package/src/context.ts +18 -0
- package/src/derive.ts +223 -0
- package/src/errors.ts +196 -0
- package/src/event.ts +77 -0
- package/src/fetch.ts +138 -0
- package/src/guards.ts +37 -0
- package/src/health.ts +23 -0
- package/src/hike.ts +77 -0
- package/src/index.ts +158 -0
- package/src/job.ts +20 -0
- package/src/layer.ts +44 -0
- package/src/path-security.ts +90 -0
- package/src/patterns/bulk.ts +16 -0
- package/src/patterns/change.ts +12 -0
- package/src/patterns/date-range.ts +12 -0
- package/src/patterns/index.ts +8 -0
- package/src/patterns/pagination.ts +22 -0
- package/src/patterns/progress.ts +13 -0
- package/src/patterns/sorting.ts +14 -0
- package/src/patterns/status.ts +11 -0
- package/src/patterns/timestamps.ts +12 -0
- package/src/redaction/index.ts +3 -0
- package/src/redaction/patterns.ts +47 -0
- package/src/redaction/redactor.ts +178 -0
- package/src/resilience.ts +234 -0
- package/src/result.ts +180 -0
- package/src/serialization.ts +183 -0
- package/src/topo.ts +123 -0
- package/src/trail.ts +130 -0
- package/src/types.ts +58 -0
- package/src/validate-topo.ts +151 -0
- package/src/validation.ts +182 -0
- package/src/workspace.ts +77 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* oxlint-disable max-classes-per-file -- error taxonomy requires co-located class definitions */
|
|
2
|
+
/**
|
|
3
|
+
* Error taxonomy for @ontrails/core
|
|
4
|
+
*
|
|
5
|
+
* Provides a structured error hierarchy with category-based mapping
|
|
6
|
+
* to exit codes, HTTP status codes, and JSON-RPC error codes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Category
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export type ErrorCategory =
|
|
14
|
+
| 'validation'
|
|
15
|
+
| 'not_found'
|
|
16
|
+
| 'conflict'
|
|
17
|
+
| 'permission'
|
|
18
|
+
| 'timeout'
|
|
19
|
+
| 'rate_limit'
|
|
20
|
+
| 'network'
|
|
21
|
+
| 'internal'
|
|
22
|
+
| 'auth'
|
|
23
|
+
| 'cancelled';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Base class
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export abstract class TrailsError extends Error {
|
|
30
|
+
abstract readonly category: ErrorCategory;
|
|
31
|
+
abstract readonly retryable: boolean;
|
|
32
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
message: string,
|
|
36
|
+
options?: { cause?: Error; context?: Record<string, unknown> }
|
|
37
|
+
) {
|
|
38
|
+
super(message, { cause: options?.cause });
|
|
39
|
+
this.name = this.constructor.name;
|
|
40
|
+
this.context = options?.context;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Concrete error classes
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export class ValidationError extends TrailsError {
|
|
49
|
+
readonly category = 'validation' as const;
|
|
50
|
+
readonly retryable = false as const;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class AmbiguousError extends TrailsError {
|
|
54
|
+
readonly category = 'validation' as const;
|
|
55
|
+
readonly retryable = false as const;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class AssertionError extends TrailsError {
|
|
59
|
+
readonly category = 'internal' as const;
|
|
60
|
+
readonly retryable = false as const;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class NotFoundError extends TrailsError {
|
|
64
|
+
readonly category = 'not_found' as const;
|
|
65
|
+
readonly retryable = false as const;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class AlreadyExistsError extends TrailsError {
|
|
69
|
+
readonly category = 'conflict' as const;
|
|
70
|
+
readonly retryable = false as const;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class ConflictError extends TrailsError {
|
|
74
|
+
readonly category = 'conflict' as const;
|
|
75
|
+
readonly retryable = false as const;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class PermissionError extends TrailsError {
|
|
79
|
+
readonly category = 'permission' as const;
|
|
80
|
+
readonly retryable = false as const;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class TimeoutError extends TrailsError {
|
|
84
|
+
readonly category = 'timeout' as const;
|
|
85
|
+
readonly retryable = true as const;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class RateLimitError extends TrailsError {
|
|
89
|
+
readonly category = 'rate_limit' as const;
|
|
90
|
+
readonly retryable = true as const;
|
|
91
|
+
readonly retryAfter?: number | undefined;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
message: string,
|
|
95
|
+
options?: {
|
|
96
|
+
cause?: Error;
|
|
97
|
+
context?: Record<string, unknown>;
|
|
98
|
+
retryAfter?: number;
|
|
99
|
+
}
|
|
100
|
+
) {
|
|
101
|
+
super(message, options);
|
|
102
|
+
this.retryAfter = options?.retryAfter;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class NetworkError extends TrailsError {
|
|
107
|
+
readonly category = 'network' as const;
|
|
108
|
+
readonly retryable = true as const;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class InternalError extends TrailsError {
|
|
112
|
+
readonly category = 'internal' as const;
|
|
113
|
+
readonly retryable = false as const;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class AuthError extends TrailsError {
|
|
117
|
+
readonly category = 'auth' as const;
|
|
118
|
+
readonly retryable = false as const;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class CancelledError extends TrailsError {
|
|
122
|
+
readonly category = 'cancelled' as const;
|
|
123
|
+
readonly retryable = false as const;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Taxonomy maps
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export const exitCodeMap: Record<ErrorCategory, number> = {
|
|
131
|
+
auth: 9,
|
|
132
|
+
cancelled: 130,
|
|
133
|
+
conflict: 3,
|
|
134
|
+
internal: 8,
|
|
135
|
+
network: 7,
|
|
136
|
+
not_found: 2,
|
|
137
|
+
permission: 4,
|
|
138
|
+
rate_limit: 6,
|
|
139
|
+
timeout: 5,
|
|
140
|
+
validation: 1,
|
|
141
|
+
} as const;
|
|
142
|
+
|
|
143
|
+
export const statusCodeMap: Record<ErrorCategory, number> = {
|
|
144
|
+
auth: 401,
|
|
145
|
+
cancelled: 499,
|
|
146
|
+
conflict: 409,
|
|
147
|
+
internal: 500,
|
|
148
|
+
network: 502,
|
|
149
|
+
not_found: 404,
|
|
150
|
+
permission: 403,
|
|
151
|
+
rate_limit: 429,
|
|
152
|
+
timeout: 504,
|
|
153
|
+
validation: 400,
|
|
154
|
+
} as const;
|
|
155
|
+
|
|
156
|
+
export const jsonRpcCodeMap: Record<ErrorCategory, number> = {
|
|
157
|
+
auth: -32_600,
|
|
158
|
+
cancelled: -32_603,
|
|
159
|
+
conflict: -32_603,
|
|
160
|
+
internal: -32_603,
|
|
161
|
+
network: -32_603,
|
|
162
|
+
not_found: -32_601,
|
|
163
|
+
permission: -32_600,
|
|
164
|
+
rate_limit: -32_603,
|
|
165
|
+
timeout: -32_603,
|
|
166
|
+
validation: -32_602,
|
|
167
|
+
} as const;
|
|
168
|
+
|
|
169
|
+
export const retryableMap: Record<ErrorCategory, boolean> = {
|
|
170
|
+
auth: false,
|
|
171
|
+
cancelled: false,
|
|
172
|
+
conflict: false,
|
|
173
|
+
internal: false,
|
|
174
|
+
network: true,
|
|
175
|
+
not_found: false,
|
|
176
|
+
permission: false,
|
|
177
|
+
rate_limit: true,
|
|
178
|
+
timeout: true,
|
|
179
|
+
validation: false,
|
|
180
|
+
} as const;
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Helper functions
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** Type guard: narrows unknown to TrailsError */
|
|
187
|
+
export const isTrailsError = (error?: unknown): error is TrailsError =>
|
|
188
|
+
error instanceof TrailsError;
|
|
189
|
+
|
|
190
|
+
/** Returns true if the error is retryable (TrailsError with retryable category). */
|
|
191
|
+
export const isRetryable = (error: Error): boolean => {
|
|
192
|
+
if (isTrailsError(error)) {
|
|
193
|
+
return retryableMap[error.category];
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
package/src/event.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event — a named payload schema with optional provenance metadata.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Spec (input to the factory)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export interface EventSpec<T> {
|
|
12
|
+
readonly payload: z.ZodType<T>;
|
|
13
|
+
readonly description?: string | undefined;
|
|
14
|
+
readonly markers?: Readonly<Record<string, unknown>> | undefined;
|
|
15
|
+
/** Trail IDs that produce this event (e.g. the trails it originates from). */
|
|
16
|
+
readonly from?: readonly string[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Shape (output of the factory)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface Event<T> {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly kind: 'event';
|
|
26
|
+
readonly payload: z.ZodType<T>;
|
|
27
|
+
readonly description?: string | undefined;
|
|
28
|
+
readonly markers?: Readonly<Record<string, unknown>> | undefined;
|
|
29
|
+
/** Trail IDs that produce this event (e.g. the trails it originates from). */
|
|
30
|
+
readonly from?: readonly string[] | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Factory
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create an event definition.
|
|
39
|
+
*
|
|
40
|
+
* An event is a named payload schema describing something that happened.
|
|
41
|
+
* Returns a frozen object with `kind: "event"` and all spec fields.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // ID as first argument
|
|
46
|
+
* const updated = event("entity.updated", {
|
|
47
|
+
* payload: EntityUpdatedSchema,
|
|
48
|
+
* from: ["entity.add", "entity.update"],
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Full spec object (programmatic)
|
|
52
|
+
* const updated = event({ id: "entity.updated", payload: ..., from: [...] });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function event<T>(id: string, spec: EventSpec<T>): Event<T>;
|
|
56
|
+
export function event<T>(
|
|
57
|
+
spec: EventSpec<T> & { readonly id: string }
|
|
58
|
+
): Event<T>;
|
|
59
|
+
export function event<T>(
|
|
60
|
+
idOrSpec: string | (EventSpec<T> & { readonly id: string }),
|
|
61
|
+
maybeSpec?: EventSpec<T>
|
|
62
|
+
): Event<T> {
|
|
63
|
+
const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
|
|
64
|
+
// oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
|
|
65
|
+
const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
description: resolvedSpec.description,
|
|
68
|
+
from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
|
|
69
|
+
id: resolvedId,
|
|
70
|
+
kind: 'event' as const,
|
|
71
|
+
markers: resolvedSpec.markers,
|
|
72
|
+
payload: resolvedSpec.payload,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Existential type for heterogeneous event collections */
|
|
77
|
+
export type AnyEvent = Event<unknown>;
|
package/src/fetch.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch utilities for @ontrails/core
|
|
3
|
+
*
|
|
4
|
+
* Wraps the standard fetch API, mapping errors and HTTP status codes
|
|
5
|
+
* to the TrailsError taxonomy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AuthError,
|
|
10
|
+
CancelledError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
InternalError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
PermissionError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
TimeoutError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
} from './errors.js';
|
|
20
|
+
import { Result } from './result.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Internal helpers (defined before usage)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const toError = (err: unknown): Error =>
|
|
27
|
+
err instanceof Error ? err : new Error(String(err));
|
|
28
|
+
|
|
29
|
+
const parseRetryAfter = (header: string | null): number | undefined => {
|
|
30
|
+
if (!header) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const seconds = Number(header);
|
|
34
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
35
|
+
return seconds;
|
|
36
|
+
}
|
|
37
|
+
// Try parsing as HTTP-date
|
|
38
|
+
const date = Date.parse(header);
|
|
39
|
+
if (!Number.isNaN(date)) {
|
|
40
|
+
const delta = Math.ceil((date - Date.now()) / 1000);
|
|
41
|
+
return delta > 0 ? delta : undefined;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mapFetchError = (err: unknown): Error => {
|
|
47
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
48
|
+
return new CancelledError('Request was aborted', { cause: toError(err) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// TypeError is thrown for network failures in the fetch spec
|
|
52
|
+
if (err instanceof TypeError) {
|
|
53
|
+
return new NetworkError('Network request failed', { cause: err });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return new NetworkError('Network request failed', {
|
|
57
|
+
cause: toError(err),
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type StatusMapper = (
|
|
62
|
+
context: Record<string, unknown>,
|
|
63
|
+
response: Response
|
|
64
|
+
) => Error;
|
|
65
|
+
|
|
66
|
+
const statusMappers: Record<number, StatusMapper> = {
|
|
67
|
+
401: (ctx) => new AuthError('Unauthorized', { context: ctx }),
|
|
68
|
+
403: (ctx) => new PermissionError('Forbidden', { context: ctx }),
|
|
69
|
+
404: (ctx) => new NotFoundError('Not found', { context: ctx }),
|
|
70
|
+
429: (ctx, response) => {
|
|
71
|
+
const retryAfter = parseRetryAfter(response.headers.get('retry-after'));
|
|
72
|
+
const opts: { context: Record<string, unknown>; retryAfter?: number } = {
|
|
73
|
+
context: ctx,
|
|
74
|
+
};
|
|
75
|
+
if (retryAfter !== undefined) {
|
|
76
|
+
opts.retryAfter = retryAfter;
|
|
77
|
+
}
|
|
78
|
+
return new RateLimitError('Rate limited', opts);
|
|
79
|
+
},
|
|
80
|
+
500: (ctx) => new InternalError('Internal server error', { context: ctx }),
|
|
81
|
+
502: (ctx) => new NetworkError('Bad gateway', { context: ctx }),
|
|
82
|
+
504: (ctx) => new TimeoutError('Gateway timeout', { context: ctx }),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Map 4xx status codes not in the explicit mapper to appropriate error types. */
|
|
86
|
+
const mapClientError = (
|
|
87
|
+
status: number,
|
|
88
|
+
context: Record<string, unknown>
|
|
89
|
+
): Error => {
|
|
90
|
+
if (status === 400 || status === 422) {
|
|
91
|
+
return new ValidationError(`Validation error (${status})`, { context });
|
|
92
|
+
}
|
|
93
|
+
if (status === 409) {
|
|
94
|
+
return new ConflictError(`Conflict (${status})`, { context });
|
|
95
|
+
}
|
|
96
|
+
return new InternalError(`HTTP error (${status})`, { context });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mapStatusCode = (response: Response): Error => {
|
|
100
|
+
const context = { status: response.status, url: response.url };
|
|
101
|
+
const mapper = statusMappers[response.status];
|
|
102
|
+
if (mapper) {
|
|
103
|
+
return mapper(context, response);
|
|
104
|
+
}
|
|
105
|
+
if (response.status >= 500) {
|
|
106
|
+
return new InternalError(`Server error (${response.status})`, { context });
|
|
107
|
+
}
|
|
108
|
+
return mapClientError(response.status, context);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// fromFetch
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Wrap a fetch call in a Result, mapping failures to TrailsError subclasses.
|
|
117
|
+
*
|
|
118
|
+
* Network errors become NetworkError. Abort signals become CancelledError.
|
|
119
|
+
* HTTP error status codes map to the appropriate error category.
|
|
120
|
+
*/
|
|
121
|
+
export const fromFetch = async (
|
|
122
|
+
input: string | URL | Request,
|
|
123
|
+
init?: RequestInit
|
|
124
|
+
): Promise<Result<Response, Error>> => {
|
|
125
|
+
let response: Response;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
response = await fetch(input, init);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return Result.err(mapFetchError(error));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
return Result.ok(response);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Result.err(mapStatusCode(response));
|
|
138
|
+
};
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guards and assertion helpers for @ontrails/core.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Narrows `T | null | undefined` to `T`. */
|
|
6
|
+
export const isDefined = <T>(value?: T | null | undefined): value is T =>
|
|
7
|
+
value !== null && value !== undefined;
|
|
8
|
+
|
|
9
|
+
/** Returns true when `value` is a string with length > 0. */
|
|
10
|
+
export const isNonEmptyString = (value?: unknown): value is string =>
|
|
11
|
+
typeof value === 'string' && value.length > 0;
|
|
12
|
+
|
|
13
|
+
/** Returns true when `value` is a plain object (not an array, Date, etc.). */
|
|
14
|
+
export const isPlainObject = (
|
|
15
|
+
value: unknown
|
|
16
|
+
): value is Record<string, unknown> => {
|
|
17
|
+
if (typeof value !== 'object' || value === null) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const proto = Object.getPrototypeOf(value) as unknown;
|
|
21
|
+
return proto === Object.prototype || proto === null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Checks that `obj` is an object with the given key present. */
|
|
25
|
+
export const hasProperty = <K extends string>(
|
|
26
|
+
obj: unknown,
|
|
27
|
+
key: K
|
|
28
|
+
): obj is Record<K, unknown> =>
|
|
29
|
+
typeof obj === 'object' && obj !== null && key in obj;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Exhaustive switch helper. Place in the `default` branch to get a compile
|
|
33
|
+
* error when a union case is unhandled.
|
|
34
|
+
*/
|
|
35
|
+
export const assertNever = (value: never): never => {
|
|
36
|
+
throw new Error(`Unexpected value: ${String(value)}`);
|
|
37
|
+
};
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Health check types
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
/** Aggregate health status */
|
|
6
|
+
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
|
|
7
|
+
|
|
8
|
+
/** Individual component check result */
|
|
9
|
+
export interface HealthCheck {
|
|
10
|
+
readonly status: HealthStatus;
|
|
11
|
+
readonly message?: string | undefined;
|
|
12
|
+
/** Latency in milliseconds */
|
|
13
|
+
readonly latency?: number | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Full health report returned by a health endpoint */
|
|
17
|
+
export interface HealthResult {
|
|
18
|
+
readonly status: HealthStatus;
|
|
19
|
+
readonly checks: Readonly<Record<string, HealthCheck>>;
|
|
20
|
+
readonly version?: string | undefined;
|
|
21
|
+
/** Uptime in seconds */
|
|
22
|
+
readonly uptime?: number | undefined;
|
|
23
|
+
}
|
package/src/hike.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hike — a composition that follows trails.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Trail, TrailSpec } from './trail.js';
|
|
6
|
+
import type { TrailContext } from './types.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Spec (input to the factory)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface HikeSpec<I, O> extends TrailSpec<I, O> {
|
|
13
|
+
readonly follows: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Shape (output of the factory)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface Hike<I, O> extends Omit<Trail<I, O>, 'kind'> {
|
|
21
|
+
readonly kind: 'hike';
|
|
22
|
+
readonly follows: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Factory
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a hike definition.
|
|
31
|
+
*
|
|
32
|
+
* A hike is a composition that declares which trails it follows.
|
|
33
|
+
* Returns a frozen object with `kind: "hike"` and all spec fields.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // ID as first argument
|
|
38
|
+
* const onboard = hike("entity.onboard", {
|
|
39
|
+
* follows: ["entity.add", "entity.relate"],
|
|
40
|
+
* input: z.object({ name: z.string() }),
|
|
41
|
+
* implementation: (input, ctx) => Result.ok(...),
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Full spec object (programmatic)
|
|
45
|
+
* const onboard = hike({ id: "entity.onboard", follows: [...], ... });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function hike<I, O>(id: string, spec: HikeSpec<I, O>): Hike<I, O>;
|
|
49
|
+
export function hike<I, O>(
|
|
50
|
+
spec: HikeSpec<I, O> & { readonly id: string }
|
|
51
|
+
): Hike<I, O>;
|
|
52
|
+
export function hike<I, O>(
|
|
53
|
+
idOrSpec: string | (HikeSpec<I, O> & { readonly id: string }),
|
|
54
|
+
maybeSpec?: HikeSpec<I, O>
|
|
55
|
+
): Hike<I, O> {
|
|
56
|
+
const resolved =
|
|
57
|
+
typeof idOrSpec === 'string'
|
|
58
|
+
? { id: idOrSpec, spec: maybeSpec }
|
|
59
|
+
: { id: idOrSpec.id, spec: idOrSpec };
|
|
60
|
+
|
|
61
|
+
if (!resolved.spec) {
|
|
62
|
+
throw new TypeError('hike() requires a spec when an id is provided');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { follows, implementation, ...rest } = resolved.spec;
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
...rest,
|
|
68
|
+
follows: Object.freeze([...follows]),
|
|
69
|
+
id: resolved.id,
|
|
70
|
+
implementation: async (input: I, ctx: TrailContext) =>
|
|
71
|
+
await implementation(input, ctx),
|
|
72
|
+
kind: 'hike' as const,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// oxlint-disable-next-line no-explicit-any -- existential type; see AnyTrail
|
|
77
|
+
export type AnyHike = Hike<any, any>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Result
|
|
2
|
+
export { Result } from './result.js';
|
|
3
|
+
|
|
4
|
+
// Errors
|
|
5
|
+
export {
|
|
6
|
+
TrailsError,
|
|
7
|
+
ValidationError,
|
|
8
|
+
AmbiguousError,
|
|
9
|
+
AssertionError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
AlreadyExistsError,
|
|
12
|
+
ConflictError,
|
|
13
|
+
PermissionError,
|
|
14
|
+
TimeoutError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
InternalError,
|
|
18
|
+
AuthError,
|
|
19
|
+
CancelledError,
|
|
20
|
+
exitCodeMap,
|
|
21
|
+
statusCodeMap,
|
|
22
|
+
jsonRpcCodeMap,
|
|
23
|
+
retryableMap,
|
|
24
|
+
isRetryable,
|
|
25
|
+
isTrailsError,
|
|
26
|
+
} from './errors.js';
|
|
27
|
+
export type { ErrorCategory } from './errors.js';
|
|
28
|
+
|
|
29
|
+
// Types
|
|
30
|
+
export type {
|
|
31
|
+
Implementation,
|
|
32
|
+
TrailContext,
|
|
33
|
+
FollowFn,
|
|
34
|
+
ProgressCallback,
|
|
35
|
+
ProgressEvent,
|
|
36
|
+
Logger,
|
|
37
|
+
Surface,
|
|
38
|
+
} from './types.js';
|
|
39
|
+
|
|
40
|
+
// Context factory
|
|
41
|
+
export { createTrailContext } from './context.js';
|
|
42
|
+
|
|
43
|
+
// Trail
|
|
44
|
+
export { trail } from './trail.js';
|
|
45
|
+
export type { AnyTrail, Trail, TrailSpec, TrailExample } from './trail.js';
|
|
46
|
+
|
|
47
|
+
// Hike
|
|
48
|
+
export { hike } from './hike.js';
|
|
49
|
+
export type { AnyHike, Hike, HikeSpec } from './hike.js';
|
|
50
|
+
|
|
51
|
+
// Event
|
|
52
|
+
export { event } from './event.js';
|
|
53
|
+
export type { AnyEvent, Event, EventSpec } from './event.js';
|
|
54
|
+
|
|
55
|
+
// Topo
|
|
56
|
+
export { topo } from './topo.js';
|
|
57
|
+
export type { Topo } from './topo.js';
|
|
58
|
+
|
|
59
|
+
// Topo validation
|
|
60
|
+
export { validateTopo } from './validate-topo.js';
|
|
61
|
+
export type { TopoIssue } from './validate-topo.js';
|
|
62
|
+
|
|
63
|
+
// Layer
|
|
64
|
+
export { composeLayers } from './layer.js';
|
|
65
|
+
export type { Layer } from './layer.js';
|
|
66
|
+
|
|
67
|
+
// Health
|
|
68
|
+
export type { HealthStatus, HealthResult } from './health.js';
|
|
69
|
+
|
|
70
|
+
// Adapters
|
|
71
|
+
export type {
|
|
72
|
+
IndexAdapter,
|
|
73
|
+
StorageAdapter,
|
|
74
|
+
CacheAdapter,
|
|
75
|
+
SearchOptions,
|
|
76
|
+
SearchResult,
|
|
77
|
+
StorageOptions,
|
|
78
|
+
} from './adapters.js';
|
|
79
|
+
|
|
80
|
+
// Derive
|
|
81
|
+
export { deriveFields } from './derive.js';
|
|
82
|
+
export type { Field, FieldOverride } from './derive.js';
|
|
83
|
+
|
|
84
|
+
// Validation
|
|
85
|
+
export {
|
|
86
|
+
validateInput,
|
|
87
|
+
validateOutput,
|
|
88
|
+
formatZodIssues,
|
|
89
|
+
zodToJsonSchema,
|
|
90
|
+
} from './validation.js';
|
|
91
|
+
|
|
92
|
+
// Serialization
|
|
93
|
+
export { serializeError, deserializeError } from './serialization.js';
|
|
94
|
+
export type { SerializedError } from './serialization.js';
|
|
95
|
+
|
|
96
|
+
// Resilience
|
|
97
|
+
export {
|
|
98
|
+
retry,
|
|
99
|
+
withTimeout,
|
|
100
|
+
shouldRetry,
|
|
101
|
+
getBackoffDelay,
|
|
102
|
+
} from './resilience.js';
|
|
103
|
+
export type { RetryOptions } from './resilience.js';
|
|
104
|
+
|
|
105
|
+
// Fetch — fromFetch is available as Result.fromFetch()
|
|
106
|
+
|
|
107
|
+
// Branded types
|
|
108
|
+
export {
|
|
109
|
+
brand,
|
|
110
|
+
unbrand,
|
|
111
|
+
uuid,
|
|
112
|
+
email,
|
|
113
|
+
nonEmptyString,
|
|
114
|
+
positiveInt,
|
|
115
|
+
shortId,
|
|
116
|
+
hashId,
|
|
117
|
+
} from './branded.js';
|
|
118
|
+
export type {
|
|
119
|
+
Branded,
|
|
120
|
+
UUID,
|
|
121
|
+
Email,
|
|
122
|
+
NonEmptyString,
|
|
123
|
+
PositiveInt,
|
|
124
|
+
} from './branded.js';
|
|
125
|
+
|
|
126
|
+
// Path Security
|
|
127
|
+
export { securePath, isPathSafe, resolveSafePath } from './path-security.js';
|
|
128
|
+
|
|
129
|
+
// Workspace
|
|
130
|
+
export {
|
|
131
|
+
findWorkspaceRoot,
|
|
132
|
+
isInsideWorkspace,
|
|
133
|
+
getRelativePath,
|
|
134
|
+
} from './workspace.js';
|
|
135
|
+
|
|
136
|
+
// Guards
|
|
137
|
+
export {
|
|
138
|
+
isDefined,
|
|
139
|
+
isNonEmptyString,
|
|
140
|
+
isPlainObject,
|
|
141
|
+
hasProperty,
|
|
142
|
+
assertNever,
|
|
143
|
+
} from './guards.js';
|
|
144
|
+
|
|
145
|
+
// Collections
|
|
146
|
+
export {
|
|
147
|
+
chunk,
|
|
148
|
+
dedupe,
|
|
149
|
+
groupBy,
|
|
150
|
+
sortBy,
|
|
151
|
+
isNonEmptyArray,
|
|
152
|
+
} from './collections.js';
|
|
153
|
+
export type {
|
|
154
|
+
NonEmptyArray,
|
|
155
|
+
DeepPartial,
|
|
156
|
+
Prettify,
|
|
157
|
+
AtLeastOne,
|
|
158
|
+
} from './collections.js';
|