brass-runtime 1.0.1 → 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.
@@ -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, withScope_1.withScope)(scope => {
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
- fiber.addFinalizer(exit => (0, asyncEffect_1.asyncTotal)(() => {
12
- console.log("RUNNING FINALIZER → exit =", exit._tag);
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("Interrupting fiber...");
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("Fiber completed:", exit);
42
+ console.log(`\n[+${Date.now() - t0}ms] Fiber completed:`, exit);
21
43
  });
22
44
  }
23
45
  main();
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("./demo");
4
+ require("./fiberFinalizer");
5
+ require("./resourceExample");
6
+ require("./test-canceler");
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  // src/resourceExample.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- const acquireRelease_1 = require("../scheduler/acquireRelease");
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, withScope_1.withScope)(scope => {
18
+ (0, scope_1.withScope)(scope => {
20
19
  const env = {};
21
- const program = (0, acquireRelease_1.acquireRelease)((0, asyncEffect_1.asyncTotal)(() => openFile("data.txt")), (file, exit) => (0, asyncEffect_1.asyncTotal)(() => {
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);
@@ -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
- // cancelación cooperativa: el código async puede chequear flags externos
43
+ this.blockedOnAsync = false;
44
+ this.schedule("interrupt-step");
37
45
  }
38
- runFiberFinalizers(exit) {
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
- fin(exit); // fire-and-forget (igual que ZIO)
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
- // ejecutar finalizers de fibra
52
- this.runFiberFinalizers(exit);
53
- // marcar estado final
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(); // siguiente paso
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; // ya terminó
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(); // reducimos el first en otro paso
126
+ //this.schedule("flatMap-step");
99
127
  return;
100
- case "Async":
101
- current.register(this.env, (exit) => {
102
- if (this.interrupted && exit._tag === "Success") {
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
- else if (exit._tag === "Success") {
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(); // arrancamos la primera reducción
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/withScope"), exports);
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
- schedule(task) {
10
- this.queue.push(task);
11
- if (!this.flushing) {
12
- this.flush();
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
- // Versión simple: drena todo de una
18
- while (this.queue.length > 0) {
19
- const t = this.queue.shift();
20
- t();
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();
@@ -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); // se ejecuta como Async, pero no esperamos
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.0.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": "node -e \"console.log('no tests yet')\"",
18
+ "test": "tsx src/examples/index.ts",
19
19
  "prepublishOnly": "npm run build"
20
20
  },
21
21
  "devDependencies": {
@@ -28,6 +28,7 @@
28
28
  "conventional-changelog-conventionalcommits": "^7.x.x",
29
29
  "semantic-release": "^25.0.2",
30
30
  "ts-node-dev": "^2.0.0",
31
+ "tsx": "^4.21.0",
31
32
  "typescript": "^5.9.3"
32
33
  }
33
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
- }