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/CHANGELOG.md +53 -0
- package/README.md +189 -602
- package/dist/index.d.ts +308 -25
- package/dist/index.js +2 -2
- package/package.json +17 -5
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# slang-ts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/slang-ts)
|
|
4
|
+
[][license]
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Quick start
|
|
100
22
|
|
|
101
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
`
|
|
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
|
-
|
|
44
|
+
## Imports
|
|
164
45
|
|
|
165
|
-
|
|
46
|
+
Import only what you use:
|
|
166
47
|
|
|
167
48
|
```ts
|
|
168
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
### SafeTry
|
|
52
|
+
## Choose by problem
|
|
303
53
|
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
if (failed.isErr) println(failed.error); // "Not found"
|
|
73
|
+
### Error handling and flow
|
|
316
74
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
if (missing.isErr) println(missing.error);
|
|
82
|
+
### Composition and conversion
|
|
323
83
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
88
|
+
### Collections and data
|
|
331
89
|
|
|
332
|
-
|
|
333
|
-
|
|
90
|
+
- [list][list]
|
|
91
|
+
- [struct][struct]
|
|
92
|
+
- [sets][sets]
|
|
93
|
+
- [Ok.iterate][iterate]
|
|
94
|
+
- [memoized][memoized]
|
|
334
95
|
|
|
335
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
98
|
+
- [SuperPromise][super-promise]
|
|
99
|
+
- [SuperRunner][super-runner]
|
|
100
|
+
- [taskGroup][task-group]
|
|
101
|
+
- [Signal][signal]
|
|
102
|
+
- [Channel][channel]
|
|
344
103
|
|
|
345
|
-
|
|
104
|
+
### Small utilities
|
|
346
105
|
|
|
347
|
-
|
|
348
|
-
|
|
106
|
+
- [Panic][panic]
|
|
107
|
+
- [println][logging] (with `setEnvironment`)
|
|
108
|
+
- [createStepper][stepper]
|
|
349
109
|
|
|
350
|
-
|
|
351
|
-
if (returned.isErr) println(returned.error); // "Not found"
|
|
110
|
+
Full reference: [docs index][docs-index]. Feature list: [CHANGELOG.md][changelog].
|
|
352
111
|
|
|
353
|
-
|
|
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
|
-
###
|
|
114
|
+
### Load a value that can fail: SafeTry + Result
|
|
360
115
|
|
|
361
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
|
139
|
+
println("Could not load role:", role.error);
|
|
424
140
|
}
|
|
425
141
|
```
|
|
426
142
|
|
|
427
|
-
|
|
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
|
-
|
|
146
|
+
type Role = "admin" | "member";
|
|
494
147
|
|
|
495
|
-
function
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
534
|
-
ready.fire(); // flip it
|
|
535
|
-
```
|
|
168
|
+
### Call an API with a timeout and retries: SuperPromise
|
|
536
169
|
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
const id = "123";
|
|
550
177
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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 (
|
|
563
|
-
println("User:",
|
|
187
|
+
if (user.isOk) {
|
|
188
|
+
println("User:", user.value.name);
|
|
564
189
|
} else {
|
|
565
|
-
println("
|
|
190
|
+
println("Could not load user:", user.error);
|
|
566
191
|
}
|
|
567
192
|
```
|
|
568
193
|
|
|
569
|
-
|
|
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
|
-
###
|
|
196
|
+
### Page a list and shape records: list + struct
|
|
611
197
|
|
|
612
|
-
|
|
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 {
|
|
202
|
+
import { list, struct, println } from "slang-ts";
|
|
616
203
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
674
|
-
channel.when({
|
|
675
|
-
signal: workflow.done,
|
|
676
|
-
topic: "workflow.completed",
|
|
677
|
-
data: { workflowId },
|
|
678
|
-
});
|
|
225
|
+
println(cards);
|
|
679
226
|
```
|
|
680
227
|
|
|
681
|
-
|
|
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
|
-
|
|
231
|
+
Full detail: [list][list] and [struct][struct].
|
|
684
232
|
|
|
685
233
|
## Development
|
|
686
234
|
|
|
687
|
-
Install
|
|
235
|
+
Install with Bun, then run the full check:
|
|
688
236
|
|
|
689
237
|
```bash
|
|
690
238
|
bun install
|
|
691
|
-
bun run
|
|
239
|
+
bun run export
|
|
692
240
|
```
|
|
693
241
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|