@tsdoctor/registry 0.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.
@@ -0,0 +1,95 @@
1
+ import { isTypeDefinition } from "./internal/resolution.js";
2
+ import { Effect, Schema } from "effect";
3
+
4
+ //#region src/TsEnvironment.ts
5
+ /**
6
+ * Raised when building a virtual TypeScript environment fails — including
7
+ * when the optional `typescript` / `@typescript/vfs` /
8
+ * `@effected/tsconfig-json` peers are not installed.
9
+ *
10
+ * @public
11
+ */
12
+ var TsEnvironmentError = class extends Schema.TaggedError()("TsEnvironmentError", {
13
+ /** The underlying failure, preserved structurally. */
14
+ cause: Schema.Defect() }) {
15
+ get message() {
16
+ return "Failed to create the virtual TypeScript environment";
17
+ }
18
+ };
19
+ /**
20
+ * The `@typescript/vfs` seam: builds a `VirtualTypeScriptEnvironment` over a
21
+ * {@link Vfs} plus the TypeScript default lib files.
22
+ *
23
+ * @remarks
24
+ * The ONLY module touching the optional `typescript` / `@typescript/vfs` /
25
+ * `@effected/tsconfig-json` peers, and it loads all three lazily inside
26
+ * {@link TsEnvironment.make} — a consumer that never calls it never loads
27
+ * the compiler, and a missing peer fails typed as
28
+ * {@link TsEnvironmentError} instead of crashing at import time. Keep every
29
+ * one of them behind that dynamic `import()`: a static value import here is
30
+ * reachable from `index.ts`, so it would turn an omitted optional peer into
31
+ * an `ERR_MODULE_NOT_FOUND` on the entry graph for consumers who never
32
+ * touch this module. Only the type-only `CompilerOptions` import is safe
33
+ * statically, because it erases. The underlying `createDefaultMapFromNodeModules` /
34
+ * `createFSBackedSystem` read the real filesystem through TypeScript's own
35
+ * `sys`, outside the Effect `FileSystem` service — accepted and documented;
36
+ * this module is why the package is integrated tier on its own surface.
37
+ *
38
+ * No cache map (v3's `createTypeScriptCache` returned a one-entry `Map`
39
+ * keyed by `JSON.stringify(compilerOptions)`): a consumer that wants keyed
40
+ * reuse holds its own map.
41
+ *
42
+ * `VirtualTypeScriptEnvironment` is deliberately not re-exported — import
43
+ * the type from `@typescript/vfs`, which consumers of this module already
44
+ * declare.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { TsEnvironment } from "@tsdoctor/registry";
49
+ *
50
+ * const environment = TsEnvironment.make({
51
+ * vfs,
52
+ * compilerOptions: { strict: true, target: "es2022" },
53
+ * });
54
+ * ```
55
+ *
56
+ * @public
57
+ */
58
+ var TsEnvironment = class {
59
+ constructor() {}
60
+ /** Build a `VirtualTypeScriptEnvironment` over a {@link Vfs}. */
61
+ static make(options) {
62
+ return Effect.gen(function* () {
63
+ const [tsModule, tsVfs, { TsEnumCodec }] = yield* Effect.tryPromise({
64
+ try: () => Promise.all([
65
+ import("typescript"),
66
+ import("@typescript/vfs"),
67
+ import("@effected/tsconfig-json")
68
+ ]),
69
+ catch: (cause) => new TsEnvironmentError({ cause })
70
+ });
71
+ return yield* Effect.try({
72
+ try: () => {
73
+ const typescript = tsModule.default;
74
+ const projectRoot = options.projectRoot ?? process.cwd();
75
+ const compilerOptions = TsEnumCodec.encodeCompilerOptions(options.compilerOptions);
76
+ const executing = typescript.sys?.getExecutingFilePath?.();
77
+ const libDirectory = executing === void 0 ? void 0 : executing.slice(0, Math.max(executing.lastIndexOf("/"), executing.lastIndexOf("\\")));
78
+ const system = new Map(tsVfs.createDefaultMapFromNodeModules(compilerOptions, typescript, libDirectory));
79
+ const rootFiles = [];
80
+ for (const [path, content] of options.vfs) {
81
+ const rooted = path.startsWith("/") ? path : `${projectRoot}/${path}`;
82
+ system.set(rooted, content);
83
+ if (isTypeDefinition(rooted)) rootFiles.push(rooted);
84
+ }
85
+ const sys = tsVfs.createFSBackedSystem(system, projectRoot, typescript, libDirectory);
86
+ return tsVfs.createVirtualTypeScriptEnvironment(sys, rootFiles, typescript, compilerOptions);
87
+ },
88
+ catch: (cause) => new TsEnvironmentError({ cause })
89
+ });
90
+ }).pipe(Effect.withSpan("TsEnvironment.make"));
91
+ }
92
+ };
93
+
94
+ //#endregion
95
+ export { TsEnvironment, TsEnvironmentError };
package/TypeCache.js ADDED
@@ -0,0 +1,256 @@
1
+ import { isSafeRelativePath } from "./internal/resolution.js";
2
+ import { PackageSpec } from "./PackageSpec.js";
3
+ import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
4
+ import { Cache } from "@effected/store";
5
+
6
+ //#region src/TypeCache.ts
7
+ /**
8
+ * Per-package cache metadata: the pinned version, when it was cached and how
9
+ * long it lives.
10
+ *
11
+ * @remarks
12
+ * Stored JSON-encoded in the metadata plane (`@effected/store`'s `Cache`);
13
+ * `ttl` is also forwarded to the store's native TTL so expiry happens there
14
+ * (evict-on-read, bulk prune). An absent `ttl` means the entry never expires.
15
+ *
16
+ * @public
17
+ */
18
+ var TypeCacheMetadata = class extends Schema.Class("TypeCacheMetadata")({
19
+ /** The pinned version the files on disk belong to. */
20
+ version: Schema.String,
21
+ /** When the package was cached. */
22
+ cachedAt: Schema.DateTimeUtcFromString,
23
+ /** Time-to-live; absent = never expires. */
24
+ ttl: Schema.optionalKey(Schema.DurationFromMillis)
25
+ }) {};
26
+ /** JSON codec for the metadata plane's stored bytes. */
27
+ const MetadataFromJson = Schema.fromJsonString(TypeCacheMetadata);
28
+ /**
29
+ * Raised when a cache operation fails: disk IO, metadata-store IO, or a file
30
+ * path that tries to escape the cache directory.
31
+ *
32
+ * @remarks
33
+ * `cause` carries the underlying failure structurally (a `PlatformError`,
34
+ * the store's `CacheError`, or a `SchemaError` from metadata decoding); v3
35
+ * flattened everything to `message: String(error)`.
36
+ *
37
+ * @public
38
+ */
39
+ var TypeCacheError = class extends Schema.TaggedError()("TypeCacheError", {
40
+ /** The cache operation that failed. */
41
+ operation: Schema.Literals([
42
+ "exists",
43
+ "read",
44
+ "write",
45
+ "list",
46
+ "readMetadata",
47
+ "writeMetadata",
48
+ "getVfs",
49
+ "remove",
50
+ "prune"
51
+ ]),
52
+ /** The file path or metadata key involved. */
53
+ path: Schema.String,
54
+ /** The underlying failure, preserved structurally. */
55
+ cause: Schema.Defect()
56
+ }) {
57
+ get message() {
58
+ return `Type cache ${this.operation} failed for "${this.path}"`;
59
+ }
60
+ };
61
+ const make = (cacheDir) => Effect.gen(function* () {
62
+ const fs = yield* FileSystem.FileSystem;
63
+ const cache = yield* Cache;
64
+ const path = yield* Path.Path;
65
+ const pkgDir = (pkg) => path.join(cacheDir, pkg.name, pkg.version);
66
+ const stagingDir = (pkg) => path.join(cacheDir, pkg.name, `.staging-${pkg.version}`);
67
+ const fail = (operation, target) => (cause) => new TypeCacheError({
68
+ operation,
69
+ path: target,
70
+ cause
71
+ });
72
+ const safeJoin = (operation, root, filePath) => isSafeRelativePath(filePath) ? Effect.succeed(path.join(root, filePath)) : Effect.fail(new TypeCacheError({
73
+ operation,
74
+ path: filePath,
75
+ cause: /* @__PURE__ */ new Error("path escapes the package cache directory")
76
+ }));
77
+ const safePath = (operation, pkg, filePath) => safeJoin(operation, pkgDir(pkg), filePath);
78
+ const listRecursive = (dir, relativeTo, depth) => Effect.gen(function* () {
79
+ if (depth > 256) return yield* Effect.fail(fail("list", dir)(/* @__PURE__ */ new Error("cache tree exceeds the nesting-depth limit")));
80
+ const entries = yield* fs.readDirectory(dir).pipe(Effect.mapError(fail("list", dir)));
81
+ const files = [];
82
+ for (const entry of entries) {
83
+ const fullPath = path.join(dir, entry);
84
+ if ((yield* fs.stat(fullPath).pipe(Effect.mapError(fail("list", fullPath)))).type === "Directory") files.push(...yield* listRecursive(fullPath, relativeTo, depth + 1));
85
+ else files.push(path.relative(relativeTo, fullPath));
86
+ }
87
+ return files;
88
+ });
89
+ return {
90
+ exists: Effect.fn("TypeCache.exists")(function* (pkg) {
91
+ return yield* fs.exists(pkgDir(pkg)).pipe(Effect.mapError(fail("exists", pkgDir(pkg))));
92
+ }),
93
+ read: Effect.fn("TypeCache.read")(function* (pkg, filePath) {
94
+ const fullPath = yield* safePath("read", pkg, filePath);
95
+ return yield* fs.readFileString(fullPath).pipe(Effect.mapError(fail("read", fullPath)));
96
+ }),
97
+ write: Effect.fn("TypeCache.write")(function* (pkg, filePath, content) {
98
+ const fullPath = yield* safePath("write", pkg, filePath);
99
+ const dirPath = path.dirname(fullPath);
100
+ yield* fs.makeDirectory(dirPath, { recursive: true }).pipe(Effect.mapError(fail("write", dirPath)));
101
+ yield* fs.writeFileString(fullPath, content).pipe(Effect.mapError(fail("write", fullPath)));
102
+ }),
103
+ writePackage: Effect.fn("TypeCache.writePackage")(function* (pkg, files) {
104
+ const staging = stagingDir(pkg);
105
+ const live = pkgDir(pkg);
106
+ yield* fs.remove(staging, {
107
+ recursive: true,
108
+ force: true
109
+ }).pipe(Effect.mapError(fail("write", staging)));
110
+ yield* fs.makeDirectory(staging, { recursive: true }).pipe(Effect.mapError(fail("write", staging)));
111
+ for (const [filePath, content] of files) {
112
+ const fullPath = yield* safeJoin("write", staging, filePath);
113
+ const dirPath = path.dirname(fullPath);
114
+ yield* fs.makeDirectory(dirPath, { recursive: true }).pipe(Effect.mapError(fail("write", dirPath)));
115
+ yield* fs.writeFileString(fullPath, content).pipe(Effect.mapError(fail("write", fullPath)));
116
+ }
117
+ yield* fs.remove(live, {
118
+ recursive: true,
119
+ force: true
120
+ }).pipe(Effect.mapError(fail("write", live)));
121
+ yield* fs.rename(staging, live).pipe(Effect.mapError(fail("write", live)));
122
+ }),
123
+ listFiles: Effect.fn("TypeCache.listFiles")(function* (pkg) {
124
+ return yield* listRecursive(pkgDir(pkg), pkgDir(pkg), 0);
125
+ }),
126
+ readMetadata: Effect.fn("TypeCache.readMetadata")(function* (pkg) {
127
+ const key = pkg.cacheKey;
128
+ const entry = yield* cache.get(key).pipe(Effect.mapError(fail("readMetadata", key)));
129
+ if (Option.isNone(entry)) return Option.none();
130
+ const decoded = yield* Schema.decodeUnknownEffect(MetadataFromJson)(new TextDecoder().decode(entry.value.value)).pipe(Effect.mapError(fail("readMetadata", key)));
131
+ return Option.some(decoded);
132
+ }),
133
+ writeMetadata: Effect.fn("TypeCache.writeMetadata")(function* (pkg, metadata) {
134
+ const key = pkg.cacheKey;
135
+ const encoded = yield* Schema.encodeEffect(MetadataFromJson)(metadata).pipe(Effect.mapError(fail("writeMetadata", key)));
136
+ yield* cache.set({
137
+ key,
138
+ value: new TextEncoder().encode(encoded),
139
+ contentType: "application/json",
140
+ tags: [pkg.name],
141
+ ...metadata.ttl !== void 0 ? { ttl: metadata.ttl } : {}
142
+ }).pipe(Effect.mapError(fail("writeMetadata", key)));
143
+ }),
144
+ getVfs: Effect.fn("TypeCache.getVfs")(function* (pkg) {
145
+ const dir = pkgDir(pkg);
146
+ const files = yield* listRecursive(dir, dir, 0);
147
+ const vfs = /* @__PURE__ */ new Map();
148
+ for (const file of files) {
149
+ const fullPath = path.join(dir, file);
150
+ const content = yield* fs.readFileString(fullPath).pipe(Effect.mapError(fail("getVfs", fullPath)));
151
+ vfs.set(`node_modules/${pkg.name}/${file.replace(/\\/g, "/")}`, content);
152
+ }
153
+ return vfs;
154
+ }),
155
+ remove: Effect.fn("TypeCache.remove")(function* (pkg) {
156
+ const key = pkg.cacheKey;
157
+ const dir = pkgDir(pkg);
158
+ yield* cache.invalidate(key).pipe(Effect.mapError(fail("remove", key)));
159
+ yield* fs.remove(dir, {
160
+ recursive: true,
161
+ force: true
162
+ }).pipe(Effect.mapError(fail("remove", dir)));
163
+ }),
164
+ prune: Effect.gen(function* () {
165
+ const result = yield* cache.prune().pipe(Effect.mapError(fail("prune", cacheDir)));
166
+ const removed = [];
167
+ for (const key of result.keys) {
168
+ const parsed = PackageSpec.parseCacheKey(key);
169
+ if (Option.isNone(parsed)) continue;
170
+ const dir = pkgDir(parsed.value);
171
+ if (yield* fs.remove(dir, {
172
+ recursive: true,
173
+ force: true
174
+ }).pipe(Effect.as(true), Effect.orElseSucceed(() => false))) removed.push({
175
+ name: parsed.value.name,
176
+ version: parsed.value.version
177
+ });
178
+ }
179
+ return {
180
+ count: result.count,
181
+ removed
182
+ };
183
+ }).pipe(Effect.withSpan("TypeCache.prune"))
184
+ };
185
+ });
186
+ /**
187
+ * The two-plane cache for fetched type definitions: files on disk under
188
+ * `<cacheDir>/<name>/<version>/`, metadata in `@effected/store`'s `Cache`
189
+ * with native TTL expiry.
190
+ *
191
+ * @remarks
192
+ * The layer statics are parameterized factories — bind the built layer to a
193
+ * `const` and provide that, or two provide sites mint two caches (the layer
194
+ * memoization discipline). The metadata plane is swappable in tests: store's
195
+ * `Cache.layerTest` (`:memory:`) satisfies {@link TypeCache.layer} with no
196
+ * real database file.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * import { TypeCache } from "@tsdoctor/registry";
201
+ *
202
+ * const TypeCacheLayer = TypeCache.layer({ cacheDir: "/var/cache/my-app/types" });
203
+ * ```
204
+ *
205
+ * @public
206
+ */
207
+ var TypeCache = class TypeCache extends Context.Service()("type-registry-effect/TypeCache") {
208
+ /**
209
+ * A cache rooted at an explicit directory.
210
+ *
211
+ * @remarks
212
+ * `cacheDir` must be an absolute path — a relative one is developer wiring
213
+ * and dies at layer construction.
214
+ */
215
+ static layer(options) {
216
+ return Layer.effect(TypeCache, Effect.gen(function* () {
217
+ if (!(yield* Path.Path).isAbsolute(options.cacheDir)) return yield* Effect.die(/* @__PURE__ */ new Error(`TypeCache.layer: cacheDir must be an absolute path, received "${options.cacheDir}"`));
218
+ return yield* make(options.cacheDir);
219
+ }));
220
+ }
221
+ /**
222
+ * A cache rooted under the application's XDG cache directory:
223
+ * `<AppDirs cache>/<namespace>/`.
224
+ *
225
+ * @remarks
226
+ * Uses `AppDirs.ensureCache`, which also discharges the store's recorded
227
+ * constraint that the database directory must exist before
228
+ * `SqliteClient.layer` is built. This package never builds the store layer
229
+ * itself — the consumer composes `Cache.layerSqlite` (or `layerTest`) at
230
+ * the edge.
231
+ */
232
+ static layerXdg(options) {
233
+ const namespace = options?.namespace ?? "ts-vfs";
234
+ return Layer.effect(TypeCache, Effect.gen(function* () {
235
+ if (namespace.length === 0 || /[/\\]/.test(namespace) || namespace === "." || namespace === "..") return yield* Effect.die(/* @__PURE__ */ new Error(`TypeCache.layerXdg: \`namespace\` must be a single path component, received ${JSON.stringify(namespace)}`));
236
+ const xdg = yield* Effect.tryPromise({
237
+ try: () => import("@effected/xdg"),
238
+ catch: (cause) => new Error("TypeCache.layerXdg requires the optional `@effected/xdg` peer to be installed", { cause })
239
+ }).pipe(Effect.orDie);
240
+ const appDirs = yield* xdg.AppDirs;
241
+ const fs = yield* FileSystem.FileSystem;
242
+ const path = yield* Path.Path;
243
+ const base = yield* appDirs.ensureCache;
244
+ const cacheDir = path.join(base, namespace);
245
+ yield* fs.makeDirectory(cacheDir, { recursive: true }).pipe(Effect.mapError((cause) => new xdg.AppDirsError({
246
+ directory: "cache",
247
+ path: cacheDir,
248
+ cause
249
+ })));
250
+ return yield* make(cacheDir);
251
+ }));
252
+ }
253
+ };
254
+
255
+ //#endregion
256
+ export { TypeCache, TypeCacheError, TypeCacheMetadata };
@@ -0,0 +1,276 @@
1
+ import { packageJsonUrl } from "./internal/jsdelivr.js";
2
+ import { emit } from "./RegistryEvent.js";
3
+ import { FetchError, PackageFetcher, PackageManifest, PackageNotFoundError, VersionNotFoundError } from "./PackageFetcher.js";
4
+ import { TypeCache, TypeCacheMetadata } from "./TypeCache.js";
5
+ import { TypeResolver } from "./TypeResolver.js";
6
+ import { mergeVfs } from "./Vfs.js";
7
+ import { Context, DateTime, Effect, Layer, Option, Schema, Semaphore } from "effect";
8
+ import { Range, SemVer } from "@effected/semver";
9
+
10
+ //#region src/TypeRegistry.ts
11
+ /**
12
+ * Raised by {@link TypeRegistryShape.getVfs} when **every** requested package
13
+ * fails.
14
+ *
15
+ * @remarks
16
+ * Carries the per-package failures structurally. v3 abused
17
+ * `PackageNotFoundError` for this case, with a comma-joined `name` and an
18
+ * empty `version`.
19
+ *
20
+ * @public
21
+ */
22
+ var BatchLoadError = class extends Schema.TaggedError()("BatchLoadError", {
23
+ /** One entry per failed package, with its typed error preserved. */
24
+ failures: Schema.Array(Schema.Struct({
25
+ name: Schema.String,
26
+ version: Schema.String,
27
+ error: Schema.Defect()
28
+ })) }) {
29
+ get message() {
30
+ return `Failed to load type definitions for all ${this.failures.length} requested package(s)`;
31
+ }
32
+ };
33
+ /**
34
+ * Classify a per-package load failure into a stable `PackageLoadFailed`
35
+ * event kind — from typed error tags and structured fields, never from
36
+ * message substrings (v3's `classifyLoadError` did substring matching over
37
+ * stringified errors; it is dead).
38
+ */
39
+ const classify = (error) => {
40
+ if (typeof error !== "object" || error === null || !("_tag" in error)) return "unknown";
41
+ switch (error._tag) {
42
+ case "PackageNotFoundError": return "not-found";
43
+ case "VersionNotFoundError": return "version-range";
44
+ case "TypeCacheError": return "cache";
45
+ case "FetchError": {
46
+ const fetchError = error;
47
+ if (fetchError.status === 404) return "not-found";
48
+ if (fetchError.kind === "schema") return "schema";
49
+ return "network";
50
+ }
51
+ default: return "unknown";
52
+ }
53
+ };
54
+ const make = Effect.gen(function* () {
55
+ const cache = yield* TypeCache;
56
+ const fetcher = yield* PackageFetcher;
57
+ const mutations = yield* Semaphore.make(1);
58
+ const fetchAndCacheImpl = (pkg, options) => Effect.gen(function* () {
59
+ yield* emit({
60
+ _tag: "FetchStart",
61
+ package: pkg.name,
62
+ version: pkg.version
63
+ });
64
+ const manifest = yield* fetcher.getPackageJson(pkg);
65
+ const typeFiles = yield* fetcher.getTypeFiles(pkg);
66
+ const files = [["package.json", JSON.stringify(manifest, null, 2)]];
67
+ for (const [filePath, content] of typeFiles) {
68
+ const normalized = filePath.replace(/^\/+/, "");
69
+ if (normalized !== "package.json") files.push([normalized, content]);
70
+ }
71
+ const now = yield* DateTime.now;
72
+ yield* mutations.withPermits(1)(Effect.gen(function* () {
73
+ yield* cache.writePackage(pkg, files);
74
+ yield* cache.writeMetadata(pkg, TypeCacheMetadata.make({
75
+ version: pkg.version,
76
+ cachedAt: now,
77
+ ...options?.ttl !== void 0 ? { ttl: options.ttl } : {}
78
+ }));
79
+ }));
80
+ });
81
+ const getPackageVfsImpl = (pkg, options) => Effect.gen(function* () {
82
+ const autoFetch = options?.autoFetch ?? true;
83
+ const [duration, result] = yield* Effect.timed(Effect.gen(function* () {
84
+ const metadata = yield* cache.readMetadata(pkg);
85
+ const diskExists = yield* cache.exists(pkg);
86
+ let source = "cache";
87
+ if (Option.isSome(metadata) && diskExists) {
88
+ const now = yield* DateTime.now;
89
+ yield* emit({
90
+ _tag: "CacheHit",
91
+ package: pkg.name,
92
+ version: pkg.version,
93
+ age: DateTime.distance(metadata.value.cachedAt, now)
94
+ });
95
+ } else if (Option.isNone(metadata) && diskExists) {
96
+ yield* emit({
97
+ _tag: "CacheStale",
98
+ package: pkg.name,
99
+ version: pkg.version
100
+ });
101
+ if (autoFetch) {
102
+ yield* fetchAndCacheImpl(pkg, options);
103
+ source = "network";
104
+ }
105
+ } else if (autoFetch) {
106
+ yield* emit({
107
+ _tag: "CacheMiss",
108
+ package: pkg.name,
109
+ version: pkg.version
110
+ });
111
+ yield* fetchAndCacheImpl(pkg, options);
112
+ source = "network";
113
+ } else return yield* Effect.fail(new PackageNotFoundError({
114
+ name: pkg.name,
115
+ version: pkg.version
116
+ }));
117
+ return {
118
+ vfs: yield* cache.getVfs(pkg),
119
+ source
120
+ };
121
+ }));
122
+ yield* emit({
123
+ _tag: "PackageLoaded",
124
+ package: pkg.name,
125
+ version: pkg.version,
126
+ files: result.vfs.size,
127
+ source: result.source,
128
+ duration
129
+ });
130
+ return result.vfs;
131
+ });
132
+ const readManifest = (pkg) => Effect.gen(function* () {
133
+ const content = yield* cache.read(pkg, "package.json");
134
+ const parsed = yield* Effect.try({
135
+ try: () => JSON.parse(content),
136
+ catch: (cause) => new FetchError({
137
+ url: packageJsonUrl(pkg),
138
+ kind: "schema",
139
+ cause
140
+ })
141
+ });
142
+ return yield* Schema.decodeUnknownEffect(PackageManifest)(parsed).pipe(Effect.mapError((cause) => new FetchError({
143
+ url: packageJsonUrl(pkg),
144
+ kind: "schema",
145
+ cause
146
+ })));
147
+ });
148
+ return {
149
+ hasCached: Effect.fn("TypeRegistry.hasCached")(function* (pkg) {
150
+ return yield* cache.exists(pkg);
151
+ }),
152
+ fetchAndCache: Effect.fn("TypeRegistry.fetchAndCache")(function* (pkg, options) {
153
+ return yield* fetchAndCacheImpl(pkg, options);
154
+ }),
155
+ getPackageVfs: Effect.fn("TypeRegistry.getPackageVfs")(function* (pkg, options) {
156
+ return yield* getPackageVfsImpl(pkg, options);
157
+ }),
158
+ getVfs: Effect.fn("TypeRegistry.getVfs")(function* (packages, options) {
159
+ yield* emit({
160
+ _tag: "BatchStart",
161
+ total: packages.length,
162
+ packages: packages.map((pkg) => pkg.toString())
163
+ });
164
+ const [duration, results] = yield* Effect.timed(Effect.forEach(packages, (pkg) => getPackageVfsImpl(pkg, options).pipe(Effect.map((vfs) => ({
165
+ ok: true,
166
+ pkg,
167
+ vfs
168
+ })), Effect.catch((error) => emit({
169
+ _tag: "PackageLoadFailed",
170
+ package: pkg.name,
171
+ version: pkg.version,
172
+ kind: classify(error),
173
+ error
174
+ }).pipe(Effect.as({
175
+ ok: false,
176
+ pkg,
177
+ error
178
+ })))), { concurrency: 5 }));
179
+ const succeeded = results.filter((result) => result.ok);
180
+ const failed = results.filter((result) => !result.ok);
181
+ const merged = mergeVfs(...succeeded.map((result) => result.vfs));
182
+ yield* emit({
183
+ _tag: "BatchComplete",
184
+ loaded: succeeded.length,
185
+ failed: failed.length,
186
+ total: packages.length,
187
+ totalFiles: merged.size,
188
+ duration
189
+ });
190
+ if (failed.length === packages.length && packages.length > 0) return yield* Effect.fail(new BatchLoadError({ failures: failed.map((result) => ({
191
+ name: result.pkg.name,
192
+ version: result.pkg.version,
193
+ error: result.error
194
+ })) }));
195
+ return merged;
196
+ }),
197
+ resolveImport: Effect.fn("TypeRegistry.resolveImport")(function* (pkg, specifier) {
198
+ const manifest = yield* readManifest(pkg);
199
+ return TypeResolver.resolveImport(specifier, manifest, pkg);
200
+ }),
201
+ getTypeEntries: Effect.fn("TypeRegistry.getTypeEntries")(function* (pkg) {
202
+ const manifest = yield* readManifest(pkg);
203
+ return TypeResolver.resolveTypeEntries(manifest, pkg);
204
+ }),
205
+ resolveVersion: Effect.fn("TypeRegistry.resolveVersion")(function* (name, ref) {
206
+ const resolved = yield* Effect.gen(function* () {
207
+ const meta = yield* fetcher.getVersions(name);
208
+ if (Object.hasOwn(meta.tags, ref)) {
209
+ const tagged = meta.tags[ref];
210
+ if (typeof tagged === "string") return tagged;
211
+ }
212
+ if (meta.versions.includes(ref)) return ref;
213
+ const range = yield* Range.parse(ref).pipe(Effect.mapError(() => new VersionNotFoundError({
214
+ name,
215
+ ref,
216
+ available: meta.versions.slice(0, 20)
217
+ })));
218
+ const published = yield* Effect.forEach(meta.versions, (version) => SemVer.parse(version).pipe(Effect.option));
219
+ const best = Range.maxSatisfying(published.filter(Option.isSome).map((option) => option.value), range);
220
+ if (Option.isNone(best)) return yield* Effect.fail(new VersionNotFoundError({
221
+ name,
222
+ ref,
223
+ available: meta.versions.slice(0, 20)
224
+ }));
225
+ return best.value.toString();
226
+ }).pipe(Effect.tapError((error) => emit({
227
+ _tag: "VersionResolveFailed",
228
+ package: name,
229
+ requested: ref,
230
+ kind: error._tag === "VersionNotFoundError" ? "no-match" : error.status === 404 ? "not-found" : "network"
231
+ })));
232
+ yield* emit({
233
+ _tag: "VersionResolved",
234
+ package: name,
235
+ requested: ref,
236
+ resolved
237
+ });
238
+ return resolved;
239
+ }),
240
+ clearCache: Effect.fn("TypeRegistry.clearCache")(function* (pkg) {
241
+ return yield* mutations.withPermits(1)(cache.remove(pkg));
242
+ }),
243
+ pruneCache: mutations.withPermits(1)(cache.prune).pipe(Effect.withSpan("TypeRegistry.pruneCache"))
244
+ };
245
+ });
246
+ /**
247
+ * The facade: one service collapsing the cache, fetcher and resolver behind
248
+ * the operations documentation tooling actually calls.
249
+ *
250
+ * @remarks
251
+ * `yield* TypeRegistry` replaces the v3 floating-function namespace (which
252
+ * the rspress consumer immediately re-wrapped in its own service). Per-method
253
+ * error unions stay precise. Compose at the edge: platform layers + store
254
+ * `Cache.layerSqlite` + `TypeCache.layerXdg` + `PackageFetcher.layer` +
255
+ * `TypeRegistry.layer`.
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * import { PackageSpec, TypeRegistry } from "@tsdoctor/registry";
260
+ * import { Effect } from "effect";
261
+ *
262
+ * const program = Effect.gen(function* () {
263
+ * const registry = yield* TypeRegistry;
264
+ * return yield* registry.getVfs([PackageSpec.fromString("zod@3.23.8")]);
265
+ * });
266
+ * ```
267
+ *
268
+ * @public
269
+ */
270
+ var TypeRegistry = class TypeRegistry extends Context.Service()("type-registry-effect/TypeRegistry") {
271
+ /** The live facade over {@link TypeCache} and {@link PackageFetcher}. */
272
+ static layer = Layer.effect(TypeRegistry, make);
273
+ };
274
+
275
+ //#endregion
276
+ export { BatchLoadError, TypeRegistry };