slang-ts 0.0.7 → 0.0.8
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/README.md +345 -142
- package/dist/index.js +2 -1
- package/dist/src/channel.d.ts +39 -0
- package/dist/src/index.d.ts +12 -1
- package/dist/src/internals.d.ts +8 -0
- package/dist/src/options.d.ts +41 -0
- package/dist/src/println.d.ts +17 -4
- package/dist/src/signal.d.ts +31 -0
- package/dist/src/super-promise.d.ts +60 -0
- package/dist/src/super-runner.d.ts +42 -0
- package/package.json +27 -15
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# slang-ts
|
|
2
2
|
|
|
3
3
|
Functional programming library for TypeScript.
|
|
4
4
|
|
|
5
|
-
A collection of
|
|
5
|
+
A collection of utilities and other cool programming stuff brought over from languages like Rust.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -12,31 +12,43 @@ npm i slang-ts
|
|
|
12
12
|
|
|
13
13
|
## Implemented Utilities
|
|
14
14
|
|
|
15
|
+
- [x] Option (Some, None)
|
|
16
|
+
- [x] isFalsy (Option truthiness check)
|
|
15
17
|
- [x] Result (Ok, Err)
|
|
16
|
-
- [x] Maybe (Option)
|
|
17
|
-
- [x] andThen
|
|
18
|
-
- [x] Atom
|
|
19
18
|
- [x] Expect
|
|
20
19
|
- [x] Unwrap (on Option)
|
|
21
20
|
- [x] Else (on unwrap)
|
|
22
|
-
- [x]
|
|
23
|
-
- [x] Zip, Unzip, zipWith
|
|
24
|
-
- [x] SafeTry
|
|
21
|
+
- [x] andThen
|
|
25
22
|
- [x] Match
|
|
26
23
|
- [x] MatchAll
|
|
24
|
+
- [x] Options (normalize mixed values)
|
|
25
|
+
- [x] SafeTry
|
|
27
26
|
- [x] Pipe
|
|
28
27
|
- [x] To (converters, e.g. `userAtom.to('option')`)
|
|
28
|
+
- [x] Atom
|
|
29
|
+
- [x] Zip, Unzip, zipWith
|
|
30
|
+
- [x] Panic
|
|
31
|
+
- [x] println (environment-aware logging)
|
|
32
|
+
- [x] setEnvironment (logging mode, custom logger)
|
|
33
|
+
- [x] Signal (lifecycle trigger)
|
|
34
|
+
- [x] SuperPromise (abort, reject, resolve, defer)
|
|
35
|
+
- [x] SuperRunner (sequence, all, race)
|
|
36
|
+
- [x] Channel (send, subscribe, when)
|
|
37
|
+
- [x] scheduleMicrotask (internal scheduling helper)
|
|
29
38
|
|
|
30
|
-
All utilities fully tested
|
|
39
|
+
All utilities fully tested. See [tests](https://github.com/Hussseinkizz/slang/tree/main/tests)
|
|
31
40
|
|
|
32
|
-
##
|
|
41
|
+
## New in v0.0.8
|
|
33
42
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
43
|
+
- **Signal**: a simple on/off trigger that fires once and stays fired; use it to make something start only after something else finishes.
|
|
44
|
+
- **println now respects the environment**: `setEnvironment("production")` (or `"prod"`) turns it into a no-op, so dev logging never reaches prod logs; short forms `dev`/`prod` accepted, and it appends a real newline. Pass `{ printFn }` to route println through an existing logger instead.
|
|
45
|
+
- **SuperPromise**: native JavaScript promises plus wait, retry, time out, cancel, settling into `Ok`/`Err`; use it for any async operation that needs more than plain promise.
|
|
46
|
+
- **SuperRunner**: `Promise.all` and `Promise.race` grown up: run several SuperPromises in order, in parallel, or take first to finish.
|
|
47
|
+
- **Channel**: a simple event bus between parts of your app that don't need to know each other; use it to broadcast things like "user created" or "workflow completed" without wiring components together.
|
|
36
48
|
|
|
37
49
|
## How It Works
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
Import utilities individually or together:
|
|
40
52
|
|
|
41
53
|
```ts
|
|
42
54
|
// Individual imports
|
|
@@ -52,18 +64,6 @@ import * as slang from "slang-ts";
|
|
|
52
64
|
slang.println("Hello world!");
|
|
53
65
|
```
|
|
54
66
|
|
|
55
|
-
### println
|
|
56
|
-
|
|
57
|
-
Well there's nothing special to slang's println utility, its just who wants console.log, its not fun at all, so we instead println, clean and classic, but latter it can be made environment aware so it doesn't print in prod, but for now its just sugar for console.log.
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { println } from "slang-ts";
|
|
61
|
-
|
|
62
|
-
const name = "kizz";
|
|
63
|
-
println("name:", name);
|
|
64
|
-
println("multiple", "args", "work", { too: true });
|
|
65
|
-
```
|
|
66
|
-
|
|
67
67
|
### Option
|
|
68
68
|
|
|
69
69
|
Wraps values that may or may not be present. Returns `Some<T>` for truthy values, `None` for null, undefined, empty strings, NaN, or Infinity. Note that `0` and `false` are truthy as these are usually intentional.
|
|
@@ -130,24 +130,76 @@ if (user.isOk) {
|
|
|
130
130
|
}
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
###
|
|
133
|
+
### Expect
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
Unwraps values or throws with custom message. Use when failure is unrecoverable.
|
|
136
136
|
|
|
137
137
|
```ts
|
|
138
|
-
|
|
138
|
+
const personAge = option(25).expect("a person must have age!");
|
|
139
|
+
println("person age", personAge); // 25
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
const
|
|
141
|
+
// This would throw!
|
|
142
|
+
// const personAge2 = option("").expect("a person must have age!");
|
|
143
|
+
```
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
println(userAtom.description); // "kizz"
|
|
145
|
+
### Unwrap/Else
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
Chainable unwrapping with mandatory fallback. Must call `.else()` or throws.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const port = option(process.env.PORT).unwrap().else(3000);
|
|
151
|
+
println("Using port:", port);
|
|
152
|
+
|
|
153
|
+
// Function fallbacks
|
|
154
|
+
const retries = option(null).unwrap().else(() => 5);
|
|
155
|
+
println("Retries:", retries);
|
|
156
|
+
|
|
157
|
+
// This throws! No .else() chained
|
|
158
|
+
// const nothing = option(null).unwrap();
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### andThen
|
|
162
|
+
|
|
163
|
+
Chainable transformation for `Option`, `Result`, and `Atom`. Transforms the inner value while preserving the wrapper type. Returns original instance if provided transformation function returns `undefined`.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// Option - transforms Some, skips None
|
|
167
|
+
option(5).andThen(x => x * 2); // Some(10)
|
|
168
|
+
option(null).andThen(x => x * 2); // None (skipped)
|
|
169
|
+
option(5).andThen(() => undefined); // Some(5) - original
|
|
170
|
+
|
|
171
|
+
// Result - transforms Ok, skips Err
|
|
172
|
+
Ok(10).andThen(x => x + 5); // Ok(15)
|
|
173
|
+
Err("fail").andThen(x => x + 5); // Err("fail") (skipped)
|
|
174
|
+
|
|
175
|
+
// Atom - transforms description (sync only)
|
|
176
|
+
atom("hello").andThen(s => s.toUpperCase()); // Atom("HELLO")
|
|
177
|
+
|
|
178
|
+
// Chained andThen - multiple transformations
|
|
179
|
+
option(5)
|
|
180
|
+
.andThen(x => x + 1)
|
|
181
|
+
.andThen(x => x * 2)
|
|
182
|
+
.andThen(x => x.toString()); // Some("12")
|
|
183
|
+
|
|
184
|
+
Ok(10)
|
|
185
|
+
.andThen(x => x * 2)
|
|
186
|
+
.andThen(x => x + 5)
|
|
187
|
+
.andThen(x => ({ value: x })); // Ok({ value: 25 })
|
|
188
|
+
|
|
189
|
+
atom("hello")
|
|
190
|
+
.andThen(s => s.toUpperCase())
|
|
191
|
+
.andThen(s => s + "!"); // Atom("HELLO!")
|
|
192
|
+
|
|
193
|
+
// Async support for Option and Result
|
|
194
|
+
const data = await option(5).andThen(async x => await fetchData(x));
|
|
195
|
+
|
|
196
|
+
// Error handling
|
|
197
|
+
option(5).andThen(() => { throw "oops" }); // None (caught)
|
|
198
|
+
Ok(5).andThen(() => { throw "oops" }); // Err("oops")
|
|
199
|
+
atom("x").andThen(() => { throw "oops" }); // Panics!
|
|
200
|
+
|
|
201
|
+
// Type transformation
|
|
202
|
+
option(42).andThen(x => x.toString()); // Some("42")
|
|
151
203
|
```
|
|
152
204
|
|
|
153
205
|
### Match
|
|
@@ -200,32 +252,100 @@ const label = matchAll(isActive, {
|
|
|
200
252
|
println(label); // "Active"
|
|
201
253
|
```
|
|
202
254
|
|
|
203
|
-
###
|
|
255
|
+
### Options
|
|
204
256
|
|
|
205
|
-
|
|
257
|
+
Normalizes heterogeneous values (raw values, `Option`s, and `Result`s) into a single `Result`. Returns the first usable value as `Ok`, or the provided `Err` fallback if none is usable. A value is usable when its unwrapped inner value is truthy; `None`, `Err`, null, undefined, empty strings, NaN, and Infinity are skipped (note `0` and `false` are usable, matching `option()` semantics).
|
|
206
258
|
|
|
207
259
|
```ts
|
|
208
|
-
|
|
209
|
-
|
|
260
|
+
import { Options, option, Ok, Err } from "slang-ts";
|
|
261
|
+
|
|
262
|
+
// First usable value wins
|
|
263
|
+
let a = null;
|
|
264
|
+
let b = option(undefined);
|
|
265
|
+
let c = Ok(true);
|
|
266
|
+
let d = Err("nope!");
|
|
267
|
+
|
|
268
|
+
const something = Options([a, b, c, d], Err("No value!"));
|
|
269
|
+
// Ok(true) - c is usable
|
|
270
|
+
|
|
271
|
+
// Nothing usable -> fallback
|
|
272
|
+
let e = option(null);
|
|
273
|
+
const nothing = Options([a, b, e, d], Err("No value!"));
|
|
274
|
+
// Err("No value!")
|
|
275
|
+
|
|
276
|
+
// Raw (non-wrapped) values work too, just like a single option():
|
|
277
|
+
Options([null, undefined, "hello"]); // Ok("hello") - raw truthy wins
|
|
278
|
+
Options(["", 0, "zero is skipped, 0 used"]); // Ok("...") - skips "" first
|
|
279
|
+
Options([null, undefined, ""]); // Err("No value!") - all raw falsy
|
|
280
|
+
```
|
|
210
281
|
|
|
211
|
-
|
|
212
|
-
|
|
282
|
+
Useful when values come from different functions with inconsistent return types and all you care about is whether the end result is usable.
|
|
283
|
+
|
|
284
|
+
### SafeTry
|
|
285
|
+
|
|
286
|
+
Wraps potentially throwing functions in try-catch, returning a `Result<T, string>`. Always needs to be awaited as its async.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { safeTry } from "slang-ts";
|
|
290
|
+
|
|
291
|
+
const result = await safeTry(() => {
|
|
292
|
+
if (denom === 0) throw new Error("Cannot divide by zero");
|
|
293
|
+
return num / denom;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (result.isOk) {
|
|
297
|
+
println("Result:", result.value);
|
|
298
|
+
} else {
|
|
299
|
+
println("Error:", result.error);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Async functions work the same way
|
|
303
|
+
const data = await safeTry(async () => {
|
|
304
|
+
const res = await fetch("/api/user");
|
|
305
|
+
return res.json();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (data.isOk) {
|
|
309
|
+
println("User:", data.value);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Re-throw critical errors instead of capturing
|
|
313
|
+
await safeTry(() => {
|
|
314
|
+
throw new Error("Critical!");
|
|
315
|
+
}, { throw: true });
|
|
213
316
|
```
|
|
214
317
|
|
|
215
|
-
###
|
|
318
|
+
### Pipe
|
|
216
319
|
|
|
217
|
-
|
|
320
|
+
Sequential function composition where each function receives a `Result` and returns a `Result`. Accepts plain values, Option, Result, or Atom as initial input.
|
|
218
321
|
|
|
219
322
|
```ts
|
|
220
|
-
|
|
221
|
-
println("Using port:", port);
|
|
323
|
+
import { pipe, Ok, Err, option, type Result } from "slang-ts";
|
|
222
324
|
|
|
223
|
-
//
|
|
224
|
-
const
|
|
225
|
-
|
|
325
|
+
// Create pipeline functions
|
|
326
|
+
const add = (x: number) => (res: Result<number, string>) =>
|
|
327
|
+
res.isOk ? Ok(res.value + x) : res;
|
|
226
328
|
|
|
227
|
-
|
|
228
|
-
|
|
329
|
+
const multiply = (x: number) => (res: Result<number, string>) =>
|
|
330
|
+
res.isOk ? Ok(res.value * x) : res;
|
|
331
|
+
|
|
332
|
+
// Basic usage
|
|
333
|
+
const result = await pipe(5, add(3), multiply(2)).run();
|
|
334
|
+
println("Result:", result.value); // 16
|
|
335
|
+
|
|
336
|
+
// With Options as initial value
|
|
337
|
+
const fromOption = await pipe(option(10), add(5)).run();
|
|
338
|
+
println("From option:", fromOption.value); // 15
|
|
339
|
+
|
|
340
|
+
// With callbacks and error handling
|
|
341
|
+
const result = await pipe(5, add(3), multiply(2)).run({
|
|
342
|
+
onEach: ({ currentFn, prevResult }) => {
|
|
343
|
+
println("Executed:", currentFn);
|
|
344
|
+
},
|
|
345
|
+
onSuccess: (value) => println("Done:", value),
|
|
346
|
+
onError: (err) => println("Failed:", err.message),
|
|
347
|
+
allowErrors: false, // stops pipeline on first Err
|
|
348
|
+
});
|
|
229
349
|
```
|
|
230
350
|
|
|
231
351
|
### To
|
|
@@ -243,48 +363,24 @@ const errResult = option(null).to("result");
|
|
|
243
363
|
println("Result:", errResult.type); // "Err"
|
|
244
364
|
```
|
|
245
365
|
|
|
246
|
-
###
|
|
366
|
+
### Atom
|
|
247
367
|
|
|
248
|
-
|
|
368
|
+
Creates unique, non-interned symbols with semantic descriptions. Each call produces a distinct identity. Define them in one file and import from it everywhere else, handy for env-var style constants.
|
|
249
369
|
|
|
250
370
|
```ts
|
|
251
|
-
|
|
252
|
-
option(5).andThen(x => x * 2); // Some(10)
|
|
253
|
-
option(null).andThen(x => x * 2); // None (skipped)
|
|
254
|
-
option(5).andThen(() => undefined); // Some(5) - original
|
|
255
|
-
|
|
256
|
-
// Result - transforms Ok, skips Err
|
|
257
|
-
Ok(10).andThen(x => x + 5); // Ok(15)
|
|
258
|
-
Err("fail").andThen(x => x + 5); // Err("fail") (skipped)
|
|
259
|
-
|
|
260
|
-
// Atom - transforms description (sync only)
|
|
261
|
-
atom("hello").andThen(s => s.toUpperCase()); // Atom("HELLO")
|
|
262
|
-
|
|
263
|
-
// Chained andThen - multiple transformations
|
|
264
|
-
option(5)
|
|
265
|
-
.andThen(x => x + 1)
|
|
266
|
-
.andThen(x => x * 2)
|
|
267
|
-
.andThen(x => x.toString()); // Some("12")
|
|
268
|
-
|
|
269
|
-
Ok(10)
|
|
270
|
-
.andThen(x => x * 2)
|
|
271
|
-
.andThen(x => x + 5)
|
|
272
|
-
.andThen(x => ({ value: x })); // Ok({ value: 25 })
|
|
273
|
-
|
|
274
|
-
atom("hello")
|
|
275
|
-
.andThen(s => s.toUpperCase())
|
|
276
|
-
.andThen(s => s + "!"); // Atom("HELLO!")
|
|
371
|
+
import { atom } from "slang-ts";
|
|
277
372
|
|
|
278
|
-
|
|
279
|
-
const
|
|
373
|
+
const userAtom = atom("kizz");
|
|
374
|
+
const user2Atom = atom("kizz");
|
|
280
375
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
Ok(5).andThen(() => { throw "oops" }); // Err("oops")
|
|
284
|
-
atom("x").andThen(() => { throw "oops" }); // Panics!
|
|
376
|
+
println(userAtom === atom("kizz")); // false - non interned
|
|
377
|
+
println(userAtom.description); // "kizz"
|
|
285
378
|
|
|
286
|
-
|
|
287
|
-
|
|
379
|
+
if (userAtom === user2Atom) {
|
|
380
|
+
println("all the same");
|
|
381
|
+
} else {
|
|
382
|
+
println("not the same"); // This prints!
|
|
383
|
+
}
|
|
288
384
|
```
|
|
289
385
|
|
|
290
386
|
### Zip
|
|
@@ -348,91 +444,198 @@ println(unzip(zipped));
|
|
|
348
444
|
// [[1, 2, 3], [4, 5, 6]]
|
|
349
445
|
```
|
|
350
446
|
|
|
351
|
-
###
|
|
447
|
+
### Panic
|
|
352
448
|
|
|
353
|
-
|
|
449
|
+
Throws an error immediately. Use for unrecoverable failures.
|
|
354
450
|
|
|
355
451
|
```ts
|
|
356
|
-
import {
|
|
452
|
+
import { panic } from "slang-ts";
|
|
357
453
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
454
|
+
function processUser(user: User | null) {
|
|
455
|
+
if (!user) panic("User cannot be null");
|
|
456
|
+
return user.name;
|
|
457
|
+
}
|
|
361
458
|
|
|
362
|
-
|
|
363
|
-
|
|
459
|
+
// Guard clause pattern
|
|
460
|
+
const config = loadConfig();
|
|
461
|
+
if (!config.apiKey) panic("API key required");
|
|
462
|
+
```
|
|
364
463
|
|
|
365
|
-
|
|
366
|
-
const result = await pipe(5, add(3), multiply(2)).run();
|
|
367
|
-
println("Result:", result.value); // 16
|
|
464
|
+
### println
|
|
368
465
|
|
|
369
|
-
|
|
370
|
-
const fromOption = await pipe(option(10), add(5)).run();
|
|
371
|
-
println("From option:", fromOption.value); // 15
|
|
466
|
+
console.log sugar that stays out of production. Prints a line (args joined, newline appended) and does nothing once `setEnvironment` puts the app in production mode. Dev chatter never leaks to prod logs. Already have an advanced logger? Hand it to `setEnvironment({ printFn })` and every println routes through it: your logging code stays exactly as it is, in every mode.
|
|
372
467
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
468
|
+
```ts
|
|
469
|
+
import { println, setEnvironment } from "slang-ts";
|
|
470
|
+
|
|
471
|
+
setEnvironment("development"); // "dev" works too (default)
|
|
472
|
+
println("name:", name);
|
|
473
|
+
println("multiple", "args", "work", { too: true });
|
|
474
|
+
|
|
475
|
+
setEnvironment("production"); // "prod" works too
|
|
476
|
+
println("not printed in prod");
|
|
477
|
+
|
|
478
|
+
// Route println through an existing logger instead, even in prod
|
|
479
|
+
import { appLogger } from "./logger";
|
|
480
|
+
setEnvironment({ printFn: (line) => appLogger.info(line.trimEnd()) });
|
|
382
481
|
```
|
|
383
482
|
|
|
384
|
-
###
|
|
483
|
+
### Signal
|
|
385
484
|
|
|
386
|
-
|
|
485
|
+
A trigger (we call them signals) is a switch that fires once and stays fired. Flip it when you're ready, and anything waiting on it reacts. They're interchangeable with the platform's `AbortSignal`, so a signal you create can be handed to anything that already accepts one.
|
|
387
486
|
|
|
388
487
|
```ts
|
|
389
|
-
import {
|
|
488
|
+
import { createSignal } from "slang-ts";
|
|
390
489
|
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
490
|
+
const ready = createSignal();
|
|
491
|
+
|
|
492
|
+
ready.addEventListener("abort", () => println("ready!")); // fires when the trigger goes off
|
|
493
|
+
ready.fire(); // flip it
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
The useful part: gate work on a signal, or publish events when a lifecycle completes.
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
const task = superPromise(loadUser, { defer: ready }); // starts only once ready fires
|
|
500
|
+
|
|
501
|
+
channel.when({
|
|
502
|
+
signal: task.done, // task finished, success or not
|
|
503
|
+
topic: "user.loaded",
|
|
504
|
+
data: { id: userId },
|
|
394
505
|
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### SuperPromise
|
|
509
|
+
|
|
510
|
+
Native JavaScript promises plus execution controls: wait before starting, retry when it fails, time out when it hangs, cancel from outside, `done` trigger to wire into other things. Tasks settle into `Ok` or `Err`: just check outcome, no try/catch around awaits. Library exports no short alias: alias locally when the name gets heavy: `import { superPromise as sp }`.
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
import { superPromise } from "slang-ts";
|
|
514
|
+
|
|
515
|
+
// A fetch that gives up after 5 seconds and tries up to 3 more times
|
|
516
|
+
const result = await superPromise(
|
|
517
|
+
(signal) => fetch("/api/user", { signal }),
|
|
518
|
+
{ timeout: 5000, retry: 3 }
|
|
519
|
+
);
|
|
395
520
|
|
|
396
521
|
if (result.isOk) {
|
|
397
|
-
println("
|
|
522
|
+
println("User:", result.value);
|
|
398
523
|
} else {
|
|
399
|
-
println("
|
|
524
|
+
println("Failed:", result.error);
|
|
400
525
|
}
|
|
526
|
+
```
|
|
401
527
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
528
|
+
No `await` handy? Chain with `.then`; the callback receives the same `Result`:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
superPromise(loadUser, { timeout: 5000 }).then((result) => {
|
|
532
|
+
if (result.isOk) {
|
|
533
|
+
println("User:", result.value);
|
|
534
|
+
} else {
|
|
535
|
+
println("Failed:", result.error);
|
|
536
|
+
}
|
|
406
537
|
});
|
|
538
|
+
```
|
|
407
539
|
|
|
408
|
-
|
|
409
|
-
println("User:", data.value);
|
|
410
|
-
}
|
|
540
|
+
Need it to wait for something else first? `defer` holds execution until a signal fires; chain operations so the next starts only when the previous is done:
|
|
411
541
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
542
|
+
```ts
|
|
543
|
+
const config = superPromise(loadConfig);
|
|
544
|
+
const user = superPromise(loadUser, { defer: config.done });
|
|
545
|
+
|
|
546
|
+
const result = await user; // config runs first, then user
|
|
416
547
|
```
|
|
417
548
|
|
|
418
|
-
|
|
549
|
+
Cancel from outside, or keep retrying until you call it off:
|
|
419
550
|
|
|
420
|
-
|
|
551
|
+
```ts
|
|
552
|
+
task.abort(); // cancel; you get Err("Aborted")
|
|
553
|
+
superPromise(fetchData, {
|
|
554
|
+
retryUntil: { signal: stopSignal, delay: 2000 }, // keep trying until stopSignal fires
|
|
555
|
+
});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
One gotcha worth knowing: a task doesn't start until you consume it (`await`, `.then()`, or a runner's `start()`), so you can wire up dependencies first and they'll run in the right order when awaited.
|
|
559
|
+
|
|
560
|
+
Also handy:
|
|
421
561
|
|
|
422
562
|
```ts
|
|
423
|
-
|
|
563
|
+
const manual = superPromise<number>(); // no executor; settle it yourself
|
|
564
|
+
manual.resolve(100); // or manual.reject(error)
|
|
424
565
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return user.name;
|
|
428
|
-
}
|
|
566
|
+
await superPromise(loadUser).tap((user) => println(user.name)); // observe without changing
|
|
567
|
+
```
|
|
429
568
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
569
|
+
### SuperRunner
|
|
570
|
+
|
|
571
|
+
Same idea as `Promise.all`, `Promise.race`, and plain `await` chain, but for SuperPromises, with cancellation and clean `Result` outcomes. Use it when multiple operations are needed: run several in order, in parallel, or take first to finish. Call `start()` once and read result; calling again just hands back same result without re-running anything.
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
import { superRunner, superPromise as sp } from "slang-ts";
|
|
575
|
+
|
|
576
|
+
// In order: steps that depend on each other; stops at the first failure
|
|
577
|
+
const auth = superRunner({
|
|
578
|
+
type: "sequence",
|
|
579
|
+
runners: [sp(authenticate), sp(loadProfile), sp(loadDashboard)],
|
|
580
|
+
});
|
|
581
|
+
const ordered = await auth.start(); // [authResult, profileResult, dashboardResult]
|
|
582
|
+
|
|
583
|
+
// In parallel: all kicked off at once; result keyed by name
|
|
584
|
+
const parallel = superRunner({
|
|
585
|
+
type: "all",
|
|
586
|
+
runners: {
|
|
587
|
+
user: sp(getUser),
|
|
588
|
+
posts: sp(getPosts),
|
|
589
|
+
settings: sp(getSettings),
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
const combined = await parallel.start(); // { user, posts, settings }
|
|
593
|
+
|
|
594
|
+
// First to finish: take whichever source responds first, cancel the rest
|
|
595
|
+
const first = superRunner({
|
|
596
|
+
type: "race",
|
|
597
|
+
count: 1,
|
|
598
|
+
runners: {
|
|
599
|
+
primary: sp(fetchPrimary),
|
|
600
|
+
backup: sp(fetchBackup),
|
|
601
|
+
cache: sp(fetchCache),
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
const winner = await first.start(); // { cache: ... } or whichever won
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Cancel everything with one call:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
workflow.abort(); // cancels all active children
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Channel
|
|
614
|
+
|
|
615
|
+
Channel lets parts of your app that don't know each other talk: one side publishes, another listens, no wiring between them.
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
import { createChannel } from "slang-ts";
|
|
619
|
+
|
|
620
|
+
const channel = createChannel();
|
|
621
|
+
|
|
622
|
+
const unsubscribe = channel.subscribe({ topic: "user.created" }, (event) => {
|
|
623
|
+
println("Created:", event.data);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
channel.send({ topic: "user.created", data: user });
|
|
627
|
+
unsubscribe(); // stop listening
|
|
433
628
|
```
|
|
434
629
|
|
|
435
|
-
|
|
630
|
+
Publish when a trigger goes off, a task finishing, anything at all:
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
channel.when({
|
|
634
|
+
signal: workflow.done,
|
|
635
|
+
topic: "workflow.completed",
|
|
636
|
+
data: { workflowId },
|
|
637
|
+
});
|
|
638
|
+
```
|
|
436
639
|
|
|
437
640
|
## Code Samples
|
|
438
641
|
|
|
@@ -440,4 +643,4 @@ See [example.ts](https://github.com/Hussseinkizz/slang/blob/main/example.ts) for
|
|
|
440
643
|
|
|
441
644
|
## Contributing
|
|
442
645
|
|
|
443
|
-
Contributions are welcome
|
|
646
|
+
Contributions are welcome. I know there are a lot of cool things out there we can bring in.
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=(G)=>{let B=globalThis?.queueMicrotask;if(typeof B==="function")B(G);else Promise.resolve().then(G)},h=(G)=>G!=null&&typeof G==="object"&&("type"in G)&&(G.type==="Ok"||G.type==="Err"),O=Symbol("slang.activate"),r=(G)=>G!=null&&typeof G==="object"&&("isSome"in G)&&("isNone"in G),e=(G)=>G!=null&&typeof G==="object"&&typeof G.valueOf?.()==="symbol";function x(G){if(e(G))return{ok:!0,value:G.valueOf().description};if(h(G)){if(G.isOk)return{ok:!0,value:G.value};return{ok:!1,error:typeof G.error==="string"?G.error:String(G.error)}}if(r(G)){if(G.isSome)return{ok:!0,value:G.value};return{ok:!1,error:"Option was None"}}return{ok:!0,value:G}}var v="development",m=null,GG=(G)=>{if(typeof G==="object"&&G!==null){m=G.printFn??null;return}v=G==="production"||G==="prod"?"production":"development"},BG=(G)=>{if(typeof G==="string")return G;if(typeof G==="symbol")return String(G);if(typeof G==="function")return`[Function ${G.name}]`;try{return String(G)}catch{try{return JSON.stringify(G)??"null"}catch{return Object.prototype.toString.call(G)}}},g=(...G)=>{let B=G.map(BG).join(" ").concat(`
|
|
2
|
+
`);if(m){m(B);return}if(v==="production")return;globalThis.console?.log?.(B)};function E(G){throw Error(G)}function L(G){let B=Object.freeze({type:"Ok",value:G,isOk:!0,isErr:!1}),D={...B,expect:(X)=>B.value,unwrap:()=>{let X=!1;return R(()=>{}),{else(Z){return X=!0,B.value}}},andThen:(X)=>{try{let Z=X(B.value);if(Z instanceof Promise)return Z.then((q)=>{if(q===void 0)return D;return L(q)}).catch((q)=>{let Y=q instanceof Error?q.message:String(q);return $(Y)});if(Z===void 0)return D;return L(Z)}catch(Z){let q=Z instanceof Error?Z.message:String(Z);return $(q)}}};return D}function $(G){let D={...Object.freeze({type:"Err",error:G,isOk:!1,isErr:!0}),expect:(X)=>{throw Error(X??G)},unwrap:()=>{let X=!1;return R(()=>{if(!X)throw Error(G)}),{else(Z){if(X=!0,typeof Z==="function")return Z(G);return Z}}},andThen:(X)=>{return D}};return D}var z=(G)=>{return G===null||G===void 0||G===""||Number.isNaN(G)||G===1/0||G===-1/0};function JG(G){if(z(G))throw Error("Cannot wrap null, undefined, NaN, or empty string in Some");return Object.freeze({type:"Some",value:G,isSome:!0,isNone:!1})}var QG=Object.freeze({type:"None",isSome:!1,isNone:!0}),f=null;function u(G){f=G}function T(G){let B=z(G)?QG:JG(G),q={...B,to:(Y)=>{if(!f)throw Error("Converter not initialized");return f(B,Y)},expect:(Y)=>{if(B.isSome)return B.value;throw Error(Y??"Expected Some, got None")},unwrap:()=>{let Y=!1,Q=B;return R(()=>{if(!Y)throw Error("Expected else")}),{else(K){if(Y=!0,Q.isSome)return Q.value;let I=typeof K==="function"?K(void 0):K;if(z(I))throw Error("Fallback must be truthy");return I}}},andThen:(Y)=>{let Q=B;if(Q.isNone)return q;let K=Q.value;try{let I=Y(K);if(I instanceof Promise)return I.then((M)=>{if(M===void 0)return q;return T(M)}).catch(()=>T(null));if(I===void 0)return q;return T(I)}catch{return T(null)}}};return q}var i=(G)=>{if(typeof G!=="object"||G===null)return G;let B=G;if(B.isSome===!0||B.isOk===!0)return B.value;return G},XG=(G)=>{if(typeof G==="object"&&G!==null){let B=G;if(B.isNone===!0||B.isErr===!0)return!0}return z(i(G))};function YG(G,B=$("No value!")){for(let J of G){if(XG(J))continue;return L(i(J))}return B}var d=null;function s(G){d=G}function b(G){let B=Symbol(G),J=Object(B),D=(Z)=>{if(!d)throw Error("Converter not initialized");return d(B,Z)},X=(Z)=>{let q;try{q=Z(G)}catch(Y){let Q=Y instanceof Error?Y.message:String(Y);E(`Atom.andThen: fn threw an error: ${Q}`)}if(q===void 0)return J;if(typeof q!=="string")E(`Atom.andThen: fn must return a string, got ${typeof q}`);return b(q)};return J.to=D,J.andThen=X,J}var c=(G)=>G!=null&&typeof G==="object"&&("isSome"in G)&&("isNone"in G);function p(G,B){if(G&&(G.type==="Ok"||G.type==="Err"))throw Error("Cannot convert a Result to any other type");switch(B){case"option":{if(c(G))return G;if(typeof G==="symbol")return T(G.description);return T(G)}case"atom":{if(c(G)){if(G.isNone)throw Error("Cannot convert None to Atom");if(typeof G.value!=="string")throw Error("Only string values can be converted to Atom");return b(G.value)}if(typeof G==="symbol")return G;throw Error(`Cannot convert type ${typeof G} to Atom`)}case"result":{if(c(G))return G.isSome?L(G.value):$("Value is None");if(typeof G==="symbol")return L(G.description);return L(G)}default:throw Error(`Invalid target: ${B}`)}}u(p);s(p);function $G(G,B){let J=B[G.type];if(!J)throw Error(`Non-exhaustive match — missing handler for '${G.type}'`);return J(G)}var ZG=(G)=>{return typeof G==="string"||typeof G==="number"||typeof G==="boolean"||typeof G==="symbol"};function qG(G,B){let J=(Y)=>typeof Y?.valueOf==="function"?Y.valueOf():Y,D=(Y)=>typeof Y==="symbol"?Y.description:void 0,X=J(G);if(!ZG(X))throw Error(`Unsupported match all value type: ${typeof X}`);let Z=D(X)??X,q=typeof Z==="boolean"||typeof Z==="number"?String(Z):Z;if(q!=null&&q in B)return B[q](G);return B._()}function DG(G,B=!1){if(!B&&!Array.isArray(G))throw Error("Only arrays allowed when includeValues=false");if(Array.isArray(G))return G;if(G instanceof Set)return Array.from(G);return Object.values(G)}function o(G,B){let{fillValue:J,includeValues:D=!1}=B||{};if(G.length===0)return[];let X=G.map((K)=>DG(K,D)),Z=Math.max(...X.map((K)=>K.length)),q=Math.min(...X.map((K)=>K.length)),Y=J===void 0?q:Z,Q=[];for(let K=0;K<Y;K++)Q.push(X.map((I)=>K<I.length?I[K]:J));return Q}function WG(G,B,J){return o(G,J).map(B)}function HG(G){if(G.length===0)return[];let B=G[0]?.length??0,J=Array.from({length:B},()=>[]);for(let D of G)D.forEach((X,Z)=>J[Z]?.push(X));return J}async function jG(G,B){let J=B?.throw??!1;try{let D=await G(),X=x(D);if(X.ok)return L(X.value);return $(X.error)}catch(D){let X=D instanceof Error?D.message:String(D);if(J)throw D instanceof Error?D:Error(X);return $(X)}}function n(G){return G.error??"Unknown error"}function KG(G){let B=x(G);if(B.ok)return L(B.value);return $(B.error)}async function LG(G,B){try{let J=await G(B);if(!h(J))return $("Pipeline function must return a Result");return J}catch(J){let D=J instanceof Error?J.message:String(J);return $(D)}}function IG(G,...B){return{async run(J){let{onEach:D,onSuccess:X,onError:Z,allowErrors:q=!1}=J??{},Y=KG(G);for(let Q=0;Q<B.length;Q++){let K=B[Q],I=B[Q+1];if(Y.isErr&&!q)return Z?.(Error(n(Y))),Y;Y=await LG(K,Y),D?.({prevResult:Y,currentFn:K.name||`fn${Q+1}`,nextFn:I?I.name||`fn${Q+2}`:void 0})}if(Y.isOk)X?.(Y.value);else Z?.(Error(n(Y)));return Y}}}function w(){let G=new AbortController;return Object.assign(G.signal,{fire:()=>G.abort()})}var l=(G)=>G instanceof Error?G.message:String(G),a=(G,B)=>{if(G.aborted)return B.abort(),()=>{};let J=()=>B.abort();return G.addEventListener("abort",J,{once:!0}),()=>G.removeEventListener("abort",J)};function t(G,B={}){let J=new AbortController,D=w(),X=()=>{},Z=new Promise((W)=>{X=W}),q=!1,Y=!1,Q=(W)=>{if(Y)return;Y=!0,X(W),D.fire()},K=async(W,H,j)=>{let U=!1,N,S=j!==void 0?a(j,H):()=>{},F=a(J.signal,H);if(B.timeout!==void 0)N=setTimeout(()=>{U=!0,H.abort()},B.timeout);let P=(V)=>{if(N!==void 0)clearTimeout(N);return S(),F(),V};try{let V=await W(H.signal),_=x(V);if(H.signal.aborted){if(J.signal.aborted)return P({kind:"aborted"});if(j?.aborted)return P({kind:"stopped"});return P(U?{kind:"timedOut"}:{kind:"aborted"})}if(!_.ok)return P({kind:"error",error:_.error});return P({kind:"value",value:_.value})}catch(V){if(J.signal.aborted)return P({kind:"aborted"});if(j?.aborted)return P({kind:"stopped"});if(H.signal.aborted)return P(U?{kind:"timedOut"}:{kind:"aborted"});return P({kind:"error",error:l(V)})}},I=(W)=>new Promise((H)=>{if(W.aborted){H();return}W[O]?.();let j=()=>{U(),H()},U=()=>{W.removeEventListener("abort",j),J.signal.removeEventListener("abort",j)};W.addEventListener("abort",j,{once:!0}),J.signal.addEventListener("abort",j,{once:!0})}),M=(W)=>new Promise((H)=>{if(J.signal.aborted){H();return}let j=()=>{clearTimeout(N),U(),H()},U=()=>J.signal.removeEventListener("abort",j),N=setTimeout(j,W);J.signal.addEventListener("abort",j,{once:!0})}),k=async()=>{let{defer:W,delay:H,retry:j,retryUntil:U}=B,N=G;if(N===void 0)return;if(J.signal.aborted)return Q($("Aborted"));if(W!==void 0){if(await I(W),J.signal.aborted)return Q($("Aborted"))}if(U!==void 0){let P=U.signal;for(;;){if(J.signal.aborted)return Q($("Aborted"));if(P.aborted)return Q($("Stopped"));let V=new AbortController,_=await K(N,V,P);switch(_.kind){case"value":return Q(L(_.value));case"aborted":return Q($("Aborted"));case"stopped":return Q($("Stopped"));case"timedOut":case"error":{if(J.signal.aborted)return Q($("Aborted"));if(P.aborted)return Q($("Stopped"));if(U.delay!==void 0)await M(U.delay)}}}}if(j!==void 0&&j>0){let P=j;for(;;){if(J.signal.aborted)return Q($("Aborted"));if(H!==void 0)await M(H);if(J.signal.aborted)return Q($("Aborted"));let V=new AbortController,_=await K(N,V,void 0);switch(_.kind){case"value":return Q(L(_.value));case"aborted":return Q($("Aborted"));case"timedOut":return Q($("Timed out"));case"stopped":return Q($("Stopped"));case"error":if(P>0){P-=1;continue}return Q($(_.error))}}}if(H!==void 0)await M(H);if(J.signal.aborted)return Q($("Aborted"));let S=new AbortController,F=await K(N,S,void 0);switch(F.kind){case"value":return Q(L(F.value));case"aborted":return Q($("Aborted"));case"timedOut":return Q($("Timed out"));case"stopped":return Q($("Stopped"));case"error":return Q($(F.error))}},A=()=>{if(q)return;if(q=!0,G===void 0)return;k()};D[O]=A;let C={done:D,abort:()=>{J.abort(),Q($("Aborted"))},resolve:(W)=>Q(L(W)),reject:(W)=>Q($(l(W))),tap:(W)=>t(async()=>{let H=await C;if(H.isOk)await W(H.value);return H}),then:(W,H)=>{return A(),Z.then(W,H)},catch:(W)=>{return A(),Z.catch(W)},finally:(W)=>{return A(),Z.finally(W)}};return C}function PG(G){let{type:B}=G;if(B==="race"&&G.count<1)throw Error("superRunner race count must be at least 1");let J=new AbortController,D=w(),X=()=>{},Z=new Promise((C)=>{X=C}),q=!1,Y=!1,Q=(C)=>{if(Y)return;Y=!0,X(C),D.fire()},K=B==="sequence"?G.runners:Object.values(G.runners),I=()=>{for(let C of K)C.abort()},M=async(C)=>{let W=[];for(let H of C){if(J.signal.aborted)return Q($("Aborted"));let j=await H;if(J.signal.aborted)return Q($("Aborted"));if(j.isErr)return Q($(j.error));W.push(j.value)}Q(L(W))},k=async(C)=>{let W=Object.entries(C),H={},j=0;for(let[U,N]of W)N.then((S)=>{if(j+=1,S.isOk)H[U]=S.value;else if(!J.signal.aborted&&!Y){I(),Q($(S.error));return}if(j===W.length)Q(L(H))});if(W.length===0)Q(L(H))},A=async(C,W)=>{let H=Object.entries(C),j={},U=0,N=0;for(let[S,F]of H)F.then((P)=>{if(N+=1,P.isOk){if(j[S]=P.value,U+=1,U>=W){I(),Q(L(j));return}}if(N===H.length)Q($(U===0?"All runners failed":"Not enough runners succeeded"))});if(H.length===0)Q($("All runners failed"))},y=()=>{if(!q)if(q=!0,B==="sequence")M(G.runners);else if(B==="all")k(G.runners);else A(G.runners,G.count);return Z};return D[O]=y,{done:D,start:y,abort:()=>{J.abort(),I(),Q($("Aborted"))}}}function UG(){let G=new Map,B=(X)=>{let Z=G.get(X.topic);if(Z===void 0)return;for(let q of Z)try{q(X)}catch(Y){g("channel subscriber threw:",Y)}};return{send:B,subscribe:(X,Z)=>{let q=G.get(X.topic);if(q===void 0)q=new Set,G.set(X.topic,q);return q.add(Z),()=>{let Y=G.get(X.topic);if(Y!==void 0)Y.delete(Z)}},when:(X)=>{if(X.signal.aborted){B({topic:X.topic,data:X.data});return}X.signal.addEventListener("abort",()=>B({topic:X.topic,data:X.data}),{once:!0})}}}export{WG as zipWith,o as zip,HG as unzip,PG as superRunner,t as superPromise,GG as setEnvironment,R as scheduleMicrotask,jG as safeTry,g as println,IG as pipe,E as panic,T as option,qG as matchAll,$G as match,z as isFalsy,w as createSignal,UG as createChannel,b as atom,p as _to,YG as Options,L as Ok,$ as Err};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Event published on a Channel. */
|
|
2
|
+
export type ChannelEvent = {
|
|
3
|
+
topic: string;
|
|
4
|
+
data: unknown;
|
|
5
|
+
};
|
|
6
|
+
/** Subscriber handler receiving published events. */
|
|
7
|
+
export type ChannelSubscriber = (event: ChannelEvent) => void;
|
|
8
|
+
/** Event communication between independent parts of an application. */
|
|
9
|
+
export type Channel = {
|
|
10
|
+
/** Immediately publishes an event to subscribers of its topic. */
|
|
11
|
+
send(event: ChannelEvent): void;
|
|
12
|
+
/**
|
|
13
|
+
* Subscribes to events of a specific topic.
|
|
14
|
+
* Returns an unsubscribe function; after calling it, no further events are received.
|
|
15
|
+
*/
|
|
16
|
+
subscribe(filter: {
|
|
17
|
+
topic: string;
|
|
18
|
+
}, handler: ChannelSubscriber): () => void;
|
|
19
|
+
/** Publishes an event when a signal fires. Accepts any compatible lifecycle signal. */
|
|
20
|
+
when(config: {
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
topic: string;
|
|
23
|
+
data: unknown;
|
|
24
|
+
}): void;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates a Channel — completely independent of SuperPromise/SuperRunner.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const channel = createChannel();
|
|
31
|
+
*
|
|
32
|
+
* const unsubscribe = channel.subscribe({ topic: "user.created" }, event => {
|
|
33
|
+
* println(event.data);
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* channel.send({ topic: "user.created", data: user });
|
|
37
|
+
* unsubscribe(); // stop receiving events
|
|
38
|
+
*/
|
|
39
|
+
export declare function createChannel(): Channel;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { scheduleMicrotask } from "./internals";
|
|
2
|
-
export { println } from "./println";
|
|
2
|
+
export { println, setEnvironment } from "./println";
|
|
3
|
+
export type { EnvironmentMode, EnvironmentOptions, PrintFn } from "./println";
|
|
3
4
|
export { panic } from "./panic";
|
|
4
5
|
export { Ok, Err } from "./result";
|
|
5
6
|
export type { Result, ResultMethods, Ok as OkType, Err as ErrType } from "./result";
|
|
6
7
|
export { option, isFalsy } from "./option";
|
|
7
8
|
export type { Option, OptionMethods, Some, None, NonTruthy } from "./option";
|
|
9
|
+
export { Options } from "./options";
|
|
10
|
+
export type { OptionsValue } from "./options";
|
|
8
11
|
export { atom } from "./atom";
|
|
9
12
|
export type { Atom, AtomMethods } from "./atom";
|
|
10
13
|
import "./to";
|
|
@@ -14,3 +17,11 @@ export { zip, zipWith, unzip } from "./zip";
|
|
|
14
17
|
export { safeTry } from "./safe-try";
|
|
15
18
|
export { pipe } from "./pipe";
|
|
16
19
|
export type { PipeFn, PipeEachContext, PipeRunOptions, Pipeline } from "./pipe";
|
|
20
|
+
export { superPromise } from "./super-promise";
|
|
21
|
+
export type { SuperPromise, SuperPromiseOptions, Executor, RetryUntilOptions } from "./super-promise";
|
|
22
|
+
export { superRunner } from "./super-runner";
|
|
23
|
+
export type { SuperRunner } from "./super-runner";
|
|
24
|
+
export { createChannel } from "./channel";
|
|
25
|
+
export type { Channel, ChannelEvent, ChannelSubscriber } from "./channel";
|
|
26
|
+
export { createSignal } from "./signal";
|
|
27
|
+
export type { Signal } from "./signal";
|
package/dist/src/internals.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ import type { Option } from "./option";
|
|
|
7
7
|
export declare const scheduleMicrotask: (fn: () => void) => void;
|
|
8
8
|
/** Type guard for Result */
|
|
9
9
|
export declare const isResult: (value: unknown) => value is Result<unknown, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Hidden activation hook carried by lifecycle signals (task.done, runner.done).
|
|
12
|
+
* Lets defer chains activate their source task when awaited.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ACTIVATE_HOOK: unique symbol;
|
|
15
|
+
export type Activatable = {
|
|
16
|
+
[ACTIVATE_HOOK]?: () => void;
|
|
17
|
+
};
|
|
10
18
|
/** Type guard for Option */
|
|
11
19
|
export declare const isOption: (value: unknown) => value is Option<unknown>;
|
|
12
20
|
/** Type guard for Atom (boxed symbol) */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Err, type Result, type ResultMethods } from "./result";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the usable inner type of a value.
|
|
4
|
+
* - `Some<U>` / `Ok<U>` contribute `U`.
|
|
5
|
+
* - `None`, `Err`, null, undefined, and empty string contribute nothing.
|
|
6
|
+
* - Raw values contribute themselves.
|
|
7
|
+
*/
|
|
8
|
+
export type OptionsValue<T> = T extends {
|
|
9
|
+
isErr: true;
|
|
10
|
+
} | {
|
|
11
|
+
isNone: true;
|
|
12
|
+
} | null | undefined | "" ? never : T extends {
|
|
13
|
+
isSome: true;
|
|
14
|
+
value: infer U;
|
|
15
|
+
} | {
|
|
16
|
+
isOk: true;
|
|
17
|
+
value: infer U;
|
|
18
|
+
} ? U : T;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the first usable value from a list as an `Ok`, or the fallback
|
|
21
|
+
* `Err` if none is usable.
|
|
22
|
+
*
|
|
23
|
+
* Normalizes heterogeneous values — raw values, `Option`s, and `Result`s
|
|
24
|
+
* from different sources — when all you care about is whether any of them
|
|
25
|
+
* is usable. A value is usable when its unwrapped inner value is truthy;
|
|
26
|
+
* `None`, `Err`, null, undefined, `""`, NaN, and Infinity are skipped.
|
|
27
|
+
* Note that `0` and `false` are usable, matching `option()` semantics.
|
|
28
|
+
*
|
|
29
|
+
* @param values - Values to inspect in order; first usable one wins.
|
|
30
|
+
* @param fallback - `Err` returned when no value is usable (default `Err("No value!")`).
|
|
31
|
+
* @returns `Ok` of the first usable inner value, or the fallback.
|
|
32
|
+
* @example
|
|
33
|
+
* const a = null;
|
|
34
|
+
* const b = option(undefined);
|
|
35
|
+
* const c = Ok(true);
|
|
36
|
+
* const d = Err("nope!");
|
|
37
|
+
* Options([a, b, c, d]); // Ok(true) - c is usable
|
|
38
|
+
*
|
|
39
|
+
* const nothing = Options([a, option(null), Err("nope!")]); // Err("No value!")
|
|
40
|
+
*/
|
|
41
|
+
export declare function Options<T extends readonly unknown[]>(values: T, fallback?: Err<string> & ResultMethods<never>): Result<OptionsValue<T[number]>, string>;
|
package/dist/src/println.d.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
export type EnvironmentMode = "development" | "production" | "dev" | "prod";
|
|
2
|
+
export type PrintFn = (line: string) => void;
|
|
3
|
+
export type EnvironmentOptions = {
|
|
4
|
+
printFn?: PrintFn | null;
|
|
5
|
+
};
|
|
1
6
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
7
|
+
* Sets the runtime environment for dev-only utilities.
|
|
8
|
+
* Idempotent — safe to call any number of times, in any order; the last call wins.
|
|
9
|
+
* Accepts a mode (full words or short forms: "development"/"dev", "production"/"prod")
|
|
10
|
+
* or options `{ printFn }` to route println through an existing logger instead of
|
|
11
|
+
* the console — useful in production, where logs go somewhere real.
|
|
12
|
+
* Pass `{ printFn: null }` to switch back to the console.
|
|
13
|
+
*/
|
|
14
|
+
export declare const setEnvironment: (next: EnvironmentMode | EnvironmentOptions) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Prints a line to the console — args joined with spaces, newline appended.
|
|
17
|
+
* When a custom printFn is set, the line goes to it instead, in every mode.
|
|
18
|
+
* Otherwise production mode is silent, so dev chatter never leaks to prod logs.
|
|
6
19
|
*/
|
|
7
20
|
export declare const println: (...args: unknown[]) => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lifecycle Signal — a trigger that fires once and stays fired.
|
|
3
|
+
*
|
|
4
|
+
* Abort-compatible: structurally an `AbortSignal` plus a `fire()` method,
|
|
5
|
+
* so it can be passed anywhere a native `AbortSignal` is expected
|
|
6
|
+
* (and vice-versa). Wraps AbortController semantics.
|
|
7
|
+
*
|
|
8
|
+
* Consumed by `defer`, `retryUntil`, `channel.when`, and `task.done`.
|
|
9
|
+
*/
|
|
10
|
+
export type Signal = AbortSignal & {
|
|
11
|
+
/** Fires the signal once. Subsequent calls are no-ops. */
|
|
12
|
+
fire(): void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Creates a lifecycle signal.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const signal = createSignal();
|
|
19
|
+
*
|
|
20
|
+
* signal.addEventListener("abort", () => {
|
|
21
|
+
* println("fired");
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* signal.fire(); // "fired"
|
|
25
|
+
* signal.aborted; // true
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Listeners are NOT replayed after firing — consumers must check
|
|
29
|
+
* `signal.aborted` synchronously to detect an already-fired signal.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createSignal(): Signal;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Result } from "./result";
|
|
2
|
+
/** The operation itself. Receives the attempt's AbortSignal. */
|
|
3
|
+
export type Executor<T> = (signal: AbortSignal) => T | Promise<T> | Result<T, string> | Promise<Result<T, string>>;
|
|
4
|
+
/** Stop signal for `retryUntil`. */
|
|
5
|
+
export type RetryUntilOptions = {
|
|
6
|
+
/** When this fires, retrying ends — mid-attempt firings cancel the in-flight attempt. */
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
/** Wait between attempts. No wait applies before the first attempt. */
|
|
9
|
+
delay?: number;
|
|
10
|
+
};
|
|
11
|
+
/** Execution policies, configured at construction time. */
|
|
12
|
+
export type SuperPromiseOptions = {
|
|
13
|
+
/** Do not execute until this signal fires (accepts SuperPromise signals and AbortSignal). */
|
|
14
|
+
defer?: AbortSignal;
|
|
15
|
+
/** Wait before initial execution; normal retries also wait this long. */
|
|
16
|
+
delay?: number;
|
|
17
|
+
/** Abort the operation after this many milliseconds. Timeouts are never retried. */
|
|
18
|
+
timeout?: number;
|
|
19
|
+
/** `n` retries after the first attempt — `n + 1` attempts total. Aborts and timeouts are never retried. */
|
|
20
|
+
retry?: number;
|
|
21
|
+
/** Keep retrying until the stop signal fires. Overrides `retry` and `delay`. */
|
|
22
|
+
retryUntil?: RetryUntilOptions;
|
|
23
|
+
};
|
|
24
|
+
/** A single async operation with lifecycle ergonomics. Promise-compatible. */
|
|
25
|
+
export type SuperPromise<T> = {
|
|
26
|
+
/** Lifecycle signal: fires once when the task reaches a terminal state. */
|
|
27
|
+
done: AbortSignal;
|
|
28
|
+
/** Cancels the operation; settles with `Err("Aborted")` when not yet settled. */
|
|
29
|
+
abort(): void;
|
|
30
|
+
/** Manually settles an externally controlled task with a value. First settlement wins. */
|
|
31
|
+
resolve(value: T): void;
|
|
32
|
+
/** Manually settles an externally controlled task with a failure. First settlement wins. */
|
|
33
|
+
reject(error: unknown): void;
|
|
34
|
+
/** Observes a successful value without transforming it. Callback throws settle as `Err`. */
|
|
35
|
+
tap(fn: (value: T) => unknown | Promise<unknown>): SuperPromise<T>;
|
|
36
|
+
/** Promise-compatible. The callback receives the settled `Result`; the task never rejects. */
|
|
37
|
+
then<TR1 = Result<T, string>, TR2 = never>(onfulfilled?: ((value: Result<T, string>) => TR1 | PromiseLike<TR1>) | null, onrejected?: ((reason: unknown) => TR2 | PromiseLike<TR2>) | null): Promise<TR1 | TR2>;
|
|
38
|
+
/** Promise-compatible. Effectively dead — failures are captured into `Err`, never rejections. */
|
|
39
|
+
catch<TR = never>(onrejected?: ((reason: unknown) => TR | PromiseLike<TR>) | null): Promise<Result<T, string> | TR>;
|
|
40
|
+
/** Promise-compatible. Native behavior. */
|
|
41
|
+
finally(onfinally?: (() => void) | null): Promise<Result<T, string>>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Creates a SuperPromise — one async operation plus lifecycle/execution ergonomics.
|
|
45
|
+
*
|
|
46
|
+
* Lazy activation: the executor starts on first consumption (`await`/`.then()`,
|
|
47
|
+
* or a runner's `start()`), not at creation. Defer chains activate recursively,
|
|
48
|
+
* so awaiting a deferred task runs its dependency first.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* const task = superPromise(async signal => {
|
|
52
|
+
* const response = await fetch(url, { signal });
|
|
53
|
+
* return response.json();
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* const result = await task; // Result<T, string>
|
|
57
|
+
*/
|
|
58
|
+
export declare function superPromise<T>(fn: Executor<T>, options?: SuperPromiseOptions): SuperPromise<T>;
|
|
59
|
+
/** Creates an externally controlled SuperPromise, settled via `resolve`/`reject`. */
|
|
60
|
+
export declare function superPromise<T>(fn?: undefined, options?: SuperPromiseOptions): SuperPromise<T>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Result } from "./result";
|
|
2
|
+
import type { SuperPromise } from "./super-promise";
|
|
3
|
+
/** Sequence result: positional tuple of the runners' Ok values. */
|
|
4
|
+
type TupleValues<R extends readonly SuperPromise<unknown>[]> = {
|
|
5
|
+
-readonly [K in keyof R]: R[K] extends SuperPromise<infer T> ? T : never;
|
|
6
|
+
};
|
|
7
|
+
/** all/race result: named object of the runners' Ok values. */
|
|
8
|
+
type NamedValues<R extends Record<string, SuperPromise<unknown>>> = {
|
|
9
|
+
[K in keyof R]: R[K] extends SuperPromise<infer T> ? T : never;
|
|
10
|
+
};
|
|
11
|
+
/** race(count: 1) result: union of single-winner objects. */
|
|
12
|
+
type RaceValues<C extends number, R extends Record<string, SuperPromise<unknown>>> = C extends 1 ? {
|
|
13
|
+
[K in keyof R]: {
|
|
14
|
+
[P in K]: R[K] extends SuperPromise<infer T> ? T : never;
|
|
15
|
+
};
|
|
16
|
+
}[keyof R] : Partial<NamedValues<R>>;
|
|
17
|
+
/** Orchestrates multiple already-created SuperPromises. */
|
|
18
|
+
export type SuperRunner<Value> = {
|
|
19
|
+
/** Lifecycle signal: fires once when the runner reaches a terminal state. */
|
|
20
|
+
done: AbortSignal;
|
|
21
|
+
/** Starts execution. Idempotent: repeated calls return the same result without re-executing children. */
|
|
22
|
+
start(): Promise<Result<Value, string>>;
|
|
23
|
+
/** Cancels the runner and all active children; settles with `Err("Aborted")`. */
|
|
24
|
+
abort(): void;
|
|
25
|
+
};
|
|
26
|
+
/** Runs runners in order; stops at the first failure. */
|
|
27
|
+
export declare function superRunner<const R extends readonly SuperPromise<unknown>[]>(config: {
|
|
28
|
+
type: "sequence";
|
|
29
|
+
runners: R;
|
|
30
|
+
}): SuperRunner<TupleValues<R>>;
|
|
31
|
+
/** Runs runners concurrently; fails fast on the first failure, aborting remaining children. */
|
|
32
|
+
export declare function superRunner<const R extends Record<string, SuperPromise<unknown>>>(config: {
|
|
33
|
+
type: "all";
|
|
34
|
+
runners: R;
|
|
35
|
+
}): SuperRunner<NamedValues<R>>;
|
|
36
|
+
/** Runs runners concurrently; first `count` successful completions win, remaining children are aborted. */
|
|
37
|
+
export declare function superRunner<const C extends number, const R extends Record<string, SuperPromise<unknown>>>(config: {
|
|
38
|
+
type: "race";
|
|
39
|
+
count: C;
|
|
40
|
+
runners: R;
|
|
41
|
+
}): SuperRunner<RaceValues<C, R>>;
|
|
42
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slang-ts",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Functional programming library for TypeScript",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "Functional programming library for TypeScript: Option, Result, pattern matching, and async primitives",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -12,13 +12,6 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"test": "vitest run",
|
|
17
|
-
"test:watch": "vitest",
|
|
18
|
-
"test:ui": "vitest --ui",
|
|
19
|
-
"typecheck": "tsc --noEmit",
|
|
20
|
-
"build": "bun run test && bun run typecheck && bun run bun-build.ts && tsc -p tsconfig.build.json"
|
|
21
|
-
},
|
|
22
15
|
"homepage": "https://github.com/Hussseinkizz/slang#readme",
|
|
23
16
|
"repository": {
|
|
24
17
|
"type": "git",
|
|
@@ -31,14 +24,26 @@
|
|
|
31
24
|
"functional",
|
|
32
25
|
"typescript",
|
|
33
26
|
"option",
|
|
27
|
+
"maybe",
|
|
34
28
|
"result",
|
|
29
|
+
"safe-try",
|
|
35
30
|
"pattern-matching",
|
|
31
|
+
"match",
|
|
36
32
|
"monad",
|
|
37
|
-
"
|
|
33
|
+
"pipe",
|
|
34
|
+
"zip",
|
|
35
|
+
"async",
|
|
36
|
+
"promise",
|
|
37
|
+
"superpromise",
|
|
38
|
+
"superrunner",
|
|
39
|
+
"cancellation",
|
|
40
|
+
"abort",
|
|
41
|
+
"signal",
|
|
42
|
+
"channel",
|
|
43
|
+
"event-bus",
|
|
38
44
|
"defensive programming",
|
|
39
45
|
"fp-ts",
|
|
40
46
|
"ramda",
|
|
41
|
-
"rameda",
|
|
42
47
|
"neverthrow",
|
|
43
48
|
"lodash"
|
|
44
49
|
],
|
|
@@ -47,10 +52,17 @@
|
|
|
47
52
|
"sideEffects": false,
|
|
48
53
|
"devDependencies": {
|
|
49
54
|
"@types/bun": "latest",
|
|
50
|
-
"@vitest/ui": "^4.
|
|
51
|
-
"vitest": "^4.
|
|
55
|
+
"@vitest/ui": "^4.1.10",
|
|
56
|
+
"vitest": "^4.1.10"
|
|
52
57
|
},
|
|
53
58
|
"peerDependencies": {
|
|
54
|
-
"typescript": "^5"
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest",
|
|
64
|
+
"test:ui": "vitest --ui",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"build": "bun run test && bun run typecheck && bun run bun-build.ts && tsc -p tsconfig.build.json"
|
|
55
67
|
}
|
|
56
|
-
}
|
|
68
|
+
}
|