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