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