@telorun/kernel 0.25.0 → 0.26.1

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 (49) hide show
  1. package/dist/controller-loader.d.ts +4 -0
  2. package/dist/controller-loader.d.ts.map +1 -1
  3. package/dist/controller-loader.js +4 -1
  4. package/dist/controller-loader.js.map +1 -1
  5. package/dist/controller-loaders/npm-loader.d.ts +10 -0
  6. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  7. package/dist/controller-loaders/npm-loader.js +2 -1
  8. package/dist/controller-loaders/npm-loader.js.map +1 -1
  9. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  10. package/dist/controllers/module/import-controller.js +48 -2
  11. package/dist/controllers/module/import-controller.js.map +1 -1
  12. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  13. package/dist/controllers/resource-definition/resource-definition-controller.js +1 -0
  14. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/kernel.d.ts +8 -0
  20. package/dist/kernel.d.ts.map +1 -1
  21. package/dist/kernel.js +29 -14
  22. package/dist/kernel.js.map +1 -1
  23. package/dist/manifest-sources/analysis-stamp.d.ts +2 -2
  24. package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -1
  25. package/dist/manifest-sources/analysis-stamp.js +9 -4
  26. package/dist/manifest-sources/analysis-stamp.js.map +1 -1
  27. package/dist/manifest-sources/local-manifest-cache-source.d.ts +13 -3
  28. package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
  29. package/dist/manifest-sources/local-manifest-cache-source.js +25 -6
  30. package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
  31. package/dist/resource-context.d.ts +1 -0
  32. package/dist/resource-context.d.ts.map +1 -1
  33. package/dist/resource-context.js +3 -0
  34. package/dist/resource-context.js.map +1 -1
  35. package/dist/schema-validator.d.ts +8 -1
  36. package/dist/schema-validator.d.ts.map +1 -1
  37. package/dist/schema-validator.js +8 -2
  38. package/dist/schema-validator.js.map +1 -1
  39. package/package.json +3 -3
  40. package/src/controller-loader.ts +8 -1
  41. package/src/controller-loaders/npm-loader.ts +12 -1
  42. package/src/controllers/module/import-controller.ts +60 -4
  43. package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
  44. package/src/index.ts +1 -0
  45. package/src/kernel.ts +41 -15
  46. package/src/manifest-sources/analysis-stamp.ts +9 -2
  47. package/src/manifest-sources/local-manifest-cache-source.ts +29 -4
  48. package/src/resource-context.ts +4 -0
  49. package/src/schema-validator.ts +8 -2
@@ -55,6 +55,10 @@ export interface ControllerLoaderOptions {
55
55
  * (cargo loader has its own per-crate cache and does not need this).
56
56
  */
57
57
  entryUrl?: string;
58
+ /** Explicit npm install root (`<cache-root>/npm`), threaded from the kernel's
59
+ * single `resolveCacheRoot`. Overrides the entry-anchored default so a
60
+ * relocated `TELO_CACHE_DIR` is honoured. */
61
+ installRoot?: string;
58
62
  }
59
63
 
60
64
  /**
@@ -82,7 +86,10 @@ export class ControllerLoader {
82
86
 
83
87
  constructor(options: ControllerLoaderOptions = {}) {
84
88
  this.emit = options.emit;
85
- this.npmLoader = new NpmControllerLoader({ entryUrl: options.entryUrl });
89
+ this.npmLoader = new NpmControllerLoader({
90
+ entryUrl: options.entryUrl,
91
+ installRoot: options.installRoot,
92
+ });
86
93
  this.napiLoader = new NapiControllerLoader();
87
94
  this.bundleLoader = new BundleControllerLoader();
88
95
  }
@@ -97,6 +97,14 @@ export interface NpmControllerLoaderOptions {
97
97
  * `pkg-name` cli command resolves it from its argument.
98
98
  */
99
99
  entryUrl?: string;
100
+ /**
101
+ * Explicit install root, threaded from the kernel's single `resolveCacheRoot`
102
+ * (`<cache-root>/npm`). When set it overrides the entry-anchored
103
+ * `computeInstallRoot`, so a relocated `TELO_CACHE_DIR` (e.g. a prebuilt
104
+ * image baking deps at `/telo-cache`) is honoured without this loader reading
105
+ * the env itself.
106
+ */
107
+ installRoot?: string;
100
108
  }
