@excom/kit-utils 0.1.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.
- package/.rush/temp/chunked-rush-logs/kit-utils.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +4 -0
- package/batch-manager.ts +109 -0
- package/common.ts +234 -0
- package/config/rig.json +6 -0
- package/dom.ts +511 -0
- package/fetching.ts +120 -0
- package/form.ts +99 -0
- package/index.ts +9 -0
- package/load-dependency.ts +40 -0
- package/loop-guard.ts +187 -0
- package/package.json +39 -0
- package/property.ts +225 -0
- package/queue-manager.ts +214 -0
- package/rush-logs/kit-utils.apply-exports.cache.log +1 -0
- package/rush-logs/kit-utils.apply-exports.log +1 -0
- package/support/tests/batch-manager.test.ts +381 -0
- package/support/tests/common.test.ts +376 -0
- package/support/tests/dom.test.ts +852 -0
- package/support/tests/fetching.test.ts +230 -0
- package/support/tests/form.test.ts +297 -0
- package/support/tests/index.test.ts +20 -0
- package/support/tests/load-dependency.test.ts +98 -0
- package/support/tests/loop-guard.test.ts +225 -0
- package/support/tests/property.test.ts +355 -0
- package/support/tests/queue-manager.test.ts +471 -0
- package/support/tests/url.test.ts +111 -0
- package/tsconfig.json +5 -0
- package/url.ts +39 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
package/batch-manager.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { LoopGuard } from "./loop-guard";
|
|
2
|
+
|
|
3
|
+
type Fn = (...args: any[]) => any;
|
|
4
|
+
export type DependencyName = string;
|
|
5
|
+
export type DependencyValue = any;
|
|
6
|
+
export type BatchHandler = readonly [
|
|
7
|
+
readonly DependencyName[],
|
|
8
|
+
Fn,
|
|
9
|
+
ExecHandlerFn?,
|
|
10
|
+
];
|
|
11
|
+
export type BatchHandlers = BatchHandler[];
|
|
12
|
+
export type BatchHandlersInput = ReadonlyArray<BatchHandler>;
|
|
13
|
+
export type Notifs = Record<DependencyName, DependencyValue>;
|
|
14
|
+
export type ExecHandlerFn = (handler: BatchHandler, notifs: Notifs) => any;
|
|
15
|
+
export type ClearNotifsFn = (notifs: Notifs) => Notifs;
|
|
16
|
+
|
|
17
|
+
function defaultExecHandler([_, fn], notifs) {
|
|
18
|
+
fn.call(this, notifs);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class BatchManager {
|
|
22
|
+
isLocked: boolean = false;
|
|
23
|
+
handlers: BatchHandlers = [];
|
|
24
|
+
queuedHandlers: BatchHandlers = [];
|
|
25
|
+
notifs: Notifs = {};
|
|
26
|
+
execHandlerCtx?: any;
|
|
27
|
+
execHandler: ExecHandlerFn;
|
|
28
|
+
clearNotifs: ClearNotifsFn;
|
|
29
|
+
|
|
30
|
+
constructor({
|
|
31
|
+
handlers = [],
|
|
32
|
+
execHandler,
|
|
33
|
+
execHandlerCtx,
|
|
34
|
+
clearNotifs,
|
|
35
|
+
}: {
|
|
36
|
+
handlers: BatchHandlersInput;
|
|
37
|
+
execHandlerCtx?: any;
|
|
38
|
+
execHandler?: ExecHandlerFn;
|
|
39
|
+
clearNotifs?: ClearNotifsFn;
|
|
40
|
+
}) {
|
|
41
|
+
this.handlers = [...handlers];
|
|
42
|
+
this.notifs = {};
|
|
43
|
+
this.execHandlerCtx = execHandlerCtx;
|
|
44
|
+
this.execHandler = execHandler || defaultExecHandler;
|
|
45
|
+
this.clearNotifs = clearNotifs || ((_) => ({}));
|
|
46
|
+
}
|
|
47
|
+
lock() {
|
|
48
|
+
this.isLocked = true;
|
|
49
|
+
}
|
|
50
|
+
unlock() {
|
|
51
|
+
this.isLocked = false;
|
|
52
|
+
this.flushHandlers();
|
|
53
|
+
}
|
|
54
|
+
notify(dependencyName: DependencyName, dependencyValue: DependencyValue) {
|
|
55
|
+
this.notifs[dependencyName] = dependencyValue;
|
|
56
|
+
this.handlers.forEach((handler) => {
|
|
57
|
+
if (
|
|
58
|
+
handler[0].includes(dependencyName) &&
|
|
59
|
+
!this.queuedHandlers.includes(handler)
|
|
60
|
+
) {
|
|
61
|
+
this.queuedHandlers.push(handler);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (!this.isLocked) {
|
|
65
|
+
this.flushHandlers();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Runs per handler inside the current synchronous flush (nested flushes
|
|
70
|
+
* included); `null` between flushes. A handler that keeps re-queuing
|
|
71
|
+
* itself, an effect writing a prop it reacts to, two handlers feeding
|
|
72
|
+
* each other, is a loop: past `LoopGuard.limit` runs the queue is
|
|
73
|
+
* dropped and the trip reported.
|
|
74
|
+
*/
|
|
75
|
+
private flushRuns: Map<BatchHandler, number> | null = null;
|
|
76
|
+
flushHandlers() {
|
|
77
|
+
const isOutermost = !this.flushRuns;
|
|
78
|
+
const runs = (this.flushRuns ??= new Map());
|
|
79
|
+
try {
|
|
80
|
+
while (this.queuedHandlers.length > 0 && !this.isLocked) {
|
|
81
|
+
const handler = this.queuedHandlers.shift();
|
|
82
|
+
if (handler) {
|
|
83
|
+
const count = (runs.get(handler) ?? 0) + 1;
|
|
84
|
+
runs.set(handler, count);
|
|
85
|
+
if (count > LoopGuard.limit) {
|
|
86
|
+
this.queuedHandlers = [];
|
|
87
|
+
const name = handler[0].join(", ");
|
|
88
|
+
LoopGuard.report({
|
|
89
|
+
kind: "batch",
|
|
90
|
+
target: this.execHandlerCtx ?? this,
|
|
91
|
+
name,
|
|
92
|
+
depth: count,
|
|
93
|
+
limit: LoopGuard.limit,
|
|
94
|
+
message: `Loop guard: the handler for [${name}] re-ran ${count} times in one synchronous batch — likely an effect loop; the remaining queue was dropped.`,
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const execHandler = handler[2] || this.execHandler;
|
|
99
|
+
execHandler.call(this.execHandlerCtx, handler, this.notifs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
if (isOutermost) this.flushRuns = null;
|
|
104
|
+
}
|
|
105
|
+
if (this.queuedHandlers.length === 0) {
|
|
106
|
+
this.notifs = this.clearNotifs(this.notifs);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/common.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
type Falsy = false | 0 | "" | null | undefined;
|
|
2
|
+
|
|
3
|
+
export const toArray = <T>(val: T) =>
|
|
4
|
+
(Array.isArray(val) ? val : [val]) as T extends Array<any> ? T : T[];
|
|
5
|
+
|
|
6
|
+
export const isPojo = (obj: unknown): boolean => {
|
|
7
|
+
if (!obj || typeof obj !== "object") return false;
|
|
8
|
+
const proto = Object.getPrototypeOf(obj);
|
|
9
|
+
return proto === Object.prototype || proto === null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const deepMerge = <T extends object>(...objects: (T | Falsy)[]): T => {
|
|
13
|
+
return objects.reduce<T>((acc, source) => {
|
|
14
|
+
if (!source) return acc;
|
|
15
|
+
const output = Object.assign({}, acc);
|
|
16
|
+
Object.keys(source).forEach((key) => {
|
|
17
|
+
if (isPojo(source[key]) && key in acc) {
|
|
18
|
+
output[key] = deepMerge(acc[key], source[key]);
|
|
19
|
+
} else {
|
|
20
|
+
output[key] = source[key];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return output;
|
|
24
|
+
}, {} as T);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const pathJoin = (parts: string[], sep?: string) => {
|
|
28
|
+
const separator = sep || "/";
|
|
29
|
+
parts = parts.map((part, index) => {
|
|
30
|
+
if (index) {
|
|
31
|
+
part = part.replace(new RegExp("^" + separator), "");
|
|
32
|
+
}
|
|
33
|
+
if (index !== parts.length - 1) {
|
|
34
|
+
part = part.replace(new RegExp(separator + "$"), "");
|
|
35
|
+
}
|
|
36
|
+
return part;
|
|
37
|
+
});
|
|
38
|
+
return parts.join(separator);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const tc = (t: () => any): any => {
|
|
42
|
+
try {
|
|
43
|
+
return t();
|
|
44
|
+
} catch (_) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const wait = (ms: number = 0) =>
|
|
50
|
+
new Promise((resolve) => {
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
resolve(true);
|
|
53
|
+
}, ms);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const execWhenReady = (valOrPromise, cb) => {
|
|
57
|
+
if (valOrPromise instanceof Promise) {
|
|
58
|
+
return valOrPromise.then((a) => cb(a));
|
|
59
|
+
} else {
|
|
60
|
+
return cb(valOrPromise);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Coerce a value or already-pending Promise to a Promise. */
|
|
65
|
+
export const wrapInPromise = <T>(valOrPromise: T | Promise<T>): Promise<T> => {
|
|
66
|
+
return valOrPromise instanceof Promise
|
|
67
|
+
? valOrPromise
|
|
68
|
+
: Promise.resolve(valOrPromise);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const isNumber = (n: unknown) => typeof n === "number" && !isNaN(n);
|
|
72
|
+
|
|
73
|
+
export const deleteUndefined = <T extends Record<string, any>>(
|
|
74
|
+
obj: T,
|
|
75
|
+
opts?: { nested?: boolean }
|
|
76
|
+
): T => {
|
|
77
|
+
Object.keys(obj).forEach((key) => {
|
|
78
|
+
if (obj[key] === undefined) {
|
|
79
|
+
delete obj[key];
|
|
80
|
+
} else if (opts?.nested && isPojo(obj[key])) {
|
|
81
|
+
deleteUndefined(obj[key], opts);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return obj;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const unique = <T>(arr: T[]): T[] =>
|
|
88
|
+
arr.filter((v, i, a) => a.indexOf(v) === i);
|
|
89
|
+
|
|
90
|
+
export const deepClone = <T>(obj: T): T => {
|
|
91
|
+
if (typeof obj !== "object" || obj === null) {
|
|
92
|
+
return obj;
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(obj)) {
|
|
95
|
+
return obj.map(deepClone) as T;
|
|
96
|
+
}
|
|
97
|
+
return Object.fromEntries(
|
|
98
|
+
Object.entries(obj).map(([key, value]) => [key, deepClone(value)])
|
|
99
|
+
) as T;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type Unwrap<T> =
|
|
103
|
+
T extends WeakRef<infer U> ? U : T extends { deref(): infer U } ? U : never;
|
|
104
|
+
|
|
105
|
+
type UnwrapTuple<T extends readonly unknown[]> = {
|
|
106
|
+
[K in keyof T]: Unwrap<T[K]>;
|
|
107
|
+
};
|
|
108
|
+
type DiRef = WeakRef<WeakKey>;
|
|
109
|
+
|
|
110
|
+
function _di<T extends readonly DiRef[], R>(
|
|
111
|
+
refs: [...T],
|
|
112
|
+
cb: (...args: UnwrapTuple<T>) => R,
|
|
113
|
+
opts: { optional: boolean; method: "apply" }
|
|
114
|
+
): R | undefined;
|
|
115
|
+
function _di<T extends readonly DiRef[], R>(
|
|
116
|
+
refs: [...T],
|
|
117
|
+
cb: (...args: UnwrapTuple<T>) => R,
|
|
118
|
+
opts: { optional: boolean; method: "bind" }
|
|
119
|
+
): (() => R) | undefined;
|
|
120
|
+
function _di<T extends readonly DiRef[], R>(
|
|
121
|
+
refs: [...T],
|
|
122
|
+
cb: (...args: UnwrapTuple<T>) => R,
|
|
123
|
+
opts: { optional: boolean; method: "apply" | "bind" }
|
|
124
|
+
): R | undefined | (() => R) {
|
|
125
|
+
const wrapper = () => {
|
|
126
|
+
const objs = refs.map((r) => r?.deref());
|
|
127
|
+
if (opts.optional || objs.every((r) => r)) {
|
|
128
|
+
return cb.apply(this, objs as UnwrapTuple<T>);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
return opts.method === "apply" ? wrapper() : wrapper;
|
|
132
|
+
}
|
|
133
|
+
export const di = {
|
|
134
|
+
apply: <T extends readonly DiRef[], R>(
|
|
135
|
+
refs: [...T],
|
|
136
|
+
cb: (...args: UnwrapTuple<T>) => R,
|
|
137
|
+
opts?: { optional?: boolean }
|
|
138
|
+
): R | undefined =>
|
|
139
|
+
_di(refs, cb, { optional: opts?.optional ?? false, method: "apply" }),
|
|
140
|
+
|
|
141
|
+
bind: <T extends readonly DiRef[], R>(
|
|
142
|
+
refs: [...T],
|
|
143
|
+
cb: (...args: UnwrapTuple<T>) => R,
|
|
144
|
+
opts?: { optional?: boolean }
|
|
145
|
+
): (() => R) | undefined =>
|
|
146
|
+
_di(refs, cb, { optional: opts?.optional ?? false, method: "bind" }),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type JsonSafeSerializer = (value: any) => any;
|
|
150
|
+
|
|
151
|
+
const JSON_SAFE_SERIALIZERS: Record<string, JsonSafeSerializer> = {
|
|
152
|
+
function: () => "<Function>",
|
|
153
|
+
bigint: (value) => String(value),
|
|
154
|
+
symbol: (value) => String(value),
|
|
155
|
+
WeakRef: () => "<WeakRef>",
|
|
156
|
+
Node: (value: Node) => `<${value?.constructor?.name || value?.nodeType}>`,
|
|
157
|
+
Element: (value: Element) =>
|
|
158
|
+
`<${value?.constructor?.name || value?.nodeName}>`,
|
|
159
|
+
UnknownObject: (value) => `<${value?.constructor?.name}>`,
|
|
160
|
+
Circular: () => "<Circular>",
|
|
161
|
+
Failed: () => "<Failed>",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Deep JSON-safe clone. Non-POJOs are replaced via serializers (defaults or
|
|
166
|
+
* overrides). Useful for DevTools / structured-clone boundaries.
|
|
167
|
+
*/
|
|
168
|
+
export const toJsonSafe = (
|
|
169
|
+
value: unknown,
|
|
170
|
+
_serializers: Record<string, JsonSafeSerializer> = {}
|
|
171
|
+
): unknown => {
|
|
172
|
+
const serializers = { ...JSON_SAFE_SERIALIZERS, ..._serializers };
|
|
173
|
+
const seen = new WeakSet<object>();
|
|
174
|
+
const replace = (v: unknown): unknown => {
|
|
175
|
+
if (typeof v === "function") return serializers.function(v);
|
|
176
|
+
if (typeof v === "bigint") return serializers.bigint(v);
|
|
177
|
+
if (typeof v === "symbol") return serializers.symbol(v);
|
|
178
|
+
if (v instanceof Element) return serializers.Element(v);
|
|
179
|
+
if (v instanceof Node) return serializers.Node(v);
|
|
180
|
+
if (v && typeof v === "object") {
|
|
181
|
+
if (seen.has(v as object)) return serializers.Circular(v);
|
|
182
|
+
const ctorName = (v as object).constructor?.name;
|
|
183
|
+
if (ctorName && ctorName in serializers) {
|
|
184
|
+
// Re-run discrimination on serializer output (e.g. WeakRef → Element).
|
|
185
|
+
return replace(serializers[ctorName](v));
|
|
186
|
+
}
|
|
187
|
+
if (!Array.isArray(v) && !isPojo(v)) {
|
|
188
|
+
return serializers.UnknownObject(v);
|
|
189
|
+
}
|
|
190
|
+
seen.add(v as object);
|
|
191
|
+
}
|
|
192
|
+
return v;
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(JSON.stringify(value, (_key, v) => replace(v)));
|
|
196
|
+
} catch {
|
|
197
|
+
return serializers.Failed(value);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const deepCompare = (a: unknown, b: unknown): boolean => {
|
|
202
|
+
if (Object.is(a, b)) return true;
|
|
203
|
+
if (
|
|
204
|
+
a == null ||
|
|
205
|
+
b == null ||
|
|
206
|
+
typeof a !== "object" ||
|
|
207
|
+
typeof b !== "object"
|
|
208
|
+
) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
212
|
+
return (
|
|
213
|
+
Array.isArray(a) &&
|
|
214
|
+
Array.isArray(b) &&
|
|
215
|
+
a.length === b.length &&
|
|
216
|
+
a.every((v, i) => deepCompare(v, b[i]))
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
// Class instances / non-POJOs: reference equality only (=== via Object.is above).
|
|
220
|
+
if (!isPojo(a) || !isPojo(b)) return false;
|
|
221
|
+
const aKeys = Object.keys(a);
|
|
222
|
+
const bKeys = Object.keys(b);
|
|
223
|
+
return (
|
|
224
|
+
aKeys.length === bKeys.length &&
|
|
225
|
+
aKeys.every(
|
|
226
|
+
(key) =>
|
|
227
|
+
Object.hasOwn(b, key) &&
|
|
228
|
+
deepCompare(
|
|
229
|
+
(a as Record<string, unknown>)[key],
|
|
230
|
+
(b as Record<string, unknown>)[key]
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
};
|