slang-ts 0.1.1 → 0.2.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/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # slang-ts
2
2
 
3
- Functional programming library for TypeScript.
3
+ [![npm version](https://img.shields.io/npm/v/slang-ts.svg)](https://www.npmjs.com/package/slang-ts)
4
+ [![license](https://img.shields.io/npm/l/slang-ts.svg)][license]
4
5
 
5
- A collection of utilities and other cool programming stuff brought over from languages like Rust.
6
+ An opinionated functional programming library for TypeScript.
7
+
8
+ A collection of utilities and mechanisms brought over from languages like Rust and Erlang. Each
9
+ utility is deliberately chosen and built from scratch with zero dependencies.
6
10
 
7
11
  ## Install
8
12
 
@@ -10,694 +14,276 @@ A collection of utilities and other cool programming stuff brought over from lan
10
14
  npm i slang-ts
11
15
  ```
12
16
 
13
- ## Implemented Utilities
14
-
15
- - [x] Option (Some, None)
16
- - [x] isFalsy (Option truthiness check)
17
- - [x] Result (Ok, Err)
18
- - [x] Expect
19
- - [x] Unwrap (on Option)
20
- - [x] Else (on unwrap)
21
- - [x] andThen
22
- - [x] Match
23
- - [x] MatchAll
24
- - [x] Options (normalize mixed values)
25
- - [x] SafeTry
26
- - [x] Pipe
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)
38
-
39
- All utilities fully tested. See [tests](https://github.com/Hussseinkizz/slang/tree/main/tests)
40
-
41
- ## New in v0.1.1
42
-
43
- - **Ok messages**: `Ok(value, message?)` keeps an optional success message on the result, such as text to show the user. Results without one have no `message` property. (0.1.0 was published without this change; use 0.1.1 or later.)
44
- - **Publishing**: `npm publish` now runs `bun run export` first (tests, type check, then build), so the package always ships a fresh build.
45
-
46
- ## New in v0.0.9
47
-
48
- - **SafeTry**: unwraps nested Result, Option, and Atom values into one result; nested failures now become the outer `Err`, while plain object properties and array elements stay unchanged.
49
- - **Package types**: bundled declarations work with strict NodeNext consumers, so importing the package no longer exposes invalid declaration paths.
50
- - **Build tooling**: tsdown and TypeScript 7 build JavaScript and bundled types together.
51
-
52
- ## New in v0.0.8
53
-
54
- - **Signal**: a simple on/off trigger that fires once and stays fired; use it to make something start only after something else finishes.
55
- - **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.
56
- - **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.
57
- - **SuperRunner**: `Promise.all` and `Promise.race` grown up: run several SuperPromises in order, in parallel, or take first to finish.
58
- - **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.
59
-
60
- ## How It Works
61
-
62
- Import utilities individually or together:
63
-
64
- ```ts
65
- // Individual imports
66
- import { option } from "slang-ts";
67
- import { Ok, Err } from "slang-ts";
68
-
69
- // Or import multiple at once
70
- import { option, Ok, Err, atom, match } from "slang-ts";
71
-
72
- // Or import under namespace (not so performant)
73
- import * as slang from "slang-ts";
17
+ ## Why I Made This?
74
18
 
75
- slang.println("Hello world!");
76
- ```
19
+ Well I love functional programming so much and hate classes and inheritance with all my heart, sorry no offense intended, I prefer composition and functions over those but well that's not exactly why I made this despite part of the reasons, main reason was all functional programming (FP) libraries in typescript and javascript were really complicated, think effect ts and rameda, all seemed like needed one to be so nerdy, so was like I need something simple I can work with comfortably and can onboard a team to over an evenning, one by one, got each utility crafted out, to what you see today, here I aimed more for practicality than correctness, so yes some functions are not pure, a few rules broken, a few opinions, and all that makes this fun to use, try it!
77
20
 
78
- ### Option
21
+ ## Quick start
79
22
 
80
- 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.
23
+ So let's say you have a function that can fail. Instead of throwing and hoping someone up the chain
24
+ catches it, you return a `Result`:
81
25
 
82
26
  ```ts
83
- import { option } from "slang-ts";
84
-
85
- const a = option("hi"); // Some("hi")
86
- const b = option(null); // None
87
- const c = option(0); // Some(0) - zero is truthy!
88
- const d = option(""); // None
89
- const e = option(false); // Some(false) - false is truthy!
90
-
91
- if (a.isSome) {
92
- println("Value:", a.value);
93
- }
27
+ import { Ok, Err, type Result, println } from "slang-ts";
94
28
 
95
- if (b.isNone) {
96
- println("No value");
97
- }
98
- ```
99
-
100
- ### Result
101
-
102
- Represents operations that can succeed or fail. Returns `Ok<T>` on success or `Err<E>` on failure with typed error payload.
103
-
104
- ```ts
105
- import { Ok, Err, type Result } from "slang-ts";
106
-
107
- // Simple function returning Result
108
29
  function divide(a: number, b: number): Result<number, string> {
109
- if (b === 0) return Err("Cannot divide by zero");
110
- return Ok(a / b);
30
+ return b === 0 ? Err("Cannot divide by zero") : Ok(a / b);
111
31
  }
112
32
 
113
33
  const result = divide(10, 2);
114
34
 
115
35
  if (result.isOk) {
116
- println("Success:", result.value); // 5
36
+ println("Result:", result.value); // 5
117
37
  } else {
118
38
  println("Error:", result.error);
119
39
  }
120
-
121
- // Async API example
122
- interface User {
123
- id: string;
124
- name: string;
125
- }
126
-
127
- async function fetchUser(id: string): Promise<Result<User, string>> {
128
- try {
129
- const response = await fetch(`/api/users/${id}`);
130
- if (!response.ok) return Err("User not found");
131
- const user = await response.json();
132
- return Ok(user);
133
- } catch (error) {
134
- return Err("Network error");
135
- }
136
- }
137
-
138
- const user = await fetchUser("123");
139
- if (user.isOk) {
140
- println("User:", user.value.name);
141
- }
142
-
143
- // Optional success message as the second argument
144
- const saved = Ok({ id: "123" }, "Profile saved");
145
- if (saved.isOk) {
146
- println(saved.message); // "Profile saved"
147
- }
148
- ```
149
-
150
- `andThen` and `safeTry` return new results, so a message stays with the value it described.
151
-
152
- ### Expect
153
-
154
- Unwraps values or throws with custom message. Use when failure is unrecoverable.
155
-
156
- ```ts
157
- const personAge = option(25).expect("a person must have age!");
158
- println("person age", personAge); // 25
159
-
160
- // This would throw!
161
- // const personAge2 = option("").expect("a person must have age!");
162
- ```
163
-
164
- ### Unwrap/Else
165
-
166
- Chainable unwrapping with mandatory fallback. Must call `.else()` or throws.
167
-
168
- ```ts
169
- const port = option(process.env.PORT).unwrap().else(3000);
170
- println("Using port:", port);
171
-
172
- // Function fallbacks
173
- const retries = option(null).unwrap().else(() => 5);
174
- println("Retries:", retries);
175
-
176
- // This throws! No .else() chained
177
- // const nothing = option(null).unwrap();
178
- ```
179
-
180
- ### andThen
181
-
182
- 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`.
183
-
184
- ```ts
185
- // Option - transforms Some, skips None
186
- option(5).andThen(x => x * 2); // Some(10)
187
- option(null).andThen(x => x * 2); // None (skipped)
188
- option(5).andThen(() => undefined); // Some(5) - original
189
-
190
- // Result - transforms Ok, skips Err
191
- Ok(10).andThen(x => x + 5); // Ok(15)
192
- Err("fail").andThen(x => x + 5); // Err("fail") (skipped)
193
-
194
- // Atom - transforms description (sync only)
195
- atom("hello").andThen(s => s.toUpperCase()); // Atom("HELLO")
196
-
197
- // Chained andThen - multiple transformations
198
- option(5)
199
- .andThen(x => x + 1)
200
- .andThen(x => x * 2)
201
- .andThen(x => x.toString()); // Some("12")
202
-
203
- Ok(10)
204
- .andThen(x => x * 2)
205
- .andThen(x => x + 5)
206
- .andThen(x => ({ value: x })); // Ok({ value: 25 })
207
-
208
- atom("hello")
209
- .andThen(s => s.toUpperCase())
210
- .andThen(s => s + "!"); // Atom("HELLO!")
211
-
212
- // Async support for Option and Result
213
- const data = await option(5).andThen(async x => await fetchData(x));
214
-
215
- // Error handling
216
- option(5).andThen(() => { throw "oops" }); // None (caught)
217
- Ok(5).andThen(() => { throw "oops" }); // Err("oops")
218
- atom("x").andThen(() => { throw "oops" }); // Panics!
219
-
220
- // Type transformation
221
- option(42).andThen(x => x.toString()); // Some("42")
222
40
  ```
223
41
 
224
- ### Match
225
-
226
- Exhaustive pattern matching for `Option` and `Result` types. Forces you to handle all cases. Returns the value from the matched handler.
227
-
228
- ```ts
229
- import { match } from "slang-ts";
230
-
231
- // Matching Results - returns handler result
232
- const result = divide(10, 0);
233
- const message = match(result, {
234
- Ok: (v) => `Success: ${v.value}`,
235
- Err: (e) => `Failed: ${e.error}`,
236
- });
237
- println(message); // "Failed: Cannot divide by zero"
238
-
239
- // Matching Options - returns handler result
240
- const maybePort = option(process.env.PORT);
241
- const port = match(maybePort, {
242
- Some: (v) => parseInt(v.value),
243
- None: () => 3000,
244
- });
245
- println("Using port:", port); // Uses parsed port or default 3000
246
- ```
42
+ `println` is optional — it's a small development logger.
247
43
 
248
- ### MatchAll
44
+ ## Imports
249
45
 
250
- Pattern matching for primitives and atoms with required `_` fallback. Returns the value from the matched handler.
46
+ Import only what you use:
251
47
 
252
48
  ```ts
253
- import { matchAll } from "slang-ts";
254
-
255
- // Match atoms - returns handler result
256
- const ready = atom("ready");
257
- const status = matchAll(ready, {
258
- ready: () => "System is ready",
259
- failed: () => "System failed",
260
- _: () => "Unknown state",
261
- });
262
- println(status); // "System is ready"
263
-
264
- // Match booleans - returns handler result
265
- const isActive = true;
266
- const label = matchAll(isActive, {
267
- true: () => "Active",
268
- false: () => "Inactive",
269
- _: () => "Unknown",
270
- });
271
- println(label); // "Active"
49
+ import { option, Ok, Err, atom, match } from "slang-ts";
272
50
  ```
273
51
 
274
- ### Options
52
+ ## Choose by problem
275
53
 
276
- 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).
54
+ | If you need… | Use |
55
+ |---|---|
56
+ | A value that may be missing | [Option][option] |
57
+ | An operation that can fail | [Result][result] and [SafeTry][safe-try] |
58
+ | Pattern matching over results | [Match][match] |
59
+ | Timeout, retry, or cancellation | [SuperPromise][super-promise] |
60
+ | Ordered, parallel, or race tasks | [SuperRunner][super-runner] |
61
+ | A shared concurrency limit | [taskGroup][task-group] |
62
+ | Strict array bounds or paging | [list][list] |
63
+ | Safe nested record access | [struct][struct] |
64
+ | Set membership and algebra | [sets][sets] |
65
+ | Lazy pipelines over an `Ok` | [Ok.iterate][iterate] |
66
+ | Cached function calls | [memoized][memoized] |
67
+ | Step history or undo | [createStepper][stepper] |
68
+ | Publish/subscribe between parts of an app | [Channel][channel] |
69
+ | A lifecycle trigger that fires once | [Signal][signal] |
277
70
 
278
- ```ts
279
- import { Options, option, Ok, Err } from "slang-ts";
280
-
281
- // First usable value wins
282
- let a = null;
283
- let b = option(undefined);
284
- let c = Ok(true);
285
- let d = Err("nope!");
286
-
287
- const something = Options([a, b, c, d], Err("No value!"));
288
- // Ok(true) - c is usable
289
-
290
- // Nothing usable -> fallback
291
- let e = option(null);
292
- const nothing = Options([a, b, e, d], Err("No value!"));
293
- // Err("No value!")
294
-
295
- // Raw (non-wrapped) values work too, just like a single option():
296
- Options([null, undefined, "hello"]); // Ok("hello") - raw truthy wins
297
- Options(["", 0, "zero is skipped, 0 used"]); // Ok("...") - skips "" first
298
- Options([null, undefined, ""]); // Err("No value!") - all raw falsy
299
- ```
71
+ ## What's inside
300
72
 
301
- Useful when values come from different functions with inconsistent return types and all you care about is whether the end result is usable.
73
+ ### Error handling and flow
302
74
 
303
- ### SafeTry
75
+ - [Option][option] (Some/None, `isFalsy`, expect, unwrap/else, andThen)
76
+ - [Result][result] (Ok/Err, expect, unwrap/else, andThen, raw cause)
77
+ - [SafeTry][safe-try]
78
+ - [Match][match] (match, matchAll)
79
+ - [Options][options]
80
+ - [Pipe][pipe]
304
81
 
305
- Runs sync or async functions and captures failures as `Err<string>`. Always await it. Nested Result, Option, and Atom values resolve to one `Result`: `Ok` and `Some` unwrap until a plain value appears, `Err` stops with its error normalized through `String`, `None` becomes `Err("Option was None")`, and Atom becomes its description.
82
+ ### Composition and conversion
306
83
 
307
- ```ts
308
- import { safeTry, Ok, Err, option, atom, println, type Result } from "slang-ts";
84
+ - [Atom][atom]
85
+ - [To][to] (`.to()` converters)
86
+ - [Zip][zip] (zip, zipWith, unzip)
309
87
 
310
- // Inferred type is Promise<Result<number, string>>, not nested Results.
311
- const pending: Promise<Result<number, string>> = safeTry(() => Ok(Ok(42)));
312
- const nested = await pending; // Ok(42)
313
- if (nested.isOk) println(nested.value); // 42
88
+ ### Collections and data
314
89
 
315
- const failed = await safeTry(() => Ok(Ok(Err("Not found")))); // Err("Not found")
316
- if (failed.isErr) println(failed.error); // "Not found"
90
+ - [list][list]
91
+ - [struct][struct]
92
+ - [sets][sets]
93
+ - [Ok.iterate][iterate]
94
+ - [memoized][memoized]
317
95
 
318
- const mixed = await safeTry(() => Ok(option(Ok(atom("ready")))));
319
- // Ok("ready"); inferred success value is the literal "ready".
320
- if (mixed.isOk) println(mixed.value); // "ready"
96
+ ### Async and lifecycle
321
97
 
322
- const missing = await safeTry(() => Ok(option(null))); // Err("Option was None")
323
- if (missing.isErr) println(missing.error);
98
+ - [SuperPromise][super-promise]
99
+ - [SuperRunner][super-runner]
100
+ - [taskGroup][task-group]
101
+ - [Signal][signal]
102
+ - [Channel][channel]
324
103
 
325
- const caught = await safeTry(async () => {
326
- throw new Error("Could not load profile");
327
- }); // Err("Could not load profile")
328
- if (caught.isErr) println(caught.error);
329
- ```
104
+ ### Small utilities
330
105
 
331
- Only the returned wrapper chain unwraps. Plain object properties and array elements stay as supplied. Only the callback's return is awaited; a Promise inside `Ok` or `Some` stays a Promise.
106
+ - [Panic][panic]
107
+ - [println][logging] (with `setEnvironment`)
108
+ - [createStepper][stepper]
332
109
 
333
- ```ts
334
- import { safeTry, Ok, println } from "slang-ts";
110
+ Full reference: [docs index][docs-index]. Feature list: [CHANGELOG.md][changelog].
335
111
 
336
- const payload = { result: Ok(42), items: [Ok(7)] };
337
- const plain = await safeTry(() => payload); // Ok(payload), same object
338
- if (plain.isOk) println(plain.value === payload); // true
112
+ ## Real-world examples
339
113
 
340
- const innerPromise = Promise.resolve(42);
341
- const wrapped = await safeTry(() => Ok(innerPromise));
342
- // Ok(innerPromise), success value stays Promise<number>.
343
- if (wrapped.isOk) println(wrapped.value === innerPromise); // true
344
- ```
114
+ ### Load a value that can fail: SafeTry + Result
345
115
 
346
- `{ throw: true }` rethrows errors caught while running the callback or reading its returned wrappers. Returned `Err` values still return normally. A circular wrapper chain returns `Err("Cannot resolve a circular value")`, including with `{ throw: true }`.
116
+ The edge of your app probably mixes failures: a lookup that throws, a parser that rejects a value,
117
+ an optional result. `safeTry` brings all of that into one `Result`:
347
118
 
348
119
  ```ts
349
- import { safeTry, Ok, Err, println } from "slang-ts";
350
-
351
- const returned = await safeTry(() => Ok(Err("Not found")), { throw: true });
352
- if (returned.isErr) println(returned.error); // "Not found"
353
-
354
- await safeTry(() => {
355
- throw new Error("Critical failure");
356
- }, { throw: true }).catch((error: Error) => println(error.message));
357
- // "Critical failure": the call rejects instead of returning Err.
358
- ```
120
+ import { Err, Ok, safeTry, type Result, println } from "slang-ts";
359
121
 
360
- ### Pipe
122
+ type Role = "admin" | "member";
361
123
 
362
- Sequential function composition where each function receives a `Result` and returns a `Result`. Accepts plain values, Option, Result, or Atom as initial input.
363
-
364
- ```ts
365
- import { pipe, Ok, Err, option, type Result } from "slang-ts";
366
-
367
- // Create pipeline functions
368
- const add = (x: number) => (res: Result<number, string>) =>
369
- res.isOk ? Ok(res.value + x) : res;
370
-
371
- const multiply = (x: number) => (res: Result<number, string>) =>
372
- res.isOk ? Ok(res.value * x) : res;
373
-
374
- // Basic usage
375
- const result = await pipe(5, add(3), multiply(2)).run();
376
- println("Result:", result.value); // 16
377
-
378
- // With Options as initial value
379
- const fromOption = await pipe(option(10), add(5)).run();
380
- println("From option:", fromOption.value); // 15
124
+ function parseRole(value: string): Result<Role, string> {
125
+ return value === "admin" || value === "member"
126
+ ? Ok(value)
127
+ : Err("Unknown role");
128
+ }
381
129
 
382
- // With callbacks and error handling
383
- const result = await pipe(5, add(3), multiply(2)).run({
384
- onEach: ({ currentFn, prevResult }) => {
385
- println("Executed:", currentFn);
386
- },
387
- onSuccess: (value) => println("Done:", value),
388
- onError: (err) => println("Failed:", err.message),
389
- allowErrors: false, // stops pipeline on first Err
130
+ const role = await safeTry(() => {
131
+ const saved = localStorage.getItem("role");
132
+ if (saved === null) throw new Error("No role saved");
133
+ return parseRole(saved);
390
134
  });
391
- ```
392
-
393
- ### To
394
-
395
- Converts between Slang types.
396
-
397
- ```ts
398
- const statusAtom = atom("active").to("option");
399
- println("Option:", statusAtom); // Some("active")
400
-
401
- const stateOption = option("ready").to("atom");
402
- println("Atom:", stateOption.description); // "ready"
403
-
404
- const errResult = option(null).to("result");
405
- println("Result:", errResult.type); // "Err"
406
- ```
407
-
408
- ### Atom
409
-
410
- 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.
411
-
412
- ```ts
413
- import { atom } from "slang-ts";
414
-
415
- const userAtom = atom("kizz");
416
- const user2Atom = atom("kizz");
417
-
418
- println(userAtom === atom("kizz")); // false - non interned
419
- println(userAtom.description); // "kizz"
420
135
 
421
- if (userAtom === user2Atom) {
422
- println("all the same");
136
+ if (role.isOk) {
137
+ println("Role:", role.value);
423
138
  } else {
424
- println("not the same"); // This prints!
139
+ println("Could not load role:", role.error);
425
140
  }
426
141
  ```
427
142
 
428
- ### Zip
429
-
430
- Combines multiple collections element-wise into tuples.
431
-
432
- ```ts
433
- import { zip } from "slang-ts";
434
-
435
- // Zip arrays
436
- const arr1 = [1, 2, 3];
437
- const arr2 = [4, 5, 6];
438
- const arr3 = [7, 8, 9];
439
- println(zip([arr1, arr2, arr3]));
440
- // [[1,4,7],[2,5,8],[3,6,9]]
441
-
442
- // Zip with fillValue
443
- println(zip([arr1, [10, 20]], { fillValue: 0 }));
444
- // [[1,10],[2,20],[3,0]]
445
-
446
- // Zip Sets with includeValues=true
447
- const s1 = new Set([10, 20, 30]);
448
- const s2 = new Set([100, 200, 300]);
449
- println(zip([s1, s2], { includeValues: true }));
450
- // [[10,100],[20,200],[30,300]]
451
-
452
- // Zip objects with includeValues=true
453
- const o1 = { a: 1, b: 2, c: 3 };
454
- const o2 = { x: 100, y: 200, z: 300 };
455
- println(zip([o1, o2], { includeValues: true }));
456
- // [[1,100],[2,200],[3,300]]
457
- ```
458
-
459
- ### ZipWith
460
-
461
- Combines collections and applies transform function to each tuple.
143
+ Without it, you'd have to catch the throw and still branch on the value:
462
144
 
463
145
  ```ts
464
- import { zipWith } from "slang-ts";
146
+ type Role = "admin" | "member";
465
147
 
466
- const arr1 = [1, 2, 3];
467
- const arr2 = [4, 5, 6];
468
- const arr3 = [7, 8, 9];
469
-
470
- println(zipWith([arr1, arr2, arr3], (t) => t.reduce((sum, x) => sum + x, 0)));
471
- // [12, 15, 18]
472
- ```
473
-
474
- ### Unzip
475
-
476
- Reverses zip operation, separating tuples back into arrays.
477
-
478
- ```ts
479
- import { unzip } from "slang-ts";
480
-
481
- const arr1 = [1, 2, 3];
482
- const arr2 = [4, 5, 6];
483
-
484
- const zipped = zip([arr1, arr2]);
485
- println(unzip(zipped));
486
- // [[1, 2, 3], [4, 5, 6]]
487
- ```
488
-
489
- ### Panic
490
-
491
- Throws an error immediately. Use for unrecoverable failures.
492
-
493
- ```ts
494
- import { panic } from "slang-ts";
495
-
496
- function processUser(user: User | null) {
497
- if (!user) panic("User cannot be null");
498
- return user.name;
148
+ function loadRole(): Role {
149
+ const saved = localStorage.getItem("role");
150
+ if (saved === null) throw new Error("No role saved");
151
+ if (saved !== "admin" && saved !== "member") throw new Error("Unknown role");
152
+ return saved;
499
153
  }
500
154
 
501
- // Guard clause pattern
502
- const config = loadConfig();
503
- if (!config.apiKey) panic("API key required");
504
- ```
505
-
506
- ### println
507
-
508
- 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.
509
-
510
- ```ts
511
- import { println, setEnvironment } from "slang-ts";
512
-
513
- setEnvironment("development"); // "dev" works too (default)
514
- println("name:", name);
515
- println("multiple", "args", "work", { too: true });
516
-
517
- setEnvironment("production"); // "prod" works too
518
- println("not printed in prod");
519
-
520
- // Route println through an existing logger instead, even in prod
521
- import { appLogger } from "./logger";
522
- setEnvironment({ printFn: (line) => appLogger.info(line.trimEnd()) });
155
+ try {
156
+ const role = loadRole();
157
+ console.log("Role:", role);
158
+ } catch (error) {
159
+ console.log(
160
+ "Could not load role:",
161
+ error instanceof Error ? error.message : "Unknown error",
162
+ );
163
+ }
523
164
  ```
524
165
 
525
- ### Signal
166
+ Full detail: [SafeTry][safe-try] and [Result][result].
526
167
 
527
- 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.
528
-
529
- ```ts
530
- import { createSignal } from "slang-ts";
531
-
532
- const ready = createSignal();
533
-
534
- ready.addEventListener("abort", () => println("ready!")); // fires when the trigger goes off
535
- ready.fire(); // flip it
536
- ```
168
+ ### Call an API with a timeout and retries: SuperPromise
537
169
 
538
- The useful part: gate work on a signal, or publish events when a lifecycle completes.
170
+ Your function gets an `AbortSignal`. `timeout` aborts an attempt, `retry` runs it again after an
171
+ ordinary failure, and you get back `Ok` or `Err` — so no `try`/`catch` around the await.
539
172
 
540
173
  ```ts
541
- const task = superPromise(loadUser, { defer: ready }); // starts only once ready fires
174
+ import { superPromise, println } from "slang-ts";
542
175
 
543
- channel.when({
544
- signal: task.done, // task finished, success or not
545
- topic: "user.loaded",
546
- data: { id: userId },
547
- });
548
- ```
549
-
550
- ### SuperPromise
551
-
552
- 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 }`.
553
-
554
- ```ts
555
- import { superPromise } from "slang-ts";
176
+ const id = "123";
556
177
 
557
- // A fetch that gives up after 5 seconds and tries up to 3 more times
558
- const result = await superPromise(
559
- (signal) => fetch("/api/user", { signal }),
560
- { timeout: 5000, retry: 3 }
178
+ const user = await superPromise(
179
+ async (signal) => {
180
+ const response = await fetch(`/api/users/${id}`, { signal });
181
+ if (!response.ok) throw new Error("User not found");
182
+ return (await response.json()) as { name: string };
183
+ },
184
+ { timeout: 5000, retry: 2 },
561
185
  );
562
186
 
563
- if (result.isOk) {
564
- println("User:", result.value);
187
+ if (user.isOk) {
188
+ println("User:", user.value.name);
565
189
  } else {
566
- println("Failed:", result.error);
190
+ println("Could not load user:", user.error);
567
191
  }
568
192
  ```
569
193
 
570
- No `await` handy? Chain with `.then`; the callback receives the same `Result`:
571
-
572
- ```ts
573
- superPromise(loadUser, { timeout: 5000 }).then((result) => {
574
- if (result.isOk) {
575
- println("User:", result.value);
576
- } else {
577
- println("Failed:", result.error);
578
- }
579
- });
580
- ```
581
-
582
- 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:
583
-
584
- ```ts
585
- const config = superPromise(loadConfig);
586
- const user = superPromise(loadUser, { defer: config.done });
194
+ Full detail: [SuperPromise][super-promise].
587
195
 
588
- const result = await user; // config runs first, then user
589
- ```
196
+ ### Page a list and shape records: list + struct
590
197
 
591
- Cancel from outside, or keep retrying until you call it off:
198
+ `list` keeps bounds strict: a bad range fails instead of quietly giving you less data. `struct`
199
+ reads and shapes records without spreading fields you didn't choose.
592
200
 
593
201
  ```ts
594
- task.abort(); // cancel; you get Err("Aborted")
595
- superPromise(fetchData, {
596
- retryUntil: { signal: stopSignal, delay: 2000 }, // keep trying until stopSignal fires
597
- });
598
- ```
202
+ import { list, struct, println } from "slang-ts";
599
203
 
600
- 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.
601
-
602
- Also handy:
603
-
604
- ```ts
605
- const manual = superPromise<number>(); // no executor; settle it yourself
606
- manual.resolve(100); // or manual.reject(error)
607
-
608
- await superPromise(loadUser).tap((user) => println(user.name)); // observe without changing
609
- ```
610
-
611
- ### SuperRunner
612
-
613
- 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.
614
-
615
- ```ts
616
- import { superRunner, superPromise as sp } from "slang-ts";
617
-
618
- // In order: steps that depend on each other; stops at the first failure
619
- const auth = superRunner({
620
- type: "sequence",
621
- runners: [sp(authenticate), sp(loadProfile), sp(loadDashboard)],
622
- });
623
- const ordered = await auth.start(); // [authResult, profileResult, dashboardResult]
624
-
625
- // In parallel: all kicked off at once; result keyed by name
626
- const parallel = superRunner({
627
- type: "all",
628
- runners: {
629
- user: sp(getUser),
630
- posts: sp(getPosts),
631
- settings: sp(getSettings),
632
- },
633
- });
634
- const combined = await parallel.start(); // { user, posts, settings }
635
-
636
- // First to finish: take whichever source responds first, cancel the rest
637
- const first = superRunner({
638
- type: "race",
639
- count: 1,
640
- runners: {
641
- primary: sp(fetchPrimary),
642
- backup: sp(fetchBackup),
643
- cache: sp(fetchCache),
644
- },
645
- });
646
- const winner = await first.start(); // { cache: ... } or whichever won
647
- ```
648
-
649
- Cancel everything with one call:
650
-
651
- ```ts
652
- workflow.abort(); // cancels all active children
653
- ```
654
-
655
- ### Channel
656
-
657
- Channel lets parts of your app that don't know each other talk: one side publishes, another listens, no wiring between them.
658
-
659
- ```ts
660
- import { createChannel } from "slang-ts";
661
-
662
- const channel = createChannel();
663
-
664
- const unsubscribe = channel.subscribe({ topic: "user.created" }, (event) => {
665
- println("Created:", event.data);
666
- });
667
-
668
- channel.send({ topic: "user.created", data: user });
669
- unsubscribe(); // stop listening
670
- ```
671
-
672
- Publish when a trigger goes off, a task finishing, anything at all:
204
+ type User = {
205
+ id: string;
206
+ name: string;
207
+ avatar?: string;
208
+ email: string;
209
+ };
210
+
211
+ const users: User[] = await getUsers(); // your data source
212
+
213
+ const page = 2;
214
+ const size = 20;
215
+ const total = list(users).count().else(0);
216
+ const start = Math.min(page * size, total);
217
+ const paged = list(users)
218
+ .range(start, Math.min(start + size, total))
219
+ .else([]);
220
+
221
+ const cards = paged.map((user) =>
222
+ struct(user).pick("id", "name", "avatar").else(user),
223
+ );
673
224
 
674
- ```ts
675
- channel.when({
676
- signal: workflow.done,
677
- topic: "workflow.completed",
678
- data: { workflowId },
679
- });
225
+ println(cards);
680
226
  ```
681
227
 
682
- ## Code Samples
228
+ `list` won't clamp a range for you, so the example clamps the window and lets the last page be
229
+ short. `struct` then keeps only the fields the card needs.
683
230
 
684
- See [example.ts](https://github.com/Hussseinkizz/slang/blob/main/example.ts) for usage of currently implemented methods.
231
+ Full detail: [list][list] and [struct][struct].
685
232
 
686
233
  ## Development
687
234
 
688
- Install project dependencies with Bun, then export:
235
+ Install with Bun, then run the full check:
689
236
 
690
237
  ```bash
691
238
  bun install
692
239
  bun run export
693
240
  ```
694
241
 
695
- `export` runs the tests, checks types with TypeScript 7.0.2, then runs `bun run build`, which uses tsdown 0.23.1 to produce minified ESM at `dist/index.js` and bundled types at `dist/index.d.ts`. `npm publish` runs `export` first through `prepublishOnly`, so a release never ships a stale `dist/`. Package imports keep the same entry points. tsdown currently marks its TypeScript 7 integration as experimental; the project build passes with this setup.
696
-
697
- For building, tsdown requires Node.js `^22.18.0 || ^24.11.0 || >=26.0.0`. This is a build-tool requirement, not a new runtime requirement for Slang consumers.
242
+ `export` runs the tests, checks types, and builds minified ESM with bundled declarations into
243
+ `dist/`. `npm publish` runs it through `prepublishOnly`, so you never ship stale output. Tests live
244
+ in [tests](https://github.com/Hussseinkizz/slang/tree/main/tests).
698
245
 
699
- Packaged output passes strict consumer checks with TypeScript 5.9.3 and 7.0.2 under both NodeNext and Bundler resolution, including declaration checking. See [migration context](context.md) for verification details.
246
+ Building needs Node.js `^22.18.0 || ^24.11.0 || >=26.0.0`. That's for the build tool, not for
247
+ projects using Slang.
700
248
 
701
249
  ## Contributing
702
250
 
703
- Contributions are welcome. I know there are a lot of cool things out there we can bring in.
251
+ Got a utility from another language that belongs here? Open an issue or a pull request.
252
+ Contributions are welcome.
253
+
254
+ ## Requirements
255
+
256
+ - ESM only. You'll use `import`; `require` isn't supported.
257
+ - Node.js 20+ or a modern browser.
258
+ - TypeScript 5.9.x or 7.0.x if you want the bundled types. Plain JavaScript works too.
259
+ - Zero runtime dependencies.
260
+
261
+ ## License
262
+
263
+ MIT License. See [LICENSE][license].
264
+
265
+ [option]: https://github.com/Hussseinkizz/slang/blob/main/docs/option.md
266
+ [result]: https://github.com/Hussseinkizz/slang/blob/main/docs/result.md
267
+ [safe-try]: https://github.com/Hussseinkizz/slang/blob/main/docs/safe-try.md
268
+ [match]: https://github.com/Hussseinkizz/slang/blob/main/docs/match.md
269
+ [options]: https://github.com/Hussseinkizz/slang/blob/main/docs/match.md#options
270
+ [pipe]: https://github.com/Hussseinkizz/slang/blob/main/docs/pipe.md
271
+ [atom]: https://github.com/Hussseinkizz/slang/blob/main/docs/atom.md
272
+ [to]: https://github.com/Hussseinkizz/slang/blob/main/docs/to.md
273
+ [zip]: https://github.com/Hussseinkizz/slang/blob/main/docs/zip.md
274
+ [list]: https://github.com/Hussseinkizz/slang/blob/main/docs/list.md
275
+ [struct]: https://github.com/Hussseinkizz/slang/blob/main/docs/struct.md
276
+ [sets]: https://github.com/Hussseinkizz/slang/blob/main/docs/sets.md
277
+ [iterate]: https://github.com/Hussseinkizz/slang/blob/main/docs/iterate.md
278
+ [memoized]: https://github.com/Hussseinkizz/slang/blob/main/docs/memoized.md
279
+ [super-promise]: https://github.com/Hussseinkizz/slang/blob/main/docs/super-promise.md
280
+ [super-runner]: https://github.com/Hussseinkizz/slang/blob/main/docs/super-runner.md
281
+ [task-group]: https://github.com/Hussseinkizz/slang/blob/main/docs/task-group.md
282
+ [signal]: https://github.com/Hussseinkizz/slang/blob/main/docs/signal.md
283
+ [channel]: https://github.com/Hussseinkizz/slang/blob/main/docs/channel.md
284
+ [panic]: https://github.com/Hussseinkizz/slang/blob/main/docs/panic.md
285
+ [logging]: https://github.com/Hussseinkizz/slang/blob/main/docs/logging.md
286
+ [stepper]: https://github.com/Hussseinkizz/slang/blob/main/docs/stepper.md
287
+ [docs-index]: https://github.com/Hussseinkizz/slang/blob/main/docs/README.md
288
+ [changelog]: https://github.com/Hussseinkizz/slang/blob/main/CHANGELOG.md
289
+ [license]: https://github.com/Hussseinkizz/slang/blob/main/LICENSE