brass-runtime 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/examples/demo.js +3 -3
- package/dist/examples/fiberFinalizer.js +27 -5
- package/dist/examples/index.js +6 -0
- package/dist/examples/resourceExample.js +3 -4
- package/dist/examples/test-canceler.js +29 -0
- package/dist/fibers/fiber.js +86 -29
- package/dist/index.js +1 -3
- package/dist/scheduler/scheduler.js +35 -11
- package/dist/scheduler/scope.js +19 -3
- package/dist/stream/buffer.js +50 -0
- package/dist/types/asyncEffect.js +25 -0
- package/package.json +4 -2
- package/dist/scheduler/acquireRelease.js +0 -16
- package/dist/scheduler/withScope.js +0 -19
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 🛠️ brass-
|
|
1
|
+
# 🛠️ brass-runtime — Mini runtime funcional al estilo ZIO en TypeScript
|
|
2
2
|
|
|
3
|
-
**brass-
|
|
3
|
+
**brass-runtime** es un runtime funcional inspirado en **ZIO 2**, escrito en **TypeScript vanilla** y **sin usar Promises ni async/await** como primitiva principal de modelado.
|
|
4
4
|
|
|
5
5
|
El objetivo del proyecto es explorar cómo construir, desde cero, un sistema de:
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ Todo con un diseño **determinístico**, **pure FP**, y sin depender de `Promise
|
|
|
21
21
|
|
|
22
22
|
### 1. `Effect` sincrónico (núcleo funcional)
|
|
23
23
|
|
|
24
|
-
En `brass-
|
|
24
|
+
En `brass-runtime`, un efecto puro se modela como:
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
type Exit<E, A> =
|
|
@@ -48,7 +48,7 @@ Este núcleo no usa `Promise` ni `async/await`. Es **100% sincrónico y determin
|
|
|
48
48
|
|
|
49
49
|
### 2. `Async` — efectos asincrónicos sin Promises
|
|
50
50
|
|
|
51
|
-
Para modelar operaciones asincrónicas, `brass-
|
|
51
|
+
Para modelar operaciones asincrónicas, `brass-runtime` define un tipo de datos algebraico:
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
54
|
type Async<R, E, A> =
|
|
@@ -147,7 +147,7 @@ si algo vive en un `Scope`, se limpia cuando el scope termina.
|
|
|
147
147
|
|
|
148
148
|
### 6. Acquire / Release — Resource Safety
|
|
149
149
|
|
|
150
|
-
Al estilo `ZIO.acquireRelease`, `brass-
|
|
150
|
+
Al estilo `ZIO.acquireRelease`, `brass-runtime` implementa:
|
|
151
151
|
|
|
152
152
|
```ts
|
|
153
153
|
acquireRelease(
|
|
@@ -199,7 +199,7 @@ Esto replica la semántica de **ZIO 2 structured concurrency**.
|
|
|
199
199
|
|
|
200
200
|
### 8. ZStream-like — Streams estructurados con backpressure
|
|
201
201
|
|
|
202
|
-
`brass-
|
|
202
|
+
`brass-runtime` incluye una base de **streams estructurados** inspirados en `ZStream`:
|
|
203
203
|
|
|
204
204
|
```ts
|
|
205
205
|
type Pull<R, E, A> = Async<R, Option<E>, A>;
|
|
@@ -241,7 +241,7 @@ y el scope del stream garantiza que todos los recursos/finalizers se limpien al
|
|
|
241
241
|
|
|
242
242
|
## 📁 Estructura sugerida del proyecto
|
|
243
243
|
|
|
244
|
-
Una posible organización de archivos para tu repo de **brass-
|
|
244
|
+
Una posible organización de archivos para tu repo de **brass-runtime**:
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
247
|
src/
|
|
@@ -357,5 +357,5 @@ Algunas direcciones interesantes para futuro:
|
|
|
357
357
|
|
|
358
358
|
Hecho con ❤️ en TypeScript, para aprender y jugar con runtimes funcionales.
|
|
359
359
|
|
|
360
|
-
**Nombre del proyecto:** `brass-
|
|
360
|
+
**Nombre del proyecto:** `brass-runtime`
|
|
361
361
|
**Objetivo:** construir un mini ZIO-like runtime en el ecosistema JS/TS, pero manteniendo el control total sobre la semántica de los efectos desde el código de usuario.
|
package/dist/examples/demo.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sleep = sleep;
|
|
4
|
+
const fiber_1 = require("../fibers/fiber");
|
|
4
5
|
const effect_1 = require("../types/effect");
|
|
5
6
|
const stream_1 = require("../stream/stream");
|
|
7
|
+
const scope_1 = require("../scheduler/scope");
|
|
6
8
|
const asyncEffect_1 = require("../types/asyncEffect");
|
|
7
|
-
const fiber_1 = require("../fibers/fiber");
|
|
8
|
-
const withScope_1 = require("../scheduler/withScope");
|
|
9
9
|
function main() {
|
|
10
10
|
const env = {};
|
|
11
11
|
const fiberA = (0, fiber_1.fork)(task("A", 1000), env);
|
|
@@ -27,7 +27,7 @@ function main() {
|
|
|
27
27
|
const sMapped = (0, stream_1.mapStream)(s, (n) => n * 10);
|
|
28
28
|
const collected = (0, stream_1.collectStream)(sMapped, env);
|
|
29
29
|
console.log("Stream mapeado:", collected);
|
|
30
|
-
(0,
|
|
30
|
+
(0, scope_1.withScope)(scope => {
|
|
31
31
|
const f1 = scope.fork(task("A", 1000), env);
|
|
32
32
|
const f2 = scope.fork(task("B", 1500), env);
|
|
33
33
|
const f3 = scope.fork(task("C", 2000), env);
|
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// fiberFinalizer.ts
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// fiberFinalizer.ts
|
|
4
4
|
const asyncEffect_1 = require("../types/asyncEffect");
|
|
5
5
|
const fiber_1 = require("../fibers/fiber");
|
|
6
6
|
const demo_1 = require("./demo");
|
|
7
|
+
function formatExit(exit) {
|
|
8
|
+
if (!exit || typeof exit !== "object")
|
|
9
|
+
return String(exit);
|
|
10
|
+
if (exit._tag === "Success")
|
|
11
|
+
return `Success(value=${JSON.stringify(exit.value)})`;
|
|
12
|
+
if (exit._tag === "Failure")
|
|
13
|
+
return `Failure(error=${JSON.stringify(exit.error)})`;
|
|
14
|
+
return JSON.stringify(exit);
|
|
15
|
+
}
|
|
7
16
|
function main() {
|
|
8
17
|
const env = {};
|
|
18
|
+
const t0 = Date.now();
|
|
9
19
|
const eff = (0, asyncEffect_1.asyncFlatMap)((0, asyncEffect_1.asyncTotal)(() => console.log("Start")), () => (0, asyncEffect_1.asyncFlatMap)((0, demo_1.sleep)(1000), () => (0, asyncEffect_1.asyncTotal)(() => "done")));
|
|
10
20
|
const fiber = (0, fiber_1.fork)(eff, env);
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
// 👇 Instrumentación del finalizer
|
|
22
|
+
let finCount = 0;
|
|
23
|
+
fiber.addFinalizer((exit) => (0, asyncEffect_1.asyncTotal)(() => {
|
|
24
|
+
var _a;
|
|
25
|
+
finCount += 1;
|
|
26
|
+
const ms = Date.now() - t0;
|
|
27
|
+
const stack = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").slice(1, 8).join("\n");
|
|
28
|
+
console.log("\n================ FINALIZER START ================");
|
|
29
|
+
console.log(`time: +${ms}ms`);
|
|
30
|
+
console.log(`finalizer call #: ${finCount}`);
|
|
31
|
+
console.log(`exit: ${formatExit(exit)}`);
|
|
32
|
+
console.log("raw exit object:", exit);
|
|
33
|
+
console.log("stack (where finalizer ran):\n" + stack);
|
|
34
|
+
console.log("================= FINALIZER END =================\n");
|
|
13
35
|
}));
|
|
14
36
|
// cancelamos antes de terminar para ver que el finalizer corre
|
|
15
37
|
setTimeout(() => {
|
|
16
|
-
console.log(
|
|
38
|
+
console.log(`\n[+${Date.now() - t0}ms] Interrupting fiber...`);
|
|
17
39
|
fiber.interrupt();
|
|
18
40
|
}, 500);
|
|
19
41
|
fiber.join((exit) => {
|
|
20
|
-
console.log(
|
|
42
|
+
console.log(`\n[+${Date.now() - t0}ms] Fiber completed:`, exit);
|
|
21
43
|
});
|
|
22
44
|
}
|
|
23
45
|
main();
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/resourceExample.ts
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const
|
|
4
|
+
const scope_1 = require("../scheduler/scope");
|
|
5
5
|
const asyncEffect_1 = require("../types/asyncEffect");
|
|
6
|
-
const withScope_1 = require("../scheduler/withScope");
|
|
7
6
|
function openFile(name) {
|
|
8
7
|
console.log("OPEN FILE:", name);
|
|
9
8
|
return {
|
|
@@ -16,9 +15,9 @@ function openFile(name) {
|
|
|
16
15
|
};
|
|
17
16
|
}
|
|
18
17
|
function main() {
|
|
19
|
-
(0,
|
|
18
|
+
(0, scope_1.withScope)(scope => {
|
|
20
19
|
const env = {};
|
|
21
|
-
const program = (0,
|
|
20
|
+
const program = (0, asyncEffect_1.acquireRelease)((0, asyncEffect_1.asyncTotal)(() => openFile("data.txt")), (file, exit) => (0, asyncEffect_1.asyncTotal)(() => {
|
|
22
21
|
console.log("Finalizer running due to:", exit._tag);
|
|
23
22
|
file.close();
|
|
24
23
|
}), scope);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fiber_1 = require("../fibers/fiber");
|
|
4
|
+
const asyncEffect_1 = require("../types/asyncEffect");
|
|
5
|
+
let ticks = 0;
|
|
6
|
+
const effect = (0, asyncEffect_1.async)((_env, cb) => {
|
|
7
|
+
const id = setInterval(() => {
|
|
8
|
+
ticks++;
|
|
9
|
+
}, 10);
|
|
10
|
+
cb({ _tag: "Success", value: undefined });
|
|
11
|
+
return () => clearInterval(id);
|
|
12
|
+
});
|
|
13
|
+
(0, fiber_1.unsafeRunAsync)(effect, undefined, (exit) => {
|
|
14
|
+
console.log("Fiber exit:", exit);
|
|
15
|
+
});
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
const t1 = ticks;
|
|
18
|
+
console.log("ticks @50ms:", t1);
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
const t2 = ticks;
|
|
21
|
+
console.log("ticks @150ms:", t2);
|
|
22
|
+
if (t2 > t1 + 1) {
|
|
23
|
+
console.log("❌ LEAK: el interval siguió vivo después de terminar la fiber");
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log("✅ OK: el interval se limpió (canceler ejecutado)");
|
|
27
|
+
}
|
|
28
|
+
}, 100);
|
|
29
|
+
}, 50);
|
package/dist/fibers/fiber.js
CHANGED
|
@@ -7,12 +7,17 @@ let nextId = 1;
|
|
|
7
7
|
class RuntimeFiber {
|
|
8
8
|
constructor(effect, env, scheduler) {
|
|
9
9
|
this.scheduler = scheduler;
|
|
10
|
+
this.closing = null;
|
|
11
|
+
this.finishing = false;
|
|
10
12
|
this.statusValue = "Running";
|
|
11
13
|
this.interrupted = false;
|
|
12
14
|
this.result = null;
|
|
13
15
|
this.joiners = [];
|
|
14
16
|
this.stack = [];
|
|
15
17
|
this.fiberFinalizers = [];
|
|
18
|
+
this.scheduled = false;
|
|
19
|
+
this.finalizersDrained = false;
|
|
20
|
+
this.blockedOnAsync = false;
|
|
16
21
|
this.id = nextId++;
|
|
17
22
|
this.current = effect;
|
|
18
23
|
this.env = env;
|
|
@@ -24,36 +29,55 @@ class RuntimeFiber {
|
|
|
24
29
|
return this.statusValue;
|
|
25
30
|
}
|
|
26
31
|
join(cb) {
|
|
27
|
-
if (this.result != null)
|
|
32
|
+
if (this.result != null)
|
|
28
33
|
cb(this.result);
|
|
29
|
-
|
|
30
|
-
else {
|
|
34
|
+
else
|
|
31
35
|
this.joiners.push(cb);
|
|
32
|
-
}
|
|
33
36
|
}
|
|
34
37
|
interrupt() {
|
|
38
|
+
if (this.result != null)
|
|
39
|
+
return;
|
|
40
|
+
if (this.interrupted)
|
|
41
|
+
return;
|
|
35
42
|
this.interrupted = true;
|
|
36
|
-
|
|
43
|
+
this.blockedOnAsync = false;
|
|
44
|
+
this.schedule("interrupt-step");
|
|
37
45
|
}
|
|
38
|
-
|
|
46
|
+
schedule(tag = "step") {
|
|
47
|
+
if (this.result != null)
|
|
48
|
+
return;
|
|
49
|
+
if (this.scheduled)
|
|
50
|
+
return;
|
|
51
|
+
this.scheduled = true;
|
|
52
|
+
this.scheduler.schedule(() => {
|
|
53
|
+
this.scheduled = false;
|
|
54
|
+
this.step();
|
|
55
|
+
}, `fiber#${this.id}.${tag}`);
|
|
56
|
+
}
|
|
57
|
+
runFinalizersOnce(exit) {
|
|
58
|
+
if (this.finalizersDrained)
|
|
59
|
+
return;
|
|
60
|
+
this.finalizersDrained = true;
|
|
39
61
|
while (this.fiberFinalizers.length > 0) {
|
|
40
62
|
const fin = this.fiberFinalizers.pop();
|
|
41
|
-
|
|
63
|
+
try {
|
|
64
|
+
fin(exit);
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
42
67
|
}
|
|
43
68
|
}
|
|
44
|
-
/** Programa un paso de la fibra en el scheduler */
|
|
45
|
-
schedule() {
|
|
46
|
-
this.scheduler.schedule(() => this.step());
|
|
47
|
-
}
|
|
48
69
|
notify(exit) {
|
|
49
70
|
if (this.result != null)
|
|
50
71
|
return;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
if (this.closing != null)
|
|
73
|
+
return;
|
|
74
|
+
this.finishing = true;
|
|
75
|
+
this.closing = exit;
|
|
76
|
+
// ✅ ejecutar finalizers YA (garantiza clearInterval)
|
|
77
|
+
this.runFinalizersOnce(exit);
|
|
78
|
+
// completar
|
|
54
79
|
this.statusValue = this.interrupted ? "Interrupted" : "Done";
|
|
55
80
|
this.result = exit;
|
|
56
|
-
// notificar joiners
|
|
57
81
|
for (const j of this.joiners)
|
|
58
82
|
j(exit);
|
|
59
83
|
this.joiners.length = 0;
|
|
@@ -61,20 +85,24 @@ class RuntimeFiber {
|
|
|
61
85
|
onSuccess(value) {
|
|
62
86
|
const cont = this.stack.pop();
|
|
63
87
|
if (!cont) {
|
|
64
|
-
// terminamos con éxito
|
|
65
88
|
this.notify({ _tag: "Success", value });
|
|
66
89
|
return;
|
|
67
90
|
}
|
|
68
91
|
this.current = cont(value);
|
|
69
|
-
this.schedule();
|
|
92
|
+
//this.schedule("onSuccess-step");
|
|
70
93
|
}
|
|
71
94
|
onFailure(error) {
|
|
72
95
|
this.notify({ _tag: "Failure", error });
|
|
73
96
|
}
|
|
74
|
-
/** Un *paso* de evaluación de la fibra */
|
|
75
97
|
step() {
|
|
76
98
|
if (this.result != null)
|
|
77
|
-
return;
|
|
99
|
+
return;
|
|
100
|
+
if (this.blockedOnAsync)
|
|
101
|
+
return;
|
|
102
|
+
if (this.interrupted && this.closing == null) {
|
|
103
|
+
this.notify({ _tag: "Failure", error: { _tag: "Interrupted" } });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
78
106
|
const current = this.current;
|
|
79
107
|
switch (current._tag) {
|
|
80
108
|
case "Succeed":
|
|
@@ -95,28 +123,57 @@ class RuntimeFiber {
|
|
|
95
123
|
case "FlatMap":
|
|
96
124
|
this.stack.push(current.andThen);
|
|
97
125
|
this.current = current.first;
|
|
98
|
-
this.schedule();
|
|
126
|
+
//this.schedule("flatMap-step");
|
|
99
127
|
return;
|
|
100
|
-
case "Async":
|
|
101
|
-
|
|
102
|
-
|
|
128
|
+
case "Async": {
|
|
129
|
+
if (this.finishing)
|
|
130
|
+
return;
|
|
131
|
+
this.blockedOnAsync = true;
|
|
132
|
+
let pending = null;
|
|
133
|
+
let completedSync = false;
|
|
134
|
+
const resume = () => {
|
|
135
|
+
if (!pending)
|
|
136
|
+
return;
|
|
137
|
+
const exit = pending;
|
|
138
|
+
pending = null;
|
|
139
|
+
this.blockedOnAsync = false;
|
|
140
|
+
if (this.result != null || this.closing != null)
|
|
141
|
+
return;
|
|
142
|
+
if (this.interrupted) {
|
|
103
143
|
this.onFailure({ _tag: "Interrupted" });
|
|
144
|
+
return;
|
|
104
145
|
}
|
|
105
|
-
|
|
146
|
+
if (exit._tag === "Success")
|
|
106
147
|
this.onSuccess(exit.value);
|
|
107
|
-
|
|
108
|
-
else {
|
|
148
|
+
else
|
|
109
149
|
this.onFailure(exit.error);
|
|
110
|
-
|
|
150
|
+
this.schedule("async-resume");
|
|
151
|
+
};
|
|
152
|
+
const canceler = current.register(this.env, (exit) => {
|
|
153
|
+
// guardamos el resultado, pero NO ejecutamos todavía
|
|
154
|
+
pending = exit;
|
|
155
|
+
completedSync = true;
|
|
156
|
+
// no llamamos resume acá
|
|
111
157
|
});
|
|
158
|
+
if (typeof canceler === "function") {
|
|
159
|
+
this.addFinalizer((_exit) => {
|
|
160
|
+
try {
|
|
161
|
+
canceler();
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (completedSync) {
|
|
167
|
+
this.scheduler.schedule(resume, `fiber#${this.id}.async-sync-resume`);
|
|
168
|
+
}
|
|
112
169
|
return;
|
|
170
|
+
}
|
|
113
171
|
}
|
|
114
172
|
}
|
|
115
173
|
}
|
|
116
|
-
// API pública: fork + helper unsafeRunAsync
|
|
117
174
|
function fork(effect, env, scheduler = scheduler_1.globalScheduler) {
|
|
118
175
|
const fiber = new RuntimeFiber(effect, env, scheduler);
|
|
119
|
-
fiber.schedule();
|
|
176
|
+
fiber.schedule("initial-step");
|
|
120
177
|
return fiber;
|
|
121
178
|
}
|
|
122
179
|
// “correr” un Async como antes, pero apoyado en fibras + scheduler
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,6 @@ __exportStar(require("./types/asyncEffect"), exports);
|
|
|
19
19
|
__exportStar(require("./stream/stream"), exports);
|
|
20
20
|
__exportStar(require("./types/option"), exports);
|
|
21
21
|
__exportStar(require("./types/effect"), exports);
|
|
22
|
-
__exportStar(require("./scheduler/
|
|
23
|
-
__exportStar(require("./scheduler/acquireRelease"), exports);
|
|
22
|
+
__exportStar(require("./scheduler/scope"), exports);
|
|
24
23
|
__exportStar(require("./scheduler/scheduler"), exports);
|
|
25
24
|
__exportStar(require("./types/cancel"), exports);
|
|
26
|
-
__exportStar(require("./scheduler/scope"), exports);
|
|
@@ -5,23 +5,47 @@ class Scheduler {
|
|
|
5
5
|
constructor() {
|
|
6
6
|
this.queue = [];
|
|
7
7
|
this.flushing = false;
|
|
8
|
+
this.requested = false;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// ✅ tag opcional
|
|
11
|
+
schedule(task, tag = "anonymous") {
|
|
12
|
+
var _a;
|
|
13
|
+
this.queue.push({ tag, task });
|
|
14
|
+
this.requestFlush();
|
|
15
|
+
// log: tamaño + próximos tags (head)
|
|
16
|
+
console.log("SCHEDULER", {
|
|
17
|
+
flushing: this.flushing,
|
|
18
|
+
q: this.queue.length,
|
|
19
|
+
next: (_a = this.queue[0]) === null || _a === void 0 ? void 0 : _a.tag,
|
|
20
|
+
head: this.queue.slice(0, 5).map(t => t.tag),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
requestFlush() {
|
|
24
|
+
if (this.requested)
|
|
25
|
+
return;
|
|
26
|
+
this.requested = true;
|
|
27
|
+
queueMicrotask(() => this.flush());
|
|
14
28
|
}
|
|
15
29
|
flush() {
|
|
30
|
+
if (this.flushing)
|
|
31
|
+
return;
|
|
16
32
|
this.flushing = true;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
this.requested = false;
|
|
34
|
+
try {
|
|
35
|
+
while (this.queue.length > 0) {
|
|
36
|
+
const { task } = this.queue.shift();
|
|
37
|
+
try {
|
|
38
|
+
task();
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
this.flushing = false;
|
|
45
|
+
if (this.queue.length > 0)
|
|
46
|
+
this.requestFlush();
|
|
21
47
|
}
|
|
22
|
-
this.flushing = false;
|
|
23
48
|
}
|
|
24
49
|
}
|
|
25
50
|
exports.Scheduler = Scheduler;
|
|
26
|
-
// Un scheduler global para todo el runtime
|
|
27
51
|
exports.globalScheduler = new Scheduler();
|
package/dist/scheduler/scope.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Scope = void 0;
|
|
4
|
+
exports.withScope = withScope;
|
|
4
5
|
// src/scope.ts
|
|
5
6
|
const fiber_1 = require("../fibers/fiber");
|
|
6
7
|
let nextScopeId = 1;
|
|
7
8
|
class Scope {
|
|
8
|
-
constructor() {
|
|
9
|
+
constructor(env) {
|
|
10
|
+
this.env = env;
|
|
9
11
|
this.closed = false;
|
|
10
12
|
this.children = new Set();
|
|
11
13
|
this.subScopes = new Set();
|
|
@@ -23,7 +25,7 @@ class Scope {
|
|
|
23
25
|
subScope() {
|
|
24
26
|
if (this.closed)
|
|
25
27
|
throw new Error("Scope closed");
|
|
26
|
-
const s = new Scope();
|
|
28
|
+
const s = new Scope(this.env);
|
|
27
29
|
this.subScopes.add(s);
|
|
28
30
|
return s;
|
|
29
31
|
}
|
|
@@ -54,7 +56,8 @@ class Scope {
|
|
|
54
56
|
// 3) ejecutar finalizers en orden LIFO
|
|
55
57
|
while (this.finalizers.length > 0) {
|
|
56
58
|
const fin = this.finalizers.pop();
|
|
57
|
-
fin(exit);
|
|
59
|
+
const eff = fin(exit);
|
|
60
|
+
(0, fiber_1.fork)(eff, this.env); // <-- esto hace que se ejecute el Async
|
|
58
61
|
}
|
|
59
62
|
this.children.clear();
|
|
60
63
|
this.subScopes.clear();
|
|
@@ -64,3 +67,16 @@ class Scope {
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
exports.Scope = Scope;
|
|
70
|
+
/**
|
|
71
|
+
* Ejecuta una función dentro de un scope estructurado.
|
|
72
|
+
* Al final (éxito o error), se cierra el scope garantizando cleanup.
|
|
73
|
+
*/
|
|
74
|
+
function withScope(body) {
|
|
75
|
+
const scope = new Scope({});
|
|
76
|
+
try {
|
|
77
|
+
return body(scope);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
scope.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
import {ZStream} from "./stream";
|
|
4
|
+
|
|
5
|
+
type Signal<E, A> =
|
|
6
|
+
| { _tag: "Elem"; value: A }
|
|
7
|
+
| { _tag: "End" }
|
|
8
|
+
| { _tag: "Fail"; error: E }
|
|
9
|
+
function buffer<R, E, A>(
|
|
10
|
+
stream: ZStream<R, E, A>,
|
|
11
|
+
capacity: number,
|
|
12
|
+
strategy: "backpressure" | "dropping" | "sliding" = "backpressure",
|
|
13
|
+
): ZStream<R, E, A> {
|
|
14
|
+
return new ZStream((scope) =>
|
|
15
|
+
Async.gen(function* (_) {
|
|
16
|
+
const pullUp = yield* _(stream.open(scope))
|
|
17
|
+
const q = yield* _(Queue.bounded<Signal<E, A>>(capacity, strategy))
|
|
18
|
+
|
|
19
|
+
// Producer fiber: llena la cola
|
|
20
|
+
const producer = yield* _(Async.forkScoped(scope, // importante: scoped para que se interrumpa al cerrar
|
|
21
|
+
Async.forever(
|
|
22
|
+
pullUp.foldCauseAsync(
|
|
23
|
+
// upstream terminó/falló
|
|
24
|
+
(cause) =>
|
|
25
|
+
cause.match({
|
|
26
|
+
end: () => q.offer({ _tag: "End" }).unit(),
|
|
27
|
+
fail: (e) => q.offer({ _tag: "Fail", error: e }).unit(),
|
|
28
|
+
}),
|
|
29
|
+
// got elem
|
|
30
|
+
(a) => q.offer({ _tag: "Elem", value: a }).unit(),
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
))
|
|
34
|
+
|
|
35
|
+
// Downstream Pull
|
|
36
|
+
const pullDown: Pull<R, E, A> = q.take().flatMap((sig) => {
|
|
37
|
+
switch (sig._tag) {
|
|
38
|
+
case "Elem": return Async.succeed(sig.value)
|
|
39
|
+
case "End": return Async.fail(None) // fin del stream
|
|
40
|
+
case "Fail": return Async.fail(Some(sig.error))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Importante: si el consumidor termina antes,
|
|
45
|
+
// el scope debería interrumpir producer automáticamente.
|
|
46
|
+
return pullDown
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
*/
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.async = exports.asyncTotal = exports.asyncSync = exports.asyncFail = exports.asyncSucceed = void 0;
|
|
4
|
+
exports.unit = unit;
|
|
4
5
|
exports.asyncMap = asyncMap;
|
|
5
6
|
exports.asyncFlatMap = asyncFlatMap;
|
|
6
7
|
exports.fromPromise = fromPromise;
|
|
7
8
|
exports.tryPromiseAbortable = tryPromiseAbortable;
|
|
8
9
|
exports.fromPromiseAbortable = fromPromiseAbortable;
|
|
10
|
+
exports.acquireRelease = acquireRelease;
|
|
11
|
+
function unit() {
|
|
12
|
+
return (0, exports.asyncSync)(() => undefined);
|
|
13
|
+
}
|
|
9
14
|
const asyncSucceed = (value) => ({
|
|
10
15
|
_tag: "Succeed",
|
|
11
16
|
value,
|
|
@@ -79,3 +84,23 @@ function fromPromiseAbortable(thunk, onError) {
|
|
|
79
84
|
};
|
|
80
85
|
});
|
|
81
86
|
}
|
|
87
|
+
function acquireRelease(acquire, release, scope) {
|
|
88
|
+
return asyncFlatMap(acquire, (resource) => {
|
|
89
|
+
// registrar finalizer
|
|
90
|
+
scope.addFinalizer((exit) => release(resource, exit));
|
|
91
|
+
return (0, exports.asyncSucceed)(resource);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function registerInterruptible(register) {
|
|
95
|
+
return (0, exports.async)((env, cb) => {
|
|
96
|
+
const canceler = register(env, cb);
|
|
97
|
+
return typeof canceler === "function"
|
|
98
|
+
? (0, exports.asyncSync)((_env) => {
|
|
99
|
+
try {
|
|
100
|
+
canceler();
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
})
|
|
104
|
+
: unit();
|
|
105
|
+
});
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brass-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Effect runtime utilities for TypeScript",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "ts-node-dev src/examples/demo.ts",
|
|
17
17
|
"build": "tsc",
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "tsx src/examples/index.ts",
|
|
19
19
|
"prepublishOnly": "npm run build"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
"@semantic-release/github": "^11.0.0",
|
|
26
26
|
"@semantic-release/npm": "^13.1.3",
|
|
27
27
|
"@semantic-release/release-notes-generator": "^14.0.0",
|
|
28
|
+
"conventional-changelog-conventionalcommits": "^7.x.x",
|
|
28
29
|
"semantic-release": "^25.0.2",
|
|
29
30
|
"ts-node-dev": "^2.0.0",
|
|
31
|
+
"tsx": "^4.21.0",
|
|
30
32
|
"typescript": "^5.9.3"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.acquireRelease = acquireRelease;
|
|
4
|
-
const asyncEffect_1 = require("../types/asyncEffect");
|
|
5
|
-
/**
|
|
6
|
-
* acquireRelease:
|
|
7
|
-
* acquire: Async<R, E, A>
|
|
8
|
-
* release: (A, Exit) => Async<R, never, void>
|
|
9
|
-
*/
|
|
10
|
-
function acquireRelease(acquire, release, scope) {
|
|
11
|
-
return (0, asyncEffect_1.asyncFlatMap)(acquire, (resource) => {
|
|
12
|
-
// registrar finalizer
|
|
13
|
-
scope.addFinalizer((exit) => release(resource, exit));
|
|
14
|
-
return (0, asyncEffect_1.asyncSucceed)(resource);
|
|
15
|
-
});
|
|
16
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// src/withScope.ts
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.withScope = withScope;
|
|
5
|
-
const scope_1 = require("./scope");
|
|
6
|
-
/**
|
|
7
|
-
* Ejecuta una función dentro de un scope estructurado.
|
|
8
|
-
* Al final (éxito o error), se cierra el scope garantizando cleanup.
|
|
9
|
-
*/
|
|
10
|
-
function withScope(body) {
|
|
11
|
-
const scope = new scope_1.Scope();
|
|
12
|
-
try {
|
|
13
|
-
const result = body(scope);
|
|
14
|
-
return result;
|
|
15
|
-
}
|
|
16
|
-
finally {
|
|
17
|
-
scope.close(); // cleanup garantizado
|
|
18
|
-
}
|
|
19
|
-
}
|