whet 0.4.0 → 0.5.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.
@@ -55,6 +55,7 @@ export declare class Exception extends Error {
55
55
  protected __skipStack: number
56
56
  protected __nativeException: any
57
57
  protected __previousException: null | Exception
58
+ protected unwrap(): any
58
59
 
59
60
  /**
60
61
  Returns exception message.
@@ -52,6 +52,9 @@ class Exception extends Register.inherits(() => Error, true) {
52
52
  this.__previousException = previous;
53
53
  this.__nativeException = ($native != null) ? $native : this;
54
54
  }
55
+ unwrap() {
56
+ return this.__nativeException;
57
+ }
55
58
 
56
59
  /**
57
60
  Returns exception message.
@@ -21,4 +21,13 @@ export declare class ValueException extends Exception {
21
21
  Thrown value.
22
22
  */
23
23
  value: any
24
+
25
+ /**
26
+ Extract an originally thrown value.
27
+
28
+ This method must return the same value on subsequent calls.
29
+ Used internally for catching non-native exceptions.
30
+ Do _not_ override unless you know what you are doing.
31
+ */
32
+ protected unwrap(): any
24
33
  }
@@ -23,6 +23,17 @@ class ValueException extends Register.inherits(() => Exception, true) {
23
23
  super[Register.new](String(value), previous, $native);
24
24
  this.value = value;
25
25
  }
26
+
27
+ /**
28
+ Extract an originally thrown value.
29
+
30
+ This method must return the same value on subsequent calls.
31
+ Used internally for catching non-native exceptions.
32
+ Do _not_ override unless you know what you are doing.
33
+ */
34
+ unwrap() {
35
+ return this.value;
36
+ }
26
37
  static get __name__() {
27
38
  return "haxe.ValueException"
28
39
  }
