@zingage/scheduling 1.1.0 → 1.2.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.
Files changed (42) hide show
  1. package/README.md +184 -5
  2. package/dist/batcher.d.ts +231 -0
  3. package/dist/batcher.d.ts.map +1 -0
  4. package/dist/batcher.js +391 -0
  5. package/dist/batcher.js.map +1 -0
  6. package/dist/collapsed-task-creator.d.ts +235 -0
  7. package/dist/collapsed-task-creator.d.ts.map +1 -0
  8. package/dist/collapsed-task-creator.js +491 -0
  9. package/dist/collapsed-task-creator.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/logger.d.ts +16 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/logger.js +14 -0
  17. package/dist/logger.js.map +1 -0
  18. package/dist/pending-promises-tracker.d.ts +32 -0
  19. package/dist/pending-promises-tracker.d.ts.map +1 -0
  20. package/dist/pending-promises-tracker.js +60 -0
  21. package/dist/pending-promises-tracker.js.map +1 -0
  22. package/dist/performance-now.d.ts +14 -0
  23. package/dist/performance-now.d.ts.map +1 -0
  24. package/dist/performance-now.js +10 -0
  25. package/dist/performance-now.js.map +1 -0
  26. package/dist/scheduling-contract-error.d.ts +9 -0
  27. package/dist/scheduling-contract-error.d.ts.map +1 -0
  28. package/dist/scheduling-contract-error.js +9 -0
  29. package/dist/scheduling-contract-error.js.map +1 -0
  30. package/dist/wrap-with-task-collapsing.d.ts +30 -0
  31. package/dist/wrap-with-task-collapsing.d.ts.map +1 -0
  32. package/dist/wrap-with-task-collapsing.js +80 -0
  33. package/dist/wrap-with-task-collapsing.js.map +1 -0
  34. package/package.json +10 -4
  35. package/src/batcher.ts +614 -0
  36. package/src/collapsed-task-creator.ts +774 -0
  37. package/src/index.ts +22 -0
  38. package/src/logger.ts +32 -0
  39. package/src/pending-promises-tracker.ts +62 -0
  40. package/src/performance-now.ts +21 -0
  41. package/src/scheduling-contract-error.ts +8 -0
  42. package/src/wrap-with-task-collapsing.ts +103 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Tracks promises until they settle, so that an owner's `close` can wait for