101
109
 
102
110
  /**
@@ -127,6 +135,8 @@ export interface NpmControllerLoaderOptions {
127
135
  */
128
136
  export class NpmControllerLoader {
129
137
  private readonly entryUrl?: string;
138
+ /** Threaded install root (`<cache-root>/npm`); overrides `computeInstallRoot`. */
139
+ private readonly installRootOverride?: string;
130
140
 
131
141
  /**
132
142
  * Per-process cache of "this controller's package + version is already
@@ -164,6 +174,7 @@ export class NpmControllerLoader {
164
174
 
165
175
  constructor(options: NpmControllerLoaderOptions = {}) {
166
176
  this.entryUrl = options.entryUrl;
177
+ this.installRootOverride = options.installRoot;
167
178
  }
168
179
 
169
180
  async load(purl: string, baseUri: string): Promise<NpmLoadResult> {
@@ -219,7 +230,7 @@ export class NpmControllerLoader {
219
230
  );
220
231
  }
221
232
  const entryUrlStr = this.entryUrl;
222
- const installRoot = computeInstallRoot(entryUrlStr);
233
+ const installRoot = this.installRootOverride ?? computeInstallRoot(entryUrlStr);
223
234
 
224
235
  // Build the install-root package.json: kernel-runtime deps as `file:` refs
225
236
  // pointing at the kernel-side realpath. Modules declare these names as
@@ -121,10 +121,22 @@ export async function create(
121
121
  // no resources/env/ports in import inputs — is enforced by the analyzer, not here;
122
122
  // a `${{ resources.X }}` slipping past analysis (skipValidation / programmatic
123
123
  // load) would evaluate at runtime rather than being rejected.
124
- const importVariables =
125
- (ctx.expandValue(resource.variables, {}) as Record<string, unknown>) ?? {};
126
- const importSecrets =
127
- (ctx.expandValue(resource.secrets, {}) as Record<string, unknown>) ?? {};
124
+ // Defaults declared on the library's own `variables` / `secrets` contract fill
125
+ // any input the importer didn't override — the import-time analogue of the root
126
+ // Application's env defaulting (`resolveApplicationEnv`). Child modules are
127
+ // isolated from the host environment, so there is no env lookup here: the value
128
+ // is the importer's override, else the library default. Without this a contract
129
+ // var with a default but no override reaches a `${{ variables.X }}` template as
130
+ // a missing key, even though analysis validated the reference against the
131
+ // (defaulted) contract.
132
+ const importVariables = applyDefaults(
133
+ (ctx.expandValue(resource.variables, {}) as Record<string, unknown>) ?? {},
134
+ moduleManifest.variables ?? {},
135
+ );
136
+ const importSecrets = applyDefaults(
137
+ (ctx.expandValue(resource.secrets, {}) as Record<string, unknown>) ?? {},
138
+ moduleManifest.secrets ?? {},
139
+ );
128
140
  const childCtx = new ModuleContext(
129
141
  ctx.moduleContext.source,
130
142
  importVariables,
@@ -159,6 +171,30 @@ export async function create(
159
171
  child.registerManifest(manifest);
160
172
  }
161
173
 
174
+ // Initialize the child's resources in dependency order, mirroring the root
175
+ // context (kernel.boot → setInitOrder). Without this, an imported library's
176
+ // resources init in registration order, so a dependent can run Phase 5
177
+ // injection before its dependency exists — e.g. an Http.Api whose inline route
178
+ // handler is appended after it during inline-resource extraction would init,
179
+ // and inject, before the handler is created, leaving the handler ref
180
+ // unresolved. The order is computed in the library's OWN scope (childRegistry),
181
+ // so an anonymous child resolves against the declaring library, not the
182
+ // consumer. setInitOrder ignores names it doesn't recognize, so a partial order
183
+ // is safe. A cycle purely among a library's own resources is invisible to the
184
+ // root graph (which stops at the import boundary), so this is the only place it
185
+ // surfaces — throw it the same way the root does rather than silently falling
186
+ // back to registration order (which would re-manifest as a confusing runtime
187
+ // ERR_RESOURCE_NOT_INVOKABLE). `prepare` returns null order on a ref-validation
188
+ // error too (already reported by analyze above); those we leave to fall back.
189
+ const { order, cycleError } = analyzer.prepare(manifests, childRegistry);
190
+ if (cycleError) {
191
+ throw new RuntimeError(
192
+ "ERR_CIRCULAR_DEPENDENCY",
193
+ `Circular dependency in imported library '${targetModule}': ${cycleError}`,
194
+ );
195
+ }
196
+ if (order) (child as ModuleContext).setInitOrder(order);
197
+
162
198
  // Link the target module context as a child of the declaring module context in
163
199
  // the lifecycle tree. This enables cascading teardown (parent → child order)
164
200
  // and makes the import hierarchy visible at runtime.
@@ -251,6 +287,26 @@ export async function create(
251
287
  };
252
288
  }
253
289
 
290
+ /**
291
+ * Fill in library-declared `default:` values for any input the importer left
292
+ * unset. Mirrors the root Application's env defaulting: a provided value (incl.
293
+ * an explicit `null`) wins; otherwise the contract default applies. Returns a new
294
+ * object — the resolved input map is never mutated in place.
295
+ */
296
+ function applyDefaults(
297
+ provided: Record<string, unknown>,
298
+ schemaDefs: Record<string, any>,
299
+ ): Record<string, unknown> {
300
+ const out = { ...provided };
301
+ for (const [key, def] of Object.entries(schemaDefs)) {
302
+ if (typeof def !== "object" || def === null || !("default" in def)) continue;
303
+ if (out[key] === undefined || out[key] === null) {
304
+ out[key] = def.default;
305
+ }
306
+ }
307
+ return out;
308
+ }
309
+
254
310
  function validateRequiredInputs(
255
311
  schemaDefs: Record<string, any>,
256
312
  provided: Record<string, unknown>,
@@ -54,6 +54,7 @@ class ResourceDefinition implements ResourceInstance {
54
54
  const loader = new ControllerLoader({
55
55
  emit: (e) => ctx.emit(e.name, e.payload),
56
56
  entryUrl: ctx.getEntryUrl(),
57
+ installRoot: ctx.getInstallRoot(),
57
58
  });
58
59
  const controllerInstance = await loader.load(
59
60
  this.resource.controllers,
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export { LocalFileSource } from "./manifest-sources/local-file-source.js";
5
5
  export {
6
6
  LocalManifestCacheSource,
7
7
  cachePathForCanonical,
8
+ resolveCacheRoot,
8
9
  resolveEntryDir,
9
10
  writeManifestCache,
10
11
  } from "./manifest-sources/local-manifest-cache-source.js";
package/src/kernel.ts CHANGED
@@ -40,7 +40,9 @@ import {
40
40
  readAnalysisStamp,
41
41
  writeAnalysisStamp,
42
42
  } from "./manifest-sources/analysis-stamp.js";
43
- import { resolveEntryDir } from "./manifest-sources/local-manifest-cache-source.js";
43
+ import {
44
+ resolveCacheRoot,
45
+ } from "./manifest-sources/local-manifest-cache-source.js";
44
46
  import {
45
47
  precompileApplicationEnvSchemas,
46
48
  precompileDefinitionSchemas,
@@ -169,6 +171,9 @@ export class Kernel implements IKernel {
169
171
  private rootContext!: ModuleContext;
170
172
  private staticManifests: ResourceManifest[] = [];
171
173
  private _entryUrl?: string;
174
+ /** The `.telo` cache root for this load, resolved once in `load()` and
175
+ * threaded to the validator, analysis stamp, and npm install root. */
176
+ private _cacheRoot?: string | null;
172
177
  private _loadedGraph?: LoadedGraph;
173
178
  // Lifecycle state — guards boot/runTargets/teardown/invoke transitions.
174
179
  // teardown() is the only idempotent method; everything else throws on misuse.
@@ -319,18 +324,32 @@ export class Kernel implements IKernel {
319
324
  * session rootfs — hits the stamp and skips the validation walk entirely
320
325
  * instead of failing to write the caches on every boot.
321
326
  */
322
- async load(url: string, options?: { analyzeOnly?: boolean }): Promise<void> {
327
+ async load(
328
+ url: string,
329
+ options?: { analyzeOnly?: boolean; cacheDir?: string | null; writeCache?: boolean },
330
+ ): Promise<void> {
323
331
  const sourceUrl = await this.loader.resolveEntryPoint(url);
324
332
  this._entryUrl = sourceUrl;
325
- // Point the shared schema validator at the entry-anchored cache so
326
- // compiled AJV validators are persisted (standalone CJS) under
327
- // `<entry-dir>/.telo/manifests/__validators/`. Memory- or HTTP-rooted
328
- // entries skip the cache; their schema compiles stay in-process only.
329
- const validatorCacheDir = resolveEntryDir(sourceUrl);
333
+ // Resolve the `.telo` cache root ONCE and thread it to every consumer
334
+ // (validators, analysis stamp, npm install root) — no consumer re-derives
335
+ // it or reads the env independently. A caller (the CLI) may pass `cacheDir`
336
+ // so the env is read exactly once per invocation; otherwise resolve here.
337
+ const cacheRoot =
338
+ options?.cacheDir !== undefined ? options.cacheDir : resolveCacheRoot(sourceUrl);
339
+ this._cacheRoot = cacheRoot;
340
+ const manifestsDir = cacheRoot ? `${cacheRoot}/manifests` : undefined;
341
+ // `writeCache: false` (`telo run --no-cache-write`) keeps the cache
342
+ // READ-only: compiled validators and the analysis stamp are still loaded
343
+ // from disk, but never written back — so an ephemeral, read-only session
344
+ // rootfs validates in-memory without touching the baked cache.
345
+ const writeCache = options?.writeCache !== false;
346
+ // Point the shared schema validator at the cache so compiled AJV validators
347
+ // are loaded (and, when writable, persisted) under
348
+ // `<cache-root>/manifests/__validators/`. Memory-/HTTP-rooted entries skip
349
+ // the cache; their schema compiles stay in-process only.
330
350
  this.sharedSchemaValidator.setCacheDir(
331
- validatorCacheDir
332
- ? `${validatorCacheDir}/.telo/manifests/__validators`
333
- : undefined,
351
+ manifestsDir ? `${manifestsDir}/__validators` : undefined,
352
+ { write: writeCache },
334
353
  );
335
354
  this.rootContext = new ModuleContext(
336
355
  sourceUrl,
@@ -393,9 +412,10 @@ export class Kernel implements IKernel {
393
412
  // and inline-resource normalisation still runs — only the diagnostic
394
413
  // passes are elided. Memory- / HTTP-rooted entries have no
395
414
  // local stamp store and always re-validate.
396
- const entryDir = resolveEntryDir(sourceUrl);
397
415
  const analysisSignature = computeAnalysisSignature(analysisGraph);
398
- const stamp = entryDir ? await readAnalysisStamp(entryDir) : undefined;
416
+ const stamp = manifestsDir
417
+ ? await readAnalysisStamp("", manifestsDir)
418
+ : undefined;
399
419
  const skipValidation = stamp?.signature === analysisSignature;
400
420
  const errors = this.analyzer.analyzeErrors(
401
421
  staticManifests,
@@ -416,13 +436,13 @@ export class Kernel implements IKernel {
416
436
  })),
417
437
  );
418
438
  }
419
- if (entryDir && !skipValidation) {
439
+ if (manifestsDir && writeCache && !skipValidation) {
420
440
  // Best-effort: stamp the verdict so subsequent loads hit the fast
421
441
  // path. A read-only filesystem (baked Docker image) reports the
422
442
  // failure on stderr and keeps running — the lookup above will
423
- // simply miss next time.
443
+ // simply miss next time. Skipped under `--no-cache-write`.
424
444
  try {
425
- await writeAnalysisStamp(entryDir, analysisSignature);
445
+ await writeAnalysisStamp("", analysisSignature, manifestsDir);
426
446
  } catch (err) {
427
447
  this.stderr.write(
428
448
  `[telo:kernel] analysis stamp write failed: ${err instanceof Error ? err.message : String(err)}\n`,
@@ -744,6 +764,12 @@ export class Kernel implements IKernel {
744
764
  return this._entryUrl;
745
765
  }
746
766
 
767
+ /** The npm install root for this load (`<cache-root>/npm`), threaded to the
768
+ * controller loader so it doesn't re-derive it from the entry URL. */
769
+ getInstallRoot(): string | undefined {
770
+ return this._cacheRoot ? `${this._cacheRoot}/npm` : undefined;
771
+ }
772
+
747
773
  /** Authored `kind` of a declared resource by name, from the static manifest
748
774
  * set. Init-order-independent (unlike `resourceInstances`), so a controller
749
775
  * resolving a `!ref <name>` sentinel before the target initializes can still
@@ -136,9 +136,13 @@ export function computeAnalysisSignature(graph: LoadedGraph): string {
136
136
  * reading a newer stamp (or vice versa) discard rather than misparse. */
137
137
  export async function readAnalysisStamp(
138
138
  entryDir: string,
139
+ manifestsDir?: string,
139
140
  ): Promise<AnalysisStamp | undefined> {
141
+ const stampPath = manifestsDir
142
+ ? path.join(manifestsDir, ".validated.json")
143
+ : path.join(entryDir, ANALYSIS_STAMP_FILE);
140
144
  try {
141
- const text = await fs.readFile(path.join(entryDir, ANALYSIS_STAMP_FILE), "utf-8");
145
+ const text = await fs.readFile(stampPath, "utf-8");
142
146
  const parsed = JSON.parse(text) as Partial<AnalysisStamp>;
143
147
  if (
144
148
  parsed?.version === ANALYSIS_STAMP_FORMAT_VERSION &&
@@ -158,12 +162,15 @@ export async function readAnalysisStamp(
158
162
  export async function writeAnalysisStamp(
159
163
  entryDir: string,
160
164
  signature: string,
165
+ manifestsDir?: string,
161
166
  ): Promise<void> {
162
167
  const stamp: AnalysisStamp = {
163
168
  version: ANALYSIS_STAMP_FORMAT_VERSION,
164
169
  signature,
165
170
  };
166
- const target = path.join(entryDir, ANALYSIS_STAMP_FILE);
171
+ const target = manifestsDir
172
+ ? path.join(manifestsDir, ".validated.json")
173
+ : path.join(entryDir, ANALYSIS_STAMP_FILE);
167
174
  await fs.mkdir(path.dirname(target), { recursive: true });
168
175
  await fs.writeFile(target, JSON.stringify(stamp), "utf-8");
169
176
  }
@@ -133,8 +133,15 @@ export class LocalManifestCacheSource implements ManifestSource {
133
133
  private readonly cacheRoot: string;
134
134
  private readonly registryUrl: string;
135
135
 
136
- constructor(entryDir: string, registryUrl: string = DEFAULT_REGISTRY_URL) {
137
- this.cacheRoot = path.join(entryDir, CACHE_SUBDIR);
136
+ constructor(
137
+ entryDir: string,
138
+ registryUrl: string = DEFAULT_REGISTRY_URL,
139
+ manifestsDir?: string,
140
+ ) {
141
+ // `manifestsDir` is the resolved manifest-cache directory threaded from a
142
+ // single `resolveCacheRoot` (honours `TELO_CACHE_DIR`); when absent we fall
143
+ // back to the entry-anchored default so library/test callers are unchanged.
144
+ this.cacheRoot = manifestsDir ?? path.join(entryDir, CACHE_SUBDIR);
138
145
  this.registryUrl = registryUrl;
139
146
  }
140
147
 
@@ -189,8 +196,9 @@ export function cachePathForCanonical(
189
196
  canonicalSource: string,
190
197
  entryDir: string,
191
198
  registryUrl: string,
199
+ manifestsDir?: string,
192
200
  ): string | null {
193
- const cacheRoot = path.join(entryDir, CACHE_SUBDIR);
201
+ const cacheRoot = manifestsDir ?? path.join(entryDir, CACHE_SUBDIR);
194
202
  return cachePathForUrl(canonicalSource, cacheRoot, registryUrl);
195
203
  }
196
204
 
@@ -210,6 +218,7 @@ export async function writeManifestCache(
210
218
  graph: LoadedGraph,
211
219
  entryDir: string,
212
220
  registryUrl: string = DEFAULT_REGISTRY_URL,
221
+ manifestsDir?: string,
213
222
  ): Promise<string[]> {
214
223
  const written: string[] = [];
215
224
  const seen = new Set<string>();
@@ -220,7 +229,7 @@ export async function writeManifestCache(
220
229
  if (seen.has(file.source)) continue;
221
230
  seen.add(file.source);
222
231
 
223
- const target = cachePathForCanonical(file.source, entryDir, registryUrl);
232
+ const target = cachePathForCanonical(file.source, entryDir, registryUrl, manifestsDir);
224
233
  if (!target) continue;
225
234
 
226
235
  await fs.mkdir(path.dirname(target), { recursive: true });
@@ -254,3 +263,19 @@ export function resolveEntryDir(entryPath: string): string | null {
254
263
  return path.dirname(absolute);
255
264
  }
256
265
  }
266
+
267
+ /** The single `.telo` cache root for an entry, resolved once and threaded to
268
+ * every consumer (manifest cache, compiled validators, analysis stamp, npm
269
+ * install root) so none of them re-derive it or read the env independently.
270
+ *
271
+ * `TELO_CACHE_DIR` (the relocated root a prebuilt image bakes its deps into)
272
+ * wins; otherwise the root sits beside the entry at `<entry-dir>/.telo`.
273
+ * Returns `null` for http(s) entries with no local anchor (disk cache skipped).
274
+ * Consumers append the conventional subdirs: `manifests/`, `manifests/__validators/`,
275
+ * `npm/`. */
276
+ export function resolveCacheRoot(entryPath: string): string | null {
277
+ const override = process.env.TELO_CACHE_DIR;
278
+ if (override && override.trim()) return path.resolve(override.trim());
279
+ const entryDir = resolveEntryDir(entryPath);
280
+ return entryDir ? path.join(entryDir, ".telo") : null;
281
+ }
@@ -344,6 +344,10 @@ export class ResourceContextImpl implements ResourceContext {
344
344
  return this.kernel.getEntryUrl();
345
345
  }
346
346
 
347
+ getInstallRoot(): string | undefined {
348
+ return this.kernel.getInstallRoot();
349
+ }
350
+
347
351
  on(event: string, handler: (payload?: any) => void | Promise<void>): void {
348
352
  this.kernel.on(event, handler);
349
353
  }
@@ -117,6 +117,11 @@ export class SchemaValidator {
117
117
  private rawSchemas = new Map<string, object>();
118
118
  private compiledValidators = new WeakMap<object, DataValidator>();
119
119
  private cacheDir: string | undefined;
120
+ /** When false, the disk cache is read-only: compiled validators are still
121
+ * loaded from `cacheDir` but never written back. `telo run --no-cache-write`
122
+ * sets this so an ephemeral, read-only session rootfs validates in-memory
123
+ * without touching (or failing to write) the baked cache. */
124
+ private cacheWritable = true;
120
125
  /** Tracks (schema-hash → in-memory compiled validator) so two distinct
121
126
  * but content-equal schema objects share one compile across the kernel
122
127
  * process — `compiledValidators` is keyed by object identity and would
@@ -183,8 +188,9 @@ export class SchemaValidator {
183
188
  * kernel anchors this under `<entry-dir>/.telo/manifests/__validators/`
184
189
  * so it lives next to the manifest cache and rides along in
185
190
  * `COPY --from=build /srv /srv` Docker images. */
186
- setCacheDir(dir: string | undefined): void {
191
+ setCacheDir(dir: string | undefined, opts?: { write?: boolean }): void {
187
192
  this.cacheDir = dir;
193
+ this.cacheWritable = opts?.write ?? true;
188
194
  }
189
195
 
190
196
  compile(schema: any): DataValidator {
@@ -315,7 +321,7 @@ export class SchemaValidator {
315
321
  }
316
322
 
317
323
  const validate = this.ajv.compile(schema) as ValidateFunction;
318
- if (cacheDir) {
324
+ if (cacheDir && this.cacheWritable) {
319
325
  try {
320
326
  const body = standaloneCode(this.ajv, validate);
321
327
  const integrity = createHash("sha256").update(body).digest("hex");