@effected/runtimes 0.1.5 → 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/BunResolver.js CHANGED
@@ -47,11 +47,26 @@ const BunResolverOptions = Schema.Struct({
47
47
  * @public
48
48
  */
49
49
  var BunResolver = class extends Context.Service()("@effected/runtimes/BunResolver") {
50
- /** Try GitHub, fall back to the bundled snapshot. */
50
+ /**
51
+ * Try GitHub, fall back to the bundled snapshot.
52
+ *
53
+ * Lazy: building the layer performs no IO. The feed is fetched by the first
54
+ * `resolve` and shared by every later (and concurrent) one; a fallback to
55
+ * the snapshot is memoized like any other successful population.
56
+ */
51
57
  static layer = mk(this, (index, live, offline) => populateAuto(index, "bun", live, offline));
52
- /** GitHub or nothing. Fails with `FreshnessError` when it cannot be reached. */
58
+ /**
59
+ * GitHub or nothing.
60
+ *
61
+ * Lazy: building the layer performs no IO, so an unreachable feed surfaces
62
+ * as a `FreshnessError` from `resolve`, not from layer acquisition. A failed
63
+ * fetch is not memoized — the next `resolve` retries; a successful one is.
64
+ */
53
65
  static layerFresh = mk(this, (index, live) => populateFresh(index, "bun", live));
54
- /** The bundled snapshot only. Performs no IO and requires nothing. */
66
+ /**
67
+ * The bundled snapshot only. Requires nothing and performs no IO — the
68
+ * snapshot is decoded lazily by the first `resolve`, like the live layers.
69
+ */
55
70
  static layerOffline = mk(this, (index, _live, offline) => populateOffline(index, offline));
56
71
  };
57
72
  /**
package/DenoResolver.js CHANGED
@@ -47,11 +47,26 @@ const DenoResolverOptions = Schema.Struct({
47
47
  * @public
48
48
  */
49
49
  var DenoResolver = class extends Context.Service()("@effected/runtimes/DenoResolver") {
50
- /** Try GitHub, fall back to the bundled snapshot. */
50
+ /**
51
+ * Try GitHub, fall back to the bundled snapshot.
52
+ *
53
+ * Lazy: building the layer performs no IO. The feed is fetched by the first
54
+ * `resolve` and shared by every later (and concurrent) one; a fallback to
55
+ * the snapshot is memoized like any other successful population.
56
+ */
51
57
  static layer = mk(this, (index, live, offline) => populateAuto(index, "deno", live, offline));
52
- /** GitHub or nothing. Fails with `FreshnessError` when it cannot be reached. */
58
+ /**
59
+ * GitHub or nothing.
60
+ *
61
+ * Lazy: building the layer performs no IO, so an unreachable feed surfaces
62
+ * as a `FreshnessError` from `resolve`, not from layer acquisition. A failed
63
+ * fetch is not memoized — the next `resolve` retries; a successful one is.
64
+ */
53
65
  static layerFresh = mk(this, (index, live) => populateFresh(index, "deno", live));
54
- /** The bundled snapshot only. Performs no IO and requires nothing. */
66
+ /**
67
+ * The bundled snapshot only. Requires nothing and performs no IO — the
68
+ * snapshot is decoded lazily by the first `resolve`, like the live layers.
69
+ */
55
70
  static layerOffline = mk(this, (index, _live, offline) => populateOffline(index, offline));
56
71
  };
57
72
  /**
package/NodeResolver.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NodePhase, NodeSchedule } from "./NodeSchedule.js";
2
2
  import { buildNodeReleases, fetchNodeReleases, fetchNodeSchedule } from "./internal/feeds.js";
3
+ import { once } from "./internal/once.js";
3
4
  import { make } from "./internal/releaseIndex.js";
4
5
  import { Increments } from "./ResolvedVersions.js";
5
6
  import { resolveWith } from "./internal/resolve.js";
@@ -47,25 +48,40 @@ var NodeResolver = class extends Context.Service()("@effected/runtimes/NodeResol
47
48
  /**
48
49
  * Try the live feeds, fall back to the bundled snapshot.
49
50
  *
50
- * A fallback is recorded as `source: "cache"` and logged, so a caller can
51
+ * Lazy: building the layer performs no IO. The feeds are fetched by the
52
+ * first `resolve` and shared by every later (and concurrent) one; a
53
+ * fallback to the snapshot is memoized like any other successful
54
+ * population, recorded as `source: "cache"` and logged, so a caller can
51
55
  * always tell a live answer from a snapshot.
52
56
  */
53
57
  static layer = build(this, (index, live, offline) => populateAuto(index, "node", live, offline));
54
58
  /**
55
- * Live feeds or nothing. Fails with `FreshnessError` when they cannot be reached.
59
+ * Live feeds or nothing.
60
+ *
61
+ * Lazy: building the layer performs no IO, so unreachable feeds surface as
62
+ * a `FreshnessError` from `resolve`, not from layer acquisition. A failed
63
+ * fetch is not memoized — the next `resolve` retries; a successful one is.
56
64
  */
57
65
  static layerFresh = build(this, (index, live) => populateFresh(index, "node", live));
58
66
  /**
59
- * The bundled snapshot only. Performs no IO and requires nothing.
67
+ * The bundled snapshot only. Requires nothing and performs no IO the
68
+ * snapshot is decoded lazily by the first `resolve`, like the live layers.
60
69
  */
61
70
  static layerOffline = build(this, (index, _live, offline) => populateOffline(index, offline));
62
71
  };
63
72
  /**
64
- * Assemble a Node resolver layer around a strategy.
73
+ * Assemble a **lazy** Node resolver layer around a strategy.
74
+ *
75
+ * Acquisition performs no IO: it builds the empty index, captures the
76
+ * strategy's requirements from the layer scope, and gates the strategy behind
77
+ * a run-once memo (see `internal/once.ts`). The first `resolve` runs the
78
+ * population; concurrent first resolves share it; success is memoized for the
79
+ * layer's lifetime while a failure (only the fresh strategy can produce one)
80
+ * is retried by the next resolve.
65
81
  *
66
- * The strategy's own error and requirement channels flow straight out into the
67
- * layer's, so `layerOffline` genuinely requires nothing and `layer` genuinely
68
- * cannot fail — no casts, and the types say what is true.
82
+ * The strategy's requirement channel still flows straight out into the
83
+ * layer's, so `layerOffline` genuinely requires nothing, and its error channel
84
+ * flows into `resolve`'s — no casts, and the types say what is true.
69
85
  *
70
86
  * The tag arrives as `this`: a static initializer runs while the module-scope
71
87
  * `NodeResolver` binding is still in its temporal dead zone, so naming the class
@@ -75,15 +91,16 @@ function build(tag, strategy) {
75
91
  return Layer.effect(tag, Effect.gen(function* () {
76
92
  const index = yield* make();
77
93
  const scheduleRef = yield* Ref.make(NodeSchedule.empty);
78
- yield* strategy(index, Effect.gen(function* () {
94
+ const populate = yield* once(strategy(index, Effect.gen(function* () {
79
95
  const [raw, scheduleData] = yield* Effect.all([fetchNodeReleases(), fetchNodeSchedule()]);
80
96
  yield* Ref.set(scheduleRef, yield* NodeSchedule.fromData(scheduleData));
81
97
  return yield* buildNodeReleases(raw);
82
98
  }).pipe(Effect.catchTag("InvalidVersionError", (cause) => Effect.die(cause))), Effect.gen(function* () {
83
99
  yield* Ref.set(scheduleRef, yield* NodeSchedule.fromData(nodeScheduleDefaults));
84
100
  return yield* buildNodeReleases(nodeDefaults);
85
- }).pipe(Effect.orDie));
101
+ }).pipe(Effect.orDie)));
86
102
  return { resolve: Effect.fn("NodeResolver.resolve")(function* (options) {
103
+ yield* populate;
87
104
  const range = options?.range ?? "*";
88
105
  const phases = options?.phases ?? DEFAULT_PHASES;
89
106
  const now = options?.date ?? (yield* DateTime.now);
package/README.md CHANGED
@@ -23,7 +23,7 @@ Resolve semver-compatible Node.js, Bun and Deno versions from the live release f
23
23
 
24
24
  Every answer carries an honest `source`. A resolver that fetches live data, silently fails, serves a snapshot and reports `source: "api"` is worse than one that never had a snapshot — you cannot tell a fresh answer from a stale one, and a CI job pinning its toolchain will happily install a version that was current last quarter. Here the field is set by whichever strategy actually populated the index: `"api"` when the feed answered, `"cache"` when the snapshot did, including when the automatic strategy fell back to it, and the fallback logs a warning on the way through.
25
25
 
26
- The freshness policy is a layer, not a flag, and the types follow: `layerOffline` has no error channel and no requirements, because a snapshot read cannot fail and does no IO. `layerFresh` fails with `FreshnessError` and nothing else, because you chose it to say a snapshot is not an acceptable substitute. A single resolver switching on a strategy enum would union all three error channels together and force the offline layer to advertise a failure it cannot have.
26
+ The freshness policy is a layer, not a flag, and the layers are lazy: building one performs no IO, the first `resolve` fetches the feed, and every later resolve shares that population. The types follow: `layerOffline` requires nothing, because a snapshot read needs no transport. Under `layerFresh`, `resolve` fails with `FreshnessError`, because you chose it to say a snapshot is not an acceptable substitute and because the fetch happens at resolve time, that failure arrives where you can catch it, retry it or fall back, not buried in layer construction.
27
27
 
28
28
  There is no HTTP client in the dependency tree either. The v3 library carried Octokit and `@octokit/auth-app` to fund exactly two REST GETs; this one goes through `HttpClient` from `effect/unstable/http` and lets you supply the transport. `@effected/semver` is the only runtime dependency, and it is first-party.
29
29
 
@@ -76,12 +76,14 @@ No credentials are needed for Node: both feeds it reads — nodejs.org's release
76
76
 
77
77
  Each of the three resolvers exposes the same three layer **constants**:
78
78
 
79
- | Layer | Behavior | Fails with | Requires |
80
- | ----- | -------- | ---------- | -------- |
79
+ | Layer | Behavior | `resolve` fails with | Requires |
80
+ | ----- | -------- | -------------------- | -------- |
81
81
  | `layer` | Fetch live; on failure fall back to the bundled snapshot, log a warning and report `source: "cache"` | never | the resolver's transport |
82
82
  | `layerFresh` | Live data or nothing | `FreshnessError` | the resolver's transport |
83
83
  | `layerOffline` | The bundled snapshot only. No IO. | never | nothing |
84
84
 
85
+ Every layer is **lazy**: building it performs no IO, so merging all three resolvers costs nothing until you actually resolve one. The first `resolve` fetches the feed, and later and concurrent resolves share that one population for the layer's lifetime — including the automatic strategy's snapshot fallback, which counts as a successful population and is not re-fetched. Only a *failed* population is not memoized: a `layerFresh` resolve that fails with `FreshnessError` leaves the next resolve free to retry. The lazy timing is also why `FreshnessError` sits in `resolve`'s error channel rather than the layer's — building `layerFresh` cannot fail; resolving through it can.
86
+
85
87
  The transport differs by runtime, and that is not an accident:
86
88
 
87
89
  | Resolver | Feed | Requires |
@@ -137,7 +139,7 @@ The Node schedule is keyed by release *line*, not by major — `nodejs/Release`
137
139
  | `InvalidRangeError` | The semver range is malformed. Raised by `@effected/semver` and imported from there. | A typo in a range is a typo, not a not-found. Report it as one. |
138
140
  | `NoMatchingVersionError` | The range is fine and nothing matched it. Carries `runtime`, `constraint` and the `phases` searched. | Widen the range, or accept more phases. |
139
141
  | `UnresolvableDefaultError` | An explicit `defaultVersion` was asked for and nothing matched it. Carries `runtime` and `defaultVersion`. | Distinct from the above, because Node's default otherwise falls back to the LTS pick — silently dropping it would hand you LTS as though you had asked for it. |
140
- | `FreshnessError` | `layerFresh` could not reach the feed. Carries `runtime` and the structural `cause`. | Retry, or fall back to `layer` and accept a snapshot. |
142
+ | `FreshnessError` | A `resolve` under `layerFresh` could not reach the feed. Carries `runtime` and the structural `cause`. | Retry the resolve (a failed fetch is not memoized), or fall back to `layer` and accept a snapshot. |
141
143
  | `RateLimitError` | GitHub's rate limit is exhausted. Carries `limit`, `remaining` and, when GitHub said, `retryAfter` in seconds. | Authenticate, or back off by `retryAfter`. A `403` is classified from the response headers, never guessed from the body. |
142
144
  | `AuthenticationError` | GitHub rejected the credential. Carries `method` — `"token"` or `"anonymous"`. | Check the token, or supply one. |
143
145
  | `NetworkError` | The request failed, or returned a status that is neither auth nor rate limit. Carries `url`, the `status` where there was one, and the structural `cause`. | A permission `403` lands here and is not retried. |
package/index.d.ts CHANGED
@@ -338,7 +338,7 @@ declare const BunResolverOptions: Schema.Struct<{
338
338
  */
339
339
  type BunResolverOptions = typeof BunResolverOptions.Type;
340
340
  declare const BunResolver_base: Context.ServiceClass<BunResolver, "@effected/runtimes/BunResolver", {
341
- readonly resolve: (options?: BunResolverOptions) => Effect.Effect<ResolvedVersions, InvalidRangeError | NoMatchingVersionError | UnresolvableDefaultError>;
341
+ readonly resolve: (options?: BunResolverOptions) => Effect.Effect<ResolvedVersions, InvalidRangeError | NoMatchingVersionError | UnresolvableDefaultError | FreshnessError>;
342
342
  }>;
343
343
  /**
344
344
  * The Bun resolver.
@@ -357,11 +357,26 @@ declare const BunResolver_base: Context.ServiceClass<BunResolver, "@effected/run
357
357
  * @public
358
358
  */
359
359
  declare class BunResolver extends BunResolver_base {
360
- /** Try GitHub, fall back to the bundled snapshot. */
360
+ /**
361
+ * Try GitHub, fall back to the bundled snapshot.
362
+ *
363
+ * Lazy: building the layer performs no IO. The feed is fetched by the first
364
+ * `resolve` and shared by every later (and concurrent) one; a fallback to
365
+ * the snapshot is memoized like any other successful population.
366
+ */
361
367
  static readonly layer: Layer.Layer<BunResolver, never, GitHubClient>;
362
- /** GitHub or nothing. Fails with `FreshnessError` when it cannot be reached. */
363
- static readonly layerFresh: Layer.Layer<BunResolver, FreshnessError, GitHubClient>;
364
- /** The bundled snapshot only. Performs no IO and requires nothing. */
368
+ /**
369
+ * GitHub or nothing.
370
+ *
371
+ * Lazy: building the layer performs no IO, so an unreachable feed surfaces
372
+ * as a `FreshnessError` from `resolve`, not from layer acquisition. A failed
373
+ * fetch is not memoized — the next `resolve` retries; a successful one is.
374
+ */
375
+ static readonly layerFresh: Layer.Layer<BunResolver, never, GitHubClient>;
376
+ /**
377
+ * The bundled snapshot only. Requires nothing and performs no IO — the
378
+ * snapshot is decoded lazily by the first `resolve`, like the live layers.
379
+ */
365
380
  static readonly layerOffline: Layer.Layer<BunResolver>;
366
381
  }
367
382
  //#endregion
@@ -398,7 +413,7 @@ declare const DenoResolverOptions: Schema.Struct<{
398
413
  */
399
414
  type DenoResolverOptions = typeof DenoResolverOptions.Type;
400
415
  declare const DenoResolver_base: Context.ServiceClass<DenoResolver, "@effected/runtimes/DenoResolver", {
401
- readonly resolve: (options?: DenoResolverOptions) => Effect.Effect<ResolvedVersions, InvalidRangeError | NoMatchingVersionError | UnresolvableDefaultError>;
416
+ readonly resolve: (options?: DenoResolverOptions) => Effect.Effect<ResolvedVersions, InvalidRangeError | NoMatchingVersionError | UnresolvableDefaultError | FreshnessError>;
402
417
  }>;
403
418
  /**
404
419
  * The Deno resolver.
@@ -417,11 +432,26 @@ declare const DenoResolver_base: Context.ServiceClass<DenoResolver, "@effected/r
417
432
  * @public
418
433
  */
419
434
  declare class DenoResolver extends DenoResolver_base {
420
- /** Try GitHub, fall back to the bundled snapshot. */
435
+ /**
436
+ * Try GitHub, fall back to the bundled snapshot.
437
+ *
438
+ * Lazy: building the layer performs no IO. The feed is fetched by the first
439
+ * `resolve` and shared by every later (and concurrent) one; a fallback to
440
+ * the snapshot is memoized like any other successful population.
441
+ */
421
442
  static readonly layer: Layer.Layer<DenoResolver, never, GitHubClient>;
422
- /** GitHub or nothing. Fails with `FreshnessError` when it cannot be reached. */
423
- static readonly layerFresh: Layer.Layer<DenoResolver, FreshnessError, GitHubClient>;
424
- /** The bundled snapshot only. Performs no IO and requires nothing. */
443
+ /**
444
+ * GitHub or nothing.
445
+ *
446
+ * Lazy: building the layer performs no IO, so an unreachable feed surfaces
447
+ * as a `FreshnessError` from `resolve`, not from layer acquisition. A failed
448
+ * fetch is not memoized — the next `resolve` retries; a successful one is.
449
+ */
450
+ static readonly layerFresh: Layer.Layer<DenoResolver, never, GitHubClient>;
451
+ /**
452
+ * The bundled snapshot only. Requires nothing and performs no IO — the
453
+ * snapshot is decoded lazily by the first `resolve`, like the live layers.
454
+ */
425
455
  static readonly layerOffline: Layer.Layer<DenoResolver>;
426
456
  }
427
457
  //#endregion
@@ -696,7 +726,7 @@ declare const NodeResolverOptions: Schema.Struct<{
696
726
  */
697
727
  type NodeResolverOptions = typeof NodeResolverOptions.Type;
698
728
  declare const NodeResolver_base: Context.ServiceClass<NodeResolver, "@effected/runtimes/NodeResolver", {
699
- readonly resolve: (options?: NodeResolverOptions) => Effect.Effect<ResolvedVersions, InvalidRangeError | NoMatchingVersionError | UnresolvableDefaultError>;
729
+ readonly resolve: (options?: NodeResolverOptions) => Effect.Effect<ResolvedVersions, InvalidRangeError | NoMatchingVersionError | UnresolvableDefaultError | FreshnessError>;
700
730
  }>;
701
731
  /**
702
732
  * The Node.js resolver.
@@ -719,16 +749,24 @@ declare class NodeResolver extends NodeResolver_base {
719
749
  /**
720
750
  * Try the live feeds, fall back to the bundled snapshot.
721
751
  *
722
- * A fallback is recorded as `source: "cache"` and logged, so a caller can
752
+ * Lazy: building the layer performs no IO. The feeds are fetched by the
753
+ * first `resolve` and shared by every later (and concurrent) one; a
754
+ * fallback to the snapshot is memoized like any other successful
755
+ * population, recorded as `source: "cache"` and logged, so a caller can
723
756
  * always tell a live answer from a snapshot.
724
757
  */
725
758
  static readonly layer: Layer.Layer<NodeResolver, never, HttpClient.HttpClient>;
726
759
  /**
727
- * Live feeds or nothing. Fails with `FreshnessError` when they cannot be reached.
760
+ * Live feeds or nothing.
761
+ *
762
+ * Lazy: building the layer performs no IO, so unreachable feeds surface as
763
+ * a `FreshnessError` from `resolve`, not from layer acquisition. A failed
764
+ * fetch is not memoized — the next `resolve` retries; a successful one is.
728
765
  */
729
- static readonly layerFresh: Layer.Layer<NodeResolver, FreshnessError, HttpClient.HttpClient>;
766
+ static readonly layerFresh: Layer.Layer<NodeResolver, never, HttpClient.HttpClient>;
730
767
  /**
731
- * The bundled snapshot only. Performs no IO and requires nothing.
768
+ * The bundled snapshot only. Requires nothing and performs no IO the
769
+ * snapshot is decoded lazily by the first `resolve`, like the live layers.
732
770
  */
733
771
  static readonly layerOffline: Layer.Layer<NodeResolver>;
734
772
  }
@@ -1,25 +1,34 @@
1
1
  import { GitHubClient } from "../GitHub.js";
2
2
  import { buildReleases, fetchGitHubReleases } from "./feeds.js";
3
+ import { once } from "./once.js";
3
4
  import { make } from "./releaseIndex.js";
4
5
  import { resolveWith } from "./resolve.js";
5
6
  import { Effect, Layer, Option } from "effect";
6
7
 
7
8
  //#region src/internal/githubRuntime.ts
8
9
  /**
9
- * Build a layer for a GitHub-hosted runtime around a strategy.
10
+ * Build a **lazy** layer for a GitHub-hosted runtime around a strategy.
10
11
  *
11
- * The strategy's channels flow out into the layer's, so `layerOffline` requires
12
- * no `GitHubClient` and `layer` cannot fail stated by the types, not asserted
13
- * by a cast.
12
+ * Acquisition performs no IO: it builds the empty index, captures the
13
+ * strategy's requirements from the layer scope, and gates the strategy behind
14
+ * a run-once memo (see `internal/once.ts`). The first `resolve` runs the
15
+ * population; concurrent first resolves share it; success is memoized for the
16
+ * layer's lifetime while a failure (only the fresh strategy can produce one)
17
+ * is retried by the next resolve.
18
+ *
19
+ * The strategy's requirements still flow out into the layer's (`layerOffline`
20
+ * requires no `GitHubClient`), and its error channel flows into `resolve`'s —
21
+ * stated by the types, not asserted by a cast.
14
22
  */
15
23
  const build = (spec, strategy) => {
16
24
  const { tag, runtime, owner, repo, spanName, defaults, make: make$1 } = spec;
17
25
  return Layer.effect(tag, Effect.gen(function* () {
18
26
  const index = yield* make();
19
- yield* strategy(index, Effect.gen(function* () {
27
+ const populate = yield* once(strategy(index, Effect.gen(function* () {
20
28
  return yield* buildReleases(yield* fetchGitHubReleases(yield* GitHubClient, owner, repo), make$1);
21
- }), buildReleases(defaults, make$1));
29
+ }), buildReleases(defaults, make$1)));
22
30
  return { resolve: Effect.fn(spanName)(function* (options) {
31
+ yield* populate;
23
32
  const range = options?.range ?? "*";
24
33
  yield* Effect.annotateCurrentSpan({
25
34
  runtime,
@@ -0,0 +1,50 @@
1
+ import { Effect, Ref, Semaphore } from "effect";
2
+
3
+ //#region src/internal/once.ts
4
+ /**
5
+ * A run-at-most-once gate for deferred layer population.
6
+ *
7
+ * The resolver layers are lazy: acquiring one performs no IO, and the feed
8
+ * fetch happens on the first `resolve`. This helper is the memoization behind
9
+ * that contract, chosen over `Effect.cached` deliberately — `cached` memoizes
10
+ * the *Exit*, so a transient network failure (or an interrupted first resolve)
11
+ * would poison the resolver for the layer's whole lifetime.
12
+ *
13
+ * Semantics, exactly:
14
+ *
15
+ * - **Success is memoized.** The first successful run flips the gate; every
16
+ * later and every concurrently-waiting caller skips the work.
17
+ * - **Concurrent first callers share one run.** A semaphore serializes entry;
18
+ * waiters re-check the gate under the permit and skip when the winner
19
+ * succeeded.
20
+ * - **Failure is not memoized.** The gate only flips after success, so a
21
+ * failed run leaves it open and the next caller retries. Same for an
22
+ * interrupted run — `withPermits` releases the permit on interruption.
23
+ *
24
+ * The effect's requirements are captured from the *acquiring* scope
25
+ * (`Effect.context`), so the gated effect needs nothing at call time — which
26
+ * is what lets a lazy `resolve` keep `R = never` while the layer still
27
+ * honestly advertises the strategy's requirements.
28
+ *
29
+ * @internal
30
+ */
31
+ /**
32
+ * Capture `effect`'s requirements now; return a gated effect that runs it at
33
+ * most once successfully, retrying after failure or interruption.
34
+ */
35
+ const once = (effect) => Effect.gen(function* () {
36
+ const context = yield* Effect.context();
37
+ const done = yield* Ref.make(false);
38
+ const run = (yield* Semaphore.make(1)).withPermits(1)(Effect.gen(function* () {
39
+ if (yield* Ref.get(done)) return;
40
+ yield* Effect.provideContext(effect, context);
41
+ yield* Ref.set(done, true);
42
+ }));
43
+ return Effect.gen(function* () {
44
+ if (yield* Ref.get(done)) return;
45
+ yield* run;
46
+ });
47
+ });
48
+
49
+ //#endregion
50
+ export { once };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/runtimes",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Resolve semver-compatible Node.js, Bun and Deno runtime versions with offline fallback.",
6
6
  "keywords": [