3
+ * the work it accepted.
4
+ *
5
+ * Each tracked promise leaves the tracker when it settles. Tracking never
6
+ * creates an unhandled rejection: the tracker observes a rejection, but the
7
+ * caller still owns the promise and its rejection.
8
+ *
9
+ * The tracker has no closed state. Each owner decides what new work does
10
+ * after its own `close` starts, and in what order it drains its trackers.
11
+ */
12
+ export class PendingPromisesTracker {
13
+ #pending = new Set();
14
+ /** The number of tracked promises that have not settled yet. */
15
+ get size() {
16
+ return this.#pending.size;
17
+ }
18
+ /** Tracks `promise` until it settles, and returns it unchanged. */
19
+ // oxlint-disable-next-line typescript/promise-function-async -- Returns the same promise it tracks.
20
+ track(promise) {
21
+ this.#pending.add(promise);
22
+ const untrack = () => {
23
+ this.#pending.delete(promise);
24
+ };
25
+ // oxlint-disable-next-line no-void -- Untracks on settle; the caller owns the promise and its rejection.
26
+ void promise.then(untrack, untrack);
27
+ return promise;
28
+ }
29
+ /**
30
+ * Calls `start` and tracks its result. The promise is tracked before
31
+ * `start` runs, so a `drain` that `start` itself triggers synchronously
32
+ * still waits for it. A synchronous throw from `start` becomes a rejection.
33
+ */
34
+ // oxlint-disable-next-line typescript/promise-function-async -- Returns the tracked promise, and must call start synchronously.
35
+ run(start) {
36
+ const { promise, resolve, reject } = Promise.withResolvers();
37
+ // oxlint-disable-next-line no-void -- track returns the same promise, which run returns.
38
+ void this.track(promise);
39
+ try {
40
+ resolve(start());
41
+ }
42
+ catch (error) {
43
+ reject(error);
44
+ }
45
+ return promise;
46
+ }
47
+ /**
48
+ * Resolves once no tracked promise is pending, including promises tracked
49
+ * while it waits. It loops because settling work can start more work, for
50
+ * example a producer that reads other keys from its own wrapper. It never
51
+ * rejects.
52
+ */
53
+ async drain() {
54
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Loops until nothing is pending, because settling work can track more.
55
+ while (this.#pending.size > 0) {
56
+ await Promise.allSettled(this.#pending);
57
+ }
58
+ }
59
+ }
60
+ //# sourceMappingURL=pending-promises-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-promises-tracker.js","sourceRoot":"","sources":["../src/pending-promises-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,OAAO,sBAAsB;IACxB,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAEhD,gEAAgE;IAChE,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,mEAAmE;IACnE,oGAAoG;IACpG,KAAK,CAAI,OAAmB;QAC1B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC,CAAC;QACF,yGAAyG;QACzG,KAAK,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,gIAAgI;IAChI,GAAG,CAAI,KAAuB;QAC5B,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAK,CAAC;QAChE,yFAAyF;QACzF,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,iIAAiI;QACjI,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ import type { Tagged } from "type-fest";
2
+ /**
3
+ * A reading of `performance.now()`, in milliseconds since the time origin.
4
+ *
5
+ * The tag keeps other numbers out of a parameter that expects a reading of
6
+ * this clock. Examples are a `Date.now()` time, which uses a different
7
+ * origin, and a duration.
8
+ */
9
+ export type PerformanceNow = Tagged<number, "PerformanceNow">;
10
+ /** Reads `performance.now()`. */
11
+ export declare function performanceNow(): PerformanceNow;
12
+ /** The milliseconds that passed between `start` and now. */
13
+ export declare function msSince(start: PerformanceNow): number;
14
+ //# sourceMappingURL=performance-now.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-now.d.ts","sourceRoot":"","sources":["../src/performance-now.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAE9D,iCAAiC;AACjC,wBAAgB,cAAc,IAAI,cAAc,CAE/C;AAED,4DAA4D;AAC5D,wBAAgB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAErD"}
@@ -0,0 +1,10 @@
1
+ import { instantiateTaggedType } from "type-party/runtime/tagged-types.js";
2
+ /** Reads `performance.now()`. */
3
+ export function performanceNow() {
4
+ return instantiateTaggedType(performance.now());
5
+ }
6
+ /** The milliseconds that passed between `start` and now. */
7
+ export function msSince(start) {
8
+ return performance.now() - start;
9
+ }
10
+ //# sourceMappingURL=performance-now.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-now.js","sourceRoot":"","sources":["../src/performance-now.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAW3E,iCAAiC;AACjC,MAAM,UAAU,cAAc;IAC5B,OAAO,qBAAqB,CAAiB,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,OAAO,CAAC,KAAqB;IAC3C,OAAO,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;AACnC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Reports that a caller broke a contract of this package. Examples are a
3
+ * `dispatch` or `schedule` callback that returns the wrong number of results,
4
+ * and a `schedule` callback that requests a key it is starting a task for.
5
+ */
6
+ export declare class SchedulingContractError extends Error {
7
+ readonly name: string;
8
+ }
9
+ //# sourceMappingURL=scheduling-contract-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduling-contract-error.d.ts","sourceRoot":"","sources":["../src/scheduling-contract-error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,SAAkB,IAAI,EAAE,MAAM,CAA6B;CAC5D"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Reports that a caller broke a contract of this package. Examples are a
3
+ * `dispatch` or `schedule` callback that returns the wrong number of results,
4
+ * and a `schedule` callback that requests a key it is starting a task for.
5
+ */
6
+ export class SchedulingContractError extends Error {
7
+ name = "SchedulingContractError";
8
+ }
9
+ //# sourceMappingURL=scheduling-contract-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduling-contract-error.js","sourceRoot":"","sources":["../src/scheduling-contract-error.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAC9B,IAAI,GAAW,yBAAyB,CAAC;CAC5D"}
@@ -0,0 +1,30 @@
1
+ import { type Logger } from "./logger.js";
2
+ /**
3
+ * Wraps a function that starts a task and returns a promise for its result.
4
+ * The returned function behaves like the original, except that a call reuses
5
+ * the promise of an earlier call if and only if: 1) the calls have the same
6
+ * key, which `makeKey` derives from their arguments [a la memoize]; 2) less
7
+ * than `collapseTasksMs` milliseconds have passed since the earlier call
8
+ * started its task [a la throttle]; and 3) that task is still running
9
+ * [unique]. Otherwise, the call calls through to the original function. So it
10
+ * is sort of a combination of memoize, throttle, and promise chaining.
11
+ *
12
+ * It runs on {@link CollapsedTaskCreator}: each call is one task request,
13
+ * whose input carries the call's arguments and key. See that class for why
14
+ * these semantics are useful.
15
+ *
16
+ * It does not support task cancellation. A call takes no `AbortSignal`, so no
17
+ * individual caller can cancel a task that other callers share. Users who
18
+ * need cancellation should use `CollapsedTaskCreator` directly.
19
+ *
20
+ * @param taskCreator The task creation function
21
+ * @param collapseTasksMs The number of milliseconds up until which, if a caller
22
+ * tries to start the same task while a previous version of the task is still
23
+ * running, the promise for the result of the currently-running task will be
24
+ * returned instead.
25
+ * @param makeKey A function for converting the arguments passed to the task
26
+ * creation into a cache key, like in your standard memoize implementation.
27
+ * @param logger Receives the trace lines that each call writes.
28
+ */
29
+ export declare function wrapWithTaskCollapsing<Args extends unknown[], Key, Result>(taskCreator: (...args: Args) => Promise<Result>, collapseTasksMs: number, makeKey: (args: Args) => Key, logger?: Logger): (...args: Args) => Promise<Result>;
30
+ //# sourceMappingURL=wrap-with-task-collapsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrap-with-task-collapsing.d.ts","sourceRoot":"","sources":["../src/wrap-with-task-collapsing.ts"],"names":[],"mappings":"AAIA,OAAO,EAA6B,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAuBrE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EACxE,WAAW,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,EAC/C,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,GAAG,EAC5B,MAAM,GAAE,MAA4D,aA2B7C,IAAI,qBAiB5B"}
@@ -0,0 +1,80 @@
1
+ import { CollapsedTaskCreator, } from "./collapsed-task-creator.js";
2
+ import { defaultLoggersByComponent } from "./logger.js";
3
+ // The creator reserves `undefined` for inputs that never share work, but here
4
+ // `undefined` is an ordinary key. So it stands in for `undefined` with a
5
+ // key that no caller can produce.
6
+ const undefinedKey = Symbol("undefined key");
7
+ /**
8
+ * The "started" log line for each reason a call starts a task. A call here
9
+ * has no signal, so its task is never cancelled, and its key is never
10
+ * `undefined` for the creator. But every reason has a line, so the log stays
11
+ * accurate if that changes.
12
+ */
13
+ const startedMessages = {
14
+ noPendingTask: "started new task b/c there was no in-progress task for these args",
15
+ pendingTaskTooOld: "started new task; there _was_ an in-progress one, but it's too old",
16
+ pendingTaskCancelled: "started new task; there _was_ an in-progress one, but it was cancelled",
17
+ noKey: "started new task b/c these args have no key",
18
+ };
19
+ /**
20
+ * Wraps a function that starts a task and returns a promise for its result.
21
+ * The returned function behaves like the original, except that a call reuses
22
+ * the promise of an earlier call if and only if: 1) the calls have the same
23
+ * key, which `makeKey` derives from their arguments [a la memoize]; 2) less
24
+ * than `collapseTasksMs` milliseconds have passed since the earlier call
25
+ * started its task [a la throttle]; and 3) that task is still running
26
+ * [unique]. Otherwise, the call calls through to the original function. So it
27
+ * is sort of a combination of memoize, throttle, and promise chaining.
28
+ *
29
+ * It runs on {@link CollapsedTaskCreator}: each call is one task request,
30
+ * whose input carries the call's arguments and key. See that class for why
31
+ * these semantics are useful.
32
+ *
33
+ * It does not support task cancellation. A call takes no `AbortSignal`, so no
34
+ * individual caller can cancel a task that other callers share. Users who
35
+ * need cancellation should use `CollapsedTaskCreator` directly.
36
+ *
37
+ * @param taskCreator The task creation function
38
+ * @param collapseTasksMs The number of milliseconds up until which, if a caller
39
+ * tries to start the same task while a previous version of the task is still
40
+ * running, the promise for the result of the currently-running task will be
41
+ * returned instead.
42
+ * @param makeKey A function for converting the arguments passed to the task
43
+ * creation into a cache key, like in your standard memoize implementation.
44
+ * @param logger Receives the trace lines that each call writes.
45
+ */
46
+ export function wrapWithTaskCollapsing(taskCreator, collapseTasksMs, makeKey, logger = defaultLoggersByComponent["collapsed-task-creator"]) {
47
+ const logTrace = logger.bind(null, "collapsed-task-creator", "trace");
48
+ // The creator's input carries the key, so `makeKey` runs once per call and
49
+ // every log line below can include the key.
50
+ const creator = new CollapsedTaskCreator({
51
+ collapseTasksMs,
52
+ makeKey: ({ taskKey }) => taskKey === undefined ? undefinedKey : taskKey,
53
+ schedule: (toStart) => toStart.map(({ input: { args, taskKey }, startReason }) => {
54
+ logTrace(startedMessages[startReason], args);
55
+ const taskRes = taskCreator(...args);
56
+ logTrace("pending = new state for taskKey/args", { args, taskKey });
57
+ return {
58
+ result: taskRes.finally(() => {
59
+ logTrace("completed = new state for taskKey/args", {
60
+ args,
61
+ taskKey,
62
+ });
63
+ }),
64
+ handle: undefined,
65
+ };
66
+ }),
67
+ });
68
+ return async (...args) => {
69
+ const taskKey = makeKey(args);
70
+ logTrace("requested = new state for taskKey/args", { args, taskKey });
71
+ const [{ result, rode }] = creator.startOrRideTasks([
72
+ { input: { args, taskKey } },
73
+ ]);
74
+ if (rode) {
75
+ logTrace("reusing result from prior, still-in-progress run of task for args/taskKey", { args, taskKey });
76
+ }
77
+ return result;
78
+ };
79
+ }
80
+ //# sourceMappingURL=wrap-with-task-collapsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrap-with-task-collapsing.js","sourceRoot":"","sources":["../src/wrap-with-task-collapsing.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,GAErB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,yBAAyB,EAAe,MAAM,aAAa,CAAC;AAErE,8EAA8E;AAC9E,yEAAyE;AACzE,kCAAkC;AAClC,MAAM,YAAY,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,eAAe,GAAoC;IACvD,aAAa,EACX,mEAAmE;IACrE,iBAAiB,EACf,oEAAoE;IACtE,oBAAoB,EAClB,wEAAwE;IAC1E,KAAK,EAAE,6CAA6C;CACrD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,sBAAsB,CACpC,WAA+C,EAC/C,eAAuB,EACvB,OAA4B,EAC5B,MAAM,GAAW,yBAAyB,CAAC,wBAAwB,CAAC;IAEpE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,wBAAwB,EAAE,OAAO,CAAC,CAAC;IAEtE,2EAA2E;IAC3E,4CAA4C;IAC5C,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC;QACvC,eAAe;QACf,OAAO,EAAE,CAAC,EAAE,OAAO,EAAgC,EAAE,EAAE,CACrD,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO;QAChD,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CACpB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE;YACxD,QAAQ,CAAC,eAAe,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;YAC7C,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC;YACrC,QAAQ,CAAC,sCAAsC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACpE,OAAO;gBACL,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;oBAC3B,QAAQ,CAAC,wCAAwC,EAAE;wBACjD,IAAI;wBACJ,OAAO;qBACR,CAAC,CAAC;gBACL,CAAC,CAAC;gBACF,MAAM,EAAE,SAAS;aAClB,CAAC;QACJ,CAAC,CAAC;KACL,CAAC,CAAC;IAEH,OAAO,KAAK,EAAE,GAAG,IAAU,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,QAAQ,CAAC,wCAAwC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAEtE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAClD,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;SAC7B,CAAC,CAAC;QAEH,IAAI,IAAI,EAAE,CAAC;YACT,QAAQ,CACN,2EAA2E,EAC3E,EAAE,IAAI,EAAE,OAAO,EAAE,CAClB,CAAC;QACJ,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zingage/scheduling",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/joinava/sensitive-packages.git",
@@ -24,16 +24,22 @@
24
24
  "access": "public",
25
25
  "registry": "https://registry.npmjs.org/"
26
26
  },
27
- "dependencies": {},
27
+ "dependencies": {
28
+ "debug": "^4.4.1",
29
+ "es-toolkit": "1.47.1",
30
+ "type-fest": "5.8.0",
31
+ "type-party": "0.10.2"
32
+ },
28
33
  "devDependencies": {
34
+ "@types/debug": "^4.1.12",
29
35
  "@types/node": "^26.1.1",
30
36
  "fast-check": "^4.2.0",
31
37
  "oxlint": "^1.83.0",
32
38
  "oxlint-tsgolint": "^7.0.2001",
33
39
  "typescript": "npm:@typescript/typescript6@^6.0.0",
34
40
  "typescript-7": "npm:typescript@^7.0.2",
35
- "@zingage/base-configs-oxlint": "1.0.0",
36
- "@zingage/base-configs-ts": "1.0.0"
41
+ "@zingage/base-configs-ts": "1.0.0",
42
+ "@zingage/base-configs-oxlint": "1.0.0"
37
43
  },
38
44
  "scripts": {
39
45
  "tsc:watch": "tsc -b --watch",