@wavehouse/chtypes 0.5.2 → 1.0.2

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.
Files changed (195) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +34 -38
  3. package/dist/abi1/buildinfo.d.ts +40 -0
  4. package/dist/abi1/buildinfo.d.ts.map +1 -0
  5. package/dist/abi1/buildinfo.js +83 -0
  6. package/dist/abi1/buildinfo.js.map +1 -0
  7. package/dist/abi1/calls.gen.d.ts +55 -0
  8. package/dist/abi1/calls.gen.d.ts.map +1 -0
  9. package/dist/abi1/calls.gen.js +110 -0
  10. package/dist/abi1/calls.gen.js.map +1 -0
  11. package/dist/abi1/decls.gen.d.ts +115 -0
  12. package/dist/abi1/decls.gen.d.ts.map +1 -0
  13. package/dist/abi1/decls.gen.js +1613 -0
  14. package/dist/abi1/decls.gen.js.map +1 -0
  15. package/dist/abi1/errmap.gen.d.ts +18 -0
  16. package/dist/abi1/errmap.gen.d.ts.map +1 -0
  17. package/dist/abi1/errmap.gen.js +66 -0
  18. package/dist/abi1/errmap.gen.js.map +1 -0
  19. package/dist/abi1/errors.d.ts +107 -0
  20. package/dist/abi1/errors.d.ts.map +1 -0
  21. package/dist/abi1/errors.js +134 -0
  22. package/dist/abi1/errors.js.map +1 -0
  23. package/dist/abi1/handles.d.ts +31 -0
  24. package/dist/abi1/handles.d.ts.map +1 -0
  25. package/dist/abi1/handles.js +76 -0
  26. package/dist/abi1/handles.js.map +1 -0
  27. package/dist/abi1/index.d.ts +17 -0
  28. package/dist/abi1/index.d.ts.map +1 -0
  29. package/dist/abi1/index.js +17 -0
  30. package/dist/abi1/index.js.map +1 -0
  31. package/dist/abi1/libc.d.ts +54 -0
  32. package/dist/abi1/libc.d.ts.map +1 -0
  33. package/dist/abi1/libc.gen.d.ts +12 -0
  34. package/dist/abi1/libc.gen.d.ts.map +1 -0
  35. package/dist/abi1/libc.gen.js +75 -0
  36. package/dist/abi1/libc.gen.js.map +1 -0
  37. package/dist/abi1/libc.js +109 -0
  38. package/dist/abi1/libc.js.map +1 -0
  39. package/dist/abi1/loader.d.ts +101 -0
  40. package/dist/abi1/loader.d.ts.map +1 -0
  41. package/dist/abi1/loader.js +274 -0
  42. package/dist/abi1/loader.js.map +1 -0
  43. package/dist/abi1/raw.d.ts +104 -0
  44. package/dist/abi1/raw.d.ts.map +1 -0
  45. package/dist/abi1/raw.js +296 -0
  46. package/dist/abi1/raw.js.map +1 -0
  47. package/dist/abi1/strictjson.d.ts +19 -0
  48. package/dist/abi1/strictjson.d.ts.map +1 -0
  49. package/dist/abi1/strictjson.js +78 -0
  50. package/dist/abi1/strictjson.js.map +1 -0
  51. package/dist/abi1/vocab.gen.d.ts +169 -0
  52. package/dist/abi1/vocab.gen.d.ts.map +1 -0
  53. package/dist/abi1/vocab.gen.js +306 -0
  54. package/dist/abi1/vocab.gen.js.map +1 -0
  55. package/dist/cli.d.ts +25 -26
  56. package/dist/cli.d.ts.map +1 -1
  57. package/dist/cli.js +206 -218
  58. package/dist/cli.js.map +1 -1
  59. package/dist/documents.d.ts +195 -0
  60. package/dist/documents.d.ts.map +1 -0
  61. package/dist/documents.js +389 -0
  62. package/dist/documents.js.map +1 -0
  63. package/dist/env.d.ts +16 -0
  64. package/dist/env.d.ts.map +1 -0
  65. package/dist/env.js +57 -0
  66. package/dist/env.js.map +1 -0
  67. package/dist/index.d.ts +19 -24
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +17 -23
  70. package/dist/index.js.map +1 -1
  71. package/dist/json.d.ts +11 -89
  72. package/dist/json.d.ts.map +1 -1
  73. package/dist/json.js +16 -180
  74. package/dist/json.js.map +1 -1
  75. package/dist/library.d.ts +53 -309
  76. package/dist/library.d.ts.map +1 -1
  77. package/dist/library.js +77 -315
  78. package/dist/library.js.map +1 -1
  79. package/dist/ocifetch/constants.gen.d.ts +84 -0
  80. package/dist/ocifetch/constants.gen.d.ts.map +1 -0
  81. package/dist/ocifetch/constants.gen.js +92 -0
  82. package/dist/ocifetch/constants.gen.js.map +1 -0
  83. package/dist/ocifetch/dsse.d.ts +114 -0
  84. package/dist/ocifetch/dsse.d.ts.map +1 -0
  85. package/dist/ocifetch/dsse.js +331 -0
  86. package/dist/ocifetch/dsse.js.map +1 -0
  87. package/dist/ocifetch/ensure.d.ts +65 -0
  88. package/dist/ocifetch/ensure.d.ts.map +1 -0
  89. package/dist/ocifetch/ensure.js +775 -0
  90. package/dist/ocifetch/ensure.js.map +1 -0
  91. package/dist/ocifetch/errors.d.ts +92 -0
  92. package/dist/ocifetch/errors.d.ts.map +1 -0
  93. package/dist/ocifetch/errors.js +101 -0
  94. package/dist/ocifetch/errors.js.map +1 -0
  95. package/dist/ocifetch/goldens.d.ts +44 -0
  96. package/dist/ocifetch/goldens.d.ts.map +1 -0
  97. package/dist/ocifetch/goldens.js +62 -0
  98. package/dist/ocifetch/goldens.js.map +1 -0
  99. package/dist/ocifetch/http.d.ts +124 -0
  100. package/dist/ocifetch/http.d.ts.map +1 -0
  101. package/dist/ocifetch/http.js +549 -0
  102. package/dist/ocifetch/http.js.map +1 -0
  103. package/dist/ocifetch/index.d.ts +17 -0
  104. package/dist/ocifetch/index.d.ts.map +1 -0
  105. package/dist/ocifetch/index.js +14 -0
  106. package/dist/ocifetch/index.js.map +1 -0
  107. package/dist/ocifetch/layout.d.ts +126 -0
  108. package/dist/ocifetch/layout.d.ts.map +1 -0
  109. package/dist/ocifetch/layout.js +375 -0
  110. package/dist/ocifetch/layout.js.map +1 -0
  111. package/dist/ocifetch/localverify.d.ts +43 -0
  112. package/dist/ocifetch/localverify.d.ts.map +1 -0
  113. package/dist/ocifetch/localverify.js +164 -0
  114. package/dist/ocifetch/localverify.js.map +1 -0
  115. package/dist/ocifetch/lock.d.ts +39 -0
  116. package/dist/ocifetch/lock.d.ts.map +1 -0
  117. package/dist/ocifetch/lock.js +140 -0
  118. package/dist/ocifetch/lock.js.map +1 -0
  119. package/dist/ocifetch/oci.d.ts +100 -0
  120. package/dist/ocifetch/oci.d.ts.map +1 -0
  121. package/dist/ocifetch/oci.js +323 -0
  122. package/dist/ocifetch/oci.js.map +1 -0
  123. package/dist/ocifetch/referrers.d.ts +55 -0
  124. package/dist/ocifetch/referrers.d.ts.map +1 -0
  125. package/dist/ocifetch/referrers.js +132 -0
  126. package/dist/ocifetch/referrers.js.map +1 -0
  127. package/dist/ocifetch/types.d.ts +167 -0
  128. package/dist/ocifetch/types.d.ts.map +1 -0
  129. package/dist/ocifetch/types.js +88 -0
  130. package/dist/ocifetch/types.js.map +1 -0
  131. package/dist/ocifetch/unpack.d.ts +55 -0
  132. package/dist/ocifetch/unpack.d.ts.map +1 -0
  133. package/dist/ocifetch/unpack.js +205 -0
  134. package/dist/ocifetch/unpack.js.map +1 -0
  135. package/dist/registry.d.ts +38 -387
  136. package/dist/registry.d.ts.map +1 -1
  137. package/dist/registry.js +95 -819
  138. package/dist/registry.js.map +1 -1
  139. package/dist/schema.d.ts +77 -588
  140. package/dist/schema.d.ts.map +1 -1
  141. package/dist/schema.js +87 -549
  142. package/dist/schema.js.map +1 -1
  143. package/dist/settings.d.ts +27 -36
  144. package/dist/settings.d.ts.map +1 -1
  145. package/dist/settings.js +75 -25
  146. package/dist/settings.js.map +1 -1
  147. package/dist/setup.d.ts +46 -0
  148. package/dist/setup.d.ts.map +1 -0
  149. package/dist/setup.js +79 -0
  150. package/dist/setup.js.map +1 -0
  151. package/dist/tar.d.ts +27 -11
  152. package/dist/tar.d.ts.map +1 -1
  153. package/dist/tar.js +54 -32
  154. package/dist/tar.js.map +1 -1
  155. package/package.json +2 -2
  156. package/dist/discover.d.ts +0 -142
  157. package/dist/discover.d.ts.map +0 -1
  158. package/dist/discover.js +0 -288
  159. package/dist/discover.js.map +0 -1
  160. package/dist/error-codes.d.ts +0 -80
  161. package/dist/error-codes.d.ts.map +0 -1
  162. package/dist/error-codes.js +0 -139
  163. package/dist/error-codes.js.map +0 -1
  164. package/dist/errors.d.ts +0 -284
  165. package/dist/errors.d.ts.map +0 -1
  166. package/dist/errors.js +0 -321
  167. package/dist/errors.js.map +0 -1
  168. package/dist/fetch.d.ts +0 -382
  169. package/dist/fetch.d.ts.map +0 -1
  170. package/dist/fetch.js +0 -1498
  171. package/dist/fetch.js.map +0 -1
  172. package/dist/ffi.d.ts +0 -410
  173. package/dist/ffi.d.ts.map +0 -1
  174. package/dist/ffi.js +0 -1154
  175. package/dist/ffi.js.map +0 -1
  176. package/dist/format.d.ts +0 -128
  177. package/dist/format.d.ts.map +0 -1
  178. package/dist/format.js +0 -132
  179. package/dist/format.js.map +0 -1
  180. package/dist/numeric.d.ts +0 -20
  181. package/dist/numeric.d.ts.map +0 -1
  182. package/dist/numeric.js +0 -107
  183. package/dist/numeric.js.map +0 -1
  184. package/dist/paths.d.ts +0 -55
  185. package/dist/paths.d.ts.map +0 -1
  186. package/dist/paths.js +0 -87
  187. package/dist/paths.js.map +0 -1
  188. package/dist/results.d.ts +0 -495
  189. package/dist/results.d.ts.map +0 -1
  190. package/dist/results.js +0 -406
  191. package/dist/results.js.map +0 -1
  192. package/dist/transform.d.ts +0 -73
  193. package/dist/transform.d.ts.map +0 -1
  194. package/dist/transform.js +0 -533
  195. package/dist/transform.js.map +0 -1
