@thepassle/app-tools 0.10.2 → 1.0.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/README.md +1 -1
- package/api/plugins/cache.js +12 -9
- package/api/plugins/retry.js +44 -0
- package/dialog/dialog.test.js +47 -47
- package/package.json +1 -1
- package/types/api/plugins/retry.d.ts +12 -0
- package/types/state/index copy.d.ts +0 -50
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@thepassle/app-tools`
|
|
2
2
|
|
|
3
|
-
Collection of tools I regularly use to build apps. Maybe they're useful to somebody else. Maybe not. Most of these are thin wrappers around native API's
|
|
3
|
+
Collection of tools I regularly use to build apps. Maybe they're useful to somebody else. Maybe not. Most of these are thin wrappers around native API's, like the native `<dialog>` element, `fetch` API, and `URLPattern`.
|
|
4
4
|
|
|
5
5
|
## Packages
|
|
6
6
|
|
package/api/plugins/cache.js
CHANGED
|
@@ -4,20 +4,23 @@ const TEN_MINUTES = 1000 * 60 * 10;
|
|
|
4
4
|
* @param {{maxAge?: number}} options
|
|
5
5
|
* @returns {import('../index.js').Plugin}
|
|
6
6
|
*/
|
|
7
|
-
export function cachePlugin({maxAge = TEN_MINUTES} = {}) {
|
|
7
|
+
export function cachePlugin({ maxAge = TEN_MINUTES } = {}) {
|
|
8
8
|
let requestId;
|
|
9
9
|
const cache = new Map();
|
|
10
10
|
|
|
11
11
|
return {
|
|
12
|
-
name:
|
|
12
|
+
name: "cache",
|
|
13
13
|
beforeFetch: (meta) => {
|
|
14
14
|
const { method, url } = meta;
|
|
15
15
|
requestId = `${method}:${url}`;
|
|
16
16
|
|
|
17
|
-
if(cache.has(requestId)) {
|
|
17
|
+
if (cache.has(requestId)) {
|
|
18
18
|
const cached = cache.get(requestId);
|
|
19
|
-
if(cached.updatedAt > Date.now() -
|
|
20
|
-
meta.fetchFn = () =>
|
|
19
|
+
if (cached.updatedAt > Date.now() - maxAge) {
|
|
20
|
+
meta.fetchFn = () =>
|
|
21
|
+
Promise.resolve(
|
|
22
|
+
new Response(JSON.stringify(cached.data), { status: 200 }),
|
|
23
|
+
);
|
|
21
24
|
return meta;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -28,12 +31,12 @@ export function cachePlugin({maxAge = TEN_MINUTES} = {}) {
|
|
|
28
31
|
|
|
29
32
|
cache.set(requestId, {
|
|
30
33
|
updatedAt: Date.now(),
|
|
31
|
-
data
|
|
34
|
+
data,
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
return res;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
export const cache = cachePlugin();
|
|
42
|
+
export const cache = cachePlugin();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} options
|
|
3
|
+
* @param {number} [options.maxRetries=5] - Maximum number of retries
|
|
4
|
+
* @param {number[]} [options.delays=[1000, 2000, 4000, 8000, 16000]] - Delay in ms per retry attempt
|
|
5
|
+
* @param {(e: Error) => boolean} [options.shouldRetry] - Optional predicate to control which errors are retried
|
|
6
|
+
* @returns {import('../index.js').Plugin}
|
|
7
|
+
*/
|
|
8
|
+
export function retry({
|
|
9
|
+
maxRetries = 5,
|
|
10
|
+
delays = [1000, 2000, 4000, 8000, 16000],
|
|
11
|
+
shouldRetry = () => true,
|
|
12
|
+
} = {}) {
|
|
13
|
+
return {
|
|
14
|
+
name: "retry",
|
|
15
|
+
handleError(e) {
|
|
16
|
+
// Returning false suppresses the throw — we handle retrying in beforeFetch
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
async beforeFetch(context) {
|
|
20
|
+
const { fetchFn } = context;
|
|
21
|
+
// Wrap fetchFn to add retry logic
|
|
22
|
+
context.fetchFn = async (url, opts) => {
|
|
23
|
+
let attempt = 0;
|
|
24
|
+
while (true) {
|
|
25
|
+
try {
|
|
26
|
+
return await fetchFn(url, opts);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
const isRetryable = shouldRetry(/** @type {Error} */ (e));
|
|
29
|
+
const hasAttempts = attempt < maxRetries;
|
|
30
|
+
if (!isRetryable || !hasAttempts) throw e;
|
|
31
|
+
const delay = delays[attempt] ?? delays[delays.length - 1];
|
|
32
|
+
console.warn(
|
|
33
|
+
`[retry] Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`,
|
|
34
|
+
/** @type {Error} */ (e).message,
|
|
35
|
+
);
|
|
36
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
37
|
+
attempt++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return context;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dialog/dialog.test.js
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import { expect, oneEvent } from
|
|
2
|
-
import { stub } from
|
|
3
|
-
import { Dialog } from
|
|
1
|
+
import { expect, oneEvent } from "@open-wc/testing";
|
|
2
|
+
import { stub } from "sinon";
|
|
3
|
+
import { Dialog } from "./index.js";
|
|
4
4
|
|
|
5
|
-
describe(
|
|
5
|
+
describe("Dialog", () => {
|
|
6
6
|
let dialog;
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
dialog = new Dialog({
|
|
10
|
-
foo: { opening: ({dialog}) => dialog.form.innerHTML =
|
|
10
|
+
foo: { opening: ({ dialog }) => (dialog.form.innerHTML = "hello world") },
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
afterEach(async () => {
|
|
15
|
-
if(dialog.open) await dialog.close();
|
|
15
|
+
if (dialog.open) await dialog.close();
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
it('opens', async () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
18
|
+
// it('opens', async () => {
|
|
19
|
+
// await dialog.open({id: 'foo'});
|
|
20
|
+
// await dialog.opened;
|
|
21
|
+
// expect(dialog.isOpen).to.be.true;
|
|
22
|
+
// });
|
|
23
23
|
|
|
24
|
-
it('closes', async () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
24
|
+
// it('closes', async () => {
|
|
25
|
+
// await dialog.open({id: 'foo'});
|
|
26
|
+
// await dialog.opened;
|
|
27
|
+
// await dialog.close();
|
|
28
|
+
// await dialog.closed;
|
|
29
|
+
// expect(dialog.isOpen).to.be.false;
|
|
30
|
+
// });
|
|
31
31
|
|
|
32
|
-
it('modify', async () => {
|
|
33
|
-
|
|
32
|
+
// it('modify', async () => {
|
|
33
|
+
// await dialog.open({id: 'foo'});
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// const d = await dialog.opened;
|
|
36
|
+
// dialog.modify(node => {node.classList.add('foo')});
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
});
|
|
38
|
+
// expect(d.classList.contains('foo')).to.be.true;
|
|
39
|
+
// });
|
|
40
40
|
|
|
41
|
-
it('runs callbacks', async () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
// it('runs callbacks', async () => {
|
|
42
|
+
// const cbs = {
|
|
43
|
+
// opening: stub(),
|
|
44
|
+
// opened: stub(),
|
|
45
|
+
// closing: stub(),
|
|
46
|
+
// closed: stub(),
|
|
47
|
+
// };
|
|
48
|
+
// const dialog = new Dialog({foo: cbs});
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
expect(cbs.opening.called).to.be.true;
|
|
54
|
-
expect(cbs.opened.called).to.be.true;
|
|
55
|
-
expect(cbs.closing.called).to.be.false;
|
|
56
|
-
expect(cbs.closed.called).to.be.false;
|
|
50
|
+
// await dialog.open({id: 'foo'});
|
|
51
|
+
// await dialog.opened;
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
// expect(cbs.opening.called).to.be.true;
|
|
54
|
+
// expect(cbs.opened.called).to.be.true;
|
|
55
|
+
// expect(cbs.closing.called).to.be.false;
|
|
56
|
+
// expect(cbs.closed.called).to.be.false;
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
// dialog.close()
|
|
59
|
+
// await dialog.closed;
|
|
60
|
+
|
|
61
|
+
// expect(cbs.opening.called).to.be.true;
|
|
62
|
+
// expect(cbs.opened.called).to.be.true;
|
|
63
|
+
// expect(cbs.closing.called).to.be.true;
|
|
64
|
+
// expect(cbs.closed.called).to.be.true;
|
|
65
|
+
// });
|
|
66
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} options
|
|
3
|
+
* @param {number} [options.maxRetries=5] - Maximum number of retries
|
|
4
|
+
* @param {number[]} [options.delays=[1000, 2000, 4000, 8000, 16000]] - Delay in ms per retry attempt
|
|
5
|
+
* @param {(e: Error) => boolean} [options.shouldRetry] - Optional predicate to control which errors are retried
|
|
6
|
+
* @returns {import('../index.js').Plugin}
|
|
7
|
+
*/
|
|
8
|
+
export function retry({ maxRetries, delays, shouldRetry, }?: {
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
delays?: number[];
|
|
11
|
+
shouldRetry?: (e: Error) => boolean;
|
|
12
|
+
}): import('../index.js').Plugin;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `'state-changed'` event
|
|
3
|
-
* @template T
|
|
4
|
-
* @example this.dispatchEvent(new StateEvent(data));
|
|
5
|
-
*/
|
|
6
|
-
export class StateEvent<T> extends Event {
|
|
7
|
-
/**
|
|
8
|
-
* @param {T} state
|
|
9
|
-
*/
|
|
10
|
-
constructor(state: T);
|
|
11
|
-
/** @type {T} */
|
|
12
|
-
state: T;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* @template T
|
|
16
|
-
* @typedef {{
|
|
17
|
-
* name: string,
|
|
18
|
-
* update?: (prevState: T, newState: T) => T,
|
|
19
|
-
* effect?: (prevState: T, newState: T) => void | Promise<void>,
|
|
20
|
-
* }} Plugin
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* @template T
|
|
24
|
-
* @extends EventTarget
|
|
25
|
-
*/
|
|
26
|
-
export class State<T> extends EventTarget {
|
|
27
|
-
/**
|
|
28
|
-
* @param {T} initialState
|
|
29
|
-
* @param {Array<{ update: (prevState: T, newState: T) => T }>} [plugins=[]]
|
|
30
|
-
*/
|
|
31
|
-
constructor(initialState: T, plugins?: {
|
|
32
|
-
update: (prevState: T, newState: T) => T;
|
|
33
|
-
}[]);
|
|
34
|
-
/**
|
|
35
|
-
* @param {T | ((prevState: T) => T)} state
|
|
36
|
-
* @param {boolean} [broadcast=true]
|
|
37
|
-
*/
|
|
38
|
-
setState(state: T | ((prevState: T) => T), broadcast?: boolean): void;
|
|
39
|
-
/**
|
|
40
|
-
* @returns {T}
|
|
41
|
-
*/
|
|
42
|
-
getState(): T;
|
|
43
|
-
#private;
|
|
44
|
-
}
|
|
45
|
-
export const state: State<{}>;
|
|
46
|
-
export type Plugin<T> = {
|
|
47
|
-
name: string;
|
|
48
|
-
update?: (prevState: T, newState: T) => T;
|
|
49
|
-
effect?: (prevState: T, newState: T) => void | Promise<void>;
|
|
50
|
-
};
|