slang-ts 0.0.7 → 0.0.9
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 +389 -142
- package/dist/index.d.ts +656 -3
- package/dist/index.js +2 -1
- package/package.json +34 -16
- package/dist/src/atom.d.ts +0 -51
- package/dist/src/index.d.ts +0 -16
- package/dist/src/internals.d.ts +0 -26
- package/dist/src/match.d.ts +0 -58
- package/dist/src/option.d.ts +0 -97
- package/dist/src/panic.d.ts +0 -11
- package/dist/src/pipe.d.ts +0 -47
- package/dist/src/println.d.ts +0 -7
- package/dist/src/result.d.ts +0 -68
- package/dist/src/safe-try.d.ts +0 -28
- package/dist/src/to.d.ts +0 -15
- package/dist/src/zip.d.ts +0 -56
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# slang-ts
|
|
2
2
|
|
|
3
3
|
Functional programming library for TypeScript.
|
|
4
4
|
|
|
5
|
-
A collection of
|
|
5
|
+
A collection of utilities and other cool programming stuff brought over from languages like Rust.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -12,31 +12,49 @@ npm i slang-ts
|
|
|
12
12
|
|
|
13
13
|
## Implemented Utilities
|
|
14
14
|
|
|
15
|
+
- [x] Option (Some, None)
|
|
16
|
+
- [x] isFalsy (Option truthiness check)
|
|
15
17
|
- [x] Result (Ok, Err)
|
|
16
|
-
- [x] Maybe (Option)
|
|
17
|
-
- [x] andThen
|
|
18
|
-
- [x] Atom
|
|
19
18
|
- [x] Expect
|
|
20
19
|
- [x] Unwrap (on Option)
|
|
21
20
|
- [x] Else (on unwrap)
|
|
22
|
-
- [x]
|
|
23
|
-
- [x] Zip, Unzip, zipWith
|
|
24
|
-
- [x] SafeTry
|
|
21
|
+
- [x] andThen
|
|
25
22
|
- [x] Match
|
|
26
23
|
- [x] MatchAll
|
|
24
|
+
- [x] Options (normalize mixed values)
|
|
25
|
+
- [x] SafeTry
|
|
27
26
|
- [x] Pipe
|
|
28
27
|
- [x] To (converters, e.g. `userAtom.to('option')`)
|
|
28
|
+
- [x] Atom
|
|
29
|
+
- [x] Zip, Unzip, zipWith
|
|
30
|
+
- [x] Panic
|
|
31
|
+
- [x] println (environment-aware logging)
|
|
32
|
+
- [x] setEnvironment (logging mode, custom logger)
|
|
33
|
+
- [x] Signal (lifecycle trigger)
|
|
34
|
+
- [x] SuperPromise (abort, reject, resolve, defer)
|
|
35
|
+
- [x] SuperRunner (sequence, all, race)
|
|
36
|
+
- [x] Channel (send, subscribe, when)
|
|
37
|
+
- [x] scheduleMicrotask (internal scheduling helper)
|
|
29
38
|
|
|
30
|
-
All utilities fully tested
|
|
39
|
+
All utilities fully tested. See [tests](https://github.com/Hussseinkizz/slang/tree/main/tests)
|
|
31
40
|
|
|
32
|
-
##
|
|
41
|
+
## New in v0.0.9
|
|
33
42
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
43
|
+
- **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.
|
|
44
|
+
- **Package types**: bundled declarations work with strict NodeNext consumers, so importing the package no longer exposes invalid declaration paths.
|
|
45
|
+
- **Build tooling**: tsdown and TypeScript 7 build JavaScript and bundled types together.
|
|
46
|
+
|
|
47
|
+
## New in v0.0.8
|
|
48
|
+
|
|
49
|
+
- **Signal**: a simple on/off trigger that fires once and stays fired; use it to make something start only after something else finishes.
|
|
50
|
+
- **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.
|
|
51
|
+
- **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.
|
|
52
|
+
- **SuperRunner**: `Promise.all` and `Promise.race` grown up: run several SuperPromises in order, in parallel, or take first to finish.
|
|
53
|
+
- **Channel**: a simple event bus between parts of your app that don't need to know each other; use it to broadcast things like "user created" or "workflow completed" without wiring components together.
|
|
36
54
|
|
|
37
55
|
## How It Works
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
Import utilities individually or together:
|
|
40
58
|
|
|
41
59
|
```ts
|
|
42
60
|
// Individual imports
|
|
@@ -52,18 +70,6 @@ import * as slang from "slang-ts";
|
|
|
52
70
|
slang.println("Hello world!");
|
|
53
71
|
```
|
|
54
72
|
|
|
55
|
-
### println
|
|
56
|
-
|
|
57
|
-
Well there's nothing special to slang's println utility, its just who wants console.log, its not fun at all, so we instead println, clean and classic, but latter it can be made environment aware so it doesn't print in prod, but for now its just sugar for console.log.
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { println } from "slang-ts";
|
|
61
|
-
|
|
62
|
-
const name = "kizz";
|
|
63
|
-
println("name:", name);
|
|
64
|
-
println("multiple", "args", "work", { too: true });
|
|
65
|
-
```
|
|
66
|
-
|
|
67
73
|
### Option
|
|
68
74
|
|
|
69
75
|
Wraps values that may or may not be present. Returns `Some<T>` for truthy values, `None` for null, undefined, empty strings, NaN, or Infinity. Note that `0` and `false` are truthy as these are usually intentional.
|
|
@@ -130,24 +136,76 @@ if (user.isOk) {
|
|
|
130
136
|
}
|
|
131
137
|
```
|
|
132
138
|
|
|
133
|
-
###
|
|
139
|
+
### Expect
|
|
134
140
|
|
|
135
|
-
|
|
141
|
+
Unwraps values or throws with custom message. Use when failure is unrecoverable.
|
|
136
142
|
|
|
137
143
|
```ts
|
|
138
|
-
|
|
144
|
+
const personAge = option(25).expect("a person must have age!");
|
|
145
|
+
println("person age", personAge); // 25
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
const
|
|
147
|
+
// This would throw!
|
|
148
|
+
// const personAge2 = option("").expect("a person must have age!");
|
|
149
|
+
```
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
println(userAtom.description); // "kizz"
|
|
151
|
+
### Unwrap/Else
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
Chainable unwrapping with mandatory fallback. Must call `.else()` or throws.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const port = option(process.env.PORT).unwrap().else(3000);
|
|
157
|
+
println("Using port:", port);
|
|
158
|
+
|
|
159
|
+
// Function fallbacks
|
|
160
|
+
const retries = option(null).unwrap().else(() => 5);
|
|
161
|
+
println("Retries:", retries);
|
|
162
|
+
|
|
163
|
+
// This throws! No .else() chained
|
|
164
|
+
// const nothing = option(null).unwrap();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### andThen
|
|
168
|
+
|
|
169
|
+
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`.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Option - transforms Some, skips None
|
|
173
|
+
option(5).andThen(x => x * 2); // Some(10)
|
|
174
|
+
option(null).andThen(x => x * 2); // None (skipped)
|
|
175
|
+
option(5).andThen(() => undefined); // Some(5) - original
|
|
176
|
+
|
|
177
|
+
// Result - transforms Ok, skips Err
|
|
178
|
+
Ok(10).andThen(x => x + 5); // Ok(15)
|
|
179
|
+
Err("fail").andThen(x => x + 5); // Err("fail") (skipped)
|
|
180
|
+
|
|
181
|
+
// Atom - transforms description (sync only)
|
|
182
|
+
atom("hello").andThen(s => s.toUpperCase()); // Atom("HELLO")
|
|
183
|
+
|
|
184
|
+
// Chained andThen - multiple transformations
|
|
185
|
+
option(5)
|
|
186
|
+
.andThen(x => x + 1)
|
|
187
|
+
.andThen(x => x * 2)
|
|
188
|
+
.andThen(x => x.toString()); // Some("12")
|
|
189
|
+
|
|
190
|
+
Ok(10)
|
|
191
|
+
.andThen(x => x * 2)
|
|
192
|
+
.andThen(x => x + 5)
|
|
193
|
+
.andThen(x => ({ value: x })); // Ok({ value: 25 })
|
|
194
|
+
|
|
195
|
+
atom("hello")
|
|
196
|
+
.andThen(s => s.toUpperCase())
|
|
197
|
+
.andThen(s => s + "!"); // Atom("HELLO!")
|
|
198
|
+
|
|
199
|
+
// Async support for Option and Result
|
|
200
|
+
const data = await option(5).andThen(async x => await fetchData(x));
|
|
201
|
+
|
|
202
|
+
// Error handling
|
|
203
|
+
option(5).andThen(() => { throw "oops" }); // None (caught)
|
|
204
|
+
Ok(5).andThen(() => { throw "oops" }); // Err("oops")
|
|
205
|
+
atom("x").andThen(() => { throw "oops" }); // Panics!
|
|
206
|
+
|
|
207
|
+
// Type transformation
|
|
208
|
+
option(42).andThen(x => x.toString()); // Some("42")
|
|
151
209
|
```
|
|
152
210
|
|
|
153
211
|
### Match
|
|
@@ -200,32 +258,123 @@ const label = matchAll(isActive, {
|
|
|
200
258
|
println(label); // "Active"
|
|
201
259
|
```
|
|
202
260
|
|
|
203
|
-
###
|
|
261
|
+
### Options
|
|
204
262
|
|
|
205
|
-
|
|
263
|
+
Normalizes heterogeneous values (raw values, `Option`s, and `Result`s) into a single `Result`. Returns the first usable value as `Ok`, or the provided `Err` fallback if none is usable. A value is usable when its unwrapped inner value is truthy; `None`, `Err`, null, undefined, empty strings, NaN, and Infinity are skipped (note `0` and `false` are usable, matching `option()` semantics).
|
|
206
264
|
|
|
207
265
|
```ts
|
|
208
|
-
|
|
209
|
-
|
|
266
|
+
import { Options, option, Ok, Err } from "slang-ts";
|
|
267
|
+
|
|
268
|
+
// First usable value wins
|
|
269
|
+
let a = null;
|
|
270
|
+
let b = option(undefined);
|
|
271
|
+
let c = Ok(true);
|
|
272
|
+
let d = Err("nope!");
|
|
273
|
+
|
|
274
|
+
const something = Options([a, b, c, d], Err("No value!"));
|
|
275
|
+
// Ok(true) - c is usable
|
|
276
|
+
|
|
277
|
+
// Nothing usable -> fallback
|
|
278
|
+
let e = option(null);
|
|
279
|
+
const nothing = Options([a, b, e, d], Err("No value!"));
|
|
280
|
+
// Err("No value!")
|
|
281
|
+
|
|
282
|
+
// Raw (non-wrapped) values work too, just like a single option():
|
|
283
|
+
Options([null, undefined, "hello"]); // Ok("hello") - raw truthy wins
|
|
284
|
+
Options(["", 0, "zero is skipped, 0 used"]); // Ok("...") - skips "" first
|
|
285
|
+
Options([null, undefined, ""]); // Err("No value!") - all raw falsy
|
|
286
|
+
```
|
|
210
287
|
|
|
211
|
-
|
|
212
|
-
|
|
288
|
+
Useful when values come from different functions with inconsistent return types and all you care about is whether the end result is usable.
|
|
289
|
+
|
|
290
|
+
### SafeTry
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { safeTry, Ok, Err, option, atom, println, type Result } from "slang-ts";
|
|
296
|
+
|
|
297
|
+
// Inferred type is Promise<Result<number, string>>, not nested Results.
|
|
298
|
+
const pending: Promise<Result<number, string>> = safeTry(() => Ok(Ok(42)));
|
|
299
|
+
const nested = await pending; // Ok(42)
|
|
300
|
+
if (nested.isOk) println(nested.value); // 42
|
|
301
|
+
|
|
302
|
+
const failed = await safeTry(() => Ok(Ok(Err("Not found")))); // Err("Not found")
|
|
303
|
+
if (failed.isErr) println(failed.error); // "Not found"
|
|
304
|
+
|
|
305
|
+
const mixed = await safeTry(() => Ok(option(Ok(atom("ready")))));
|
|
306
|
+
// Ok("ready"); inferred success value is the literal "ready".
|
|
307
|
+
if (mixed.isOk) println(mixed.value); // "ready"
|
|
308
|
+
|
|
309
|
+
const missing = await safeTry(() => Ok(option(null))); // Err("Option was None")
|
|
310
|
+
if (missing.isErr) println(missing.error);
|
|
311
|
+
|
|
312
|
+
const caught = await safeTry(async () => {
|
|
313
|
+
throw new Error("Could not load profile");
|
|
314
|
+
}); // Err("Could not load profile")
|
|
315
|
+
if (caught.isErr) println(caught.error);
|
|
213
316
|
```
|
|
214
317
|
|
|
215
|
-
|
|
318
|
+
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.
|
|
216
319
|
|
|
217
|
-
|
|
320
|
+
```ts
|
|
321
|
+
import { safeTry, Ok, println } from "slang-ts";
|
|
322
|
+
|
|
323
|
+
const payload = { result: Ok(42), items: [Ok(7)] };
|
|
324
|
+
const plain = await safeTry(() => payload); // Ok(payload), same object
|
|
325
|
+
if (plain.isOk) println(plain.value === payload); // true
|
|
326
|
+
|
|
327
|
+
const innerPromise = Promise.resolve(42);
|
|
328
|
+
const wrapped = await safeTry(() => Ok(innerPromise));
|
|
329
|
+
// Ok(innerPromise), success value stays Promise<number>.
|
|
330
|
+
if (wrapped.isOk) println(wrapped.value === innerPromise); // true
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`{ 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 }`.
|
|
218
334
|
|
|
219
335
|
```ts
|
|
220
|
-
|
|
221
|
-
println("Using port:", port);
|
|
336
|
+
import { safeTry, Ok, Err, println } from "slang-ts";
|
|
222
337
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
println("Retries:", retries);
|
|
338
|
+
const returned = await safeTry(() => Ok(Err("Not found")), { throw: true });
|
|
339
|
+
if (returned.isErr) println(returned.error); // "Not found"
|
|
226
340
|
|
|
227
|
-
|
|
228
|
-
|
|
341
|
+
await safeTry(() => {
|
|
342
|
+
throw new Error("Critical failure");
|
|
343
|
+
}, { throw: true }).catch((error: Error) => println(error.message));
|
|
344
|
+
// "Critical failure": the call rejects instead of returning Err.
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Pipe
|
|
348
|
+
|
|
349
|
+
Sequential function composition where each function receives a `Result` and returns a `Result`. Accepts plain values, Option, Result, or Atom as initial input.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { pipe, Ok, Err, option, type Result } from "slang-ts";
|
|
353
|
+
|
|
354
|
+
// Create pipeline functions
|
|
355
|
+
const add = (x: number) => (res: Result<number, string>) =>
|
|
356
|
+
res.isOk ? Ok(res.value + x) : res;
|
|
357
|
+
|
|
358
|
+
const multiply = (x: number) => (res: Result<number, string>) =>
|
|
359
|
+
res.isOk ? Ok(res.value * x) : res;
|
|
360
|
+
|
|
361
|
+
// Basic usage
|
|
362
|
+
const result = await pipe(5, add(3), multiply(2)).run();
|
|
363
|
+
println("Result:", result.value); // 16
|
|
364
|
+
|
|
365
|
+
// With Options as initial value
|
|
366
|
+
const fromOption = await pipe(option(10), add(5)).run();
|
|
367
|
+
println("From option:", fromOption.value); // 15
|
|
368
|
+
|
|
369
|
+
// With callbacks and error handling
|
|
370
|
+
const result = await pipe(5, add(3), multiply(2)).run({
|
|
371
|
+
onEach: ({ currentFn, prevResult }) => {
|
|
372
|
+
println("Executed:", currentFn);
|
|
373
|
+
},
|
|
374
|
+
onSuccess: (value) => println("Done:", value),
|
|
375
|
+
onError: (err) => println("Failed:", err.message),
|
|
376
|
+
allowErrors: false, // stops pipeline on first Err
|
|
377
|
+
});
|
|
229
378
|
```
|
|
230
379
|
|
|
231
380
|
### To
|
|
@@ -243,48 +392,24 @@ const errResult = option(null).to("result");
|
|
|
243
392
|
println("Result:", errResult.type); // "Err"
|
|
244
393
|
```
|
|
245
394
|
|
|
246
|
-
###
|
|
395
|
+
### Atom
|
|
247
396
|
|
|
248
|
-
|
|
397
|
+
Creates unique, non-interned symbols with semantic descriptions. Each call produces a distinct identity. Define them in one file and import from it everywhere else, handy for env-var style constants.
|
|
249
398
|
|
|
250
399
|
```ts
|
|
251
|
-
|
|
252
|
-
option(5).andThen(x => x * 2); // Some(10)
|
|
253
|
-
option(null).andThen(x => x * 2); // None (skipped)
|
|
254
|
-
option(5).andThen(() => undefined); // Some(5) - original
|
|
255
|
-
|
|
256
|
-
// Result - transforms Ok, skips Err
|
|
257
|
-
Ok(10).andThen(x => x + 5); // Ok(15)
|
|
258
|
-
Err("fail").andThen(x => x + 5); // Err("fail") (skipped)
|
|
259
|
-
|
|
260
|
-
// Atom - transforms description (sync only)
|
|
261
|
-
atom("hello").andThen(s => s.toUpperCase()); // Atom("HELLO")
|
|
262
|
-
|
|
263
|
-
// Chained andThen - multiple transformations
|
|
264
|
-
option(5)
|
|
265
|
-
.andThen(x => x + 1)
|
|
266
|
-
.andThen(x => x * 2)
|
|
267
|
-
.andThen(x => x.toString()); // Some("12")
|
|
268
|
-
|
|
269
|
-
Ok(10)
|
|
270
|
-
.andThen(x => x * 2)
|
|
271
|
-
.andThen(x => x + 5)
|
|
272
|
-
.andThen(x => ({ value: x })); // Ok({ value: 25 })
|
|
273
|
-
|
|
274
|
-
atom("hello")
|
|
275
|
-
.andThen(s => s.toUpperCase())
|
|
276
|
-
.andThen(s => s + "!"); // Atom("HELLO!")
|
|
400
|
+
import { atom } from "slang-ts";
|
|
277
401
|
|
|
278
|
-
|
|
279
|
-
const
|
|
402
|
+
const userAtom = atom("kizz");
|
|
403
|
+
const user2Atom = atom("kizz");
|
|
280
404
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
Ok(5).andThen(() => { throw "oops" }); // Err("oops")
|
|
284
|
-
atom("x").andThen(() => { throw "oops" }); // Panics!
|
|
405
|
+
println(userAtom === atom("kizz")); // false - non interned
|
|
406
|
+
println(userAtom.description); // "kizz"
|
|
285
407
|
|
|
286
|
-
|
|
287
|
-
|
|
408
|
+
if (userAtom === user2Atom) {
|
|
409
|
+
println("all the same");
|
|
410
|
+
} else {
|
|
411
|
+
println("not the same"); // This prints!
|
|
412
|
+
}
|
|
288
413
|
```
|
|
289
414
|
|
|
290
415
|
### Zip
|
|
@@ -348,96 +473,218 @@ println(unzip(zipped));
|
|
|
348
473
|
// [[1, 2, 3], [4, 5, 6]]
|
|
349
474
|
```
|
|
350
475
|
|
|
351
|
-
###
|
|
476
|
+
### Panic
|
|
352
477
|
|
|
353
|
-
|
|
478
|
+
Throws an error immediately. Use for unrecoverable failures.
|
|
354
479
|
|
|
355
480
|
```ts
|
|
356
|
-
import {
|
|
481
|
+
import { panic } from "slang-ts";
|
|
357
482
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
483
|
+
function processUser(user: User | null) {
|
|
484
|
+
if (!user) panic("User cannot be null");
|
|
485
|
+
return user.name;
|
|
486
|
+
}
|
|
361
487
|
|
|
362
|
-
|
|
363
|
-
|
|
488
|
+
// Guard clause pattern
|
|
489
|
+
const config = loadConfig();
|
|
490
|
+
if (!config.apiKey) panic("API key required");
|
|
491
|
+
```
|
|
364
492
|
|
|
365
|
-
|
|
366
|
-
const result = await pipe(5, add(3), multiply(2)).run();
|
|
367
|
-
println("Result:", result.value); // 16
|
|
493
|
+
### println
|
|
368
494
|
|
|
369
|
-
|
|
370
|
-
const fromOption = await pipe(option(10), add(5)).run();
|
|
371
|
-
println("From option:", fromOption.value); // 15
|
|
495
|
+
console.log sugar that stays out of production. Prints a line (args joined, newline appended) and does nothing once `setEnvironment` puts the app in production mode. Dev chatter never leaks to prod logs. Already have an advanced logger? Hand it to `setEnvironment({ printFn })` and every println routes through it: your logging code stays exactly as it is, in every mode.
|
|
372
496
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
497
|
+
```ts
|
|
498
|
+
import { println, setEnvironment } from "slang-ts";
|
|
499
|
+
|
|
500
|
+
setEnvironment("development"); // "dev" works too (default)
|
|
501
|
+
println("name:", name);
|
|
502
|
+
println("multiple", "args", "work", { too: true });
|
|
503
|
+
|
|
504
|
+
setEnvironment("production"); // "prod" works too
|
|
505
|
+
println("not printed in prod");
|
|
506
|
+
|
|
507
|
+
// Route println through an existing logger instead, even in prod
|
|
508
|
+
import { appLogger } from "./logger";
|
|
509
|
+
setEnvironment({ printFn: (line) => appLogger.info(line.trimEnd()) });
|
|
382
510
|
```
|
|
383
511
|
|
|
384
|
-
###
|
|
512
|
+
### Signal
|
|
513
|
+
|
|
514
|
+
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.
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
import { createSignal } from "slang-ts";
|
|
518
|
+
|
|
519
|
+
const ready = createSignal();
|
|
385
520
|
|
|
386
|
-
|
|
521
|
+
ready.addEventListener("abort", () => println("ready!")); // fires when the trigger goes off
|
|
522
|
+
ready.fire(); // flip it
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
The useful part: gate work on a signal, or publish events when a lifecycle completes.
|
|
387
526
|
|
|
388
527
|
```ts
|
|
389
|
-
|
|
528
|
+
const task = superPromise(loadUser, { defer: ready }); // starts only once ready fires
|
|
390
529
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
530
|
+
channel.when({
|
|
531
|
+
signal: task.done, // task finished, success or not
|
|
532
|
+
topic: "user.loaded",
|
|
533
|
+
data: { id: userId },
|
|
394
534
|
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### SuperPromise
|
|
538
|
+
|
|
539
|
+
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 }`.
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { superPromise } from "slang-ts";
|
|
543
|
+
|
|
544
|
+
// A fetch that gives up after 5 seconds and tries up to 3 more times
|
|
545
|
+
const result = await superPromise(
|
|
546
|
+
(signal) => fetch("/api/user", { signal }),
|
|
547
|
+
{ timeout: 5000, retry: 3 }
|
|
548
|
+
);
|
|
395
549
|
|
|
396
550
|
if (result.isOk) {
|
|
397
|
-
println("
|
|
551
|
+
println("User:", result.value);
|
|
398
552
|
} else {
|
|
399
|
-
println("
|
|
553
|
+
println("Failed:", result.error);
|
|
400
554
|
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
No `await` handy? Chain with `.then`; the callback receives the same `Result`:
|
|
401
558
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
559
|
+
```ts
|
|
560
|
+
superPromise(loadUser, { timeout: 5000 }).then((result) => {
|
|
561
|
+
if (result.isOk) {
|
|
562
|
+
println("User:", result.value);
|
|
563
|
+
} else {
|
|
564
|
+
println("Failed:", result.error);
|
|
565
|
+
}
|
|
406
566
|
});
|
|
567
|
+
```
|
|
407
568
|
|
|
408
|
-
|
|
409
|
-
println("User:", data.value);
|
|
410
|
-
}
|
|
569
|
+
Need it to wait for something else first? `defer` holds execution until a signal fires; chain operations so the next starts only when the previous is done:
|
|
411
570
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
571
|
+
```ts
|
|
572
|
+
const config = superPromise(loadConfig);
|
|
573
|
+
const user = superPromise(loadUser, { defer: config.done });
|
|
574
|
+
|
|
575
|
+
const result = await user; // config runs first, then user
|
|
416
576
|
```
|
|
417
577
|
|
|
418
|
-
|
|
578
|
+
Cancel from outside, or keep retrying until you call it off:
|
|
419
579
|
|
|
420
|
-
|
|
580
|
+
```ts
|
|
581
|
+
task.abort(); // cancel; you get Err("Aborted")
|
|
582
|
+
superPromise(fetchData, {
|
|
583
|
+
retryUntil: { signal: stopSignal, delay: 2000 }, // keep trying until stopSignal fires
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
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.
|
|
588
|
+
|
|
589
|
+
Also handy:
|
|
421
590
|
|
|
422
591
|
```ts
|
|
423
|
-
|
|
592
|
+
const manual = superPromise<number>(); // no executor; settle it yourself
|
|
593
|
+
manual.resolve(100); // or manual.reject(error)
|
|
424
594
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return user.name;
|
|
428
|
-
}
|
|
595
|
+
await superPromise(loadUser).tap((user) => println(user.name)); // observe without changing
|
|
596
|
+
```
|
|
429
597
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
598
|
+
### SuperRunner
|
|
599
|
+
|
|
600
|
+
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.
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
import { superRunner, superPromise as sp } from "slang-ts";
|
|
604
|
+
|
|
605
|
+
// In order: steps that depend on each other; stops at the first failure
|
|
606
|
+
const auth = superRunner({
|
|
607
|
+
type: "sequence",
|
|
608
|
+
runners: [sp(authenticate), sp(loadProfile), sp(loadDashboard)],
|
|
609
|
+
});
|
|
610
|
+
const ordered = await auth.start(); // [authResult, profileResult, dashboardResult]
|
|
611
|
+
|
|
612
|
+
// In parallel: all kicked off at once; result keyed by name
|
|
613
|
+
const parallel = superRunner({
|
|
614
|
+
type: "all",
|
|
615
|
+
runners: {
|
|
616
|
+
user: sp(getUser),
|
|
617
|
+
posts: sp(getPosts),
|
|
618
|
+
settings: sp(getSettings),
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
const combined = await parallel.start(); // { user, posts, settings }
|
|
622
|
+
|
|
623
|
+
// First to finish: take whichever source responds first, cancel the rest
|
|
624
|
+
const first = superRunner({
|
|
625
|
+
type: "race",
|
|
626
|
+
count: 1,
|
|
627
|
+
runners: {
|
|
628
|
+
primary: sp(fetchPrimary),
|
|
629
|
+
backup: sp(fetchBackup),
|
|
630
|
+
cache: sp(fetchCache),
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
const winner = await first.start(); // { cache: ... } or whichever won
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Cancel everything with one call:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
workflow.abort(); // cancels all active children
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Channel
|
|
643
|
+
|
|
644
|
+
Channel lets parts of your app that don't know each other talk: one side publishes, another listens, no wiring between them.
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import { createChannel } from "slang-ts";
|
|
648
|
+
|
|
649
|
+
const channel = createChannel();
|
|
650
|
+
|
|
651
|
+
const unsubscribe = channel.subscribe({ topic: "user.created" }, (event) => {
|
|
652
|
+
println("Created:", event.data);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
channel.send({ topic: "user.created", data: user });
|
|
656
|
+
unsubscribe(); // stop listening
|
|
433
657
|
```
|
|
434
658
|
|
|
435
|
-
|
|
659
|
+
Publish when a trigger goes off, a task finishing, anything at all:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
channel.when({
|
|
663
|
+
signal: workflow.done,
|
|
664
|
+
topic: "workflow.completed",
|
|
665
|
+
data: { workflowId },
|
|
666
|
+
});
|
|
667
|
+
```
|
|
436
668
|
|
|
437
669
|
## Code Samples
|
|
438
670
|
|
|
439
671
|
See [example.ts](https://github.com/Hussseinkizz/slang/blob/main/example.ts) for usage of currently implemented methods.
|
|
440
672
|
|
|
673
|
+
## Development
|
|
674
|
+
|
|
675
|
+
Install project dependencies with Bun, then build:
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
bun install
|
|
679
|
+
bun run build
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
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.
|
|
683
|
+
|
|
684
|
+
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.
|
|
685
|
+
|
|
686
|
+
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.
|
|
687
|
+
|
|
441
688
|
## Contributing
|
|
442
689
|
|
|
443
|
-
Contributions are welcome
|
|
690
|
+
Contributions are welcome. I know there are a lot of cool things out there we can bring in.
|