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/CHANGELOG.md +53 -0
- package/README.md +188 -602
- package/dist/index.d.ts +303 -25
- package/dist/index.js +2 -2
- package/package.json +15 -4
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,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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Quick start
|
|
79
22
|
|
|
80
|
-
|
|
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 {
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
## Imports
|
|
249
45
|
|
|
250
|
-
|
|
46
|
+
Import only what you use:
|
|
251
47
|
|
|
252
48
|
```ts
|
|
253
|
-
import {
|
|
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
|
-
|
|
52
|
+
## Choose by problem
|
|
275
53
|
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
### Error handling and flow
|
|
302
74
|
|
|
303
|
-
|
|
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
|
-
|
|
82
|
+
### Composition and conversion
|
|
306
83
|
|
|
307
|
-
|
|
308
|
-
|
|
84
|
+
- [Atom][atom]
|
|
85
|
+
- [To][to] (`.to()` converters)
|
|
86
|
+
- [Zip][zip] (zip, zipWith, unzip)
|
|
309
87
|
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
90
|
+
- [list][list]
|
|
91
|
+
- [struct][struct]
|
|
92
|
+
- [sets][sets]
|
|
93
|
+
- [Ok.iterate][iterate]
|
|
94
|
+
- [memoized][memoized]
|
|
317
95
|
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
98
|
+
- [SuperPromise][super-promise]
|
|
99
|
+
- [SuperRunner][super-runner]
|
|
100
|
+
- [taskGroup][task-group]
|
|
101
|
+
- [Signal][signal]
|
|
102
|
+
- [Channel][channel]
|
|
324
103
|
|
|
325
|
-
|
|
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
|
-
|
|
106
|
+
- [Panic][panic]
|
|
107
|
+
- [println][logging] (with `setEnvironment`)
|
|
108
|
+
- [createStepper][stepper]
|
|
332
109
|
|
|
333
|
-
|
|
334
|
-
import { safeTry, Ok, println } from "slang-ts";
|
|
110
|
+
Full reference: [docs index][docs-index]. Feature list: [CHANGELOG.md][changelog].
|
|
335
111
|
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
122
|
+
type Role = "admin" | "member";
|
|
361
123
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
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 (
|
|
422
|
-
println("
|
|
136
|
+
if (role.isOk) {
|
|
137
|
+
println("Role:", role.value);
|
|
423
138
|
} else {
|
|
424
|
-
println("not
|
|
139
|
+
println("Could not load role:", role.error);
|
|
425
140
|
}
|
|
426
141
|
```
|
|
427
142
|
|
|
428
|
-
|
|
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
|
-
|
|
146
|
+
type Role = "admin" | "member";
|
|
465
147
|
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
166
|
+
Full detail: [SafeTry][safe-try] and [Result][result].
|
|
526
167
|
|
|
527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
import { superPromise, println } from "slang-ts";
|
|
542
175
|
|
|
543
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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 (
|
|
564
|
-
println("User:",
|
|
187
|
+
if (user.isOk) {
|
|
188
|
+
println("User:", user.value.name);
|
|
565
189
|
} else {
|
|
566
|
-
println("
|
|
190
|
+
println("Could not load user:", user.error);
|
|
567
191
|
}
|
|
568
192
|
```
|
|
569
193
|
|
|
570
|
-
|
|
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
|
-
|
|
589
|
-
```
|
|
196
|
+
### Page a list and shape records: list + struct
|
|
590
197
|
|
|
591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
675
|
-
channel.when({
|
|
676
|
-
signal: workflow.done,
|
|
677
|
-
topic: "workflow.completed",
|
|
678
|
-
data: { workflowId },
|
|
679
|
-
});
|
|
225
|
+
println(cards);
|
|
680
226
|
```
|
|
681
227
|
|
|
682
|
-
|
|
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
|
-
|
|
231
|
+
Full detail: [list][list] and [struct][struct].
|
|
685
232
|
|
|
686
233
|
## Development
|
|
687
234
|
|
|
688
|
-
Install
|
|
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
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|