package/dist/registry.js CHANGED
@@ -1,836 +1,112 @@
1
1
  /**
2
- * The artifact-directory loader: one subdirectory per ClickHouse minor line,
3
- * each self-contained (docs/reference/artifact.md), plus — since #284 — every
4
- * OTHER installed exact patch of a line, in a sibling `patches/` tree:
2
+ * `Registry`: from a version request to a loaded `Library`
3
+ * (`docs/reference/bindings-v1.md` §6, "The sequence").
5
4
  *
6
- * <registry>/25.8/{manifest.json, libchtypes.dylib, CH_VERSION, unsafe_families.txt}
7
- * <registry>/patches/25.8/25.8.28.1-lts/{manifest.json, libchtypes.dylib, ...}
5
+ * registry.for(request)
6
+ * 1. the registry's memo: a request opened before returns the same Library,
7
+ * for the registry's life (a line request never moves mid-process: a
8
+ * newer patch installed later is picked up by a new registry)
9
+ * 2. resolved = resolveInstalled(request, host platform, fetch options) never the network
10
+ * 3. on a miss: autofetch on -> resolved = ensure(request, fetch options) may use the network
11
+ * autofetch off -> ArtifactMissingError, naming request and platform
12
+ * 4. input = adapt(resolved): the library path, the predicate VERBATIM (never
13
+ * re-encoded: a re-encoding is a second derivation of the statement the
14
+ * signature covered) and the platform
15
+ * 5. loader steps 1-6, once per image; step 7 under the process setup
16
+ * 6. the image's Library, whose `resolved` is the record that first opened it
8
17
  *
9
- * The flat `<registry>/<minor>/` slot is what a LINE request resolves to —
10
- * exactly the pre-#284 layout, so every SDK through 0.4.x keeps reading it
11
- * unchanged. Any OTHER exact patch of that line lives in
12
- * `<registry>/patches/<minor>/<clickhouse_version>/`, which no released SDK
13
- * (0.4.x and earlier) scans, fetches into or deletes (measured, issue #284
14
- * comment 5919199794): it is a new, additive tree, not a migration of the old
15
- * one. When a line fetch changes which patch occupies the flat slot, the
16
- * outgoing install is DEMOTED — an atomic, same-filesystem rename into
17
- * `patches/<minor>/<its version>/` — never deleted, so a server still on the
18
- * older patch keeps its exact match with no re-fetch.
19
- *
20
- * Two rules here were paid for and are not negotiable:
21
- *
22
- * - **The shared library's file name comes from `manifest.json`'s `library`
23
- * field.** Not from a hard-coded `libchtypes.so`, not from a glob, not from
24
- * the platform. The current tree proves why: every darwin artifact says
25
- * `libchtypes.dylib` while the *shipping* linux artifacts still say
26
- * `libchtypes_s1.so`, so a loader that constructs the name finds nothing on
27
- * the platform that matters.
28
- * - **Each artifact is loaded into its own symbol scope (`RTLD_LOCAL`).** That
29
- * is the entire mechanism by which two builds that both define
30
- * `DB::DataTypeFactory` live in one process — now routinely two builds of
31
- * the SAME minor line, one per patch. ffi-rs loads through libloading,
32
- * which uses `RTLD_LAZY | RTLD_LOCAL`; `assertLocalSymbolScope()` in the
33
- * test suite proves it from outside rather than trusting the claim.
34
- *
35
- * Where a registry IS follows the search path of docs/guides/fetch.md §1 (`paths.ts`):
36
- * the explicit directory, `CHTYPES_REGISTRY`, the per-user cache, then the
37
- * reserved system locations. Construction READS THE MANIFESTS on that path and
38
- * `dlopen`s nothing; a version is taken, on request, from the first directory
39
- * that has it. Resolution (docs/reference/bindings.md §Version selection):
40
- *
41
- * - **A line request** ("25.8") never falls back and never crosses lines: the
42
- * newest patch of the line, in the first search-path directory that holds
43
- * any patch of it (a nested install beating a flat one on a version tie),
44
- * pinned for this registry for as long as it runs.
45
- * - **A patch request** ("25.8.28.1-lts") loads that exact patch when it is
46
- * open or installed anywhere on the search path. Otherwise it falls back to
47
- * the newest installed patch of the same line, flags the result
48
- * `exact: false`, and warns once per (requested, actual) pair per process —
49
- * never another line, which stays the one §7 error, `ArtifactMissingError`.
50
- *
51
- * A patch spelled with no channel suffix matches that patch on any channel
52
- * (docs/guides/fetch.md Decision 7); ordering is numeric, channel ignored.
18
+ * Construction opens nothing: `Registry.open` is asynchronous only because the
19
+ * fetch layer is, and nothing opens an artifact except a request for a version
20
+ * or `preload`. The binding orders and matches no versions itself: which
21
+ * installed build a floating request means is the fetch layer's.
53
22
  */
