whet 0.5.0 → 0.6.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.
@@ -1,6 +1,4 @@
1
- import {Router} from "./route/Router.js"
2
1
  import {Utils} from "./Utils.js"
3
- import {Stone} from "./Stone.js"
4
2
  import * as Path from "path"
5
3
  import {Register} from "../genes/Register.js"
6
4
  import * as Fs from "fs"
@@ -250,16 +248,16 @@ class ConfigStore extends Register.inherits() {
250
248
  if (val == null) {
251
249
  return true;
252
250
  };
253
- if (((val) instanceof Stone)) {
251
+ if (typeof val === "function") {
254
252
  return false;
255
253
  };
256
- if (((val) instanceof Router)) {
257
- return false;
254
+ if (typeof val !== "object") {
255
+ return true;
258
256
  };
259
- if (typeof val === "function") {
260
- return false;
257
+ if (((val) instanceof Array)) {
258
+ return true;
261
259
  };
262
- return true;
260
+ return (Object.getPrototypeOf(val) === null || Object.getPrototypeOf(val) === Object.prototype);
263
261
  }
264
262
  static deepClone(val) {
265
263
  if (val == null) {
package/bin/whet/Log.js CHANGED
@@ -72,7 +72,7 @@ class Log {
72
72
 
73
73
 
74
74
  Log.logLevel = 30
75
- Log.stream = process.stdout
75
+ Log.stream = process.stderr
76
76
  export const LogLevel = Register.global("$hxClasses")["whet._Log.LogLevel"] =
77
77
  class LogLevel {
78
78
  static fromString(s) {
@@ -17,6 +17,12 @@ export declare class Stone<T extends StoneConfig> {
17
17
  If true, hash of a cached file (not `stone.getHash()` but actual file contents) won't be checked.
18
18
  */
19
19
  ignoreFileHash: boolean
20
+
21
+ /**
22
+ * If true, this stone's own method source code is excluded from its hash (opt out of the
23
+ * code-aware self-hash). Only relevant for stones that override `generateHash()`. See `codeHash`.
24
+ */
25
+ ignoreCodeHash: boolean
20
26
  id: string
21
27
  cacheStrategy: CacheStrategy
22
28
  readonly cache: CacheManager
@@ -89,6 +95,10 @@ export declare class Stone<T extends StoneConfig> {
89
95
  * **Do not override.**
90
96
  * Used by cache. Returns either null, or result of `generateHash` finalized by adding
91
97
  * dependencies.
98
+ *
99
+ * When `generateHash()` returns a non-null hash, the stone's own code hash (`codeHash`) is mixed
100
+ * in, so editing the stone's generation logic busts the cache even though config/inputs are
101
+ * unchanged.
92
102
  */
93
103
  protected finalMaybeHash(): Promise<null | SourceHash>
94
104
 
@@ -108,8 +118,9 @@ export declare class Stone<T extends StoneConfig> {
108
118
  protected list(): Promise<null | string[]>
109
119
 
110
120
  /**
111
- * Public API for getting output IDs. Calls list(), falls back to
112
- * full generation if list() returns null.
121
+ * Public API for getting output IDs. Calls list(); if that returns null, tries the cache
122
+ * metadata fast path (`cache.tryListIds`, zero file reads / zero generation); only if the cache
123
+ * can't answer does it fall back to a full `getSource()`.
113
124
  */
114
125
  listIds(): Promise<string[]>
115
126
 
@@ -255,6 +266,34 @@ export declare class Stone<T extends StoneConfig> {
255
266
  protected profilerGetCurrentSpan(): null | AnySpan
256
267
  protected get_cache(): CacheManager
257
268
  toString(): string
269
+
270
+ /**
271
+ Per-class memo for `codeHash`, keyed by constructor. Computed once per process per class.
272
+ */
273
+ protected static codeHashCache: Map<any, SourceHash>
274
+
275
+ /**
276
+ * Hash of the stone class's own source: `Function.prototype.toString()` of *every* own method
277
+ * (instance/prototype methods, accessors, and static methods) defined on the stone's class and
278
+ * its ancestors down to — but excluding — `Stone` itself. Memoized per class.
279
+
280
+ *
281
+ * **Limitations** (not reachable by reflection from the class object): does NOT capture changes
282
+ * in *module-level free functions* in the same file, nor in *imported helper modules* a method
283
+ * delegates to. Only class-level methods (instance + static) participate; instance-own function
284
+ * properties are intentionally excluded so the per-class memo stays sound.
285
+ */
286
+ protected static codeHash(stone: AnyStone): SourceHash
287
+ protected static ownsFinalizeHash(obj: any): boolean
288
+
289
+ /**
290
+ * Appends `name:slot:source` for every own function-valued slot (`value`/`get`/`set`) of `obj`'s
291
+ * own properties to `parts`, sorted by name for determinism. Uses property descriptors so getters
292
+ * and setters are read by source rather than invoked. Skips `constructor` and Haxe-internal
293
+ * bookkeeping props (`__class__`, `__super__`, `__name__`, …) — those aren't stone logic and
294
+ * `__super__` would otherwise pull the parent class's source into every subclass's hash.
295
+ */
296
+ protected static collectOwnFunctions(obj: any, parts: string[]): void
258
297
  }
259
298
 
260
299
  export type StoneConfig = {
package/bin/whet/Stone.js CHANGED
@@ -10,6 +10,7 @@ import {Project} from "./Project.js"
10
10
  import {Log} from "./Log.js"
11
11
  import * as Path from "path"
12
12
  import {Register} from "../genes/Register.js"
13
+ import {Reflect as Reflect__1} from "../Reflect.js"
13
14
 
14
15
  const $global = Register.$global
15
16
 
@@ -19,6 +20,7 @@ class Stone extends Register.inherits() {
19
20
  this._contextPromise = null;
20
21
  this.locked = false;
21
22
  this.lockQueue = [];
23
+ this.ignoreCodeHash = false;
22
24
  this.ignoreFileHash = false;
23
25
  Log.log(10, ...["Instantiating new Stone.", {"type": StoneId_Fields_.getTypeName(this)}]);
24
26
  if (config == null) {
@@ -310,6 +312,10 @@ class Stone extends Register.inherits() {
310
312
  * **Do not override.**
311
313
  * Used by cache. Returns either null, or result of `generateHash` finalized by adding
312
314
  * dependencies.
315
+ *
316
+ * When `generateHash()` returns a non-null hash, the stone's own code hash (`codeHash`) is mixed
317
+ * in, so editing the stone's generation logic busts the cache even though config/inputs are
318
+ * unchanged.
313
319
  */
314
320
  finalMaybeHash() {
315
321
  let tmp = this.config.configStore;
@@ -318,6 +324,9 @@ class Stone extends Register.inherits() {
318
324
  let _gthis = this;
319
325
  return patchPromise.then(function (_) {
320
326
  return _gthis.generateHash().then(function (hash) {
327
+ if (hash != null && !_gthis.ignoreCodeHash) {
328
+ hash = hash.add(Stone.codeHash(_gthis));
329
+ };
321
330
  return _gthis.finalizeHash(hash);
322
331
  });
323
332
  });
@@ -364,8 +373,9 @@ class Stone extends Register.inherits() {
364
373
  }
365
374
 
366
375
  /**
367
- * Public API for getting output IDs. Calls list(), falls back to
368
- * full generation if list() returns null.
376
+ * Public API for getting output IDs. Calls list(); if that returns null, tries the cache
377
+ * metadata fast path (`cache.tryListIds`, zero file reads / zero generation); only if the cache
378
+ * can't answer does it fall back to a full `getSource()`.
369
379
  */
370
380
  listIds() {
371
381
  let _gthis = this;
@@ -373,16 +383,21 @@ class Stone extends Register.inherits() {
373
383
  if (ids != null) {
374
384
  return ids;
375
385
  };
376
- return _gthis.getSource().then(function (source) {
377
- let _this = source.data;
378
- let result = new Array(_this.length);
379
- let _g = 0;
380
- let _g1 = _this.length;
381
- while (_g < _g1) {
382
- let i = _g++;
383
- result[i] = _this[i].id;
386
+ return _gthis.project.cache.tryListIds(_gthis).then(function (cachedIds) {
387
+ if (cachedIds != null) {
388
+ return cachedIds;
384
389
  };
385
- return result;
390
+ return _gthis.getSource().then(function (source) {
391
+ let _this = source.data;
392
+ let result = new Array(_this.length);
393
+ let _g = 0;
394
+ let _g1 = _this.length;
395
+ while (_g < _g1) {
396
+ let i = _g++;
397
+ result[i] = _this[i].id;
398
+ };
399
+ return result;
400
+ });
386
401
  });
387
402
  });
388
403
  }
@@ -701,6 +716,83 @@ class Stone extends Register.inherits() {
701
716
  toString() {
702
717
  return "" + this.id + ":" + StoneId_Fields_.getTypeName(this);
703
718
  }
719
+
720
+ /**
721
+ * Hash of the stone class's own source: `Function.prototype.toString()` of *every* own method
722
+ * (instance/prototype methods, accessors, and static methods) defined on the stone's class and
723
+ * its ancestors down to — but excluding — `Stone` itself. Memoized per class.
724
+
725
+ *
726
+ * **Limitations** (not reachable by reflection from the class object): does NOT capture changes
727
+ * in *module-level free functions* in the same file, nor in *imported helper modules* a method
728
+ * delegates to. Only class-level methods (instance + static) participate; instance-own function
729
+ * properties are intentionally excluded so the per-class memo stays sound.
730
+ */
731
+ static codeHash(stone) {
732
+ let ctor = stone.constructor;
733
+ let cached = Stone.codeHashCache.get(ctor);
734
+ if (cached != null) {
735
+ return cached;
736
+ };
737
+ let parts = [];
738
+ let proto = Object.getPrototypeOf(stone);
739
+ while (proto != null && !Object.prototype.hasOwnProperty.call(proto, 'finalizeHash')) {
740
+ Stone.collectOwnFunctions(proto, parts);
741
+ proto = Object.getPrototypeOf(proto);
742
+ };
743
+ let funcProto = Function.prototype;
744
+ let klass = ctor;
745
+ while (klass != null && klass != funcProto) {
746
+ let kproto = klass.prototype;
747
+ if (kproto != null && Object.prototype.hasOwnProperty.call(kproto, 'finalizeHash')) {
748
+ break;
749
+ };
750
+ Stone.collectOwnFunctions(klass, parts);
751
+ klass = Object.getPrototypeOf(klass);
752
+ };
753
+ let hash = SourceHash.fromString(parts.join("\n"));
754
+ Stone.codeHashCache.set(ctor, hash);
755
+ return hash;
756
+ }
757
+ static ownsFinalizeHash(obj) {
758
+ return Object.prototype.hasOwnProperty.call(obj, 'finalizeHash');
759
+ }
760
+
761
+ /**
762
+ * Appends `name:slot:source` for every own function-valued slot (`value`/`get`/`set`) of `obj`'s
763
+ * own properties to `parts`, sorted by name for determinism. Uses property descriptors so getters
764
+ * and setters are read by source rather than invoked. Skips `constructor` and Haxe-internal
765
+ * bookkeeping props (`__class__`, `__super__`, `__name__`, …) — those aren't stone logic and
766
+ * `__super__` would otherwise pull the parent class's source into every subclass's hash.
767
+ */
768
+ static collectOwnFunctions(obj, parts) {
769
+ let names = Object.getOwnPropertyNames(obj);
770
+ names.sort(Reflect__1.compare);
771
+ let _g = 0;
772
+ while (_g < names.length) {
773
+ let name = names[_g];
774
+ ++_g;
775
+ if (name == "constructor" || name.startsWith("__")) {
776
+ continue;
777
+ };
778
+ let desc = Object.getOwnPropertyDescriptor(obj, name);
779
+ if (desc == null) {
780
+ continue;
781
+ };
782
+ let fn = desc["value"];
783
+ if (fn != null && typeof fn === 'function') {
784
+ parts.push(name + ":" + "value" + ":" + Function.prototype.toString.call(fn));
785
+ };
786
+ let fn1 = desc["get"];
787
+ if (fn1 != null && typeof fn1 === 'function') {
788
+ parts.push(name + ":" + "get" + ":" + Function.prototype.toString.call(fn1));
789
+ };
790
+ let fn2 = desc["set"];
791
+ if (fn2 != null && typeof fn2 === 'function') {
792
+ parts.push(name + ":" + "set" + ":" + Function.prototype.toString.call(fn2));
793
+ };
794
+ };
795
+ }
704
796
  static get __name__() {
705
797
  return "whet.Stone"
706
798
  }
@@ -710,6 +802,7 @@ class Stone extends Register.inherits() {
710
802
  }
711
803
  Stone.prototype.config = null;
712
804
  Stone.prototype.ignoreFileHash = null;
805
+ Stone.prototype.ignoreCodeHash = null;
713
806
  Stone.prototype.id = null;
714
807
  Stone.prototype.cacheStrategy = null;
715
808
  Stone.prototype.project = null;
@@ -717,3 +810,5 @@ Stone.prototype.lockQueue = null;
717
810
  Stone.prototype.locked = null;
718
811
  Stone.prototype._contextPromise = null;
719
812
 
813
+
814
+ Stone.codeHashCache = new Map()
@@ -1,4 +1,4 @@
1
- import {Command, Option} from "commander"
1
+ import {Command, Option, Argument} from "commander"
2
2
 
3
3
  export const program: Command
4
4
  export const main: () => void
package/bin/whet/Whet.js CHANGED
@@ -5,7 +5,7 @@ import PinoPretty from "pino-pretty"
5
5
  import {Exception} from "../haxe/Exception.js"
6
6
  import {Register} from "../genes/Register.js"
7
7
  import * as Fs from "fs"
8
- import {Command, CommanderError} from "commander"
8
+ import {Command, Option, CommanderError} from "commander"
9
9
  import {Std} from "../Std.js"
10
10
 
11
11
  const $global = Register.$global
@@ -26,7 +26,7 @@ class Whet_Fields_ {
26
26
  if (entryUrl != thisUrl) {
27
27
  return;
28
28
  };
29
- Whet_Fields_.program.enablePositionalOptions().passThroughOptions().description("Project tooling.").usage("[options] [command] [+ [command]...]").version("0.5.0", "-v, --version").allowUnknownOption(true).allowExcessArguments(true).showSuggestionAfterError(true).option("-p, --project <file>", "project to run", "Project.mjs").option("-l, --log-level <level>", "log level, a string/number", "info").option("--no-pretty", "disable pretty logging").option("--profile <format>", "enable profiling, export to whet-profile.json on exit (format: json or trace, default: json)").exitOverride();
29
+ Whet_Fields_.program.enablePositionalOptions().passThroughOptions().description("Project tooling.").usage("[options] [command] [+ [command]...]").version("0.6.0", "-v, --version").allowUnknownOption(true).allowExcessArguments(true).showSuggestionAfterError(true).helpOption(false).option("-p, --project <file>", "project to run", "Project.mjs").addOption(new Option("-l, --log-level <level>", "log level, a string/number")["default"]("info").env("WHET_LOG_LEVEL")).option("--no-pretty", "disable pretty logging").option("-q, --quiet", "quiet output: warn level + no color (the default when stdout is not a TTY)").option("--profile <format>", "enable profiling, export to whet-profile.json on exit (format: json or trace, default: json)").exitOverride();
30
30
  try {
31
31
  Whet_Fields_.program.parse();
32
32
  }catch (_g) {
@@ -37,6 +37,8 @@ class Whet_Fields_ {
37
37
  throw Exception.thrown(_g1);
38
38
  };
39
39
  };
40
+ let firstSegment = Whet_Fields_.getCommands(Whet_Fields_.program.args)[0];
41
+ Whet_Fields_.topLevelHelp = firstSegment.length > 0 && (firstSegment[0] == "--help" || firstSegment[0] == "-h");
40
42
  let options = Whet_Fields_.program.opts();
41
43
  if (options.logLevel != null) {
42
44
  let n = Std.parseInt(options.logLevel);
@@ -49,8 +51,14 @@ class Whet_Fields_ {
49
51
  Log.logLevel = n;
50
52
  };
51
53
  };
52
- if (options.pretty) {
53
- Log.stream = PinoPretty();
54
+ let nonTty = process.stdout.isTTY != true;
55
+ let levelSource = Whet_Fields_.program.getOptionValueSource("logLevel");
56
+ let prettySource = Whet_Fields_.program.getOptionValueSource("pretty");
57
+ if (options.quiet || nonTty && levelSource == "default") {
58
+ Log.logLevel = 40;
59
+ };
60
+ if ((options.quiet) ? false : (prettySource != "default") ? options.pretty : !nonTty) {
61
+ Log.stream = PinoPretty({"destination": 2});
54
62
  };
55
63
  global.setImmediate(Whet_Fields_.init, options);
56
64
  }
@@ -66,9 +74,11 @@ class Whet_Fields_ {
66
74
  Log.log(10, ...["Project module imported."]);
67
75
  Whet_Fields_.initProjects();
68
76
  })["catch"](function (e) {
69
- Log.log(50, ...["Error loading project.", {"error": e}]);
70
- if (((e) instanceof Error)) {
71
- Log.log(50, ...[e.stack]);
77
+ if (!Whet_Fields_.topLevelHelp) {
78
+ Log.log(50, ...["Error loading project.", {"error": e}]);
79
+ if (((e) instanceof Error)) {
80
+ Log.log(50, ...[e.stack]);
81
+ };
72
82
  };
73
83
  try {
74
84
  Whet_Fields_.program.help();
@@ -87,6 +97,7 @@ class Whet_Fields_ {
87
97
  while (_g2 < _g3.length) Whet_Fields_.program.addOption(_g3[_g2++]);
88
98
  };
89
99
  Whet_Fields_.program.allowUnknownOption(false);
100
+ Whet_Fields_.program.helpOption("-h, --help", "display help for command");
90
101
  let schemaCmd = new Command("schema");
91
102
  schemaCmd.description("Export project schema as JSON.");
92
103
  schemaCmd.action(Whet_Fields_.outputSchema);
@@ -144,7 +155,7 @@ class Whet_Fields_ {
144
155
  Whet_Fields_.program.parseAsync(c, {"from": "user"}).then(function (_) {
145
156
  nextCommand();
146
157
  })["catch"](function (err) {
147
- if (((err) instanceof CommanderError) && err.code == "commander.help") {
158
+ if (((err) instanceof CommanderError) && (err.code == "commander.help" || err.code == "commander.helpDisplayed")) {
148
159
  return;
149
160
  };
150
161
  Log.log(50, ...["Error while executing command.", {"error": err}]);
@@ -188,6 +199,9 @@ class Whet_Fields_ {
188
199
  static serializeOption(opt) {
189
200
  return {"name": opt.name(), "attributeName": opt.attributeName(), "flags": opt.flags, "description": opt.description, "choices": opt.argChoices, "defaultValue": opt.defaultValue, "required": opt.required, "mandatory": opt.mandatory, "boolean": opt.isBoolean(), "hidden": opt.hidden};
190
201
  }
202
+ static serializeArgument(arg) {
203
+ return {"name": arg.name(), "description": arg.description, "choices": arg.argChoices, "defaultValue": arg.defaultValue, "required": arg.required, "variadic": arg.variadic};
204
+ }
191
205
  static serializeCommand(cmd) {
192
206
  let tmp = cmd.name();
193
207
  let tmp1 = cmd.description();
@@ -196,7 +210,11 @@ class Whet_Fields_ {
196
210
  let _g1 = 0;
197
211
  let _g2 = cmd.options;
198
212
  while (_g1 < _g2.length) _g.push(Whet_Fields_.serializeOption(_g2[_g1++]));
199
- return {"name": tmp, "description": tmp1, "aliases": tmp2, "options": _g};
213
+ let _g3 = [];
214
+ let _g4 = 0;
215
+ let _g5 = cmd.registeredArguments;
216
+ while (_g4 < _g5.length) _g3.push(Whet_Fields_.serializeArgument(_g5[_g4++]));
217
+ return {"name": tmp, "description": tmp1, "aliases": tmp2, "options": _g, "arguments": _g3};
200
218
  }
201
219
  static getCommands(args) {
202
220
  let commands = [];
@@ -221,5 +239,6 @@ class Whet_Fields_ {
221
239
 
222
240
 
223
241
  Whet_Fields_.program = new Command("whet")
242
+ Whet_Fields_.topLevelHelp = false
224
243
  export const program = Whet_Fields_.program
225
244
  export const main = Whet_Fields_.main
@@ -15,6 +15,20 @@ export declare class BaseCache<Key, Value extends {
15
15
  protected rootDir: string
16
16
  get(stone: AnyStone, durability: CacheDurability, check: DurabilityCheck): Promise<Source>
17
17
  getPartial(stone: AnyStone, sourceId: string, durability: CacheDurability, check: DurabilityCheck): Promise<null | Source>
18
+
19
+ /**
20
+ * Enumerate a stone's output ids from cache metadata alone — no file reads, no generation.
21
+ * Used by `Stone.listIds()` as a fast path before falling back to a full `getSource()`.
22
+ *
23
+ * Returns null (caller falls back) unless there is a *complete* entry whose hash matches the
24
+ * stone's current `finalMaybeHash()`. Sound because the output id-set is a pure function of that
25
+ * hash, so a complete matching entry's files are the authoritative id list. Stale/missing files
26
+ * on disk are irrelevant here — they're stat-validated (and regenerated) when actually fetched.
27
+ *
28
+ * Intentionally takes no lock and does not touch use-order/durability: listing is a read-only
29
+ * "what does this produce" query, not a use of the cached bytes.
30
+ */
31
+ tryListIds(stone: AnyStone): Promise<null | string[]>
18
32
  protected set(source: Source): Promise<Value>
19
33
  getUniqueDir(stone: AnyStone, baseDir: string, hash?: null | SourceHash): string
20
34
 
@@ -35,6 +49,13 @@ export declare class BaseCache<Key, Value extends {
35
49
  protected key(stone: AnyStone): Key
36
50
  protected value(source: Source): Promise<Value>
37
51
  protected source(stone: AnyStone, value: Value): Promise<Source>
52
+
53
+ /**
54
+ * Read & validate just the requested file from an entry, without touching the rest. Resolves
55
+ * the single-output Source when valid, or null when that file is invalid/missing (the caller
56
+ * then regenerates it).
57
+ */
58
+ protected sourcePartial(stone: AnyStone, value: Value, sourceId: string): Promise<null | Source>
38
59
  protected getExistingDirs(stone: AnyStone): string[]
39
60
  protected getDirFor(value: Value): string
40
61
 
@@ -43,6 +64,11 @@ export declare class BaseCache<Key, Value extends {
43
64
  */
44
65
  protected hasSourceId(value: Value, sourceId: string): boolean
45
66
 
67
+ /**
68
+ All output ids held by an entry, read from metadata without touching files.
69
+ */
70
+ protected getValueIds(value: Value): string[]
71
+
46
72
  /**
47
73
  Merge partial source data into an existing entry, returns the updated value.
48
74
  */
@@ -179,17 +179,7 @@ class BaseCache extends Register.inherits() {
179
179
  _gthis.setRecentUseOrder(values, value);
180
180
  };
181
181
  };
182
- if (value != null && _gthis.hasSourceId(value, sourceId)) {
183
- return _gthis.source(stone, value).then(function (src) {
184
- if (src != null) {
185
- return src.filterTo(sourceId);
186
- } else {
187
- return null;
188
- };
189
- });
190
- } else if (value != null && value.complete) {
191
- return Promise.resolve(null);
192
- } else {
182
+ let regenerate = function () {
193
183
  return ((stone.project.profiler != null) ? stone.project.profiler.withSpan(stone, "GeneratePartial", function () {
194
184
  return stone.generatePartialSource(sourceId, hash);
195
185
  }, {"sourceId": sourceId}) : stone.generatePartialSource(sourceId, hash)).then(function (result) {
@@ -208,7 +198,55 @@ class BaseCache extends Register.inherits() {
208
198
  };
209
199
  });
210
200
  };
201
+ if (value != null && _gthis.hasSourceId(value, sourceId)) {
202
+ return _gthis.sourcePartial(stone, value, sourceId).then(function (src) {
203
+ if (src != null) {
204
+ return src;
205
+ };
206
+ return regenerate();
207
+ });
208
+ } else if (value != null && value.complete) {
209
+ return Promise.resolve(null);
210
+ } else {
211
+ return regenerate();
212
+ };
213
+ });
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Enumerate a stone's output ids from cache metadata alone — no file reads, no generation.
219
+ * Used by `Stone.listIds()` as a fast path before falling back to a full `getSource()`.
220
+ *
221
+ * Returns null (caller falls back) unless there is a *complete* entry whose hash matches the
222
+ * stone's current `finalMaybeHash()`. Sound because the output id-set is a pure function of that
223
+ * hash, so a complete matching entry's files are the authoritative id list. Stale/missing files
224
+ * on disk are irrelevant here — they're stat-validated (and regenerated) when actually fetched.
225
+ *
226
+ * Intentionally takes no lock and does not touch use-order/durability: listing is a read-only
227
+ * "what does this produce" query, not a use of the cached bytes.
228
+ */
229
+ tryListIds(stone) {
230
+ let _gthis = this;
231
+ return stone.finalMaybeHash().then(function (hash) {
232
+ if (hash == null) {
233
+ return null;
234
+ };
235
+ let values = _gthis.cache.get(_gthis.key(stone));
236
+ if (values == null) {
237
+ return null;
238
+ };
239
+ let value = Lambda.find(values, function (v) {
240
+ if (SourceHash.equals(v.hash, hash)) {
241
+ return v.complete;
242
+ } else {
243
+ return false;
244
+ };
211
245
  });
246
+ if (value == null) {
247
+ return null;
248
+ };
249
+ return _gthis.getValueIds(value);
212
250
  });
213
251
  }
214
252
  set(source) {
@@ -20,6 +20,13 @@ export declare class CacheManager {
20
20
  getSource(stone: AnyStone): Promise<Source>
21
21
  getPartialSource(stone: AnyStone, sourceId: string): Promise<null | Source>
22
22
 
23
+ /**
24
+ * Fast path for `Stone.listIds()`: enumerate output ids from cache metadata, without reading
25
+ * files or generating. Returns null when no usable entry exists (caller falls back to
26
+ * `getSource()`). `None` is never cached, so there's nothing to answer from.
27
+ */
28
+ tryListIds(stone: AnyStone): Promise<null | string[]>
29
+
23
30
  /**
24
31
  * Re-generates source even if the currently cached value is valid.
25
32
  */
@@ -128,6 +128,29 @@ class CacheManager extends Register.inherits() {
128
128
  };
129
129
  }
130
130
 
131
+ /**
132
+ * Fast path for `Stone.listIds()`: enumerate output ids from cache metadata, without reading
133
+ * files or generating. Returns null when no usable entry exists (caller falls back to
134
+ * `getSource()`). `None` is never cached, so there's nothing to answer from.
135
+ */
136
+ tryListIds(stone) {
137
+ switch (stone.cacheStrategy._hx_index) {
138
+ case 0:
139
+ return Promise.resolve(null);
140
+ break
141
+ case 1:
142
+ return this.memCache.tryListIds(stone);
143
+ break
144
+ case 2:
145
+ return this.fileCache.tryListIds(stone);
146
+ break
147
+ case 3:
148
+ return this.fileCache.tryListIds(stone);
149
+ break
150
+
151
+ };
152
+ }
153
+
131
154
  /**
132
155
  * Re-generates source even if the currently cached value is valid.
133
156
  */
@@ -1,7 +1,7 @@
1
1
  import {BaseCache} from "./BaseCache"
2
2
  import {AnyStone} from "../Stone"
3
3
  import {SourceHash} from "../SourceHash"
4
- import {Source} from "../Source"
4
+ import {Source, SourceData} from "../Source"
5
5
 
6
6
  export declare class FileCache extends BaseCache<string, RuntimeFileCacheValue> {
7
7
  constructor(rootDir: string)
@@ -11,7 +11,15 @@ export declare class FileCache extends BaseCache<string, RuntimeFileCacheValue>
11
11
  protected flushPromise: Promise<void>
12
12
  protected key(stone: AnyStone): string
13
13
  protected value(source: Source): Promise<RuntimeFileCacheValue>
14
+
15
+ /**
16
+ * Read & validate a single cached file. Resolves with its `SourceData` when valid, or `null`
17
+ * when the file is missing or fails mtime+hash validation (a cache miss for that file). Rejects
18
+ * only on unexpected IO errors.
19
+ */
20
+ protected readCachedFile(stone: AnyStone, file: RuntimeFileEntry): Promise<null | SourceData>
14
21
  protected source(stone: AnyStone, value: RuntimeFileCacheValue): Promise<Source>
22
+ protected sourcePartial(stone: AnyStone, value: RuntimeFileCacheValue, sourceId: string): Promise<null | Source>
15
23
  protected set(source: Source): Promise<RuntimeFileCacheValue>
16
24
  protected getExistingDirs(stone: AnyStone): string[]
17
25
  protected remove(stone: AnyStone, value: RuntimeFileCacheValue): Promise<any>
@@ -19,6 +27,7 @@ export declare class FileCache extends BaseCache<string, RuntimeFileCacheValue>
19
27
  protected setRecentUseOrder(values: RuntimeFileCacheValue[], value: RuntimeFileCacheValue): boolean
20
28
  protected getDirFor(value: RuntimeFileCacheValue): string
21
29
  protected hasSourceId(value: RuntimeFileCacheValue, sourceId: string): boolean
30
+ protected getValueIds(value: RuntimeFileCacheValue): string[]
22
31
  protected mergePartial(stone: AnyStone, existing: RuntimeFileCacheValue, addition: Source, markComplete: boolean): Promise<RuntimeFileCacheValue>
23
32
  protected replaceEntry(stone: AnyStone, existing: RuntimeFileCacheValue, replacement: Source): Promise<RuntimeFileCacheValue>
24
33
  protected flush(): Promise<void>
@@ -44,3 +53,11 @@ export type FileCacheValue<H, S> = {
44
53
  export type DbJson = {[key: string]: FileCacheValue<string, string>[]}
45
54
 
46
55
  export type RuntimeFileCacheValue = FileCacheValue<SourceHash, string>
56
+
57
+ export type RuntimeFileEntry = {
58
+ fileHash: SourceHash,
59
+ filePath: string,
60
+ id: string,
61
+ mtime?: null | number,
62
+ size?: null | number
63
+ }
@@ -84,67 +84,102 @@ class FileCache extends Register.inherits(() => BaseCache, true) {
84
84
  return {"hash": source.hash, "ctime": source.ctime, "baseDir": source.getDirPath(), "complete": source.complete, "ctimePretty": null, "files": files};
85
85
  });
86
86
  }
87
+
88
+ /**
89
+ * Read & validate a single cached file. Resolves with its `SourceData` when valid, or `null`
90
+ * when the file is missing or fails mtime+hash validation (a cache miss for that file). Rejects
91
+ * only on unexpected IO errors.
92
+ */
93
+ readCachedFile(stone, file) {
94
+ let _gthis = this;
95
+ return new Promise(function (res, rej) {
96
+ let path = Path.posix.join(".", _gthis.rootDir, ".", file.filePath);
97
+ Fs.stat(path, function (statErr, stats) {
98
+ if (statErr != null) {
99
+ if (((statErr) instanceof Error) && statErr.code == "ENOENT") {
100
+ res(null);
101
+ } else {
102
+ rej(statErr);
103
+ };
104
+ return;
105
+ };
106
+ let mtimeMatch = file.mtime != null && stats.mtimeMs == file.mtime && (stats.size | 0) == file.size;
107
+ if (mtimeMatch && !stone.ignoreFileHash) {
108
+ SourceData.fromFileSkipHash(file.id, path, file.filePath, file.fileHash).then(res, rej);
109
+ } else {
110
+ SourceData.fromFile(file.id, path, file.filePath).then(function (sourceData) {
111
+ if (sourceData == null || !stone.ignoreFileHash && !SourceHash.equals(sourceData.hash, file.fileHash)) {
112
+ res(null);
113
+ } else {
114
+ res(sourceData);
115
+ };
116
+ }, function (err) {
117
+ if (((err) instanceof Error) && err.code == "ENOENT") {
118
+ res(null);
119
+ } else {
120
+ rej(err);
121
+ };
122
+ });
123
+ };
124
+ });
125
+ });
126
+ }
87
127
  source(stone, value) {
128
+ let tmp;
88
129
  let _g = stone.cacheStrategy;
89
130
  if (_g._hx_index == 3) {
90
131
  let _g1 = _g.path;
91
- let invalidPath;
92
132
  if (value.files.length == 1 && !(_g1.length == 0 || _g1.charCodeAt(_g1.length - 1) == 47)) {
93
- invalidPath = value.files[0].filePath != _g1;
133
+ tmp = value.files[0].filePath != _g1;
94
134
  } else {
95
135
  let dir = _g1.substring(0, _g1.lastIndexOf("/") + 1);
96
- invalidPath = value.baseDir != ((dir.length == 0) ? "./" : dir);
97
- };
98
- if (invalidPath) {
99
- return Promise.resolve(null);
136
+ tmp = value.baseDir != ((dir.length == 0) ? "./" : dir);
100
137
  };
138
+ } else {
139
+ tmp = false;
140
+ };
141
+ if (tmp) {
142
+ return Promise.resolve(null);
101
143
  };
102
- let _gthis = this;
103
144
  let _g1 = [];
104
145
  let _g2 = 0;
105
146
  let _g3 = value.files;
106
- while (_g2 < _g3.length) {
107
- let file = _g3[_g2];
108
- ++_g2;
109
- _g1.push(new Promise(function (res, rej) {
110
- let path = Path.posix.join(".", _gthis.rootDir, ".", file.filePath);
111
- Fs.stat(path, function (statErr, stats) {
112
- if (statErr != null) {
113
- if (((statErr) instanceof Error) && statErr.code == "ENOENT") {
114
- rej("Invalid.");
115
- } else {
116
- rej(statErr);
117
- };
118
- return;
119
- };
120
- let mtimeMatch = file.mtime != null && stats.mtimeMs == file.mtime && (stats.size | 0) == file.size;
121
- if (mtimeMatch && !stone.ignoreFileHash) {
122
- SourceData.fromFileSkipHash(file.id, path, file.filePath, file.fileHash).then(res, rej);
123
- } else {
124
- SourceData.fromFile(file.id, path, file.filePath).then(function (sourceData) {
125
- if (sourceData == null || !stone.ignoreFileHash && !SourceHash.equals(sourceData.hash, file.fileHash)) {
126
- rej("Invalid.");
127
- } else {
128
- res(sourceData);
129
- };
130
- }, function (err) {
131
- if (((err) instanceof Error) && err.code == "ENOENT") {
132
- rej("Invalid.");
133
- } else {
134
- rej(err);
135
- };
136
- });
137
- };
138
- });
139
- }));
140
- };
147
+ while (_g2 < _g3.length) _g1.push(this.readCachedFile(stone, _g3[_g2++]));
141
148
  return Promise.all(_g1).then(function (data) {
142
- return new Source(data, value.hash, stone, value.ctime, (value.complete != null) ? value.complete : true);
143
- }, function (rejected) {
144
- if (rejected == "Invalid.") {
149
+ let _g = 0;
150
+ while (_g < data.length) if (data[_g++] == null) {
145
151
  return null;
152
+ };
153
+ return new Source(data, value.hash, stone, value.ctime, (value.complete != null) ? value.complete : true);
154
+ });
155
+ }
156
+ sourcePartial(stone, value, sourceId) {
157
+ let tmp;
158
+ let _g = stone.cacheStrategy;
159
+ if (_g._hx_index == 3) {
160
+ let _g1 = _g.path;
161
+ if (value.files.length == 1 && !(_g1.length == 0 || _g1.charCodeAt(_g1.length - 1) == 47)) {
162
+ tmp = value.files[0].filePath != _g1;
163
+ } else {
164
+ let dir = _g1.substring(0, _g1.lastIndexOf("/") + 1);
165
+ tmp = value.baseDir != ((dir.length == 0) ? "./" : dir);
166
+ };
167
+ } else {
168
+ tmp = false;
169
+ };
170
+ if (tmp) {
171
+ return Promise.resolve(null);
172
+ };
173
+ let file = Lambda.find(value.files, function (f) {
174
+ return f.id == sourceId;
175
+ });
176
+ if (file == null) {
177
+ return Promise.resolve(null);
178
+ };
179
+ return this.readCachedFile(stone, file).then(function (sd) {
180
+ if (sd != null) {
181
+ return new Source([sd], value.hash, stone, value.ctime, false);
146
182
  } else {
147
- throw rejected;
148
183
  return null;
149
184
  };
150
185
  });
@@ -239,6 +274,13 @@ class FileCache extends Register.inherits(() => BaseCache, true) {
239
274
  return f.id == sourceId;
240
275
  });
241
276
  }
277
+ getValueIds(value) {
278
+ let _g = [];
279
+ let _g1 = 0;
280
+ let _g2 = value.files;
281
+ while (_g1 < _g2.length) _g.push(_g2[_g1++].id);
282
+ return _g;
283
+ }
242
284
  mergePartial(stone, existing, addition, markComplete) {
243
285
  let _g = [];
244
286
  let _g1 = 0;
@@ -7,9 +7,11 @@ export declare class MemoryCache extends BaseCache<AnyStone, Source> {
7
7
  protected key(stone: AnyStone): AnyStone
8
8
  protected value(source: Source): Promise<Source>
9
9
  protected source(stone: AnyStone, value: Source): Promise<Source>
10
+ protected sourcePartial(stone: AnyStone, value: Source, sourceId: string): Promise<null | Source>
10
11
  protected getExistingDirs(stone: AnyStone): string[]
11
12
  protected getDirFor(value: Source): string
12
13
  protected hasSourceId(value: Source, sourceId: string): boolean
14
+ protected getValueIds(value: Source): string[]
13
15
  protected mergePartial(stone: AnyStone, existing: Source, addition: Source, markComplete: boolean): Promise<Source>
14
16
  protected replaceEntry(stone: AnyStone, existing: Source, replacement: Source): Promise<Source>
15
17
  }
@@ -20,6 +20,9 @@ class MemoryCache extends Register.inherits(() => BaseCache, true) {
20
20
  source(stone, value) {
21
21
  return Promise.resolve(value);
22
22
  }
23
+ sourcePartial(stone, value, sourceId) {
24
+ return Promise.resolve(value.filterTo(sourceId));
25
+ }
23
26
  getExistingDirs(stone) {
24
27
  let list = this.cache.inst.get(stone);
25
28
  if (list != null) {
@@ -52,6 +55,13 @@ class MemoryCache extends Register.inherits(() => BaseCache, true) {
52
55
  return d.id == sourceId;
53
56
  });
54
57
  }
58
+ getValueIds(value) {
59
+ let _g = [];
60
+ let _g1 = 0;
61
+ let _g2 = value.data;
62
+ while (_g1 < _g2.length) _g.push(_g2[_g1++].id);
63
+ return _g;
64
+ }
55
65
  mergePartial(stone, existing, addition, markComplete) {
56
66
  let mergedData = existing.data.slice();
57
67
  if (addition != null) {
@@ -13,6 +13,7 @@ export declare class Files extends Stone<FilesConfig> {
13
13
  protected generateHash(): Promise<SourceHash>
14
14
  protected list(): Promise<null | string[]>
15
15
  protected generate(hash: SourceHash): Promise<SourceData[]>
16
+ protected generatePartial(sourceId: string, hash: SourceHash): Promise<null | SourceData[]>
16
17
  protected walk<T>(onFile: ((arg0: string) => T), onDirFile: ((arg0: string, arg1: string) => T)): Promise<T[]>
17
18
  protected fromCwd(file: string, dir: string): {
18
19
  id: string,
@@ -7,6 +7,7 @@ import {SourceHash} from "../SourceHash.js"
7
7
  import {SourceData} from "../Source.js"
8
8
  import {Project} from "../Project.js"
9
9
  import {Register} from "../../genes/Register.js"
10
+ import {Lambda} from "../../Lambda.js"
10
11
 
11
12
  const $global = Register.$global
12
13
 
@@ -128,6 +129,55 @@ class Files extends Register.inherits(() => Stone, true) {
128
129
  return Promise.all(fileProms);
129
130
  });
130
131
  }
132
+ generatePartial(sourceId, hash) {
133
+ let _gthis = this;
134
+ let onDirFile = function (dir, dirFile) {
135
+ let p_id;
136
+ let pathId = IdUtils.fromCwdPath(dirFile, RootDir.fromProject(_gthis.project));
137
+ if (dir == "/") {
138
+ p_id = pathId;
139
+ } else {
140
+ if (!(dir.length == 0 || dir.charCodeAt(dir.length - 1) == 47)) {
141
+ throw new Error("\"" + dir + "\" is not a directory.");
142
+ };
143
+ let p_id1;
144
+ let dir1 = pathId.substring(0, pathId.lastIndexOf("/") + 1);
145
+ p_id1 = ((dir1.length == 0) ? "./" : dir1).indexOf(dir) == 0;
146
+ p_id = (p_id1) ? pathId.substring(dir.length) : null;
147
+ };
148
+ return {"id": p_id, "cwd": dirFile, "pathId": pathId};
149
+ };
150
+ let files = [];
151
+ let dirs = [];
152
+ let _g = 0;
153
+ let _g1 = MaybeArray_Fields_.makeArray(this.config.paths);
154
+ while (_g < _g1.length) {
155
+ let path = _g1[_g];
156
+ ++_g;
157
+ if (path.length == 0 || path.charCodeAt(path.length - 1) == 47) {
158
+ dirs.push(Utils.listDirectoryFiles(this.cwdPath(path), this.config.recursive).then(function (arr) {
159
+ let _g = 0;
160
+ while (_g < arr.length) files.push(onDirFile(path, arr[_g++]));
161
+ }));
162
+ } else {
163
+ let id = path;
164
+ files.push({"id": id.substring(id.lastIndexOf("/") + 1), "cwd": _gthis.cwdPath(path), "pathId": path});
165
+ };
166
+ };
167
+ return Promise.all(dirs).then(function (_) {
168
+ return files;
169
+ }).then(function (entries) {
170
+ let match = Lambda.find(entries, function (e) {
171
+ return e.id == sourceId;
172
+ });
173
+ if (match == null) {
174
+ return Promise.resolve(null);
175
+ };
176
+ return SourceData.fromFile(match.id, match.cwd, match.pathId).then(function (sd) {
177
+ return [sd];
178
+ });
179
+ });
180
+ }
131
181
  walk(onFile, onDirFile) {
132
182
  let files = [];
133
183
  let dirs = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whet",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "NodeJS based assets management and project tooling library.",
5
5
  "scripts": {
6
6
  "devinit": "npx dts2hx commander pino-pretty minimatch --modular --noLibWrap",