@fuzdev/fuz_util 0.42.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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/array.d.ts +15 -0
- package/dist/array.d.ts.map +1 -0
- package/dist/array.js +25 -0
- package/dist/async.d.ts +62 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.js +147 -0
- package/dist/colors.d.ts +41 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +106 -0
- package/dist/counter.d.ts +7 -0
- package/dist/counter.d.ts.map +1 -0
- package/dist/counter.js +7 -0
- package/dist/deep_equal.d.ts +18 -0
- package/dist/deep_equal.d.ts.map +1 -0
- package/dist/deep_equal.js +152 -0
- package/dist/dom.d.ts +35 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +95 -0
- package/dist/error.d.ts +15 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +18 -0
- package/dist/fetch.d.ts +81 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +162 -0
- package/dist/fs.d.ts +34 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +73 -0
- package/dist/function.d.ts +27 -0
- package/dist/function.d.ts.map +1 -0
- package/dist/function.js +21 -0
- package/dist/git.d.ts +132 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +288 -0
- package/dist/id.d.ts +18 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +18 -0
- package/dist/iterator.d.ts +5 -0
- package/dist/iterator.d.ts.map +1 -0
- package/dist/iterator.js +9 -0
- package/dist/json.d.ts +30 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +44 -0
- package/dist/library_json.d.ts +42 -0
- package/dist/library_json.d.ts.map +1 -0
- package/dist/library_json.js +76 -0
- package/dist/log.d.ts +188 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +393 -0
- package/dist/map.d.ts +12 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +14 -0
- package/dist/maths.d.ts +85 -0
- package/dist/maths.d.ts.map +1 -0
- package/dist/maths.js +87 -0
- package/dist/object.d.ts +46 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +89 -0
- package/dist/package_json.d.ts +90 -0
- package/dist/package_json.d.ts.map +1 -0
- package/dist/package_json.js +112 -0
- package/dist/path.d.ts +63 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +83 -0
- package/dist/print.d.ts +52 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +89 -0
- package/dist/process.d.ts +77 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +148 -0
- package/dist/random.d.ts +25 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +35 -0
- package/dist/random_alea.d.ts +23 -0
- package/dist/random_alea.d.ts.map +1 -0
- package/dist/random_alea.js +95 -0
- package/dist/regexp.d.ts +12 -0
- package/dist/regexp.d.ts.map +1 -0
- package/dist/regexp.js +16 -0
- package/dist/result.d.ts +64 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +48 -0
- package/dist/source_json.d.ts +375 -0
- package/dist/source_json.d.ts.map +1 -0
- package/dist/source_json.js +189 -0
- package/dist/string.d.ts +51 -0
- package/dist/string.d.ts.map +1 -0
- package/dist/string.js +92 -0
- package/dist/throttle.d.ts +26 -0
- package/dist/throttle.d.ts.map +1 -0
- package/dist/throttle.js +53 -0
- package/dist/timings.d.ts +33 -0
- package/dist/timings.d.ts.map +1 -0
- package/dist/timings.js +75 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/url.d.ts +10 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +8 -0
- package/package.json +125 -0
- package/src/lib/array.ts +30 -0
- package/src/lib/async.ts +182 -0
- package/src/lib/colors.ts +132 -0
- package/src/lib/counter.ts +11 -0
- package/src/lib/deep_equal.ts +155 -0
- package/src/lib/dom.ts +108 -0
- package/src/lib/error.ts +22 -0
- package/src/lib/fetch.ts +231 -0
- package/src/lib/fs.ts +128 -0
- package/src/lib/function.ts +32 -0
- package/src/lib/git.ts +390 -0
- package/src/lib/id.ts +30 -0
- package/src/lib/iterator.ts +8 -0
- package/src/lib/json.ts +61 -0
- package/src/lib/library_json.ts +122 -0
- package/src/lib/log.ts +469 -0
- package/src/lib/map.ts +18 -0
- package/src/lib/maths.ts +91 -0
- package/src/lib/object.ts +110 -0
- package/src/lib/package_json.ts +135 -0
- package/src/lib/path.ts +137 -0
- package/src/lib/print.ts +111 -0
- package/src/lib/process.ts +207 -0
- package/src/lib/random.ts +48 -0
- package/src/lib/random_alea.ts +107 -0
- package/src/lib/regexp.ts +17 -0
- package/src/lib/result.ts +67 -0
- package/src/lib/source_json.ts +209 -0
- package/src/lib/string.ts +99 -0
- package/src/lib/throttle.ts +70 -0
- package/src/lib/timings.ts +93 -0
- package/src/lib/types.ts +99 -0
- package/src/lib/url.ts +14 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep equality comparison that checks both structure and type.
|
|
3
|
+
*
|
|
4
|
+
* Key behaviors:
|
|
5
|
+
*
|
|
6
|
+
* - Compares by constructor to prevent type confusion (security: `{}` ≠ `[]`, `{}` ≠ `new Map()`, `new ClassA()` ≠ `new ClassB()`)
|
|
7
|
+
* - Prevents asymmetry bugs: `deep_equal(a, b)` always equals `deep_equal(b, a)`
|
|
8
|
+
* - Compares only enumerable own properties (ignores prototypes, symbols, non-enumerable)
|
|
9
|
+
* - Special handling for: Date (timestamp), Number/Boolean (boxed primitives), Error (message/name)
|
|
10
|
+
* - Promises always return false (cannot be meaningfully compared)
|
|
11
|
+
* - Maps/Sets compare by reference for object keys/values
|
|
12
|
+
*
|
|
13
|
+
* @param a first value to compare
|
|
14
|
+
* @param b second value to compare
|
|
15
|
+
* @returns true if deeply equal, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export const deep_equal = (a, b) => {
|
|
18
|
+
if (Object.is(a, b))
|
|
19
|
+
return true;
|
|
20
|
+
const a_type = typeof a;
|
|
21
|
+
if (a_type !== typeof b)
|
|
22
|
+
return false;
|
|
23
|
+
switch (a_type) {
|
|
24
|
+
case 'string':
|
|
25
|
+
case 'number':
|
|
26
|
+
case 'bigint':
|
|
27
|
+
case 'boolean':
|
|
28
|
+
case 'symbol':
|
|
29
|
+
case 'undefined':
|
|
30
|
+
case 'function':
|
|
31
|
+
return false;
|
|
32
|
+
default:
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
// a_type === 'object'
|
|
36
|
+
if (a === null || b === null)
|
|
37
|
+
return false;
|
|
38
|
+
// Constructor equality check prevents type confusion and ensures symmetry
|
|
39
|
+
// This means: {} ≠ [], {} ≠ new Map(), new ClassA() ≠ new ClassB()
|
|
40
|
+
// Security: prevents structural type confusion in equality checks
|
|
41
|
+
// Cache constructor for reuse in subsequent checks (avoids repeated property access)
|
|
42
|
+
const a_ctor = a.constructor;
|
|
43
|
+
if (a_ctor !== b.constructor)
|
|
44
|
+
return false;
|
|
45
|
+
// Regular arrays: inline length check before function call (fast-fail for mismatched lengths)
|
|
46
|
+
// Use Array.isArray() instead of instanceof Array (JIT-optimized, works cross-realm)
|
|
47
|
+
if (Array.isArray(a)) {
|
|
48
|
+
const len = a.length;
|
|
49
|
+
if (b.length !== len)
|
|
50
|
+
return false;
|
|
51
|
+
for (let i = 0; i < len; i++) {
|
|
52
|
+
if (!deep_equal(a[i], b[i]))
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
// Use cached constructor for type checks (faster than instanceof - avoids prototype chain walk)
|
|
58
|
+
if (a_ctor === Set) {
|
|
59
|
+
if (a.size !== b.size)
|
|
60
|
+
return false;
|
|
61
|
+
for (const a_value of a) {
|
|
62
|
+
if (!b.has(a_value))
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (a_ctor === Map) {
|
|
68
|
+
if (a.size !== b.size)
|
|
69
|
+
return false;
|
|
70
|
+
for (const [key, a_value] of a) {
|
|
71
|
+
if (!b.has(key) ||
|
|
72
|
+
!deep_equal(a_value, b.get(key)))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (a_ctor === RegExp) {
|
|
78
|
+
return (a.source === b.source && a.flags === b.flags);
|
|
79
|
+
}
|
|
80
|
+
// Date objects: compare by timestamp value
|
|
81
|
+
if (a_ctor === Date) {
|
|
82
|
+
// Using Object.is to handle NaN correctly (invalid dates)
|
|
83
|
+
return Object.is(a.getTime(), b.getTime());
|
|
84
|
+
}
|
|
85
|
+
// ArrayBuffer: convert to Uint8Array views for byte-by-byte comparison
|
|
86
|
+
if (a_ctor === ArrayBuffer) {
|
|
87
|
+
if (a.byteLength !== b.byteLength)
|
|
88
|
+
return false;
|
|
89
|
+
const a_view = new Uint8Array(a);
|
|
90
|
+
const b_view = new Uint8Array(b);
|
|
91
|
+
const len = a.byteLength;
|
|
92
|
+
for (let i = 0; i < len; i++) {
|
|
93
|
+
if (a_view[i] !== b_view[i])
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// TypedArrays: specialized fast path (no recursion needed - elements are always primitives)
|
|
99
|
+
// ArrayBuffer.isView() catches TypedArrays (Uint8Array, Int32Array, Float64Array, etc.)
|
|
100
|
+
// DataView is also caught by isView but needs special handling (no indexed access)
|
|
101
|
+
if (ArrayBuffer.isView(a)) {
|
|
102
|
+
// DataView: compare byte-by-byte using getUint8
|
|
103
|
+
if (a_ctor === DataView) {
|
|
104
|
+
const byte_len = a.byteLength;
|
|
105
|
+
if (b.byteLength !== byte_len)
|
|
106
|
+
return false;
|
|
107
|
+
for (let i = 0; i < byte_len; i++) {
|
|
108
|
+
if (a.getUint8(i) !== b.getUint8(i))
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// TypedArrays: use indexed access (much faster)
|
|
114
|
+
if (b.length !== a.length)
|
|
115
|
+
return false;
|
|
116
|
+
const len = a.length;
|
|
117
|
+
for (let i = 0; i < len; i++) {
|
|
118
|
+
if (a[i] !== b[i])
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// Boxed Number objects: compare by primitive value
|
|
124
|
+
if (a_ctor === Number) {
|
|
125
|
+
return Object.is(a.valueOf(), b.valueOf());
|
|
126
|
+
}
|
|
127
|
+
// Boxed Boolean objects: compare by primitive value
|
|
128
|
+
if (a_ctor === Boolean) {
|
|
129
|
+
return a.valueOf() === b.valueOf();
|
|
130
|
+
}
|
|
131
|
+
// Error objects: compare by message and name
|
|
132
|
+
if (a_ctor === Error) {
|
|
133
|
+
return a.message === b.message && a.name === b.name;
|
|
134
|
+
}
|
|
135
|
+
// Promise objects: cannot be meaningfully compared for deep equality
|
|
136
|
+
if (a_ctor === Promise) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// Plain objects: compare enumerable own properties
|
|
140
|
+
const a_keys = Object.keys(a);
|
|
141
|
+
const a_keys_length = a_keys.length;
|
|
142
|
+
if (a_keys_length !== Object.keys(b).length)
|
|
143
|
+
return false;
|
|
144
|
+
for (let i = 0; i < a_keys_length; i++) {
|
|
145
|
+
const key = a_keys[i];
|
|
146
|
+
if (!(key in b))
|
|
147
|
+
return false;
|
|
148
|
+
if (!deep_equal(a[key], b[key]))
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
};
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the given element is editable.
|
|
3
|
+
* Returns `true` for text-based input types, textareas, and contenteditable elements.
|
|
4
|
+
*/
|
|
5
|
+
export declare const is_editable: (el: any) => boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Returns `true` if the element is within a `contenteditable` ancestor.
|
|
8
|
+
*/
|
|
9
|
+
export declare const inside_editable: (el: Element) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Checks if the element is interactive (clickable, focusable, or otherwise accepts user input).
|
|
12
|
+
* Returns `true` for buttons, links, form controls,
|
|
13
|
+
* and elements with interactive attributes and ARIA roles.
|
|
14
|
+
*/
|
|
15
|
+
export declare const is_interactive: (el: any) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Stops an event from bubbling and doing default behavior.
|
|
18
|
+
* @param event
|
|
19
|
+
* @param immediate defaults to `true` to use `stopImmediatePropagation` over `stopPropagation`
|
|
20
|
+
* @param preventDefault defaults to `true`
|
|
21
|
+
* @mutates event calls preventDefault(), stopPropagation(), or stopImmediatePropagation()
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
export declare const swallow: <T extends Pick<Event, "preventDefault" | "stopPropagation" | "stopImmediatePropagation">>(event: T, immediate?: boolean, preventDefault?: boolean) => T;
|
|
25
|
+
/**
|
|
26
|
+
* Handles the value of an event's target and invokes a callback.
|
|
27
|
+
* Defaults to swallowing the event to prevent default actions and propagation.
|
|
28
|
+
* @mutates event calls `swallow()` which mutates the event if `swallow_event` is true
|
|
29
|
+
*/
|
|
30
|
+
export declare const handle_target_value: (cb: (value: any, event: any) => void, swallow_event?: boolean) => (e: any) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a boolean indicating if the current browser window, if any, is iframed inside of another.
|
|
33
|
+
*/
|
|
34
|
+
export declare const is_iframed: () => boolean;
|
|
35
|
+
//# sourceMappingURL=dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/dom.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,IAAI,GAAG,KAAG,OASrC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,OAAO,KAAG,OAG7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,IAAI,GAAG,KAAG,OA8BxC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,GACnB,CAAC,SAAS,IAAI,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,GAAG,0BAA0B,CAAC,EAExF,OAAO,CAAC,EACR,mBAAgB,EAChB,wBAAqB,KACnB,CAQF,CAAC;AAGF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,EAAE,uBAAoB,MAC1D,GAAG,GAAG,KAAG,IAGT,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,UAAU,QAAO,OAO7B,CAAC"}
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the given element is editable.
|
|
3
|
+
* Returns `true` for text-based input types, textareas, and contenteditable elements.
|
|
4
|
+
*/
|
|
5
|
+
export const is_editable = (el) => {
|
|
6
|
+
if (!el)
|
|
7
|
+
return false;
|
|
8
|
+
const { tagName } = el;
|
|
9
|
+
return ((tagName === 'INPUT' && el.type !== 'hidden') ||
|
|
10
|
+
tagName === 'TEXTAREA' ||
|
|
11
|
+
el.contentEditable === 'true' ||
|
|
12
|
+
el.contentEditable === '' // Some browsers treat empty string as `'true'`
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Returns `true` if the element is within a `contenteditable` ancestor.
|
|
17
|
+
*/
|
|
18
|
+
export const inside_editable = (el) => {
|
|
19
|
+
const found = el.closest('[contenteditable]');
|
|
20
|
+
return found !== null && found.contentEditable !== 'false';
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Checks if the element is interactive (clickable, focusable, or otherwise accepts user input).
|
|
24
|
+
* Returns `true` for buttons, links, form controls,
|
|
25
|
+
* and elements with interactive attributes and ARIA roles.
|
|
26
|
+
*/
|
|
27
|
+
export const is_interactive = (el) => {
|
|
28
|
+
if (!el)
|
|
29
|
+
return false;
|
|
30
|
+
const { tagName } = el;
|
|
31
|
+
if (tagName === 'BUTTON' ||
|
|
32
|
+
tagName === 'SELECT' ||
|
|
33
|
+
tagName === 'TEXTAREA' ||
|
|
34
|
+
tagName === 'A' ||
|
|
35
|
+
tagName === 'AUDIO' ||
|
|
36
|
+
tagName === 'VIDEO' ||
|
|
37
|
+
(tagName === 'INPUT' && el.type !== 'hidden')) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const role = el.getAttribute?.('role');
|
|
41
|
+
return ((role &&
|
|
42
|
+
(role === 'button' ||
|
|
43
|
+
role === 'link' ||
|
|
44
|
+
role === 'menuitem' ||
|
|
45
|
+
role === 'option' ||
|
|
46
|
+
role === 'switch' ||
|
|
47
|
+
role === 'tab')) ||
|
|
48
|
+
(el.hasAttribute?.('tabindex') && el.getAttribute('tabindex') !== '-1') ||
|
|
49
|
+
el.contentEditable === 'true' ||
|
|
50
|
+
el.contentEditable === '' ||
|
|
51
|
+
el.getAttribute?.('draggable') === 'true');
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Stops an event from bubbling and doing default behavior.
|
|
55
|
+
* @param event
|
|
56
|
+
* @param immediate defaults to `true` to use `stopImmediatePropagation` over `stopPropagation`
|
|
57
|
+
* @param preventDefault defaults to `true`
|
|
58
|
+
* @mutates event calls preventDefault(), stopPropagation(), or stopImmediatePropagation()
|
|
59
|
+
* @returns
|
|
60
|
+
*/
|
|
61
|
+
export const swallow = (event, immediate = true, preventDefault = true) => {
|
|
62
|
+
if (preventDefault)
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
if (immediate) {
|
|
65
|
+
event.stopImmediatePropagation();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
event.stopPropagation();
|
|
69
|
+
}
|
|
70
|
+
return event;
|
|
71
|
+
};
|
|
72
|
+
// TODO improve these types, the motivation was the strictness of Svelte DOM types
|
|
73
|
+
/**
|
|
74
|
+
* Handles the value of an event's target and invokes a callback.
|
|
75
|
+
* Defaults to swallowing the event to prevent default actions and propagation.
|
|
76
|
+
* @mutates event calls `swallow()` which mutates the event if `swallow_event` is true
|
|
77
|
+
*/
|
|
78
|
+
export const handle_target_value = (cb, swallow_event = true) => (e) => {
|
|
79
|
+
if (swallow_event)
|
|
80
|
+
swallow(e);
|
|
81
|
+
cb(e.target.value, e);
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Returns a boolean indicating if the current browser window, if any, is iframed inside of another.
|
|
85
|
+
*/
|
|
86
|
+
export const is_iframed = () => {
|
|
87
|
+
if (typeof window === 'undefined')
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
return window.self !== window.top; // some browsers may throw here due to the same origin policy
|
|
91
|
+
}
|
|
92
|
+
catch (_err) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error for asserting unreachable code paths in TypeScript.
|
|
3
|
+
* Useful for exhaustive matching.
|
|
4
|
+
*/
|
|
5
|
+
export declare class UnreachableError extends Error {
|
|
6
|
+
constructor(value: never, message?: string, options?: ErrorOptions);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Beyond terseness, this is useful because `throw` is not an expression,
|
|
10
|
+
* and therefore can't be used in places like Svelte markup without a workaround,
|
|
11
|
+
* at least until this proposal is accepted and widely available:
|
|
12
|
+
* https://github.com/tc39/proposal-throw-expressions
|
|
13
|
+
*/
|
|
14
|
+
export declare const unreachable: (value: never, message?: string) => asserts value is never;
|
|
15
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,KAAK,EAAE,KAAK,EAAE,OAAO,SAA+B,EAAE,OAAO,CAAC,EAAE,YAAY;CAGxF;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,IAAI,KAK9E,CAAC"}
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error for asserting unreachable code paths in TypeScript.
|
|
3
|
+
* Useful for exhaustive matching.
|
|
4
|
+
*/
|
|
5
|
+
export class UnreachableError extends Error {
|
|
6
|
+
constructor(value, message = `Unreachable case: ${value}`, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Beyond terseness, this is useful because `throw` is not an expression,
|
|
12
|
+
* and therefore can't be used in places like Svelte markup without a workaround,
|
|
13
|
+
* at least until this proposal is accepted and widely available:
|
|
14
|
+
* https://github.com/tc39/proposal-throw-expressions
|
|
15
|
+
*/
|
|
16
|
+
export const unreachable = (value, message) => {
|
|
17
|
+
throw new UnreachableError(value, message);
|
|
18
|
+
};
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Flavored } from './types.js';
|
|
3
|
+
import type { Logger } from './log.js';
|
|
4
|
+
import type { Result } from './result.js';
|
|
5
|
+
export interface FetchValueOptions<TValue, TParams = undefined> {
|
|
6
|
+
/**
|
|
7
|
+
* The `request.headers` take precedence over the headers computed from other options.
|
|
8
|
+
*/
|
|
9
|
+
request?: RequestInit;
|
|
10
|
+
params?: TParams;
|
|
11
|
+
parse?: (v: any) => TValue;
|
|
12
|
+
token?: string | null;
|
|
13
|
+
cache?: FetchValueCache | null;
|
|
14
|
+
return_early_from_cache?: boolean;
|
|
15
|
+
log?: Logger;
|
|
16
|
+
fetch?: typeof globalThis.fetch;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Specializes `fetch` with some slightly different behavior and additional features:
|
|
20
|
+
*
|
|
21
|
+
* - throws on ratelimit errors to mitigate unintentional abuse
|
|
22
|
+
* - optional `parse` function called on the return value
|
|
23
|
+
* - optional cache (different from the browser cache,
|
|
24
|
+
* the caller can serialize it so e.g. dev setups can avoid hitting the network)
|
|
25
|
+
* - optional simplified API for authorization and data types
|
|
26
|
+
* (you can still provide headers directly)
|
|
27
|
+
*
|
|
28
|
+
* Unlike `fetch`, this throws on ratelimits (status code 429)
|
|
29
|
+
* to halt whatever is happpening in its tracks to avoid accidental abuse,
|
|
30
|
+
* but returns a `Result` in all other cases.
|
|
31
|
+
* Handling ratelimit headers with more sophistication gets tricky because behavior
|
|
32
|
+
* differs across services.
|
|
33
|
+
* (e.g. Mastodon returns an ISO string for `x-ratelimit-reset`,
|
|
34
|
+
* but GitHub returns `Date.now()/1000`,
|
|
35
|
+
* and other services may do whatever, or even use a different header)
|
|
36
|
+
*
|
|
37
|
+
* It's also stateless to avoid the complexity and bugs,
|
|
38
|
+
* so we don't try to track `x-ratelimit-remaining` per domain.
|
|
39
|
+
*
|
|
40
|
+
* If the `value` is cached, only the cached safe subset of the `headers` are returned.
|
|
41
|
+
* (currently just `etag` and `last-modified`)
|
|
42
|
+
* Otherwise the full `res.headers` are included.
|
|
43
|
+
* @mutates options.cache calls `cache.set()` to store fetched results if cache is provided
|
|
44
|
+
*/
|
|
45
|
+
export declare const fetch_value: <TValue = any, TParams = undefined>(url: string | URL, options?: FetchValueOptions<TValue, TParams>) => Promise<Result<{
|
|
46
|
+
value: TValue;
|
|
47
|
+
headers: Headers;
|
|
48
|
+
}, {
|
|
49
|
+
status: number;
|
|
50
|
+
message: string;
|
|
51
|
+
}>>;
|
|
52
|
+
export declare const FetchValueCacheKey: z.ZodString;
|
|
53
|
+
export type FetchValueCacheKey = Flavored<z.infer<typeof FetchValueCacheKey>, 'FetchValueCacheKey'>;
|
|
54
|
+
export declare const FetchValueCacheItem: z.ZodObject<{
|
|
55
|
+
key: z.ZodString;
|
|
56
|
+
url: z.ZodString;
|
|
57
|
+
params: z.ZodAny;
|
|
58
|
+
value: z.ZodAny;
|
|
59
|
+
etag: z.ZodNullable<z.ZodString>;
|
|
60
|
+
last_modified: z.ZodNullable<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
export type FetchValueCacheItem = z.infer<typeof FetchValueCacheItem>;
|
|
63
|
+
export declare const FetchValueCache: z.ZodMap<z.ZodString, z.ZodObject<{
|
|
64
|
+
key: z.ZodString;
|
|
65
|
+
url: z.ZodString;
|
|
66
|
+
params: z.ZodAny;
|
|
67
|
+
value: z.ZodAny;
|
|
68
|
+
etag: z.ZodNullable<z.ZodString>;
|
|
69
|
+
last_modified: z.ZodNullable<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
export type FetchValueCache = z.infer<typeof FetchValueCache>;
|
|
72
|
+
export declare const to_fetch_value_cache_key: (url: string, params: any, method: string) => FetchValueCacheKey;
|
|
73
|
+
/**
|
|
74
|
+
* Converts `FetchValueCache` to a JSON string.
|
|
75
|
+
*/
|
|
76
|
+
export declare const serialize_cache: (cache: FetchValueCache) => string;
|
|
77
|
+
/**
|
|
78
|
+
* Converts a serialized cache string to a `FetchValueCache`.
|
|
79
|
+
*/
|
|
80
|
+
export declare const deserialize_cache: (serialized: string) => FetchValueCache;
|
|
81
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,UAAU,CAAC;AAErC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAMxC,MAAM,WAAW,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS;IAC7D;;OAEG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,WAAW,GAAU,MAAM,GAAG,GAAG,EAAE,OAAO,GAAG,SAAS,EAClE,KAAK,MAAM,GAAG,GAAG,EACjB,UAAU,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1C,OAAO,CAAC,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,EAAE;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAAC,CA2GtF,CAAC;AA4BF,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAC7C,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,EAAE,oBAAoB,CAAC,CAAC;AAEpG,eAAO,MAAM,mBAAmB;;;;;;;iBAO9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,eAAe;;;;;;;kBAAiD,CAAC;AAC9E,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAI9D,eAAO,MAAM,wBAAwB,GACpC,KAAK,MAAM,EACX,QAAQ,GAAG,EACX,QAAQ,MAAM,KACZ,kBAMF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,eAAe,KAAG,MACb,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,eACA,CAAC"}
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { EMPTY_OBJECT } from './object.js';
|
|
3
|
+
import { json_stringify_deterministic } from './json.js';
|
|
4
|
+
const DEFAULT_GITHUB_API_ACCEPT_HEADER = 'application/vnd.github+json';
|
|
5
|
+
const DEFAULT_GITHUB_API_VERSION_HEADER = '2022-11-28';
|
|
6
|
+
/**
|
|
7
|
+
* Specializes `fetch` with some slightly different behavior and additional features:
|
|
8
|
+
*
|
|
9
|
+
* - throws on ratelimit errors to mitigate unintentional abuse
|
|
10
|
+
* - optional `parse` function called on the return value
|
|
11
|
+
* - optional cache (different from the browser cache,
|
|
12
|
+
* the caller can serialize it so e.g. dev setups can avoid hitting the network)
|
|
13
|
+
* - optional simplified API for authorization and data types
|
|
14
|
+
* (you can still provide headers directly)
|
|
15
|
+
*
|
|
16
|
+
* Unlike `fetch`, this throws on ratelimits (status code 429)
|
|
17
|
+
* to halt whatever is happpening in its tracks to avoid accidental abuse,
|
|
18
|
+
* but returns a `Result` in all other cases.
|
|
19
|
+
* Handling ratelimit headers with more sophistication gets tricky because behavior
|
|
20
|
+
* differs across services.
|
|
21
|
+
* (e.g. Mastodon returns an ISO string for `x-ratelimit-reset`,
|
|
22
|
+
* but GitHub returns `Date.now()/1000`,
|
|
23
|
+
* and other services may do whatever, or even use a different header)
|
|
24
|
+
*
|
|
25
|
+
* It's also stateless to avoid the complexity and bugs,
|
|
26
|
+
* so we don't try to track `x-ratelimit-remaining` per domain.
|
|
27
|
+
*
|
|
28
|
+
* If the `value` is cached, only the cached safe subset of the `headers` are returned.
|
|
29
|
+
* (currently just `etag` and `last-modified`)
|
|
30
|
+
* Otherwise the full `res.headers` are included.
|
|
31
|
+
* @mutates options.cache calls `cache.set()` to store fetched results if cache is provided
|
|
32
|
+
*/
|
|
33
|
+
export const fetch_value = async (url, options) => {
|
|
34
|
+
const { request, params, parse, token, cache, return_early_from_cache, log, fetch = globalThis.fetch, } = options ?? EMPTY_OBJECT;
|
|
35
|
+
const url_obj = typeof url === 'string' ? new URL(url) : url;
|
|
36
|
+
const url_str = url_obj.href;
|
|
37
|
+
const method = request?.method ?? (params ? 'POST' : 'GET');
|
|
38
|
+
// local cache?
|
|
39
|
+
let cached;
|
|
40
|
+
let key;
|
|
41
|
+
if (cache) {
|
|
42
|
+
key = to_fetch_value_cache_key(url_str, params, method);
|
|
43
|
+
cached = cache.get(key);
|
|
44
|
+
if (return_early_from_cache && cached) {
|
|
45
|
+
log?.info('[fetch_value] cached locally and returning early', url_str);
|
|
46
|
+
log?.debug('[fetch_value] cached value', cached);
|
|
47
|
+
return { ok: true, value: cached.value, headers: to_cached_headers(cached) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const body = request?.body ?? (method === 'GET' || method === 'HEAD' ? null : JSON.stringify(params ?? {}));
|
|
51
|
+
const headers = new Headers(request?.headers);
|
|
52
|
+
if (!headers.has('accept')) {
|
|
53
|
+
headers.set('accept', url_obj.hostname === 'api.github.com' ? DEFAULT_GITHUB_API_ACCEPT_HEADER : 'application/json');
|
|
54
|
+
}
|
|
55
|
+
if (headers.get('accept') === DEFAULT_GITHUB_API_ACCEPT_HEADER &&
|
|
56
|
+
!headers.has('x-github-api-version')) {
|
|
57
|
+
headers.set('x-github-api-version', DEFAULT_GITHUB_API_VERSION_HEADER);
|
|
58
|
+
}
|
|
59
|
+
if (body && !headers.has('content-type')) {
|
|
60
|
+
headers.set('content-type', 'application/json');
|
|
61
|
+
}
|
|
62
|
+
if (token && !headers.has('authorization')) {
|
|
63
|
+
headers.set('authorization', 'Bearer ' + token);
|
|
64
|
+
}
|
|
65
|
+
const etag = cached?.etag;
|
|
66
|
+
if (etag && !headers.has('if-none-match')) {
|
|
67
|
+
headers.set('if-none-match', etag);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// fall back to last-modified, ignoring if there's an etag
|
|
71
|
+
const last_modified = cached?.last_modified;
|
|
72
|
+
if (last_modified && !headers.has('if-modified-since')) {
|
|
73
|
+
headers.set('if-modified-since', last_modified);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const req = new Request(url_obj, { ...request, headers, method, body });
|
|
77
|
+
log?.info('[fetch_value] fetching url with headers', url);
|
|
78
|
+
log?.debug('[fetch_value] fetching with headers', print_headers(headers));
|
|
79
|
+
const res = await fetch(req); // don't catch network errors
|
|
80
|
+
log?.info('[fetch_value] fetched', url, res.status, print_ratelimit_headers(res.headers));
|
|
81
|
+
log?.debug('[fetch_value] fetched', Object.fromEntries(res.headers.entries()));
|
|
82
|
+
// throw on ratelimit
|
|
83
|
+
if (res.status === 429) {
|
|
84
|
+
throw Error('ratelimited exceeded fetching url ' + url);
|
|
85
|
+
}
|
|
86
|
+
// return from cache if it hits
|
|
87
|
+
if (res.status === 304) {
|
|
88
|
+
if (!cached)
|
|
89
|
+
throw Error('unexpected 304 status without a cached value');
|
|
90
|
+
log?.info('[fetch_value] cache hit', url);
|
|
91
|
+
return { ok: true, value: cached.value, headers: to_cached_headers(cached) };
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
return { ok: false, status: res.status, message: res.statusText };
|
|
95
|
+
}
|
|
96
|
+
const content_type = res.headers.get('content-type');
|
|
97
|
+
const fetched = await (!content_type || content_type.includes('json') ? res.json() : res.text()); // TODO hacky
|
|
98
|
+
const parsed = parse ? parse(fetched) : fetched;
|
|
99
|
+
log?.debug('[fetch_value] fetched json', url, parsed);
|
|
100
|
+
if (key) {
|
|
101
|
+
const result = {
|
|
102
|
+
key,
|
|
103
|
+
url: url_str,
|
|
104
|
+
params,
|
|
105
|
+
value: parsed,
|
|
106
|
+
etag: res.headers.get('etag'),
|
|
107
|
+
last_modified: res.headers.get('etag') ? null : res.headers.get('last-modified'), // fall back to last-modified, ignoring if there's an etag
|
|
108
|
+
};
|
|
109
|
+
cache.set(key, result);
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, value: parsed, headers: res.headers };
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Returns a subset of headers that are safe to store in a `fetch_value` cache.
|
|
115
|
+
*/
|
|
116
|
+
const to_cached_headers = (cached) => {
|
|
117
|
+
const headers = new Headers();
|
|
118
|
+
if (cached.etag) {
|
|
119
|
+
headers.set('etag', cached.etag);
|
|
120
|
+
}
|
|
121
|
+
if (cached.last_modified) {
|
|
122
|
+
headers.set('last-modified', cached.last_modified);
|
|
123
|
+
}
|
|
124
|
+
return headers;
|
|
125
|
+
};
|
|
126
|
+
const print_headers = (headers) => {
|
|
127
|
+
const h = Object.fromEntries(headers.entries());
|
|
128
|
+
if (h.authorization)
|
|
129
|
+
h.authorization = '[REDACTED]';
|
|
130
|
+
return h;
|
|
131
|
+
};
|
|
132
|
+
const print_ratelimit_headers = (headers) => {
|
|
133
|
+
const limit = headers.get('x-ratelimit-limit');
|
|
134
|
+
const remaining = headers.get('x-ratelimit-remaining');
|
|
135
|
+
return limit || remaining ? `ratelimit ${remaining} of ${limit}` : '';
|
|
136
|
+
};
|
|
137
|
+
export const FetchValueCacheKey = z.string();
|
|
138
|
+
export const FetchValueCacheItem = z.object({
|
|
139
|
+
key: FetchValueCacheKey,
|
|
140
|
+
url: z.string(),
|
|
141
|
+
params: z.any(),
|
|
142
|
+
value: z.any(),
|
|
143
|
+
etag: z.string().nullable(),
|
|
144
|
+
last_modified: z.string().nullable(),
|
|
145
|
+
});
|
|
146
|
+
export const FetchValueCache = z.map(FetchValueCacheKey, FetchValueCacheItem);
|
|
147
|
+
const KEY_SEPARATOR = '::';
|
|
148
|
+
export const to_fetch_value_cache_key = (url, params, method) => {
|
|
149
|
+
let key = method + KEY_SEPARATOR + url;
|
|
150
|
+
if (params != null) {
|
|
151
|
+
key += KEY_SEPARATOR + json_stringify_deterministic(params);
|
|
152
|
+
}
|
|
153
|
+
return key;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Converts `FetchValueCache` to a JSON string.
|
|
157
|
+
*/
|
|
158
|
+
export const serialize_cache = (cache) => JSON.stringify(Array.from(cache.entries()));
|
|
159
|
+
/**
|
|
160
|
+
* Converts a serialized cache string to a `FetchValueCache`.
|
|
161
|
+
*/
|
|
162
|
+
export const deserialize_cache = (serialized) => FetchValueCache.parse(new Map(JSON.parse(serialized)));
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RmOptions } from 'node:fs';
|
|
2
|
+
import type { FileFilter, ResolvedPath, PathFilter } from './path.js';
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a file or directory exists.
|
|
5
|
+
*/
|
|
6
|
+
export declare const fs_exists: (path: string) => Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Empties a directory, recursively by default. If `should_remove` is provided, only entries where it returns `true` are removed.
|
|
9
|
+
*/
|
|
10
|
+
export declare const fs_empty_dir: (dir: string, should_remove?: (name: string) => boolean, options?: RmOptions) => Promise<void>;
|
|
11
|
+
export interface FsSearchOptions {
|
|
12
|
+
/**
|
|
13
|
+
* One or more filter functions, any of which can short-circuit the search by returning `false`.
|
|
14
|
+
*/
|
|
15
|
+
filter?: PathFilter | Array<PathFilter>;
|
|
16
|
+
/**
|
|
17
|
+
* One or more file filter functions. Every filter must pass for a file to be included.
|
|
18
|
+
*/
|
|
19
|
+
file_filter?: FileFilter | Array<FileFilter>;
|
|
20
|
+
/**
|
|
21
|
+
* Pass `null` or `false` to speed things up at the cost of volatile ordering.
|
|
22
|
+
*/
|
|
23
|
+
sort?: boolean | null | ((a: ResolvedPath, b: ResolvedPath) => number);
|
|
24
|
+
/**
|
|
25
|
+
* Set to `true` to include directories. Defaults to `false`.
|
|
26
|
+
*/
|
|
27
|
+
include_directories?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Sets the cwd for `dir` unless it's an absolute path or `null`.
|
|
30
|
+
*/
|
|
31
|
+
cwd?: string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare const fs_search: (dir: string, options?: FsSearchOptions) => Promise<Array<ResolvedPath>>;
|
|
34
|
+
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/fs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAMvC,OAAO,KAAK,EAAC,UAAU,EAAE,YAAY,EAAE,UAAU,EAAC,MAAM,WAAW,CAAC;AAEpE;;GAEG;AACH,eAAO,MAAM,SAAS,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,OAAO,CAO7D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,GACxB,KAAK,MAAM,EACX,gBAAgB,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EACzC,UAAU,SAAS,KACjB,OAAO,CAAC,IAAI,CAId,CAAC;AAEF,MAAM,WAAW,eAAe;IAC/B;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IACxC;;OAEG;IACH,WAAW,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IAC7C;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,KAAK,MAAM,CAAC,CAAC;IACvE;;OAEG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,eAAO,MAAM,SAAS,GACrB,KAAK,MAAM,EACX,UAAS,eAA8B,KACrC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CA2B7B,CAAC"}
|