54
- import { createHash } from 'node:crypto';
55
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
56
- import path from 'node:path';
57
- import { ABI_REVISION, ArtifactMissingError, ArtifactUnpublishedError, FETCH_COMMAND, RegistryError } from './errors.js';
58
- import { compareVersions, ensure, parseVersionSpelling, patchMatches } from './fetch.js';
59
- import { NativeLibrary } from './ffi.js';
60
- import { Library, minorOf } from './library.js';
61
- import { cacheRegistryDir, ENV_AUTOFETCH, ENV_REGISTRY, fetchDestination, hostPlatform, registrySearchPath, systemRegistryDirs, } from './paths.js';
62
- /** Every patch (flat + `patches/`) a search-path root holds, whatever line it claims. */
63
- function flatLocationsInRoot(root, rootIndex) {
64
- const out = [];
65
- if (!isDirectory(root))
66
- return out;
67
- let entries;
68
- try {
69
- entries = readdirSync(root);
70
- }
71
- catch {
72
- return out;
73
- }
74
- for (const entry of entries.sort()) {
75
- // A dot-directory is never a version (fetch stages downloads in hidden
76
- // siblings), and `patches/` is the sibling tree, never a line itself.
77
- if (entry.startsWith('.') || entry === 'patches')
78
- continue;
79
- const sub = path.join(root, entry);
80
- if (!isDirectory(sub))
81
- continue;
82
- const manifest = readManifest(sub);
83
- if (manifest === null)
84
- continue;
85
- const claimedMinor = manifest.clickhouse_minor ?? minorOf(manifest.clickhouse_version ?? entry);
86
- const minor = claimedMinor !== '' ? claimedMinor : entry;
87
- const version = manifest.clickhouse_version !== undefined && manifest.clickhouse_version !== '' ? manifest.clickhouse_version : entry;
88
- out.push({ minor, version, dir: sub, flat: true, rootIndex });
89
- }
90
- return out;
91
- }
92
- /** Every patch under `<root>/patches/*\/*\/`. */
93
- function nestedLocationsInRoot(root, rootIndex) {
94
- const out = [];
95
- const patchesRoot = path.join(root, 'patches');
96
- if (!isDirectory(patchesRoot))
97
- return out;
98
- let minorEntries;
99
- try {
100
- minorEntries = readdirSync(patchesRoot);
101
- }
102
- catch {
103
- return out;
104
- }
105
- for (const minorEntry of minorEntries.sort()) {
106
- if (minorEntry.startsWith('.'))
107
- continue;
108
- const minorDir = path.join(patchesRoot, minorEntry);
109
- if (!isDirectory(minorDir))
110
- continue;
111
- let versionEntries;
112
- try {
113
- versionEntries = readdirSync(minorDir);
114
- }
115
- catch {
116
- continue;
117
- }
118
- for (const versionEntry of versionEntries.sort()) {
119
- if (versionEntry.startsWith('.'))
120
- continue;
121
- const sub = path.join(minorDir, versionEntry);
122
- if (!isDirectory(sub))
123
- continue;
124
- const manifest = readManifest(sub);
125
- if (manifest === null)
126
- continue;
127
- const claimedMinor = manifest.clickhouse_minor ?? minorOf(manifest.clickhouse_version ?? minorEntry);
128
- const minor = claimedMinor !== '' ? claimedMinor : minorEntry;
129
- const version = manifest.clickhouse_version !== undefined && manifest.clickhouse_version !== '' ? manifest.clickhouse_version : versionEntry;
130
- out.push({ minor, version, dir: sub, flat: false, rootIndex });
131
- }
132
- }
133
- return out;
134
- }
135
- /** Every patch (both slots) a root holds. */
136
- function locationsInRoot(root, rootIndex) {
137
- return [...flatLocationsInRoot(root, rootIndex), ...nestedLocationsInRoot(root, rootIndex)];
23
+ import os from 'node:os';
24
+ import { openAbi1 } from './abi1/index.js';
25
+ import { usageError } from './abi1/index.js';
26
+ import { libraryOf } from './library.js';
27
+ import { ArtifactMissingError, ensure, hostPlatformKey, listInstalled, resolveInstalled, } from './ocifetch/index.js';
28
+ import { ENV_AUTOFETCH_NAME, SPELLING_REGEX } from './ocifetch/constants.gen.js';
29
+ import { withEnvironment } from './env.js';
30
+ import { commitSetup, settleSetup } from './setup.js';
31
+ function envFlag(name) {
32
+ const v = process.env[name];
33
+ return v !== undefined && ['1', 'true', 'yes', 'on'].includes(v.trim().toLowerCase());
138
34
  }
139
- /** R3/R4: the newest patch, among locations from the FIRST root that has any — ties: nested beats flat. */
140
- function pickLineWinner(locs) {
141
- if (locs.length === 0)
142
- return undefined;
143
- const firstRoot = Math.min(...locs.map((l) => l.rootIndex));
144
- const candidates = locs.filter((l) => l.rootIndex === firstRoot);
145
- let best = candidates[0];
146
- for (const cur of candidates.slice(1)) {
147
- const cmp = compareVersions(cur.version, best.version);
148
- if (cmp > 0 || (cmp === 0 && !cur.flat && best.flat))
149
- best = cur;
35
+ const spelling = new RegExp(SPELLING_REGEX);
36
+ function checkSpelling(request) {
37
+ if (typeof request !== 'string' || !spelling.test(request)) {
38
+ throw usageError(`${JSON.stringify(request)} is not a version spelling: give two, three or four dotted parts (26.8, 26.8.15, 26.8.15.10), with no "v" prefix and no channel suffix`);
150
39
  }
151
- return best;
152
- }
153
- /** R4 step 2: the first root (in search-path order) holding a location whose version matches `req` under R2. */
154
- function pickPatchMatch(locs, req) {
155
- return [...locs].sort((a, b) => a.rootIndex - b.rootIndex).find((l) => patchMatches(l.version, req));
156
- }
157
- /** §3: the fallback warning, once per (requested, actual) pair per process — shared by every Registry instance. */
158
- const warnedPatchFallbacks = new Set();
159
- function emitPatchFallbackWarning(requested, actual, minor, platform, kind) {
160
- const key = `${requested}\u0000${actual}`;
161
- // Recorded BEFORE emitting: a caller's warning filter that throws must not
162
- // cause a retry to warn again for the same pair.
163
- if (warnedPatchFallbacks.has(key))
164
- return;
165
- warnedPatchFallbacks.add(key);
166
- const body = kind === 'installed'
167
- ? `ClickHouse ${requested} is not installed for ${platform}; using ${actual}, the newest installed patch of ${minor}. ` +
168
- `Behavior can differ between patches. If ${requested} is published, install it with: ${FETCH_COMMAND} ${requested}`
169
- : `ClickHouse ${requested} is not published for ${platform} at ABI revision ${ABI_REVISION}; using ${actual}, ` +
170
- `the newest published patch of ${minor}. Behavior can differ between patches.`;
171
- process.emitWarning(`chtypes: ${body}`, { type: 'PatchFallbackWarning', code: 'CHTYPES_PATCH_FALLBACK' });
172
40
  }
173
- /** TEST-ONLY: forget every warned pair, so a suite can assert a fresh warning fires. Not re-exported from index.ts. */
174
- export function resetPatchFallbackWarnings() {
175
- warnedPatchFallbacks.clear();
176
- }
177
- /**
178
- * R-c: re-scan the search path for a patch fallback at most this often, per
179
- * requested patch, per process. Caches the CHOSEN directory, not a loaded
180
- * `Library` — the load itself (one known path, not a directory read per
181
- * search-path entry) still runs on every call, so a fallback whose chosen
182
- * artifact is broken keeps reporting the same failure rather than being
183
- * silently remembered as unresolvable.
184
- */
185
- const FALLBACK_RECHECK_MS = 60_000;
186
- const fallbackCache = new Map();
187
- /**
188
- * The artifact-directory loader — the multi-version entry point of this
189
- * package.
190
- *
191
- * **Construction reads `manifest.json` files and `dlopen`s nothing**, with or
192
- * without a directory. Nothing in this package opens an artifact except a
193
- * request for a specific version (`for()` / `resolve()` / `open()` /
194
- * `openResolution()`) or an explicit `preload` — not `versions()`, not
195
- * `libraries()`, not `has()`. An open costs about 120 MB resident per patch,
196
- * which a listing call must not spend on a caller's behalf, and which grows
197
- * with every DISTINCT patch a process is asked for — libraries are never
198
- * dlclosed (docs/guides/multi-version.md).
199
- *
200
- * An open dlopens the artifact into its own symbol scope (`RTLD_LOCAL`),
201
- * verifies its ABI revision against this binding's `ABI_REVISION` (a different
202
- * nonzero revision is refused; 0 means the artifact predates the probe and
203
- * degrades per symbol), and runs that library's one-time `chs_init` with the
204
- * registry's timezone and the artifact's own `unsafe_families.txt`.
205
- *
206
- * Libraries are never dlclosed; `close()` joins background threads only.
207
- * Loading the same artifact FILE from two `Registry` instances — by one path,
208
- * another spelling, a symlink or a hardlink — shares one loaded image, so
209
- * `chs_init` still runs exactly once per artifact; a second timezone for an
210
- * image already initialized is refused as `InitConflictError`.
211
- */
212
41
  export class Registry {
213
- /**
214
- * The primary registry directory: the first on the search path that holds
215
- * artifacts — or, with `autofetch` and nothing installed anywhere, the
216
- * directory the first fetch will create.
217
- */
218
- dir;
219
- /** The §1 search path, in order, every candidate whether or not it exists. */
220
- searchPath;
221
- /** This host's platform key, e.g. `darwin-arm64` — the only artifacts a process can dlopen. */
222
- platform;
223
- byPath = new Map();
224
- loaded = [];
225
- /** Line -> the library currently answering FOR THE LINE. Set once per line; never re-pointed while this registry runs (R3/R8). */
226
- linePins = new Map();
227
- /**
228
- * Line -> every patch the construction-time manifest scan found for it,
229
- * across the whole search path. What `versions()` answers from, and what
230
- * makes "no artifact anywhere" and a bad `preload` entry decidable at
231
- * construction without a single `dlopen`. Resolution itself re-scans the
232
- * live filesystem on every call (a patch installed after construction, by
233
- * this process or another, is used from the next call on).
234
- */
235
- known = new Map();
236
- timezone;
237
- verifyChecksums;
238
- autofetch;
239
- fetchOptions;
240
- explicit;
241
- /** (dest, line) already `Ensure()`d successfully by THIS registry's autofetch — never re-read over the network again. */
242
- ensuredLines = new Set();
243
- /** (dest, exact patch) autofetch has already confirmed CHTYPES_ARTIFACT_UNPUBLISHED for — never retried within this registry's lifetime. */
244
- unpublishedPatches = new Set();
245
- /**
246
- * Scan a registry. Reads manifests; opens nothing unless `preload` names a
247
- * version.
248
- *
249
- * @param dir - the registry root; the head of the search path. Absent, the
250
- * path is `CHTYPES_REGISTRY`, the per-user artifact cache, then the system
251
- * locations (see `registrySearchPath` / `defaultRegistryDir`).
252
- * @param options - timezone, checksum verification, autofetch, preload — see
253
- * `RegistryOptions`.
254
- * @throws {RegistryError} for what manifests can decide, and only that: a
255
- * directory named explicitly (the argument or `CHTYPES_REGISTRY`) that does
256
- * not exist, and no directory on the search path holding a readable
257
- * manifest at either slot — both suppressed when autofetch is on. A
258
- * `preload` entry no directory holds is `ArtifactMissingError`. Everything
259
- * a bad artifact can be wrong about — a failed checksum, a load failure, a
260
- * library whose ClickHouse version disagrees with its manifest — is
261
- * reported by the call that opens it, which is `preload`'s open at
262
- * construction or the first `for()` otherwise.
263
- * @throws {ChtypesError} when an artifact reports a different nonzero ABI
264
- * revision than this binding speaks, or `chs_init` fails (e.g. an unknown
265
- * timezone — the message names it); again, from the call that opens it.
266
- */
267
- constructor(dir, options = {}) {
268
- this.platform = hostPlatform();
269
- this.timezone = options.timezone ?? 'UTC';
270
- this.verifyChecksums = options.verifyChecksums ?? false;
271
- this.autofetch = options.autofetch ?? envFlag(ENV_AUTOFETCH);
272
- this.fetchOptions = options.fetch ?? {};
273
- this.explicit = dir !== undefined && dir !== '' ? path.resolve(dir) : undefined;
274
- this.searchPath = registrySearchPath(dir, this.platform);
275
- // A directory somebody NAMED and that does not exist is a configuration
276
- // mistake and must be an error naming it, never a silent fallback — unless
277
- // autofetch is on, in which case it is the destination the first fetch creates.
278
- const fromEnv = process.env[ENV_REGISTRY];
279
- const named = [this.explicit, fromEnv !== undefined && fromEnv !== '' ? path.resolve(fromEnv) : undefined];
280
- for (const d of named) {
281
- if (d !== undefined && !isDirectory(d) && !this.autofetch) {
282
- throw new RegistryError(`chtypes: cannot read registry ${d}: not a directory`);
283
- }
284
- }
285
- // The scan: every directory on the path, in order, every patch at either
286
- // slot. Manifests only — this is the cheap half of what construction used
287
- // to do, and it is all that is left of it.
288
- this.searchPath.forEach((root, rootIndex) => {
289
- for (const loc of locationsInRoot(root, rootIndex)) {
290
- const arr = this.known.get(loc.minor) ?? [];
291
- arr.push(loc);
292
- this.known.set(loc.minor, arr);
293
- }
42
+ #fetch;
43
+ #autofetch;
44
+ #memo = new Map();
45
+ #opened = [];
46
+ constructor(options) {
47
+ this.#fetch = withEnvironment(options.fetch ?? {});
48
+ this.#autofetch = options.autofetch ?? envFlag(ENV_AUTOFETCH_NAME);
49
+ }
50
+ /** Construct a registry. It opens nothing, except each `preload` request, in list order. */
51
+ static async open(options = {}) {
52
+ const registry = new Registry(options);
53
+ for (const request of options.preload ?? []) {
54
+ checkSpelling(request);
55
+ await registry.#memoized(request, false);
56
+ }
57
+ return registry;
58
+ }
59
+ /** Open a version: the installed build the request names, fetched first when autofetch is on and none is installed. */
60
+ async for(request) {
61
+ checkSpelling(request);
62
+ return this.#memoized(request, this.#autofetch);
63
+ }
64
+ /** What is installed: the fetch layer's own listing. */
65
+ async installed() {
66
+ return listInstalled(this.#fetch);
67
+ }
68
+ /** The libraries this registry has opened, in order of first open. */
69
+ libraries() {
70
+ return [...this.#opened];
71
+ }
72
+ #memoized(request, mayFetch) {
73
+ const memo = this.#memo.get(request);
74
+ if (memo !== undefined)
75
+ return memo;
76
+ const opening = this.#openRequest(request, mayFetch).then((library) => {
77
+ if (!this.#opened.includes(library))
78
+ this.#opened.push(library);
79
+ return library;
80
+ }, (err) => {
81
+ this.#memo.delete(request); // a failure is never remembered
82
+ throw err;
294
83
  });
295
- const primary = this.searchPath.find((d) => looksLikeRegistry(d));
296
- if (primary === undefined) {
297
- if (!this.autofetch) {
298
- throw new RegistryError(`chtypes: no artifacts in any registry directory. Looked in: ${this.searchPath.join(', ')}.\n` +
299
- `Install one: ${FETCH_COMMAND} <line>\n` +
300
- 'or set CHTYPES_AUTOFETCH=1 to fetch on first use.');
301
- }
302
- this.dir = fetchDestination(this.explicit, this.platform);
303
- }
304
- else {
305
- this.dir = primary;
306
- }
307
- // The one eager path, and the only thing here that opens anything.
308
- for (const version of options.preload ?? []) {
309
- this.preloadLine(version);
310
- }
84
+ this.#memo.set(request, opening);
85
+ return opening;
311
86
  }
312
- /**
313
- * Open one `preload` entry, before the constructor returns, without
314
- * fetching. Resolution is `for()`'s (minus any fetch), and so is the
315
- * failure: a version no directory holds anywhere is the same
316
- * `ArtifactMissingError`, raised earlier. A patch entry that falls back
317
- * warns here, at construction, exactly as `for()` would.
318
- */
319
- preloadLine(version) {
320
- if (version === '') {
321
- throw new RegistryError("chtypes: preload: an empty version does not mean 'pick one'");
87
+ async #openRequest(request, mayFetch) {
88
+ const platform = this.#fetch.platform ?? hostPlatformKey(os.platform(), os.arch());
89
+ if (platform === undefined) {
90
+ throw new ArtifactMissingError(`chtypes: no v1 artifact is published for this host (${os.platform()}-${os.arch()})`);
322
91
  }
323
- const req = parseVersionSpelling(version);
324
- const resolved = this.resolveSync(req);
92
+ let resolved = await resolveInstalled(request, platform, this.#fetch);
325
93
  if (resolved === undefined) {
326
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
327
- }
328
- // resolveSync (resolvePatchSync, for a patch entry) already warns
329
- // internally when it falls back — nothing further to do here.
330
- }
331
- /** dlopen one artifact directory, cross-check it, `chs_init` it, index it. */
332
- load(sub, manifest) {
333
- const already = this.byPath.get(sub);
334
- if (already !== undefined)
335
- return already;
336
- const libPath = path.join(sub, manifest.library);
337
- // Unconditional, unlike the hash below: nearly free (one stat, never a
338
- // re-hash of the library's contents), and it catches the commonest shape
339
- // of a broken artifact directory — a truncated or partially-written
340
- // library file (issue #82).
341
- checkLibraryBytes(libPath, manifest);
342
- if (this.verifyChecksums)
343
- verifyChecksum(libPath, manifest);
344
- // A directory that has a manifest and does not load is broken, not absent.
345
- let native;
346
- try {
347
- native = NativeLibrary.open(libPath);
348
- }
349
- catch (err) {
350
- throw new RegistryError(`chtypes: cannot load ${libPath}: ${String(err)}`, { cause: err });
351
- }
352
- // The right bytes in the wrong directory is the one corruption a hash
353
- // cannot catch, so cross-check what the library says about itself.
354
- const declared = manifest.clickhouse_version;
355
- if (declared !== undefined && declared !== '' && declared !== native.version) {
356
- throw new RegistryError(`chtypes: ${libPath} reports ClickHouse ${native.version} but its manifest says ${declared}`);
357
- }
358
- // Each library keeps its own DateLUT and its own refuse-list. An absent or
359
- // empty unsafe_families.txt is a valid empty list, not a missing file.
360
- native.init(this.timezone, readUnsafeFamilies(sub, manifest), libPath);
361
- const library = new Library(native);
362
- this.loaded.push(library);
363
- // Full numeric version order (docs/reference/bindings.md §Version
364
- // selection, rule 2): two patches of one line both sort by their own
365
- // exact version, never merely grouped by minor.
366
- this.loaded.sort((a, b) => compareVersions(a.version, b.version));
367
- this.byPath.set(sub, library);
368
- return library;
369
- }
370
- /** Load an artifact directory whose manifest is already known-readable, or return null when it is not one. */
371
- loadDir(dir) {
372
- const manifest = readManifest(dir);
373
- if (manifest === null)
374
- return null;
375
- return this.load(dir, manifest);
376
- }
377
- /** Every patch (both slots, every search-path root) this registry can currently see for `minor` — a live scan. */
378
- scanLine(minor) {
379
- const out = [];
380
- this.searchPath.forEach((root, rootIndex) => {
381
- for (const loc of locationsInRoot(root, rootIndex)) {
382
- if (loc.minor === minor)
383
- out.push(loc);
94
+ if (!mayFetch) {
95
+ throw new ArtifactMissingError(`chtypes: ${request} (${platform}) is not installed. Fetch it first, or turn autofetch on (the autofetch option, or ${ENV_AUTOFETCH_NAME}=1)`);
384
96
  }
97
+ resolved = await ensure(request, this.#fetch);
98
+ }
99
+ const setup = commitSetup();
100
+ // The adapter: the predicate goes through exactly as the fetch layer returned it.
101
+ const image = openAbi1({
102
+ libraryPath: resolved.libraryPath,
103
+ predicate: resolved.predicate,
104
+ platform: resolved.platform,
105
+ timezone: setup.timezone,
106
+ defaults: setup.defaults,
107
+ settle: settleSetup,
385
108
  });
386
- return out;
387
- }
388
- /** R3: line request — the pin if this registry already has one, else the newest patch in the first root that holds any. */
389
- resolveLineSync(req) {
390
- const pinned = this.linePins.get(req.line);
391
- if (pinned !== undefined)
392
- return { library: pinned, actual: pinned.version, exact: true };
393
- const winner = pickLineWinner(this.scanLine(req.line));
394
- if (winner === undefined)
395
- return undefined;
396
- const library = this.loadDir(winner.dir);
397
- if (library === null)
398
- return undefined;
399
- this.linePins.set(req.line, library);
400
- return { library, actual: library.version, exact: true };
401
- }
402
- /** R4 steps 1-2: an exact match, already open or anywhere on the search path — never a fallback. */
403
- resolveExactPatchSync(req) {
404
- const already = this.loaded.find((l) => patchMatches(l.version, req));
405
- if (already !== undefined)
406
- return already;
407
- const matched = pickPatchMatch(this.scanLine(req.line), req);
408
- if (matched === undefined)
409
- return undefined;
410
- return this.loadDir(matched.dir) ?? undefined;
411
- }
412
- /**
413
- * R4 in full, synchronous shape: exact, else the newest-installed fallback
414
- * within the line. R-c: the already-open check is free (no I/O) and always
415
- * current; everything past it needs a directory read per search-path entry,
416
- * so once a request has fallen back, the WHOLE re-check — retrying the exact
417
- * match and picking the fallback alike — is throttled together, at most
418
- * once per `FALLBACK_RECHECK_MS` per requested patch per process.
419
- *
420
- * §3: the warning fires as soon as the fallback patch is CHOSEN — its
421
- * version is already known from the manifest scan, before `loadDir` ever
422
- * runs — so a caller sees the warning even when the chosen directory then
423
- * fails to load (a broken artifact is still a fallback that was taken).
424
- */
425
- resolvePatchSync(req) {
426
- const already = this.loaded.find((l) => patchMatches(l.version, req));
427
- if (already !== undefined)
428
- return { library: already, actual: already.version, exact: true };
429
- const cacheKey = req.exact;
430
- const cached = fallbackCache.get(cacheKey);
431
- const now = Date.now();
432
- if (cached !== undefined && now - cached.checkedAt < FALLBACK_RECHECK_MS) {
433
- // Within the window: skip the re-scan (the exact-match retry AND the
434
- // fallback pick alike), but still attempt to load the chosen directory
435
- // — one already-known path, not a directory read per search-path
436
- // entry, so a broken artifact keeps failing rather than being silently
437
- // remembered as fine.
438
- const library = this.loadDir(cached.dir);
439
- if (library === null)
440
- return undefined;
441
- return { library, actual: library.version, exact: false };
442
- }
443
- const locs = this.scanLine(req.line);
444
- const matched = pickPatchMatch(locs, req);
445
- if (matched !== undefined) {
446
- const library = this.loadDir(matched.dir);
447
- if (library !== null) {
448
- fallbackCache.delete(cacheKey);
449
- return { library, actual: library.version, exact: true };
450
- }
451
- }
452
- const winner = pickLineWinner(locs);
453
- if (winner === undefined) {
454
- fallbackCache.delete(cacheKey);
455
- return undefined;
456
- }
457
- this.warnFallback(req, winner.version, 'installed');
458
- fallbackCache.set(cacheKey, { dir: winner.dir, checkedAt: now });
459
- const library = this.loadDir(winner.dir);
460
- if (library === null)
461
- return undefined;
462
- return { library, actual: library.version, exact: false };
463
- }
464
- resolveSync(req) {
465
- return req.exact === null ? this.resolveLineSync(req) : this.resolvePatchSync(req);
466
- }
467
- /** Would `req` resolve without opening anything? Mirrors `resolveSync` with no `load()` call. */
468
- wouldResolve(req) {
469
- if (req.exact === null) {
470
- if (this.linePins.has(req.line))
471
- return true;
472
- return pickLineWinner(this.scanLine(req.line)) !== undefined;
473
- }
474
- if (this.loaded.some((l) => patchMatches(l.version, req)))
475
- return true;
476
- // A patch resolves via its line's fallback too (R9: has() is true when the
477
- // patch, or any patch of its line, is installed or open).
478
- return this.scanLine(req.line).length > 0;
479
- }
480
- warnFallback(req, actual, kind) {
481
- emitPatchFallbackWarning(req.exact, actual, req.line, this.platform, kind);
482
- }
483
- /**
484
- * Every ClickHouse minor line this registry CAN ANSWER FOR, oldest first —
485
- * the ones it has opened plus the ones its construction-time manifest scan
486
- * discovered on the search path, at either slot.
487
- *
488
- * That is one meaning in all four bindings, and it is the meaning that
489
- * survives lazy loading: "the lines that happen to be open" would read as an
490
- * empty registry until the first `for()`. It opens nothing.
491
- */
492
- versions() {
493
- const lines = new Set(this.loaded.map((l) => l.minor));
494
- for (const line of this.known.keys())
495
- lines.add(line);
496
- return [...lines].sort(compareMinor);
497
- }
498
- /**
499
- * The libraries this registry has OPENED, in full numeric version order —
500
- * what is open right now, never what could be. Two patches of one line both
501
- * appear, each in its own slot in this order. A discovered patch that no
502
- * `for()` and no `preload` has opened appears in `versions()` and not here.
503
- * It opens nothing.
504
- */
505
- libraries() {
506
- return this.loaded;
507
- }
508
- /**
509
- * Resolve a version to its library. A minor line ("25.8") never falls back:
510
- * the newest patch of the line, from the first search-path directory that
511
- * holds any patch of it. An exact patch ("25.8.28.1-lts") loads that patch
512
- * when it is open or installed anywhere on the search path; otherwise the
513
- * newest installed patch of the same line is loaded instead, and one
514
- * warning is written per (requested, actual) pair per process — `resolve()`
515
- * reports the same fallback as `exact: false` instead of only warning.
516
- *
517
- * **This is what opens an artifact.** Construction does not: a version is
518
- * loaded on first request, `dlopen`ed once, and joins `libraries()` from
519
- * then on. Never a fetch: this call is synchronous; `open()` /
520
- * `openResolution()` are the ones that may fetch.
521
- *
522
- * Failure is the one §7 error, never a fallback to another line: answering
523
- * 26.7 semantics from a 25.8 artifact is a lie, and silent wrongness is what
524
- * the rigs score hardest.
525
- *
526
- * @param version - a minor line (`"25.8"`) or an exact patch
527
- * (`"25.8.28.1-lts"`), e.g. what `parseVersionResult` discovered.
528
- * @returns the loaded `Library` for that version.
529
- * @throws {ArtifactMissingError} (`code` `CHTYPES_ARTIFACT_MISSING`, a
530
- * `RegistryError`) when no directory on the search path holds a matching
531
- * or fallback patch; the message names every directory looked in and the
532
- * fetch command.
533
- * @throws {ChtypesError} when `version` is not a ClickHouse version spelling at all.
534
- * @throws {RegistryError} when a directory holds a version but it does not load.
535
- */
536
- for(version) {
537
- return this.resolve(version).library;
538
- }
539
- /**
540
- * `for()`, but returns the full `Resolution` — the requested spelling, what
541
- * actually loaded, and whether the two are the same patch. Never fetches;
542
- * `openResolution()` is the async twin that may.
543
- *
544
- * @throws {ArtifactMissingError} as `for()`.
545
- * @throws {ChtypesError} when `version` is not a ClickHouse version spelling at all.
546
- */
547
- resolve(version) {
548
- const requested = version.trim();
549
- const req = parseVersionSpelling(version);
550
- // resolveSync (resolvePatchSync, for a patch request) warns internally
551
- // when it falls back, at the moment the fallback patch is CHOSEN — before
552
- // load, so the warning still fires even if that load then fails.
553
- const resolved = this.resolveSync(req);
554
- if (resolved === undefined)
555
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
556
- return { library: resolved.library, requested, version: resolved.actual, exact: resolved.exact };
557
- }
558
- /**
559
- * `for()`, with the lazy fetch of docs/guides/fetch.md §6 in front of it. A
560
- * LINE request no directory holds is fetched through `ensure()` and loaded.
561
- * A PATCH request tries `ensure()` of that exact patch first; on
562
- * `CHTYPES_ARTIFACT_UNPUBLISHED` (remembered for this registry, so it costs
563
- * one network round trip, not one per call) it falls back to `ensure()` of
564
- * the line and loads whatever that installs, with `exact: false` and one
565
- * warning. Any other fetch failure (untrusted, corrupt, pinned, source
566
- * unreachable) surfaces as itself and is never remembered, so a later call
567
- * retries. With `autofetch` off, this is `for()` behind a promise.
568
- *
569
- * @throws {ArtifactMissingError} when nothing is found and autofetch is off.
570
- * @throws {FetchError} the §7 fetch verdicts (`CHTYPES_ARTIFACT_UNTRUSTED`,
571
- * `…_CORRUPT`, `…_PINNED`, `…_UNPUBLISHED`, `CHTYPES_SOURCE_UNREACHABLE`).
572
- * @throws {RegistryError} when the fetched artifact does not load.
573
- */
574
- async open(version) {
575
- return (await this.openResolution(version)).library;
576
- }
577
- /** `open()`, but returns the full `Resolution` — see `resolve()` and `open()`. */
578
- async openResolution(version) {
579
- const requested = version.trim();
580
- const req = parseVersionSpelling(version);
581
- const dest = this.fetchOptions.dest ?? fetchDestination(this.explicit, this.platform);
582
- if (req.exact === null) {
583
- const sync = this.resolveLineSync(req);
584
- if (sync !== undefined)
585
- return { library: sync.library, requested, version: sync.actual, exact: true };
586
- if (!this.autofetch)
587
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
588
- await this.ensureLineOnce(req.line, dest);
589
- const after = this.resolveLineSync(req);
590
- if (after === undefined)
591
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
592
- return { library: after.library, requested, version: after.actual, exact: true };
593
- }
594
- const exact = this.resolveExactPatchSync(req);
595
- if (exact !== undefined)
596
- return { library: exact, requested, version: exact.version, exact: true };
597
- if (this.autofetch) {
598
- const unpublishedKey = `${dest}\u0000${req.exact}`;
599
- if (!this.unpublishedPatches.has(unpublishedKey)) {
600
- try {
601
- await ensure(req.exact, { ...this.fetchOptions, dest, platform: this.platform });
602
- }
603
- catch (err) {
604
- if (err instanceof ArtifactUnpublishedError) {
605
- this.unpublishedPatches.add(unpublishedKey);
606
- }
607
- else {
608
- // Untrusted, corrupt, pinned or unreachable: surface it as-is and
609
- // remember nothing, so a later call retries (R4 step 3).
610
- throw err;
611
- }
612
- }
613
- if (!this.unpublishedPatches.has(unpublishedKey)) {
614
- const found = this.resolveExactPatchSync(req);
615
- if (found !== undefined)
616
- return { library: found, requested, version: found.version, exact: true };
617
- }
618
- }
619
- // Step 4 (autofetch on): Ensure(line) at most once per (dest, line) for this registry.
620
- await this.ensureLineOnce(req.line, dest);
621
- const winner = pickLineWinner(this.scanLine(req.line));
622
- if (winner === undefined)
623
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
624
- // Warn on the CHOSEN version, before the load — so a caller sees it even
625
- // if the chosen directory then fails to load (§3).
626
- this.warnFallback(req, winner.version, 'published');
627
- const library = this.loadDir(winner.dir);
628
- if (library === null)
629
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
630
- return { library, requested, version: library.version, exact: false };
631
- }
632
- // Autofetch off: the same synchronous fallback `for()`/`resolve()` take
633
- // (resolvePatchSync warns internally when it falls back).
634
- const resolved = this.resolvePatchSync(req);
635
- if (resolved === undefined)
636
- throw new ArtifactMissingError(req.line, this.platform, this.searchPath);
637
- return { library: resolved.library, requested, version: resolved.actual, exact: resolved.exact };
638
- }
639
- /** `ensure(line)`, at most once per (dest, line) over this registry's lifetime — never re-read over the network again. */
640
- async ensureLineOnce(line, dest) {
641
- const key = `${dest}\u0000${line}`;
642
- if (this.ensuredLines.has(key))
643
- return;
644
- const result = await ensure(line, { ...this.fetchOptions, dest, platform: this.platform });
645
- this.loadDir(result.dir);
646
- this.ensuredLines.add(key);
647
- }
648
- /**
649
- * True when this registry can answer for a version without fetching or
650
- * loading: already open, or held by a directory on the search path (at
651
- * either slot) which `for()` would load. A patch resolves true when the
652
- * patch itself, or any patch of its line, is installed or open — the same
653
- * condition `for()` would resolve, fallback included. Never a fetch, and
654
- * never a load.
655
- */
656
- has(version) {
657
- let req;
658
- try {
659
- req = parseVersionSpelling(version);
660
- }
661
- catch {
662
- return false;
663
- }
664
- return this.wouldResolve(req);
665
- }
666
- /**
667
- * Join every loaded library's background threads. `chs_init` registers
668
- * `chs_shutdown` with `atexit`, so an ordinary process needs no call; a test
669
- * that must not depend on `atexit` should make one.
670
- */
671
- close() {
672
- for (const library of this.loaded)
673
- library.shutdown();
674
- }
675
- /** `using registry = new Registry(...)` closes it at scope exit. */
676
- [Symbol.dispose]() {
677
- this.close();
678
- }
679
- }
680
- /** Numeric ordering: 25.10 is a later minor than 25.8, whatever strings say. */
681
- export function compareMinor(a, b) {
682
- const pa = a.split('.').map((p) => Number.parseInt(p, 10));
683
- const pb = b.split('.').map((p) => Number.parseInt(p, 10));
684
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
685
- const x = pa[i] ?? -1;
686
- const y = pb[i] ?? -1;
687
- if (Number.isNaN(x) || Number.isNaN(y))
688
- return a < b ? -1 : a > b ? 1 : 0;
689
- if (x !== y)
690
- return x - y;
691
- }
692
- return 0;
693
- }
694
- /**
695
- * Where the registry is: the explicit argument, else `CHTYPES_REGISTRY`, else
696
- * the first of the per-user artifact cache and the system locations that
697
- * holds artifacts (docs/guides/fetch.md §1). An explicit path and the environment
698
- * variable are returned as given — a wrong one must produce an error naming
699
- * it, not a silent fallback — while the unnamed candidates only count if they
700
- * actually hold artifacts.
701
- */
702
- export function resolveRegistryDir(explicit) {
703
- if (explicit !== undefined && explicit !== '')
704
- return path.resolve(explicit);
705
- const fromEnv = process.env[ENV_REGISTRY];
706
- if (fromEnv !== undefined && fromEnv !== '')
707
- return path.resolve(fromEnv);
708
- const platform = hostPlatform();
709
- for (const candidate of [cacheRegistryDir(platform), ...systemRegistryDirs(platform)]) {
710
- if (looksLikeRegistry(candidate))
711
- return candidate;
712
- }
713
- return null;
714
- }
715
- /**
716
- * The per-user artifact cache for this host —
717
- * `${XDG_CACHE_HOME:-~/.cache}/chtypes/artifacts/abi<R>/<os>-<arch>`, R this
718
- * binding's `ABI_REVISION` and `<arch>` spelled the artifact way (`amd64`,
719
- * `arm64`). Where `chtypes fetch` and `scripts/fetch.sh` install, and what
720
- * every SDK's tests and playgrounds fall back to when `CHTYPES_REGISTRY` is
721
- * unset — one directory the four SDKs at the same revision agree on. A path,
722
- * not a promise: `Registry` still throws if nothing is there.
723
- */
724
- export function defaultRegistryDir() {
725
- return cacheRegistryDir(hostPlatform());
726
- }
727
- /**
728
- * Does this directory hold at least one artifact with a usable manifest, at
729
- * either slot — the flat `<dir>/<minor>/` layout or the `<dir>/patches/<minor>/<version>/`
730
- * sibling tree? A registry that holds only nested installs is not empty.
731
- */
732
- export function looksLikeRegistry(dir) {
733
- if (!isDirectory(dir))
734
- return false;
735
- try {
736
- const hasFlat = readdirSync(dir).some((entry) => {
737
- if (entry.startsWith('.') || entry === 'patches')
738
- return false;
739
- const sub = path.join(dir, entry);
740
- if (!isDirectory(sub))
741
- return false;
742
- const manifest = readManifest(sub);
743
- return manifest !== null && existsSync(path.join(sub, manifest.library));
744
- });
745
- if (hasFlat)
746
- return true;
747
- const patchesDir = path.join(dir, 'patches');
748
- if (!isDirectory(patchesDir))
749
- return false;
750
- return readdirSync(patchesDir).some((minorEntry) => {
751
- const minorDir = path.join(patchesDir, minorEntry);
752
- if (!isDirectory(minorDir))
753
- return false;
754
- let versionEntries;
755
- try {
756
- versionEntries = readdirSync(minorDir);
757
- }
758
- catch {
759
- return false;
760
- }
761
- return versionEntries.some((versionEntry) => {
762
- const sub = path.join(minorDir, versionEntry);
763
- if (!isDirectory(sub))
764
- return false;
765
- const manifest = readManifest(sub);
766
- return manifest !== null && existsSync(path.join(sub, manifest.library));
767
- });
768
- });
769
- }
770
- catch {
771
- return false;
772
- }
773
- }
774
- function envFlag(name) {
775
- const v = process.env[name];
776
- return v !== undefined && ['1', 'true', 'yes', 'on'].includes(v.trim().toLowerCase());
777
- }
778
- function isDirectory(p) {
779
- try {
780
- // statSync follows symlinks on purpose: a registry is often a symlink
781
- // into ~/.cache/chtypes, which is where the artifacts actually live.
782
- return statSync(p).isDirectory();
783
- }
784
- catch {
785
- return false;
786
- }
787
- }
788
- function readManifest(dir) {
789
- try {
790
- const parsed = JSON.parse(readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
791
- if (typeof parsed.library !== 'string' || parsed.library === '')
792
- return null;
793
- return parsed;
794
- }
795
- catch {
796
- return null;
797
- }
798
- }
799
- function readUnsafeFamilies(dir, manifest) {
800
- try {
801
- return readFileSync(path.join(dir, 'unsafe_families.txt'), 'utf8').trim();
802
- }
803
- catch {
804
- return (manifest.unsafe_families ?? '').trim();
805
- }
806
- }
807
- /**
808
- * Compare `libPath`'s on-disk size to `manifest.library_bytes` — the load-path
809
- * check `load()` runs on EVERY open, whether or not `verifyChecksums` is on
810
- * (issue #82). A manifest with no `library_bytes` (`undefined`, the shape a
811
- * manifest predating the field parses to) is not asked, so this is a no-op
812
- * for one.
813
- */
814
- function checkLibraryBytes(libPath, manifest) {
815
- if (manifest.library_bytes === undefined)
816
- return;
817
- const actual = statSync(libPath).size;
818
- if (actual !== manifest.library_bytes) {
819
- throw new RegistryError(`chtypes: ${libPath} is ${actual} bytes, manifest says ${manifest.library_bytes}`);
820
- }
821
- }
822
- function verifyChecksum(libPath, manifest) {
823
- const expected = manifest.library_sha256;
824
- if (expected === undefined || expected === '') {
825
- throw new RegistryError(`chtypes: ${libPath} cannot be verified: its manifest carries no library_sha256`);
826
- }
827
- const bytes = readFileSync(libPath);
828
- const actual = createHash('sha256').update(bytes).digest('hex');
829
- // Case-insensitive: a hex digest is the same digest in either case, and Go
830
- // and Rust already lower-case before comparing. Comparing raw here made an
831
- // uppercase manifest digest pass in two bindings and fail in two.
832
- if (actual !== expected.toLowerCase()) {
833
- throw new RegistryError(`chtypes: ${libPath} sha256 ${actual} does not match manifest ${expected}`);
109
+ return libraryOf(image, resolved);
834
110
  }
835
111
  }
836
112
  //# sourceMappingURL=registry.js.map