elements-kit 0.0.13 → 0.0.15
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/README.md +38 -0
- package/dist/{element-C_4VbkvQ.mjs → element-ChF24-2z.mjs} +1 -6
- package/dist/index.mjs +1 -1
- package/dist/jsx-runtime/index.d.mts +7 -5
- package/dist/jsx-runtime/index.mjs +1 -1
- package/dist/utilities/async.d.mts +37 -0
- package/dist/utilities/async.mjs +119 -0
- package/dist/utilities/async.test.d.mts +1 -0
- package/dist/utilities/async.test.mjs +273 -0
- package/dist/utilities/interval.d.mts +6 -5
- package/dist/utilities/interval.mjs +16 -12
- package/dist/utilities/interval.test.mjs +46 -1
- package/dist/utilities/network.d.mts +10 -0
- package/dist/utilities/network.mjs +17 -0
- package/dist/utilities/network.test.d.mts +1 -0
- package/dist/utilities/network.test.mjs +29 -0
- package/dist/utilities/promise.d.mts +56 -0
- package/dist/utilities/promise.mjs +101 -0
- package/dist/utilities/promise.test.d.mts +1 -0
- package/dist/utilities/promise.test.mjs +156 -0
- package/dist/utilities/retry.d.mts +10 -0
- package/dist/utilities/retry.mjs +32 -0
- package/dist/utilities/retry.test.d.mts +1 -0
- package/dist/utilities/retry.test.mjs +83 -0
- package/dist/utilities/timeout.d.mts +1 -1
- package/dist/utilities/timeout.mjs +6 -6
- package/dist/utilities/timeout.test.mjs +58 -3
- package/dist/utilities/window-focus.d.mts +10 -0
- package/dist/utilities/window-focus.mjs +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { _ as signal } from "../signals-BHmWX6ox.mjs";
|
|
2
|
+
import { on } from "./event-listener.mjs";
|
|
3
|
+
//#region src/utilities/network.ts
|
|
4
|
+
function createOnline() {
|
|
5
|
+
const value = signal(typeof navigator !== "undefined" ? navigator.onLine : true);
|
|
6
|
+
const update = () => value(navigator.onLine);
|
|
7
|
+
on(window, "online", update);
|
|
8
|
+
on(window, "offline", update);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Singleton `Computed<boolean>` — `true` when `navigator.onLine` is true.
|
|
13
|
+
* Reacts to `online` / `offline` window events.
|
|
14
|
+
*/
|
|
15
|
+
const online = createOnline();
|
|
16
|
+
//#endregion
|
|
17
|
+
export { online };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { n as vi, o as describe, r as afterEach, s as it, t as globalExpect } from "../test.BmQO5GaM-ANkhHvbr.mjs";
|
|
2
|
+
import { online } from "./network.mjs";
|
|
3
|
+
//#region src/utilities/network.test.ts
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
describe("online", () => {
|
|
8
|
+
it("reflects navigator.onLine", () => {
|
|
9
|
+
globalExpect(online()).toBe(navigator.onLine);
|
|
10
|
+
});
|
|
11
|
+
it("becomes false on offline event", () => {
|
|
12
|
+
Object.defineProperty(navigator, "onLine", {
|
|
13
|
+
configurable: true,
|
|
14
|
+
get: () => false
|
|
15
|
+
});
|
|
16
|
+
window.dispatchEvent(new Event("offline"));
|
|
17
|
+
globalExpect(online()).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it("becomes true on online event", () => {
|
|
20
|
+
Object.defineProperty(navigator, "onLine", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
get: () => true
|
|
23
|
+
});
|
|
24
|
+
window.dispatchEvent(new Event("online"));
|
|
25
|
+
globalExpect(online()).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { t as Computed } from "../index-DUshSQ_6.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utilities/promise.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A `Promise` subclass that exposes its state as reactive signals.
|
|
6
|
+
*
|
|
7
|
+
* Prefer the {@link promise} factory for most use cases — it returns a
|
|
8
|
+
* `ComputedPromise` that is both awaitable and callable as a signal.
|
|
9
|
+
* Use `ReactivePromise` directly when you need the lower-level class —
|
|
10
|
+
* for example, to wrap a promise and expose `.state`, `.value`, `.reason`,
|
|
11
|
+
* and `.result` without the `Computed` callable interface.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const rp = ReactivePromise.from(fetch("/api/data"));
|
|
16
|
+
*
|
|
17
|
+
* effect(() => {
|
|
18
|
+
* if (rp.state === "fulfilled") console.log(rp.value);
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare class ReactivePromise<T, E = unknown> extends Promise<T> {
|
|
23
|
+
#private;
|
|
24
|
+
get state(): "pending" | "fulfilled" | "rejected";
|
|
25
|
+
get value(): T | undefined;
|
|
26
|
+
get reason(): E | undefined;
|
|
27
|
+
get result(): T | E | undefined;
|
|
28
|
+
constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void);
|
|
29
|
+
static from<T, E = unknown>(p: Promise<T>): ReactivePromise<T, E>;
|
|
30
|
+
}
|
|
31
|
+
type ComputedPromise<T, E = unknown> = ReactivePromise<T, E> & Computed<T | E | undefined>;
|
|
32
|
+
type Executor<T, E = unknown> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: E) => void) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Wraps a promise, executor, or `ReactivePromise` into a `ComputedPromise` —
|
|
35
|
+
* an object that is both awaitable like a regular Promise and reactive like a
|
|
36
|
+
* `Computed` signal.
|
|
37
|
+
*
|
|
38
|
+
* **Awaitable:** `await promise(fetch(...))` resolves to the fulfilled value,
|
|
39
|
+
* or rejects with the rejection reason, just like a native Promise.
|
|
40
|
+
*
|
|
41
|
+
* **Reactive:** calling the returned value as a function (`cp()`) reads the
|
|
42
|
+
* current result inside an `effect` or `computed`, tracking it as a dependency.
|
|
43
|
+
* Equivalent to `.result` — returns `undefined` while pending, the fulfilled
|
|
44
|
+
* value when resolved, or the rejection reason when rejected.
|
|
45
|
+
*
|
|
46
|
+
* Reactive state is also accessible via:
|
|
47
|
+
* - `.state` — `"pending" | "fulfilled" | "rejected"`
|
|
48
|
+
* - `.value` — the resolved value (or `undefined` while pending)
|
|
49
|
+
* - `.reason` — the rejection reason (or `undefined` while pending/fulfilled)
|
|
50
|
+
* - `.result` — `T | E | undefined`; the resolved value, rejection reason, or `undefined` while pending
|
|
51
|
+
*/
|
|
52
|
+
declare function promise<T, E = unknown>(p: ReactivePromise<T>): ComputedPromise<T, E>;
|
|
53
|
+
declare function promise<T, E = unknown>(p: Promise<T>): ComputedPromise<T, E>;
|
|
54
|
+
declare function promise<T, E = unknown>(executor: Executor<T, E>): ComputedPromise<T, E>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ComputedPromise, ReactivePromise, promise };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { _ as signal, c as batch, l as computed } from "../signals-BHmWX6ox.mjs";
|
|
2
|
+
//#region src/utilities/promise.ts
|
|
3
|
+
/**
|
|
4
|
+
* A `Promise` subclass that exposes its state as reactive signals.
|
|
5
|
+
*
|
|
6
|
+
* Prefer the {@link promise} factory for most use cases — it returns a
|
|
7
|
+
* `ComputedPromise` that is both awaitable and callable as a signal.
|
|
8
|
+
* Use `ReactivePromise` directly when you need the lower-level class —
|
|
9
|
+
* for example, to wrap a promise and expose `.state`, `.value`, `.reason`,
|
|
10
|
+
* and `.result` without the `Computed` callable interface.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const rp = ReactivePromise.from(fetch("/api/data"));
|
|
15
|
+
*
|
|
16
|
+
* effect(() => {
|
|
17
|
+
* if (rp.state === "fulfilled") console.log(rp.value);
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var ReactivePromise = class ReactivePromise extends Promise {
|
|
22
|
+
#state = signal("pending");
|
|
23
|
+
#value = signal(void 0);
|
|
24
|
+
#reason = signal(void 0);
|
|
25
|
+
#result = computed(() => {
|
|
26
|
+
const state = this.#state();
|
|
27
|
+
if (state === "fulfilled") return this.#value();
|
|
28
|
+
if (state === "rejected") return this.#reason();
|
|
29
|
+
});
|
|
30
|
+
get state() {
|
|
31
|
+
return this.#state();
|
|
32
|
+
}
|
|
33
|
+
get value() {
|
|
34
|
+
return this.#value();
|
|
35
|
+
}
|
|
36
|
+
get reason() {
|
|
37
|
+
return this.#reason();
|
|
38
|
+
}
|
|
39
|
+
get result() {
|
|
40
|
+
return this.#result();
|
|
41
|
+
}
|
|
42
|
+
constructor(executor) {
|
|
43
|
+
super((res, rej) => {
|
|
44
|
+
executor(async (value) => {
|
|
45
|
+
const resolved = await value;
|
|
46
|
+
batch(() => {
|
|
47
|
+
this.#state("fulfilled");
|
|
48
|
+
this.#value(resolved);
|
|
49
|
+
});
|
|
50
|
+
res(value);
|
|
51
|
+
}, async (_reason) => {
|
|
52
|
+
const reason = await _reason;
|
|
53
|
+
batch(() => {
|
|
54
|
+
this.#state("rejected");
|
|
55
|
+
this.#reason(reason);
|
|
56
|
+
});
|
|
57
|
+
rej(reason);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
static from(p) {
|
|
62
|
+
return new ReactivePromise((resolve, reject) => {
|
|
63
|
+
p.then(resolve).catch(reject);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const PROMISE_KEYS = new Set([
|
|
68
|
+
"then",
|
|
69
|
+
"catch",
|
|
70
|
+
"finally",
|
|
71
|
+
"state",
|
|
72
|
+
"value",
|
|
73
|
+
"reason",
|
|
74
|
+
"result"
|
|
75
|
+
]);
|
|
76
|
+
function resolvePromise(from) {
|
|
77
|
+
if (from instanceof ReactivePromise) return from;
|
|
78
|
+
if (from instanceof Promise) return ReactivePromise.from(from);
|
|
79
|
+
return new ReactivePromise(from);
|
|
80
|
+
}
|
|
81
|
+
function promise(from) {
|
|
82
|
+
const p = resolvePromise(from);
|
|
83
|
+
const $value = computed(() => p.result);
|
|
84
|
+
return new Proxy($value, {
|
|
85
|
+
apply() {
|
|
86
|
+
return $value();
|
|
87
|
+
},
|
|
88
|
+
get(target, prop, receiver) {
|
|
89
|
+
if (PROMISE_KEYS.has(prop)) {
|
|
90
|
+
const val = p[prop];
|
|
91
|
+
return typeof val === "function" ? val.bind(p) : val;
|
|
92
|
+
}
|
|
93
|
+
return Reflect.get(target, prop, receiver);
|
|
94
|
+
},
|
|
95
|
+
getPrototypeOf() {
|
|
96
|
+
return ReactivePromise.prototype;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { ReactivePromise, promise };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { u as effect } from "../signals-BHmWX6ox.mjs";
|
|
2
|
+
import { o as describe, s as it, t as globalExpect } from "../test.BmQO5GaM-ANkhHvbr.mjs";
|
|
3
|
+
import { ReactivePromise, promise } from "./promise.mjs";
|
|
4
|
+
//#region src/utilities/promise.test.ts
|
|
5
|
+
describe("ReactivePromise", () => {
|
|
6
|
+
it("starts in pending state", () => {
|
|
7
|
+
const rp = new ReactivePromise(() => {});
|
|
8
|
+
globalExpect(rp.state).toBe("pending");
|
|
9
|
+
globalExpect(rp.value).toBeUndefined();
|
|
10
|
+
globalExpect(rp.reason).toBeUndefined();
|
|
11
|
+
globalExpect(rp.result).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
it("transitions to fulfilled", async () => {
|
|
14
|
+
const rp = new ReactivePromise((resolve) => resolve(42));
|
|
15
|
+
await rp;
|
|
16
|
+
globalExpect(rp.state).toBe("fulfilled");
|
|
17
|
+
globalExpect(rp.value).toBe(42);
|
|
18
|
+
globalExpect(rp.reason).toBeUndefined();
|
|
19
|
+
globalExpect(rp.result).toBe(42);
|
|
20
|
+
});
|
|
21
|
+
it("transitions to rejected", async () => {
|
|
22
|
+
const err = /* @__PURE__ */ new Error("fail");
|
|
23
|
+
const rp = new ReactivePromise((_, reject) => reject(err));
|
|
24
|
+
await rp.catch(() => {});
|
|
25
|
+
globalExpect(rp.state).toBe("rejected");
|
|
26
|
+
globalExpect(rp.reason).toBe(err);
|
|
27
|
+
globalExpect(rp.value).toBeUndefined();
|
|
28
|
+
globalExpect(rp.result).toBe(err);
|
|
29
|
+
});
|
|
30
|
+
it("unwraps a PromiseLike value", async () => {
|
|
31
|
+
const rp = new ReactivePromise((resolve) => resolve(Promise.resolve(99)));
|
|
32
|
+
await rp;
|
|
33
|
+
globalExpect(rp.value).toBe(99);
|
|
34
|
+
});
|
|
35
|
+
it("state and value update atomically — no inconsistent intermediate state", async () => {
|
|
36
|
+
const rp = new ReactivePromise((resolve) => resolve(7));
|
|
37
|
+
const snapshots = [];
|
|
38
|
+
const stop = effect(() => {
|
|
39
|
+
snapshots.push({
|
|
40
|
+
state: rp.state,
|
|
41
|
+
value: rp.value
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
await rp;
|
|
45
|
+
stop();
|
|
46
|
+
globalExpect(snapshots.find((s) => s.state === "fulfilled" && s.value === void 0)).toBeUndefined();
|
|
47
|
+
globalExpect(snapshots.at(-1)).toEqual({
|
|
48
|
+
state: "fulfilled",
|
|
49
|
+
value: 7
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it("state and reason update atomically on rejection", async () => {
|
|
53
|
+
const err = /* @__PURE__ */ new Error("oops");
|
|
54
|
+
const rp = new ReactivePromise((_, reject) => reject(err));
|
|
55
|
+
const snapshots = [];
|
|
56
|
+
const stop = effect(() => {
|
|
57
|
+
snapshots.push({
|
|
58
|
+
state: rp.state,
|
|
59
|
+
reason: rp.reason
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
await rp.catch(() => {});
|
|
63
|
+
stop();
|
|
64
|
+
globalExpect(snapshots.find((s) => s.state === "rejected" && s.reason === void 0)).toBeUndefined();
|
|
65
|
+
globalExpect(snapshots.at(-1)).toEqual({
|
|
66
|
+
state: "rejected",
|
|
67
|
+
reason: err
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it("ReactivePromise.from() wraps an existing Promise", async () => {
|
|
71
|
+
const rp = ReactivePromise.from(Promise.resolve("hello"));
|
|
72
|
+
await rp;
|
|
73
|
+
globalExpect(rp.state).toBe("fulfilled");
|
|
74
|
+
globalExpect(rp.value).toBe("hello");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("promise()", () => {
|
|
78
|
+
it("is instanceof ReactivePromise", () => {
|
|
79
|
+
globalExpect(promise(() => {}) instanceof ReactivePromise).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it("returns undefined while pending", () => {
|
|
82
|
+
const cp = promise(() => {});
|
|
83
|
+
globalExpect(cp()).toBeUndefined();
|
|
84
|
+
globalExpect(cp.state).toBe("pending");
|
|
85
|
+
});
|
|
86
|
+
it("returns the resolved value when fulfilled", async () => {
|
|
87
|
+
const cp = promise((resolve) => resolve(42));
|
|
88
|
+
await cp;
|
|
89
|
+
globalExpect(cp()).toBe(42);
|
|
90
|
+
});
|
|
91
|
+
it("returns the rejection reason when rejected", async () => {
|
|
92
|
+
const err = /* @__PURE__ */ new Error("fail");
|
|
93
|
+
const cp = promise((_, reject) => reject(err));
|
|
94
|
+
await cp.catch(() => {});
|
|
95
|
+
globalExpect(cp()).toBe(err);
|
|
96
|
+
});
|
|
97
|
+
it("wraps an existing Promise", async () => {
|
|
98
|
+
const cp = promise(Promise.resolve("hi"));
|
|
99
|
+
await cp;
|
|
100
|
+
globalExpect(cp()).toBe("hi");
|
|
101
|
+
});
|
|
102
|
+
it("passes through a ReactivePromise", async () => {
|
|
103
|
+
const cp = promise(new ReactivePromise((resolve) => resolve(5)));
|
|
104
|
+
await cp;
|
|
105
|
+
globalExpect(cp()).toBe(5);
|
|
106
|
+
});
|
|
107
|
+
it("is awaitable — resolves to the fulfilled value", async () => {
|
|
108
|
+
globalExpect(await promise((resolve) => resolve(100))).toBe(100);
|
|
109
|
+
});
|
|
110
|
+
it("is awaitable — rejects on failure", async () => {
|
|
111
|
+
const err = /* @__PURE__ */ new Error("bad");
|
|
112
|
+
const cp = promise((_, reject) => reject(err));
|
|
113
|
+
let caught;
|
|
114
|
+
await cp.catch((e) => {
|
|
115
|
+
caught = e;
|
|
116
|
+
});
|
|
117
|
+
globalExpect(caught).toBe(err);
|
|
118
|
+
});
|
|
119
|
+
it("delegates .then()", async () => {
|
|
120
|
+
globalExpect(await promise((resolve) => resolve(3)).then((v) => v * 2)).toBe(6);
|
|
121
|
+
});
|
|
122
|
+
it("delegates .catch()", async () => {
|
|
123
|
+
const err = /* @__PURE__ */ new Error("caught");
|
|
124
|
+
globalExpect(await promise((_, reject) => reject(err)).catch((e) => e)).toBe(err);
|
|
125
|
+
});
|
|
126
|
+
it("cp.catch handler runs on rejection", async () => {
|
|
127
|
+
const err = /* @__PURE__ */ new Error("caught");
|
|
128
|
+
const cp = promise((_, reject) => reject(err));
|
|
129
|
+
const caught = [];
|
|
130
|
+
await cp.catch((e) => caught.push(e));
|
|
131
|
+
globalExpect(caught).toEqual([err]);
|
|
132
|
+
});
|
|
133
|
+
it("is reactive — effect reruns on fulfillment", async () => {
|
|
134
|
+
const cp = promise((resolve) => resolve(1));
|
|
135
|
+
const values = [];
|
|
136
|
+
const stop = effect(() => {
|
|
137
|
+
values.push(cp.value);
|
|
138
|
+
});
|
|
139
|
+
await cp;
|
|
140
|
+
stop();
|
|
141
|
+
globalExpect(values).toEqual([void 0, 1]);
|
|
142
|
+
});
|
|
143
|
+
it("is reactive — effect reruns on rejection", async () => {
|
|
144
|
+
const err = /* @__PURE__ */ new Error("x");
|
|
145
|
+
const cp = promise((_, reject) => reject(err));
|
|
146
|
+
const results = [];
|
|
147
|
+
const stop = effect(() => {
|
|
148
|
+
results.push(cp());
|
|
149
|
+
});
|
|
150
|
+
await cp.catch(() => {});
|
|
151
|
+
stop();
|
|
152
|
+
globalExpect(results).toEqual([void 0, err]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
//#endregion
|
|
156
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/utilities/retry.d.ts
|
|
2
|
+
type RetryDelay = number | ((attempt: number) => number);
|
|
3
|
+
/**
|
|
4
|
+
* Wraps `fn` to retry up to `attempts` times on failure.
|
|
5
|
+
* Delay (if provided) is inserted between failures only — not after the last.
|
|
6
|
+
* Each attempt runs in an effect scope so `onCleanup` inside `fn` fires before each retry.
|
|
7
|
+
*/
|
|
8
|
+
declare function retry<T>(fn: () => Promise<T>, attempts: number, delay?: RetryDelay): () => Promise<T>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { retry };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { u as effect, y as untracked } from "../signals-BHmWX6ox.mjs";
|
|
2
|
+
//#region src/utilities/retry.ts
|
|
3
|
+
/**
|
|
4
|
+
* Wraps `fn` to retry up to `attempts` times on failure.
|
|
5
|
+
* Delay (if provided) is inserted between failures only — not after the last.
|
|
6
|
+
* Each attempt runs in an effect scope so `onCleanup` inside `fn` fires before each retry.
|
|
7
|
+
*/
|
|
8
|
+
function retry(fn, attempts, delay) {
|
|
9
|
+
return async () => {
|
|
10
|
+
let last;
|
|
11
|
+
for (let i = 0; i < attempts; i++) {
|
|
12
|
+
let stop = () => {};
|
|
13
|
+
try {
|
|
14
|
+
const result = await new Promise((res, rej) => {
|
|
15
|
+
stop = effect(() => untracked(() => fn().then(res, rej)));
|
|
16
|
+
});
|
|
17
|
+
stop();
|
|
18
|
+
return result;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
stop();
|
|
21
|
+
last = err;
|
|
22
|
+
if (i < attempts - 1 && delay !== void 0) {
|
|
23
|
+
const ms = typeof delay === "function" ? delay(i) : delay;
|
|
24
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw last;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { retry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { g as onCleanup } from "../signals-BHmWX6ox.mjs";
|
|
2
|
+
import { n as vi, o as describe, s as it, t as globalExpect } from "../test.BmQO5GaM-ANkhHvbr.mjs";
|
|
3
|
+
import { retry } from "./retry.mjs";
|
|
4
|
+
//#region src/utilities/retry.test.ts
|
|
5
|
+
describe("retry", () => {
|
|
6
|
+
it("resolves immediately on first success", async () => {
|
|
7
|
+
globalExpect(await retry(() => Promise.resolve(42), 3)()).toBe(42);
|
|
8
|
+
});
|
|
9
|
+
it("retries after failure and resolves on later success", async () => {
|
|
10
|
+
let calls = 0;
|
|
11
|
+
globalExpect(await retry(() => {
|
|
12
|
+
calls++;
|
|
13
|
+
if (calls < 3) return Promise.reject(/* @__PURE__ */ new Error("fail"));
|
|
14
|
+
return Promise.resolve(calls);
|
|
15
|
+
}, 3)()).toBe(3);
|
|
16
|
+
globalExpect(calls).toBe(3);
|
|
17
|
+
});
|
|
18
|
+
it("rejects after exhausting all attempts", async () => {
|
|
19
|
+
const err = /* @__PURE__ */ new Error("always fails");
|
|
20
|
+
await globalExpect(retry(() => Promise.reject(err), 3)()).rejects.toBe(err);
|
|
21
|
+
});
|
|
22
|
+
it("rejects with the last error", async () => {
|
|
23
|
+
let i = 0;
|
|
24
|
+
const errors = [
|
|
25
|
+
/* @__PURE__ */ new Error("a"),
|
|
26
|
+
/* @__PURE__ */ new Error("b"),
|
|
27
|
+
/* @__PURE__ */ new Error("c")
|
|
28
|
+
];
|
|
29
|
+
await globalExpect(retry(() => Promise.reject(errors[i++]), 3)()).rejects.toBe(errors[2]);
|
|
30
|
+
});
|
|
31
|
+
it("delays between failures but not after the last", async () => {
|
|
32
|
+
const delayFn = vi.fn().mockReturnValue(0);
|
|
33
|
+
await retry(() => Promise.reject(/* @__PURE__ */ new Error("x")), 3, delayFn)().catch(() => {});
|
|
34
|
+
globalExpect(delayFn).toHaveBeenCalledTimes(2);
|
|
35
|
+
});
|
|
36
|
+
it("supports dynamic delay via function — passes attempt index", async () => {
|
|
37
|
+
const delayFn = vi.fn().mockReturnValue(0);
|
|
38
|
+
await retry(() => Promise.reject(/* @__PURE__ */ new Error("x")), 3, delayFn)().catch(() => {});
|
|
39
|
+
globalExpect(delayFn).toHaveBeenNthCalledWith(1, 0);
|
|
40
|
+
globalExpect(delayFn).toHaveBeenNthCalledWith(2, 1);
|
|
41
|
+
});
|
|
42
|
+
it("onCleanup inside fn fires before each retry", async () => {
|
|
43
|
+
const cleaned = [];
|
|
44
|
+
let attempt = 0;
|
|
45
|
+
await retry(() => {
|
|
46
|
+
const i = attempt++;
|
|
47
|
+
onCleanup(() => cleaned.push(i));
|
|
48
|
+
if (i < 2) return Promise.reject(/* @__PURE__ */ new Error("fail"));
|
|
49
|
+
return Promise.resolve(i);
|
|
50
|
+
}, 3)();
|
|
51
|
+
globalExpect(cleaned).toContain(0);
|
|
52
|
+
globalExpect(cleaned).toContain(1);
|
|
53
|
+
});
|
|
54
|
+
it("composes with async()", async () => {
|
|
55
|
+
const asyncOp = (await import("./async.mjs")).async;
|
|
56
|
+
let calls = 0;
|
|
57
|
+
const op = asyncOp(() => retry(() => {
|
|
58
|
+
calls++;
|
|
59
|
+
if (calls < 2) return Promise.reject(/* @__PURE__ */ new Error("fail"));
|
|
60
|
+
return Promise.resolve(calls);
|
|
61
|
+
}, 3)());
|
|
62
|
+
op.start();
|
|
63
|
+
await op;
|
|
64
|
+
globalExpect(op.value).toBe(2);
|
|
65
|
+
op.stop();
|
|
66
|
+
});
|
|
67
|
+
it("AbortController cleanup fires between retries when composed with async()", async () => {
|
|
68
|
+
const asyncOp = (await import("./async.mjs")).async;
|
|
69
|
+
const aborted = [];
|
|
70
|
+
let attempt = 0;
|
|
71
|
+
const op = asyncOp(() => {
|
|
72
|
+
const i = attempt++;
|
|
73
|
+
onCleanup(() => aborted.push(i));
|
|
74
|
+
if (i < 2) return Promise.reject(/* @__PURE__ */ new Error("fail"));
|
|
75
|
+
return Promise.resolve(i);
|
|
76
|
+
});
|
|
77
|
+
op.start();
|
|
78
|
+
await op.catch(() => {});
|
|
79
|
+
op.stop();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
//#endregion
|
|
83
|
+
export {};
|
|
@@ -6,19 +6,19 @@ import { _ as signal, g as onCleanup } from "../signals-BHmWX6ox.mjs";
|
|
|
6
6
|
* unless `immediate` is set to `false`.
|
|
7
7
|
*/
|
|
8
8
|
function createTimeout(callback, delay, immediate = true) {
|
|
9
|
-
const
|
|
9
|
+
const pending = signal(false);
|
|
10
10
|
let id;
|
|
11
11
|
const getDelay = typeof delay === "function" ? delay : () => delay;
|
|
12
12
|
const stop = () => {
|
|
13
13
|
clearTimeout(id);
|
|
14
14
|
id = void 0;
|
|
15
|
-
|
|
15
|
+
pending(false);
|
|
16
16
|
};
|
|
17
17
|
const start = () => {
|
|
18
|
-
if (
|
|
19
|
-
|
|
18
|
+
if (pending()) return;
|
|
19
|
+
pending(true);
|
|
20
20
|
id = setTimeout(() => {
|
|
21
|
-
|
|
21
|
+
pending(false);
|
|
22
22
|
id = void 0;
|
|
23
23
|
callback();
|
|
24
24
|
}, getDelay());
|
|
@@ -31,7 +31,7 @@ function createTimeout(callback, delay, immediate = true) {
|
|
|
31
31
|
const cleanup = () => stop();
|
|
32
32
|
onCleanup(cleanup);
|
|
33
33
|
return {
|
|
34
|
-
|
|
34
|
+
pending,
|
|
35
35
|
start,
|
|
36
36
|
stop,
|
|
37
37
|
reset,
|
|
@@ -14,7 +14,7 @@ describe("createTimeout", () => {
|
|
|
14
14
|
effectScope(() => {
|
|
15
15
|
t = createTimeout(cb, 500);
|
|
16
16
|
});
|
|
17
|
-
globalExpect(t.
|
|
17
|
+
globalExpect(t.pending()).toBe(true);
|
|
18
18
|
});
|
|
19
19
|
it("fires callback after delay", () => {
|
|
20
20
|
vi.useFakeTimers();
|
|
@@ -25,14 +25,14 @@ describe("createTimeout", () => {
|
|
|
25
25
|
vi.advanceTimersByTime(600);
|
|
26
26
|
globalExpect(cb).toHaveBeenCalledOnce();
|
|
27
27
|
});
|
|
28
|
-
it("
|
|
28
|
+
it("pending becomes false after firing", () => {
|
|
29
29
|
vi.useFakeTimers();
|
|
30
30
|
let t;
|
|
31
31
|
effectScope(() => {
|
|
32
32
|
t = createTimeout(vi.fn(), 200);
|
|
33
33
|
});
|
|
34
34
|
vi.advanceTimersByTime(300);
|
|
35
|
-
globalExpect(t.
|
|
35
|
+
globalExpect(t.pending()).toBe(false);
|
|
36
36
|
});
|
|
37
37
|
it("stop() cancels the timeout", () => {
|
|
38
38
|
vi.useFakeTimers();
|
|
@@ -59,6 +59,61 @@ describe("createTimeout", () => {
|
|
|
59
59
|
vi.advanceTimersByTime(300);
|
|
60
60
|
globalExpect(cb).toHaveBeenCalledOnce();
|
|
61
61
|
});
|
|
62
|
+
it("immediate=false does not start automatically", () => {
|
|
63
|
+
vi.useFakeTimers();
|
|
64
|
+
const cb = vi.fn();
|
|
65
|
+
let t;
|
|
66
|
+
effectScope(() => {
|
|
67
|
+
t = createTimeout(cb, 200, false);
|
|
68
|
+
});
|
|
69
|
+
globalExpect(t.pending()).toBe(false);
|
|
70
|
+
vi.advanceTimersByTime(300);
|
|
71
|
+
globalExpect(cb).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
it("start() fires after delay when called manually", () => {
|
|
74
|
+
vi.useFakeTimers();
|
|
75
|
+
const cb = vi.fn();
|
|
76
|
+
let t;
|
|
77
|
+
effectScope(() => {
|
|
78
|
+
t = createTimeout(cb, 200, false);
|
|
79
|
+
});
|
|
80
|
+
t.start();
|
|
81
|
+
globalExpect(t.pending()).toBe(true);
|
|
82
|
+
vi.advanceTimersByTime(200);
|
|
83
|
+
globalExpect(cb).toHaveBeenCalledOnce();
|
|
84
|
+
globalExpect(t.pending()).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it("Symbol.dispose cancels the timeout", () => {
|
|
87
|
+
vi.useFakeTimers();
|
|
88
|
+
const cb = vi.fn();
|
|
89
|
+
let t;
|
|
90
|
+
effectScope(() => {
|
|
91
|
+
t = createTimeout(cb, 500);
|
|
92
|
+
});
|
|
93
|
+
t[Symbol.dispose]();
|
|
94
|
+
vi.advanceTimersByTime(1e3);
|
|
95
|
+
globalExpect(cb).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
it("stops when scope is disposed", () => {
|
|
98
|
+
vi.useFakeTimers();
|
|
99
|
+
const cb = vi.fn();
|
|
100
|
+
effectScope(() => {
|
|
101
|
+
createTimeout(cb, 200);
|
|
102
|
+
})();
|
|
103
|
+
vi.advanceTimersByTime(500);
|
|
104
|
+
globalExpect(cb).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
it("dynamic delay function is called at start", () => {
|
|
107
|
+
vi.useFakeTimers();
|
|
108
|
+
const cb = vi.fn();
|
|
109
|
+
const getDelay = vi.fn().mockReturnValue(300);
|
|
110
|
+
effectScope(() => {
|
|
111
|
+
createTimeout(cb, getDelay);
|
|
112
|
+
});
|
|
113
|
+
vi.advanceTimersByTime(300);
|
|
114
|
+
globalExpect(cb).toHaveBeenCalledOnce();
|
|
115
|
+
globalExpect(getDelay).toHaveBeenCalledOnce();
|
|
116
|
+
});
|
|
62
117
|
});
|
|
63
118
|
//#endregion
|
|
64
119
|
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as Computed } from "../index-DUshSQ_6.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utilities/window-focus.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Singleton `Computed<boolean>` — `true` while the browser window has focus.
|
|
6
|
+
* Reacts to `focus` / `blur` window events.
|
|
7
|
+
*/
|
|
8
|
+
declare const windowFocused: Computed<boolean>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { windowFocused };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { _ as signal } from "../signals-BHmWX6ox.mjs";
|
|
2
|
+
import { on } from "./event-listener.mjs";
|
|
3
|
+
//#region src/utilities/window-focus.ts
|
|
4
|
+
function createWindowFocused() {
|
|
5
|
+
const value = signal(typeof document !== "undefined" ? document.hasFocus() : true);
|
|
6
|
+
on(window, "focus", () => value(true));
|
|
7
|
+
on(window, "blur", () => value(false));
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Singleton `Computed<boolean>` — `true` while the browser window has focus.
|
|
12
|
+
* Reacts to `focus` / `blur` window events.
|
|
13
|
+
*/
|
|
14
|
+
const windowFocused = createWindowFocused();
|
|
15
|
+
//#endregion
|
|
16
|
+
export { windowFocused };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elements-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.15",
|
|
5
5
|
"description": "A lightweight reactive UI library that transforms native HTMLElements into reactive components with signals. Ideal for framework-agnostic applications and web components.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"webcomponents",
|