@onrails/result 0.1.4 → 0.3.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/DESIGN.md +11 -19
- package/README.md +14 -62
- package/RECIPES.md +12 -25
- package/dist/async-BR3AX0ck.d.cts +326 -0
- package/dist/async-SZ7BEl0g.d.ts +326 -0
- package/dist/compat/neverthrow.cjs +266 -94
- package/dist/compat/neverthrow.cjs.map +1 -1
- package/dist/compat/neverthrow.d.cts +4 -4
- package/dist/compat/neverthrow.d.ts +4 -4
- package/dist/compat/neverthrow.js +263 -91
- package/dist/compat/neverthrow.js.map +1 -1
- package/dist/extra.cjs +2 -25
- package/dist/extra.cjs.map +1 -1
- package/dist/extra.d.cts +7 -19
- package/dist/extra.d.ts +7 -19
- package/dist/extra.js +3 -25
- package/dist/extra.js.map +1 -1
- package/dist/fluent.cjs +55 -29
- package/dist/fluent.cjs.map +1 -1
- package/dist/fluent.d.cts +38 -11
- package/dist/fluent.d.ts +38 -11
- package/dist/fluent.js +55 -29
- package/dist/fluent.js.map +1 -1
- package/dist/index.cjs +290 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -69
- package/dist/index.d.ts +294 -69
- package/dist/index.js +287 -127
- package/dist/index.js.map +1 -1
- package/dist/pipe.cjs.map +1 -1
- package/dist/pipe.d.cts +7 -5
- package/dist/pipe.d.ts +7 -5
- package/dist/pipe.js.map +1 -1
- package/dist/railway.cjs +293 -188
- package/dist/railway.cjs.map +1 -1
- package/dist/railway.d.cts +26 -180
- package/dist/railway.d.ts +26 -180
- package/dist/railway.js +294 -178
- package/dist/railway.js.map +1 -1
- package/dist/try-gen.cjs +1 -1
- package/dist/try-gen.cjs.map +1 -1
- package/dist/try-gen.d.cts +29 -3
- package/dist/try-gen.d.ts +29 -3
- package/dist/try-gen.js +1 -1
- package/dist/try-gen.js.map +1 -1
- package/dist/types-BQ9vv0nD.d.cts +55 -0
- package/dist/types-BQ9vv0nD.d.ts +55 -0
- package/package.json +5 -18
- package/skills/railway-do-notation/SKILL.md +397 -0
- package/skills/result-composition/SKILL.md +323 -0
- package/dist/$.cjs +0 -40
- package/dist/$.cjs.map +0 -1
- package/dist/$.js +0 -36
- package/dist/$.js.map +0 -1
- package/dist/_.d.cts +0 -2
- package/dist/_.d.ts +0 -2
- package/dist/async-CH8Cs8KE.d.cts +0 -161
- package/dist/async-DKISqIBc.d.ts +0 -161
- package/dist/interop.cjs +0 -253
- package/dist/interop.cjs.map +0 -1
- package/dist/interop.d.cts +0 -35
- package/dist/interop.d.ts +0 -35
- package/dist/interop.js +0 -249
- package/dist/interop.js.map +0 -1
- package/dist/types-C2Dp1d5J.d.cts +0 -21
- package/dist/types-C2Dp1d5J.d.ts +0 -21
- package/dist/validation.cjs +0 -53
- package/dist/validation.cjs.map +0 -1
- package/dist/validation.d.cts +0 -72
- package/dist/validation.d.ts +0 -72
- package/dist/validation.js +0 -48
- package/dist/validation.js.map +0 -1
package/dist/railway.cjs
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/internal/dual.ts
|
|
4
|
+
var dual = (arity, body) => {
|
|
5
|
+
const run = body;
|
|
6
|
+
const dispatcher = (...args) => args.length >= arity ? run(...args) : (self) => run(self, ...args);
|
|
7
|
+
return dispatcher;
|
|
8
|
+
};
|
|
9
|
+
|
|
3
10
|
// src/result.ts
|
|
4
11
|
var ok = (value) => ({
|
|
5
12
|
_tag: "Ok",
|
|
@@ -11,18 +18,18 @@ var err = (error) => ({
|
|
|
11
18
|
});
|
|
12
19
|
var isOk = (result) => result._tag === "Ok";
|
|
13
20
|
var isErr = (result) => result._tag === "Err";
|
|
14
|
-
var
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
var map = dual(
|
|
22
|
+
2,
|
|
23
|
+
(result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error)
|
|
24
|
+
);
|
|
25
|
+
var mapErr = dual(
|
|
26
|
+
2,
|
|
27
|
+
(result, fn) => isErr(result) ? err(fn(result.error)) : ok(result.value)
|
|
28
|
+
);
|
|
29
|
+
var flatMap = dual(
|
|
30
|
+
2,
|
|
31
|
+
(result, fn) => isOk(result) ? fn(result.value) : err(result.error)
|
|
32
|
+
);
|
|
26
33
|
function trySync(fn, onThrow) {
|
|
27
34
|
return (...args) => {
|
|
28
35
|
try {
|
|
@@ -44,17 +51,6 @@ var UnexpectedError = class extends Error {
|
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
// src/async.ts
|
|
47
|
-
var liftPromiseResult = async (promise, onDefect) => {
|
|
48
|
-
try {
|
|
49
|
-
const result = await promise;
|
|
50
|
-
if (isErr(result)) {
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
return isResultLike(result.value) ? result.value : result;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return err(onDefect(error));
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
54
|
var sequenceSettled = (settled) => {
|
|
59
55
|
const values = [];
|
|
60
56
|
for (const result of settled) {
|
|
@@ -71,9 +67,32 @@ var ResultAsync = class _ResultAsync {
|
|
|
71
67
|
}
|
|
72
68
|
run;
|
|
73
69
|
promise = null;
|
|
70
|
+
/**
|
|
71
|
+
* Lifts an already-settled sync {@link Result} into a {@link ResultAsync}.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const ra = ResultAsync.fromResult(ok(42)); // ResultAsync<number, never>
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
74
78
|
static fromResult(result) {
|
|
75
79
|
return new _ResultAsync(async () => result);
|
|
76
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Wraps a `PromiseLike<T>` that may reject. Rejections pass through `onReject`
|
|
83
|
+
* to become a typed `Err`; resolution becomes `Ok<T>`.
|
|
84
|
+
*
|
|
85
|
+
* @param promise - the promise to wrap
|
|
86
|
+
* @param onReject - maps a rejection reason to the `Err` channel
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const body = ResultAsync.fromPromise(
|
|
91
|
+
* fetch(url).then((r) => r.text()),
|
|
92
|
+
* (e): NetError => ({ kind: "net", cause: String(e) }),
|
|
93
|
+
* );
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
77
96
|
static fromPromise(promise, onReject) {
|
|
78
97
|
return new _ResultAsync(async () => {
|
|
79
98
|
try {
|
|
@@ -83,12 +102,29 @@ var ResultAsync = class _ResultAsync {
|
|
|
83
102
|
}
|
|
84
103
|
});
|
|
85
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Wraps a `PromiseLike<T>` that is **guaranteed not to reject**, skipping the
|
|
107
|
+
* `onReject` mapper. Use only when rejection is provably impossible.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const now = ResultAsync.fromSafePromise(Promise.resolve(Date.now()));
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
86
114
|
static fromSafePromise(promise) {
|
|
87
115
|
return new _ResultAsync(async () => ok(await promise));
|
|
88
116
|
}
|
|
89
117
|
/**
|
|
90
|
-
* Defers work until {@link resolve}. Unlike {@link fromPromise},
|
|
91
|
-
* until the `ResultAsync` is resolved (e.g. by `combineTuple` /
|
|
118
|
+
* Defers work until {@link resolve}. Unlike {@link fromPromise}, the factory
|
|
119
|
+
* does not run until the `ResultAsync` is resolved (e.g. by `combineTuple` /
|
|
120
|
+
* `combineTupleParallel`). The factory runs at most once — `resolve` memoizes.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* const insert = ResultAsync.defer(() => db.orders.insert(row));
|
|
125
|
+
* // nothing has run yet
|
|
126
|
+
* const r = await insert; // factory runs exactly once here
|
|
127
|
+
* ```
|
|
92
128
|
*/
|
|
93
129
|
static defer(fn) {
|
|
94
130
|
return new _ResultAsync(fn);
|
|
@@ -96,14 +132,49 @@ var ResultAsync = class _ResultAsync {
|
|
|
96
132
|
static ok(value) {
|
|
97
133
|
return new _ResultAsync(async () => ok(value));
|
|
98
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Lifts an error into an `Err` async result.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const r = ResultAsync.err({ kind: "not_found" as const });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
99
143
|
static err(error) {
|
|
100
144
|
return new _ResultAsync(async () => err(error));
|
|
101
145
|
}
|
|
102
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
|
|
148
|
+
* {@link ResultAsync}. A thrown defect is routed through `onDefect`,
|
|
149
|
+
* defaulting to {@link UnexpectedError}. The `Ok` value passes through
|
|
150
|
+
* verbatim — even when it happens to be Result-shaped.
|
|
151
|
+
*
|
|
152
|
+
* @param promise - a promise that already yields a `Result`
|
|
153
|
+
* @param onDefect - maps an unexpected throw to the `Err` channel
|
|
154
|
+
* @see {@link fromAsync} from `@onrails/result`
|
|
155
|
+
*/
|
|
103
156
|
static fromResultPromise(promise, onDefect) {
|
|
104
157
|
const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
|
|
105
|
-
return new _ResultAsync(() =>
|
|
158
|
+
return new _ResultAsync(async () => {
|
|
159
|
+
try {
|
|
160
|
+
return await promise;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return err(mapDefect(error));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
106
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Combines a homogeneous array of async results into one. Resolves
|
|
168
|
+
* **sequentially** in input order, short-circuiting on the first `Err`.
|
|
169
|
+
* For heterogeneous tuples that preserve per-index types, use
|
|
170
|
+
* {@link combineTuple}; for wall-clock overlap, {@link combineTupleParallel}.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* const all = ResultAsync.combine([loadA(), loadB(), loadC()]);
|
|
175
|
+
* // ResultAsync<Item[], LoadError>
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
107
178
|
static combine(results) {
|
|
108
179
|
return new _ResultAsync(async () => {
|
|
109
180
|
const values = [];
|
|
@@ -117,6 +188,23 @@ var ResultAsync = class _ResultAsync {
|
|
|
117
188
|
return ok(values);
|
|
118
189
|
});
|
|
119
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Heterogeneous async tuple combine — resolves branches **sequentially**
|
|
193
|
+
* (left-to-right), returning the first `Err` in input order. Preserves each
|
|
194
|
+
* branch's `Ok` type by position, so the result destructures type-safely.
|
|
195
|
+
* This is the canonical sequential async combine (replaces the former
|
|
196
|
+
* `sequenceTupleAsync`).
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const combined = ResultAsync.combineTuple([loadCfg(), loadCatalog()] as const);
|
|
201
|
+
* // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
|
|
202
|
+
* const r = await combined;
|
|
203
|
+
* if (isOk(r)) {
|
|
204
|
+
* const [cfg, catalog] = r.value; // typed per position
|
|
205
|
+
* }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
120
208
|
static combineTuple(results) {
|
|
121
209
|
return _ResultAsync.combine(
|
|
122
210
|
results
|
|
@@ -125,18 +213,55 @@ var ResultAsync = class _ResultAsync {
|
|
|
125
213
|
/**
|
|
126
214
|
* Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
|
|
127
215
|
* parallel for independent IO). On failure, returns the first `Err` in input order.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* // independent IO — overlap them
|
|
220
|
+
* const combined = ResultAsync.combineTupleParallel([
|
|
221
|
+
* loadProfile(id),
|
|
222
|
+
* loadMetrics(id),
|
|
223
|
+
* ] as const);
|
|
224
|
+
* ```
|
|
128
225
|
*/
|
|
129
226
|
static combineTupleParallel(results) {
|
|
130
227
|
return new _ResultAsync(
|
|
131
228
|
async () => sequenceSettled(await Promise.all(results.map((ra) => ra.resolve())))
|
|
132
229
|
);
|
|
133
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Transforms the `Ok` value, passing `Err` through unchanged.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* ResultAsync.ok(2).map((n) => n * 3); // ResultAsync<number> → Ok 6
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
134
239
|
map(fn) {
|
|
135
240
|
return new _ResultAsync(async () => map(await this.resolve(), fn));
|
|
136
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Transforms the `Err` value, passing `Ok` through unchanged — useful for
|
|
244
|
+
* unifying heterogeneous failures into one app-level union.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
137
251
|
mapErr(fn) {
|
|
138
252
|
return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
|
|
139
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Canonical bind — chains a step that itself returns a `ResultAsync` or sync
|
|
256
|
+
* `Result`, short-circuiting on `Err`. Error types accumulate (`E | F`).
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* authenticate(req)
|
|
261
|
+
* .flatMap((user) => loadProfile(user.id)) // ResultAsync
|
|
262
|
+
* .flatMap((p) => validate(p)); // sync Result also accepted
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
140
265
|
flatMap(fn) {
|
|
141
266
|
return new _ResultAsync(async () => {
|
|
142
267
|
const first = await this.resolve();
|
|
@@ -144,25 +269,31 @@ var ResultAsync = class _ResultAsync {
|
|
|
144
269
|
return first;
|
|
145
270
|
}
|
|
146
271
|
const next = fn(first.value);
|
|
147
|
-
|
|
148
|
-
return next.resolve();
|
|
149
|
-
}
|
|
150
|
-
if (isResultLike(next)) {
|
|
151
|
-
return next;
|
|
152
|
-
}
|
|
153
|
-
return isCompatLike(next) ? next.inner : next;
|
|
272
|
+
return next instanceof _ResultAsync ? next.resolve() : next;
|
|
154
273
|
});
|
|
155
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
|
|
277
|
+
* tier; prefer {@link flatMap} in new code.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```ts
|
|
281
|
+
* authenticate(req).andThen((user) => loadProfile(user.id));
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
156
284
|
andThen(fn) {
|
|
157
285
|
return this.flatMap(fn);
|
|
158
286
|
}
|
|
159
287
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
288
|
+
* Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
|
|
289
|
+
* `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
|
|
290
|
+
* {@link flatMap} on the error track.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```ts
|
|
294
|
+
* loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
|
|
295
|
+
* ```
|
|
162
296
|
*/
|
|
163
|
-
chain(fn) {
|
|
164
|
-
return this.flatMap(fn);
|
|
165
|
-
}
|
|
166
297
|
recover(fn) {
|
|
167
298
|
return new _ResultAsync(async () => {
|
|
168
299
|
const first = await this.resolve();
|
|
@@ -173,9 +304,27 @@ var ResultAsync = class _ResultAsync {
|
|
|
173
304
|
return next instanceof _ResultAsync ? next.resolve() : next;
|
|
174
305
|
});
|
|
175
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* neverthrow-compat alias of {@link recover}. Kept as the documented compat
|
|
309
|
+
* tier; prefer {@link recover} in new code.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* loadFromCache(id).orElse(() => loadFromOrigin(id));
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
176
316
|
orElse(fn) {
|
|
177
317
|
return this.recover(fn);
|
|
178
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Runs a side effect on the `Ok` value and passes the result through
|
|
321
|
+
* unchanged; a no-op on `Err`. Mirrors {@link tapErr}.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* saveUser(u).tap((saved) => analytics.track("user_saved", saved.id));
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
179
328
|
tap(fn) {
|
|
180
329
|
return new _ResultAsync(async () => {
|
|
181
330
|
const result = await this.resolve();
|
|
@@ -185,6 +334,15 @@ var ResultAsync = class _ResultAsync {
|
|
|
185
334
|
return result;
|
|
186
335
|
});
|
|
187
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Runs a side effect on the `Err` value and passes the result through
|
|
339
|
+
* unchanged; a no-op on `Ok`. The error-track mirror of {@link tap}.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```ts
|
|
343
|
+
* saveUser(u).tapErr((e) => logger.warn("save failed", e));
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
188
346
|
tapErr(fn) {
|
|
189
347
|
return new _ResultAsync(async () => {
|
|
190
348
|
const result = await this.resolve();
|
|
@@ -194,28 +352,49 @@ var ResultAsync = class _ResultAsync {
|
|
|
194
352
|
return result;
|
|
195
353
|
});
|
|
196
354
|
}
|
|
197
|
-
unwrapOr(defaultValue) {
|
|
198
|
-
return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
|
|
199
|
-
}
|
|
200
355
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
356
|
+
* Settles to the `Ok` value, or `defaultValue` if `Err`. Terminal — returns a
|
|
357
|
+
* plain `Promise`, not a `ResultAsync`.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* const profile = await loadProfile(id).unwrapOr(guestProfile);
|
|
362
|
+
* ```
|
|
203
363
|
*/
|
|
204
|
-
|
|
205
|
-
return this.resolve().then((result) =>
|
|
364
|
+
unwrapOr(defaultValue) {
|
|
365
|
+
return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
|
|
206
366
|
}
|
|
207
367
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
368
|
+
* Terminal collapse — folds both tracks into a single awaited value. Returns
|
|
369
|
+
* a plain `Promise`, settling the carrier exactly once.
|
|
370
|
+
*
|
|
371
|
+
* @param onOk - handles the `Ok` value
|
|
372
|
+
* @param onErr - handles the `Err` value
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* const status = await save(row).match(
|
|
377
|
+
* () => 200,
|
|
378
|
+
* (e) => (e.kind === "conflict" ? 409 : 500),
|
|
379
|
+
* );
|
|
380
|
+
* ```
|
|
210
381
|
*/
|
|
211
|
-
isErr() {
|
|
212
|
-
return this.resolve().then((result) => isErr(result));
|
|
213
|
-
}
|
|
214
382
|
match(onOk, onErr) {
|
|
215
383
|
return this.resolve().then(
|
|
216
384
|
(result) => isErr(result) ? onErr(result.error) : onOk(result.value)
|
|
217
385
|
);
|
|
218
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Settles the carrier to a bare sync {@link Result}, memoizing so the
|
|
389
|
+
* underlying factory runs at most once. Prefer `await ra` (the thenable) or
|
|
390
|
+
* {@link match} in app code; use `resolve` when you need the tagged union back.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* const r = await load(id).resolve(); // Result<Data, LoadError>
|
|
395
|
+
* if (isOk(r)) use(r.value);
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
219
398
|
resolve() {
|
|
220
399
|
if (!this.promise) {
|
|
221
400
|
this.promise = this.run();
|
|
@@ -235,16 +414,9 @@ var ResultAsync = class _ResultAsync {
|
|
|
235
414
|
);
|
|
236
415
|
}
|
|
237
416
|
};
|
|
238
|
-
function isResultLike(v) {
|
|
239
|
-
return typeof v === "object" && v !== null && "_tag" in v && (v._tag === "Ok" || v._tag === "Err");
|
|
240
|
-
}
|
|
241
|
-
function isCompatLike(v) {
|
|
242
|
-
return typeof v === "object" && v !== null && "inner" in v && isResultLike(v.inner);
|
|
243
|
-
}
|
|
244
417
|
|
|
245
|
-
// src/railway
|
|
418
|
+
// src/railway.ts
|
|
246
419
|
var addField = (ctx, key, value) => ({ ...ctx, [key]: value });
|
|
247
|
-
var liftResult = (result) => ResultAsync.fromResult(result);
|
|
248
420
|
var Railway = class _Railway {
|
|
249
421
|
constructor(state) {
|
|
250
422
|
this.state = state;
|
|
@@ -258,10 +430,7 @@ var Railway = class _Railway {
|
|
|
258
430
|
static context(context) {
|
|
259
431
|
return new _Railway({ mode: "sync", result: ok(context) });
|
|
260
432
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Start a sync workflow with a throwing function — `onThrow` maps any
|
|
263
|
-
* exception to a typed error.
|
|
264
|
-
*/
|
|
433
|
+
/** Start a sync workflow with a throwing function. */
|
|
265
434
|
static fromSync(key, fn, onThrow) {
|
|
266
435
|
return _Railway.empty().fromSync(key, fn, onThrow);
|
|
267
436
|
}
|
|
@@ -278,167 +447,103 @@ var Railway = class _Railway {
|
|
|
278
447
|
return _Railway.empty().fromAsync(key, fn);
|
|
279
448
|
}
|
|
280
449
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
450
|
+
* Rebuild the workflow in its current mode after transforming the carried
|
|
451
|
+
* result. Single re-link point for the state/phantom invariant: the mode
|
|
452
|
+
* tag is preserved verbatim, so `M` still describes the new state — TS
|
|
453
|
+
* cannot narrow the phantom `M` from the runtime tag, hence the cast.
|
|
283
454
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
455
|
+
step(onSync, onAsync) {
|
|
456
|
+
const next = this.state.mode === "sync" ? { mode: "sync", result: onSync(this.state.result) } : { mode: "async", result: onAsync(this.state.result) };
|
|
457
|
+
return new _Railway(next);
|
|
286
458
|
}
|
|
287
459
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
460
|
+
* Project the carried result into the mode-aware output type. Counterpart
|
|
461
|
+
* of {@link Railway.step} for terminal steps: the runtime branch matches
|
|
462
|
+
* the branch `RailwayOutput` picks for `M` (state/phantom invariant), but
|
|
463
|
+
* TS cannot resolve the conditional on an unresolved `M`, hence the cast.
|
|
290
464
|
*/
|
|
465
|
+
out(onSync, onAsync) {
|
|
466
|
+
return this.state.mode === "sync" ? onSync(this.state.result) : onAsync(this.state.result);
|
|
467
|
+
}
|
|
468
|
+
/** Lift the carried result into `ResultAsync` for async-upgrading steps. */
|
|
469
|
+
toAsync() {
|
|
470
|
+
return this.state.mode === "sync" ? ResultAsync.fromResult(this.state.result) : this.state.result;
|
|
471
|
+
}
|
|
472
|
+
/** Pure sync derivation. */
|
|
473
|
+
derive(key, fn) {
|
|
474
|
+
return this.fromResult(key, (ctx) => ok(fn(ctx)));
|
|
475
|
+
}
|
|
476
|
+
/** Throwing sync transform. */
|
|
291
477
|
fromSync(key, fn, onThrow) {
|
|
292
478
|
return this.fromResult(key, (ctx) => trySync(fn, onThrow)(ctx));
|
|
293
479
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Sync `Result`-returning step. Adds `{ [key]: T }` to context on `Ok`;
|
|
296
|
-
* short-circuits the workflow on `Err`. Error union widens to `E | F`.
|
|
297
|
-
*/
|
|
480
|
+
/** Sync `Result`-returning step. */
|
|
298
481
|
fromResult(key, fn) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return new _Railway({ mode: "sync", result: err(current.error) });
|
|
303
|
-
}
|
|
304
|
-
const next = fn(current.value);
|
|
305
|
-
if (isErr(next)) {
|
|
306
|
-
return new _Railway({ mode: "sync", result: err(next.error) });
|
|
307
|
-
}
|
|
308
|
-
return new _Railway({
|
|
309
|
-
mode: "sync",
|
|
310
|
-
result: ok(addField(current.value, key, next.value))
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
return new _Railway({
|
|
314
|
-
mode: "async",
|
|
315
|
-
result: this.state.result.flatMap(
|
|
482
|
+
return this.step(
|
|
483
|
+
(result) => flatMap(result, (ctx) => map(fn(ctx), (value) => addField(ctx, key, value))),
|
|
484
|
+
(result) => result.flatMap(
|
|
316
485
|
(ctx) => ResultAsync.fromResult(fn(ctx)).map((value) => addField(ctx, key, value))
|
|
317
486
|
)
|
|
318
|
-
|
|
487
|
+
);
|
|
319
488
|
}
|
|
320
|
-
/**
|
|
321
|
-
* Promise-returning step — upgrades the workflow to async mode. Reject
|
|
322
|
-
* reasons go through `onReject` to become typed `Err<F>`.
|
|
323
|
-
*/
|
|
489
|
+
/** Promise-returning step — upgrades the workflow to async mode. */
|
|
324
490
|
fromPromise(key, fn, onReject) {
|
|
325
491
|
return this.fromAsync(key, (ctx) => ResultAsync.fromPromise(fn(ctx), onReject));
|
|
326
492
|
}
|
|
327
|
-
/**
|
|
328
|
-
* `ResultAsync`-returning step — upgrades the workflow to async mode.
|
|
329
|
-
* Already-typed error: no mapper needed.
|
|
330
|
-
*/
|
|
493
|
+
/** `ResultAsync`-returning step — upgrades the workflow to async mode. */
|
|
331
494
|
fromAsync(key, fn) {
|
|
332
|
-
const current = this.state.mode === "sync" ? liftResult(this.state.result) : this.state.result;
|
|
333
495
|
return new _Railway({
|
|
334
496
|
mode: "async",
|
|
335
|
-
result:
|
|
497
|
+
result: this.toAsync().flatMap((ctx) => fn(ctx).map((value) => addField(ctx, key, value)))
|
|
336
498
|
});
|
|
337
499
|
}
|
|
338
|
-
/**
|
|
339
|
-
* Narrow a nullable context field into a required non-null field. If the
|
|
340
|
-
* source field is `null` / `undefined`, the workflow short-circuits with
|
|
341
|
-
* `onMissing(ctx)`.
|
|
342
|
-
*
|
|
343
|
-
* @example
|
|
344
|
-
* ```ts
|
|
345
|
-
* .fromPromise("row", ({ id }) => db.users.findFirst({ where: eq(users.id, id) }))
|
|
346
|
-
* .require("user", "row", ({ id }) => ({ kind: "not_found" as const, id }))
|
|
347
|
-
* // user is now User (non-null), not User | null
|
|
348
|
-
* ```
|
|
349
|
-
*/
|
|
500
|
+
/** Narrow a nullable context field into a required non-null field. */
|
|
350
501
|
require(key, source, onMissing) {
|
|
351
502
|
return this.fromResult(key, (ctx) => {
|
|
352
503
|
const value = ctx[source];
|
|
353
504
|
return value == null ? err(onMissing(ctx)) : ok(value);
|
|
354
505
|
});
|
|
355
506
|
}
|
|
356
|
-
/**
|
|
357
|
-
* Run independent `ResultAsync` branches concurrently and merge their
|
|
358
|
-
* named outputs back into context. Upgrades the workflow to async mode.
|
|
359
|
-
* On multiple failures, the first `Err` in record-iteration order wins.
|
|
360
|
-
*
|
|
361
|
-
* @example
|
|
362
|
-
* ```ts
|
|
363
|
-
* .parallel({
|
|
364
|
-
* recent: ({ userId }) => loadRecent(userId),
|
|
365
|
-
* metrics: ({ userId }) => loadMetrics(userId),
|
|
366
|
-
* })
|
|
367
|
-
* // ctx now has { ..., recent, metrics }
|
|
368
|
-
* ```
|
|
369
|
-
*/
|
|
507
|
+
/** Run independent `ResultAsync` branches concurrently and merge outputs. */
|
|
370
508
|
parallel(branches) {
|
|
371
|
-
const
|
|
509
|
+
const merged = this.toAsync().flatMap(
|
|
510
|
+
(ctx) => ResultAsync.combineTupleParallel(
|
|
511
|
+
Object.entries(branches).map(([, branch]) => branch(ctx))
|
|
512
|
+
).map(
|
|
513
|
+
(values) => (
|
|
514
|
+
// Branch outputs land under exactly R's keys; Object.fromEntries
|
|
515
|
+
// erases that correspondence, so reassert the merged shape.
|
|
516
|
+
{
|
|
517
|
+
...ctx,
|
|
518
|
+
...Object.fromEntries(Object.keys(branches).map((key, index) => [key, values[index]]))
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
);
|
|
372
523
|
return new _Railway({
|
|
373
524
|
mode: "async",
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const next = { ...ctx };
|
|
379
|
-
Object.keys(branches).forEach((key, index) => {
|
|
380
|
-
next[key] = values[index];
|
|
381
|
-
});
|
|
382
|
-
return next;
|
|
383
|
-
})
|
|
384
|
-
)
|
|
525
|
+
// BranchFn erases per-key types to `ResultAsync<unknown, unknown>`
|
|
526
|
+
// (ParallelOutput/ParallelError recover them from R), so re-assert the
|
|
527
|
+
// precise value/error union the branches actually produce.
|
|
528
|
+
result: merged
|
|
385
529
|
});
|
|
386
530
|
}
|
|
387
|
-
/**
|
|
388
|
-
* Project the final context into the workflow's output type. Hides the
|
|
389
|
-
* internal context shape from callers.
|
|
390
|
-
*/
|
|
531
|
+
/** Project the final context into the workflow's output type. */
|
|
391
532
|
select(fn) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return this.state.result.map(fn);
|
|
533
|
+
return this.out(
|
|
534
|
+
(result) => map(result, fn),
|
|
535
|
+
(result) => result.map(fn)
|
|
536
|
+
);
|
|
397
537
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Return the accumulated context as-is. Use when downstream code needs
|
|
400
|
-
* every named field; prefer {@link select} when you can project to a DTO.
|
|
401
|
-
*/
|
|
538
|
+
/** Return the accumulated context as-is. */
|
|
402
539
|
done() {
|
|
403
|
-
return this.
|
|
540
|
+
return this.out(
|
|
541
|
+
(result) => result,
|
|
542
|
+
(result) => result
|
|
543
|
+
);
|
|
404
544
|
}
|
|
405
545
|
};
|
|
406
546
|
|
|
407
|
-
// src/railway-steps.ts
|
|
408
|
-
var parseWithParser = (parser, input) => typeof parser === "function" ? parser(input) : parser.parse(input);
|
|
409
|
-
function railway(input, ...steps) {
|
|
410
|
-
return steps.reduce(
|
|
411
|
-
(current, step) => step(current),
|
|
412
|
-
Railway.context({ input })
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
var parseWith = (parser, onThrow) => ({
|
|
416
|
-
as: (key) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow)
|
|
417
|
-
});
|
|
418
|
-
var parseNamed = (key, parser, onThrow) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow);
|
|
419
|
-
var fromSyncNamed = (key, fn, onThrow) => (workflow) => workflow.fromSync(key, fn, onThrow);
|
|
420
|
-
var fromResultNamed = (key, fn) => (workflow) => workflow.fromResult(key, fn);
|
|
421
|
-
var fromPromiseNamed = (key, fn, onReject) => (workflow) => workflow.fromPromise(key, fn, onReject);
|
|
422
|
-
var fromAsyncNamed = (key, fn) => (workflow) => workflow.fromAsync(key, fn);
|
|
423
|
-
var deriveNamed = (key, fn) => (workflow) => workflow.derive(key, fn);
|
|
424
|
-
var requireNamed = (key, source, onMissing) => (workflow) => workflow.require(key, source, onMissing);
|
|
425
|
-
var parallelNamed = (branches) => (workflow) => {
|
|
426
|
-
const typedBranches = branches;
|
|
427
|
-
return workflow.parallel(typedBranches);
|
|
428
|
-
};
|
|
429
|
-
var select = (fn) => (workflow) => workflow.select(fn);
|
|
430
|
-
|
|
431
547
|
exports.Railway = Railway;
|
|
432
|
-
exports.deriveNamed = deriveNamed;
|
|
433
|
-
exports.fromAsyncNamed = fromAsyncNamed;
|
|
434
|
-
exports.fromPromiseNamed = fromPromiseNamed;
|
|
435
|
-
exports.fromResultNamed = fromResultNamed;
|
|
436
|
-
exports.fromSyncNamed = fromSyncNamed;
|
|
437
|
-
exports.parallelNamed = parallelNamed;
|
|
438
|
-
exports.parseNamed = parseNamed;
|
|
439
|
-
exports.parseWith = parseWith;
|
|
440
|
-
exports.railway = railway;
|
|
441
|
-
exports.requireNamed = requireNamed;
|
|
442
|
-
exports.select = select;
|
|
443
548
|
//# sourceMappingURL=railway.cjs.map
|
|
444
549
|
//# sourceMappingURL=railway.cjs.map
|