@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 +18 -3
- package/DenoResolver.js +18 -3
- package/NodeResolver.js +26 -9
- package/README.md +6 -4
- package/index.d.ts +53 -15
- package/internal/githubRuntime.js +15 -6
- package/internal/once.js +50 -0
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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
|
|
67
|
-
* layer's, so `layerOffline` genuinely requires nothing and
|
|
68
|
-
*
|
|
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
|
|
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 |
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
*
|
|
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.
|
|
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,
|
|
766
|
+
static readonly layerFresh: Layer.Layer<NodeResolver, never, HttpClient.HttpClient>;
|
|
730
767
|
/**
|
|
731
|
-
* The bundled snapshot only.
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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,
|
package/internal/once.js
ADDED
|
@@ -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 };
|