@@ -121,6 +121,52 @@ export declare class Stone<T extends StoneConfig> {
121
121
  */
122
122
  protected generatePartial(sourceId: string, hash: SourceHash): Promise<null | SourceData[]>
123
123
 
124
+ /**
125
+ * Optional override: compute expensive shared state once and reuse it across all
126
+ * `list()` / `generatePartial()` calls within a generation batch. Without this, work like
127
+ * enumerating inputs, resolving pipelines, or building lookup maps gets repeated once per output.
128
+ *
129
+ * Override to do the upfront work; call `getContext()` (not this) from `list()`/`generatePartial()`
130
+ * to read it. The result is cached (see `getContext` for the caching rules). The returned object
131
+ * is held by reference and never serialized — Maps, class instances, and closures are fine.
132
+ *
133
+ * **Constraint**: `generateContext` must not call back into this *same* stone's `getSource()` /
134
+ * `getPartialSource()` — it runs inside this stone's `acquire()` lock and would deadlock. Reading
135
+ * from *other* stones (dependencies) is fine.
136
+ */
137
+ protected generateContext(hash: SourceHash): Promise<any>
138
+
139
+ /**
140
+ Instance-level context cache, keyed by a stable (non-null) hash. See getContext.
141
+ */
142
+ protected _contextPromise: null | {
143
+ hash: SourceHash,
144
+ promise: Promise<any>
145
+ }
146
+
147
+ /**
148
+ * **Do not override** (override `generateContext` instead). Returns the shared context,
149
+ * computing it via `generateContext` at most once per key. Callable from `list()`,
150
+ * `generatePartial()`, or `generateHash()`.
151
+ *
152
+ * Caching:
153
+ * - With a stable (non-null) hash, the resolved Promise is cached on the stone instance keyed by
154
+ * that hash, so it is reused across separate `getSource()`/`getPartialSource()` calls — even
155
+ * across builds — as long as the hash matches. A changed hash recomputes.
156
+ * - With a null hash (a stone without `generateHash()`), there is no stable key, so the context is
157
+ * scoped to the current request via `MemoContext` instead of the instance. This still shares it
158
+ * across the `Promise.all` batch in the default `generate()`, but avoids holding stale state
159
+ * across builds.
160
+ *
161
+ * In both cases the *Promise* (not the resolved value) is cached, so concurrent callers from the
162
+ * same batch share one in-flight computation rather than racing to start their own.
163
+ *
164
+ * @param hash Pass the hash when you already have it (from `generatePartial`); omit it elsewhere
165
+ * (e.g. `list()`) and it is derived via `finalMaybeHash()` without forcing generation.
166
+ */
167
+ protected getContext(hash?: null | SourceHash): Promise<any>
168
+ protected _contextForHash(hash: null | SourceHash): Promise<any>
169
+
124
170
  /**
125
171
  * Get source for a single output by sourceId.
126
172
  * If the stone implements generatePartial(), generates just the requested output.
package/bin/whet/Stone.js CHANGED
@@ -16,6 +16,7 @@ const $global = Register.$global
16
16
  export const Stone = Register.global("$hxClasses")["whet.Stone"] =
17
17
  class Stone extends Register.inherits() {
18
18
  [Register.new](config) {
19
+ this._contextPromise = null;
19
20
  this.locked = false;
20
21
  this.lockQueue = [];
21
22
  this.ignoreFileHash = false;
@@ -396,6 +397,74 @@ class Stone extends Register.inherits() {
396
397
  return Promise.resolve(null);
397
398
  }
398
399
 
400
+ /**
401
+ * Optional override: compute expensive shared state once and reuse it across all
402
+ * `list()` / `generatePartial()` calls within a generation batch. Without this, work like
403
+ * enumerating inputs, resolving pipelines, or building lookup maps gets repeated once per output.
404
+ *
405
+ * Override to do the upfront work; call `getContext()` (not this) from `list()`/`generatePartial()`
406
+ * to read it. The result is cached (see `getContext` for the caching rules). The returned object
407
+ * is held by reference and never serialized — Maps, class instances, and closures are fine.
408
+ *
409
+ * **Constraint**: `generateContext` must not call back into this *same* stone's `getSource()` /
410
+ * `getPartialSource()` — it runs inside this stone's `acquire()` lock and would deadlock. Reading
411
+ * from *other* stones (dependencies) is fine.
412
+ */
413
+ generateContext(hash) {
414
+ return Promise.resolve(null);
415
+ }
416
+
417
+ /**
418
+ * **Do not override** (override `generateContext` instead). Returns the shared context,
419
+ * computing it via `generateContext` at most once per key. Callable from `list()`,
420
+ * `generatePartial()`, or `generateHash()`.
421
+ *
422
+ * Caching:
423
+ * - With a stable (non-null) hash, the resolved Promise is cached on the stone instance keyed by
424
+ * that hash, so it is reused across separate `getSource()`/`getPartialSource()` calls — even
425
+ * across builds — as long as the hash matches. A changed hash recomputes.
426
+ * - With a null hash (a stone without `generateHash()`), there is no stable key, so the context is
427
+ * scoped to the current request via `MemoContext` instead of the instance. This still shares it
428
+ * across the `Promise.all` batch in the default `generate()`, but avoids holding stale state
429
+ * across builds.
430
+ *
431
+ * In both cases the *Promise* (not the resolved value) is cached, so concurrent callers from the
432
+ * same batch share one in-flight computation rather than racing to start their own.
433
+ *
434
+ * @param hash Pass the hash when you already have it (from `generatePartial`); omit it elsewhere
435
+ * (e.g. `list()`) and it is derived via `finalMaybeHash()` without forcing generation.
436
+ */
437
+ getContext(hash) {
438
+ if (hash != null) {
439
+ return this._contextForHash(hash);
440
+ };
441
+ let _gthis = this;
442
+ return this.finalMaybeHash().then(function (h) {
443
+ return _gthis._contextForHash(h);
444
+ });
445
+ }
446
+ _contextForHash(hash) {
447
+ if (hash == null) {
448
+ let ctx = MemoContext.als.getStore();
449
+ if (ctx == null) {
450
+ return this.generateContext(null);
451
+ };
452
+ let cached = ctx.contexts.get(this);
453
+ if (cached != null) {
454
+ return cached;
455
+ };
456
+ let p = this.generateContext(null);
457
+ ctx.contexts.set(this, p);
458
+ return p;
459
+ };
460
+ if (this._contextPromise != null && this._contextPromise.hash != null && SourceHash.equals(this._contextPromise.hash, hash)) {
461
+ return this._contextPromise.promise;
462
+ };
463
+ let p = this.generateContext(hash);
464
+ this._contextPromise = {"hash": hash, "promise": p};
465
+ return p;
466
+ }
467
+
399
468
  /**
400
469
  * Get source for a single output by sourceId.
401
470
  * If the stone implements generatePartial(), generates just the requested output.
@@ -646,4 +715,5 @@ Stone.prototype.cacheStrategy = null;
646
715
  Stone.prototype.project = null;
647
716
  Stone.prototype.lockQueue = null;
648
717
  Stone.prototype.locked = null;
718
+ Stone.prototype._contextPromise = null;
649
719
 
package/bin/whet/Whet.js CHANGED
@@ -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.4.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.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();
30
30
  try {
31
31
  Whet_Fields_.program.parse();
32
32
  }catch (_g) {
@@ -8,6 +8,7 @@ export declare class MemoContext {
8
8
  protected sources: Map<AnyStone, Promise<Source>>
9
9
  protected hashes: Map<AnyStone, Promise<SourceHash>>
10
10
  protected partials: Map<AnyStone, Map<string, Promise<null | Source>>>
11
+ protected contexts: Map<AnyStone, Promise<any>>
11
12
  protected static als: AsyncLocalStorage<MemoContext>
12
13
 
13
14
  /**
@@ -6,6 +6,7 @@ const $global = Register.$global
6
6
  export const MemoContext = Register.global("$hxClasses")["whet.cache.MemoContext"] =
7
7
  class MemoContext extends Register.inherits() {
8
8
  [Register.new]() {
9
+ this.contexts = new Map();
9
10
  this.partials = new Map();
10
11
  this.hashes = new Map();
11
12
  this.sources = new Map();
@@ -30,6 +31,7 @@ class MemoContext extends Register.inherits() {
30
31
  MemoContext.prototype.sources = null;
31
32
  MemoContext.prototype.hashes = null;
32
33
  MemoContext.prototype.partials = null;
34
+ MemoContext.prototype.contexts = null;
33
35
 
34
36
 
35
37
  MemoContext.als = new AsyncLocalStorage()
@@ -1,6 +1,6 @@
1
1
  import {SpanStats} from "./SpanStats"
2
2
  import {SpanRecorder} from "./SpanRecorder"
3
- import {SpanEvent, AnySpan, SpanEventType} from "./Span"
3
+ import {SpanEvent, AnySpan, SpanStatus, SpanEventType} from "./Span"
4
4
  import {AnyStone} from "../Stone"
5
5
  import {AsyncLocalStorage} from "node:async_hooks"
6
6
 
@@ -19,6 +19,11 @@ export declare class Profiler {
19
19
  */
20
20
  withSpan<T, R>(stone: AnyStone, op: string, fn: (() => Promise<R>), meta?: null | T): Promise<R>
21
21
 
22
+ /**
23
+ Finalize a span's timing/status, record it, and emit the End event.
24
+ */
25
+ protected finishSpan(span: AnySpan, status: SpanStatus): void
26
+
22
27
  /**
23
28
  Secondary API: manual start for spans where no function can be wrapped (e.g. LockWait).
24
29
  */
@@ -3,6 +3,7 @@ import {SpanRecorder} from "./SpanRecorder.js"
3
3
  import {Span, SpanEventType, SpanStatus} from "./Span.js"
4
4
  import {AsyncLocalStorage} from "node:async_hooks"
5
5
  import {IntMap} from "../../haxe/ds/IntMap.js"
6
+ import {Exception} from "../../haxe/Exception.js"
6
7
  import {EsMap} from "../../genes/util/EsMap.js"
7
8
  import {Register} from "../../genes/Register.js"
8
9
  import {Std} from "../../Std.js"
@@ -33,25 +34,36 @@ class Profiler extends Register.inherits() {
33
34
  this.emit(SpanEventType.Start, span);
34
35
  let _gthis = this;
35
36
  return this.context.run(span, function () {
36
- return fn().then(function (result) {
37
- span.endTime = performance.now();
38
- span.duration = span.endTime - span.startTime;
39
- span.status = SpanStatus.Ok;
40
- _gthis.recorder.record(span);
41
- _gthis.stats.update(stone.id, op, span.duration);
42
- _gthis.emit(SpanEventType.End, span);
43
- return result;
44
- })["catch"](function (err) {
45
- span.endTime = performance.now();
46
- span.duration = span.endTime - span.startTime;
47
- span.status = SpanStatus.Error(Std.string(err));
48
- _gthis.recorder.record(span);
49
- _gthis.emit(SpanEventType.End, span);
50
- return Promise.reject(err);
51
- });
37
+ try {
38
+ return fn().then(function (result) {
39
+ _gthis.finishSpan(span, SpanStatus.Ok);
40
+ return result;
41
+ })["catch"](function (err) {
42
+ _gthis.finishSpan(span, SpanStatus.Error(Std.string(err)));
43
+ return Promise.reject(err);
44
+ });
45
+ }catch (_g) {
46
+ let _g1 = Exception.caught(_g).unwrap();
47
+ _gthis.finishSpan(span, SpanStatus.Error(Std.string(_g1)));
48
+ return Promise.reject(_g1);
49
+ };
52
50
  });
53
51
  }
54
52
 
53
+ /**
54
+ Finalize a span's timing/status, record it, and emit the End event.
55
+ */
56
+ finishSpan(span, status) {
57
+ span.endTime = performance.now();
58
+ span.duration = span.endTime - span.startTime;
59
+ span.status = status;
60
+ this.recorder.record(span);
61
+ if (status == SpanStatus.Ok) {
62
+ this.stats.update(span.stone, span.operation, span.duration);
63
+ };
64
+ this.emit(SpanEventType.End, span);
65
+ }
66
+
55
67
  /**
56
68
  Secondary API: manual start for spans where no function can be wrapped (e.g. LockWait).
57
69
  */
@@ -34,6 +34,19 @@ export declare class Router {
34
34
  */
35
35
  getHash(pattern?: null | MinimatchType): Promise<SourceHash>
36
36
 
37
+ /**
38
+ * Hash for the no-query-filter case: a per-route token binding the source's hash to the
39
+ * static route structure (mount path, filter, extractDirs), then merged order-independently.
40
+ * Child routers recurse through getHash() with no pattern, so nested structure folds in at each
41
+ * level. No id enumeration, so stones without list() are not forced to generate.
42
+ *
43
+ * Binding structure to source per route is what keeps this false-hit-safe: the set of served
44
+ * ids is a function of each route's (mount/filter/extractDirs) plus its source id-set, and the
45
+ * source's hash moves with its id-set. Sorting the per-route tokens makes route order
46
+ * irrelevant (matching the long-standing contract) without losing that binding.
47
+ */
48
+ protected getUnfilteredHash(): Promise<SourceHash>
49
+
37
50
  /**
38
51
  * Save files filtered by `pattern` into provided `saveInto` folder.
39
52
  */
@@ -219,6 +219,11 @@ class Router extends Register.inherits() {
219
219
  */
220
220
  getHash(pattern) {
221
221
  let _gthis = this;
222
+ if (pattern == null) {
223
+ return MemoContext.ensure(function () {
224
+ return _gthis.getUnfilteredHash();
225
+ });
226
+ };
222
227
  return MemoContext.ensure(function () {
223
228
  return _gthis.get(pattern).then(function (items) {
224
229
  let uniqueStones = [];
@@ -264,6 +269,51 @@ class Router extends Register.inherits() {
264
269
  });
265
270
  }
266
271
 
272
+ /**
273
+ * Hash for the no-query-filter case: a per-route token binding the source's hash to the
274
+ * static route structure (mount path, filter, extractDirs), then merged order-independently.
275
+ * Child routers recurse through getHash() with no pattern, so nested structure folds in at each
276
+ * level. No id enumeration, so stones without list() are not forced to generate.
277
+ *
278
+ * Binding structure to source per route is what keeps this false-hit-safe: the set of served
279
+ * ids is a function of each route's (mount/filter/extractDirs) plus its source id-set, and the
280
+ * source's hash moves with its id-set. Sorting the per-route tokens makes route order
281
+ * irrelevant (matching the long-standing contract) without losing that binding.
282
+ */
283
+ getUnfilteredHash() {
284
+ let tokenProms = [];
285
+ let _g = 0;
286
+ let _g1 = this.routes;
287
+ while (_g < _g1.length) {
288
+ let route = _g1[_g];
289
+ ++_g;
290
+ let structure = route.routeUnder + "|" + ((route.filter != null) ? route.filter.pattern : "") + "|" + ((route.extractDirs != null) ? route.extractDirs.pattern : "");
291
+ let childHash;
292
+ if (((route.source) instanceof Stone)) {
293
+ childHash = route.source.getHash();
294
+ } else if (((route.source) instanceof Router)) {
295
+ childHash = route.source.getHash();
296
+ } else {
297
+ throw new Error("Router source must be a Stone or a Router.");
298
+ };
299
+ tokenProms.push(childHash.then(function (h) {
300
+ return SourceHash.fromString(structure).add(h);
301
+ }));
302
+ };
303
+ return Promise.all(tokenProms).then(function (tokens) {
304
+ tokens.sort(function (a, b) {
305
+ if (a.toString() < b.toString()) {
306
+ return -1;
307
+ } else if (a.toString() > b.toString()) {
308
+ return 1;
309
+ } else {
310
+ return 0;
311
+ };
312
+ });
313
+ return SourceHash.merge(...tokens);
314
+ });
315
+ }
316
+
267
317
  /**
268
318
  * Save files filtered by `pattern` into provided `saveInto` folder.
269
319
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whet",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",