@kirill.konshin/utils 0.0.14 → 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/.ctirc +2 -1
- package/.turbo/turbo-build.log +18 -13
- package/.turbo/turbo-test.log +78 -25
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/demo/{cache-demo-clean.ts → worker-demo.ts} +1 -1
- package/dist/bootstrap/controls.d.ts +5 -2
- package/dist/bootstrap/controls.d.ts.map +1 -1
- package/dist/bootstrap/controls.js +1 -0
- package/dist/bootstrap/controls.js.map +1 -1
- package/dist/bootstrap/controls.stories.d.ts +9 -0
- package/dist/bootstrap/controls.stories.d.ts.map +1 -0
- package/dist/bootstrap/error.stories.d.ts +8 -0
- package/dist/bootstrap/error.stories.d.ts.map +1 -0
- package/dist/bootstrap/field.stories.d.ts +8 -0
- package/dist/bootstrap/field.stories.d.ts.map +1 -0
- package/dist/bootstrap/globalLoading.stories.d.ts +7 -0
- package/dist/bootstrap/globalLoading.stories.d.ts.map +1 -0
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/loading.stories.d.ts +9 -0
- package/dist/bootstrap/loading.stories.d.ts.map +1 -0
- package/dist/bootstrap/screen.stories.d.ts +14 -0
- package/dist/bootstrap/screen.stories.d.ts.map +1 -0
- package/dist/bootstrap/toaster.stories.d.ts +8 -0
- package/dist/bootstrap/toaster.stories.d.ts.map +1 -0
- package/dist/bootstrap/useModal.stories.d.ts +9 -0
- package/dist/bootstrap/useModal.stories.d.ts.map +1 -0
- package/dist/core/cache.d.ts +7 -8
- package/dist/core/cache.d.ts.map +1 -1
- package/dist/core/cache.js +7 -4
- package/dist/core/cache.js.map +1 -1
- package/dist/core/cache.test.d.ts +2 -0
- package/dist/core/cache.test.d.ts.map +1 -0
- package/dist/core/files.d.ts +3 -0
- package/dist/core/files.d.ts.map +1 -0
- package/dist/core/files.js +27 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +14 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/ls.d.ts +6 -0
- package/dist/core/ls.d.ts.map +1 -0
- package/dist/core/ls.js +12 -0
- package/dist/core/ls.js.map +1 -0
- package/dist/core/worker.d.ts +23 -9
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +93 -90
- package/dist/core/worker.js.map +1 -1
- package/dist/core/worker.test.d.ts +2 -0
- package/dist/core/worker.test.d.ts.map +1 -0
- package/dist/core/workerUtils.d.ts +3 -0
- package/dist/core/workerUtils.d.ts.map +1 -0
- package/dist/core/workerUtils.js +31 -0
- package/dist/core/workerUtils.js.map +1 -0
- package/dist/core/wrpc.d.ts +45 -0
- package/dist/core/wrpc.d.ts.map +1 -0
- package/dist/core/wrpc.fixture.d.ts +29 -0
- package/dist/core/wrpc.fixture.d.ts.map +1 -0
- package/dist/core/wrpc.js +242 -0
- package/dist/core/wrpc.js.map +1 -0
- package/dist/core/wrpc.test.d.ts +1 -0
- package/dist/core/wrpc.test.d.ts.map +1 -0
- package/dist/mui/formControlFieldset.stories.d.ts +7 -0
- package/dist/mui/formControlFieldset.stories.d.ts.map +1 -0
- package/dist/react/form/form.d.ts +7 -6
- package/dist/react/form/form.d.ts.map +1 -1
- package/dist/react/form/form.js +14 -20
- package/dist/react/form/form.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/keyboard.d.ts +11 -0
- package/dist/react/keyboard.d.ts.map +1 -0
- package/dist/react/keyboard.js +38 -0
- package/dist/react/keyboard.js.map +1 -0
- package/package.json +5 -2
- package/src/bootstrap/controls.stories.tsx +2 -2
- package/src/bootstrap/controls.tsx +3 -3
- package/src/bootstrap/error.stories.tsx +3 -3
- package/src/bootstrap/field.stories.tsx +2 -2
- package/src/bootstrap/globalLoading.stories.tsx +2 -2
- package/src/bootstrap/loading.stories.tsx +2 -2
- package/src/bootstrap/screen.stories.tsx +2 -2
- package/src/bootstrap/toaster.stories.tsx +2 -2
- package/src/bootstrap/useModal.stories.tsx +3 -3
- package/src/core/README.md +91 -0
- package/src/core/cache.ts +12 -12
- package/src/core/files.ts +24 -0
- package/src/core/index.ts +4 -0
- package/src/core/ls.ts +11 -0
- package/src/core/worker.test.ts +197 -0
- package/src/core/worker.ts +106 -93
- package/src/core/workerUtils.ts +32 -0
- package/src/core/wrpc.fixture.ts +72 -0
- package/src/core/wrpc.test.ts +225 -0
- package/src/core/wrpc.ts +359 -0
- package/src/mui/formControlFieldset.stories.tsx +1 -1
- package/src/react/form/form.tsx +53 -33
- package/src/react/index.ts +1 -0
- package/src/react/keyboard.tsx +49 -0
- package/src/tailwind/README.md +8 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +10 -3
- package/vite.exports.ts +3 -3
- package/demo/cache-demo.ts +0 -343
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useState, useMemo, useContext, useEffect } from "react";
|
|
4
|
+
const isCtrlOrMeta = (e) => e.metaKey || e.ctrlKey;
|
|
5
|
+
const EVENT = "keydown";
|
|
6
|
+
const HotkeysContext = createContext(null);
|
|
7
|
+
const HotkeysProvider = ({ children }) => {
|
|
8
|
+
const [enabled, setEnabled] = useState(true);
|
|
9
|
+
const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);
|
|
10
|
+
return /* @__PURE__ */ jsx(HotkeysContext.Provider, { value: control, children });
|
|
11
|
+
};
|
|
12
|
+
const useHotkeys = (hotkeys) => {
|
|
13
|
+
const { enabled } = useContext(HotkeysContext);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (typeof document === "undefined" || !enabled) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const listeners = (e) => {
|
|
19
|
+
if (!isCtrlOrMeta(e)) return;
|
|
20
|
+
for (const [code, callback] of Object.entries(hotkeys)) {
|
|
21
|
+
if (e.code === code) {
|
|
22
|
+
callback(e);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
window.addEventListener(EVENT, listeners, true);
|
|
28
|
+
return () => {
|
|
29
|
+
window.removeEventListener(EVENT, listeners);
|
|
30
|
+
};
|
|
31
|
+
}, [hotkeys, enabled]);
|
|
32
|
+
};
|
|
33
|
+
export {
|
|
34
|
+
HotkeysContext,
|
|
35
|
+
HotkeysProvider,
|
|
36
|
+
useHotkeys
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=keyboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.js","sources":["../../src/react/keyboard.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, createContext, useMemo, useState, Dispatch, SetStateAction, useContext } from 'react';\n\nconst isCtrlOrMeta = (e: KeyboardEvent) => e.metaKey || e.ctrlKey;\n\nconst EVENT = 'keydown';\n\nexport type Hotkeys = Record<KeyboardEvent['code'], (e: KeyboardEvent) => void>;\n\nexport const HotkeysContext = createContext<{\n enabled: boolean;\n setEnabled: Dispatch<SetStateAction<boolean>>;\n}>(null as never);\n\nexport const HotkeysProvider = ({ children }) => {\n const [enabled, setEnabled] = useState(true);\n\n const control = useMemo(() => ({ enabled, setEnabled }), [enabled, setEnabled]);\n\n return <HotkeysContext.Provider value={control}>{children}</HotkeysContext.Provider>;\n};\n\nexport const useHotkeys = (hotkeys: Hotkeys) => {\n const { enabled } = useContext(HotkeysContext);\n\n useEffect(() => {\n if (typeof document === 'undefined' || !enabled) {\n return;\n }\n\n const listeners = (e: KeyboardEvent) => {\n if (!isCtrlOrMeta(e)) return;\n\n for (const [code, callback] of Object.entries(hotkeys)) {\n if (e.code === code) {\n callback(e);\n return;\n }\n }\n };\n\n window.addEventListener(EVENT, listeners, true);\n\n return () => {\n window.removeEventListener(EVENT, listeners);\n };\n }, [hotkeys, enabled]);\n};\n"],"names":[],"mappings":";;;AAIA,MAAM,eAAe,CAAC,MAAqB,EAAE,WAAW,EAAE;AAE1D,MAAM,QAAQ;AAID,MAAA,iBAAiB,cAG3B,IAAa;AAET,MAAM,kBAAkB,CAAC,EAAE,eAAe;AAC7C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAErC,QAAA,UAAU,QAAQ,OAAO,EAAE,SAAS,eAAe,CAAC,SAAS,UAAU,CAAC;AAE9E,6BAAQ,eAAe,UAAf,EAAwB,OAAO,SAAU,UAAS;AAC9D;AAEa,MAAA,aAAa,CAAC,YAAqB;AAC5C,QAAM,EAAE,QAAA,IAAY,WAAW,cAAc;AAE7C,YAAU,MAAM;AACZ,QAAI,OAAO,aAAa,eAAe,CAAC,SAAS;AAC7C;AAAA,IAAA;AAGE,UAAA,YAAY,CAAC,MAAqB;AAChC,UAAA,CAAC,aAAa,CAAC,EAAG;AAEtB,iBAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,OAAO,GAAG;AAChD,YAAA,EAAE,SAAS,MAAM;AACjB,mBAAS,CAAC;AACV;AAAA,QAAA;AAAA,MACJ;AAAA,IAER;AAEO,WAAA,iBAAiB,OAAO,WAAW,IAAI;AAE9C,WAAO,MAAM;AACF,aAAA,oBAAoB,OAAO,SAAS;AAAA,IAC/C;AAAA,EAAA,GACD,CAAC,SAAS,OAAO,CAAC;AACzB;"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirill.konshin/utils",
|
|
3
3
|
"description": "Utilities",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.15",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"----- BUILD -----": "",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"expo-file-system": "^18.0.7",
|
|
25
25
|
"expo-sharing": "^13.0.1",
|
|
26
26
|
"expo-updates": "^0.26.13",
|
|
27
|
+
"file-saver": "^2.0.5",
|
|
27
28
|
"many-keys-map": "^2.0.1",
|
|
28
29
|
"psd": "^3.4.0",
|
|
29
30
|
"react-hook-form": "^7.54.2",
|
|
@@ -43,8 +44,10 @@
|
|
|
43
44
|
"@storybook/react": "^8.5.2",
|
|
44
45
|
"@storybook/react-vite": "^8.5.2",
|
|
45
46
|
"@storybook/test": "^8.5.2",
|
|
47
|
+
"@types/file-saver": "^2",
|
|
46
48
|
"@types/psd": "^3.4.3",
|
|
47
49
|
"@vitest/coverage-v8": "^3.0.4",
|
|
50
|
+
"@vitest/web-worker": "^3.0.5",
|
|
48
51
|
"bootstrap": "^5.3.3",
|
|
49
52
|
"bootstrap-icons": "^1.11.3",
|
|
50
53
|
"create-ts-index": "^1.14.0",
|
|
@@ -179,4 +182,4 @@
|
|
|
179
182
|
"main": "./dist/core/index.js",
|
|
180
183
|
"module": "./dist/core/index.js",
|
|
181
184
|
"types": "./dist/core/index.d.ts"
|
|
182
|
-
}
|
|
185
|
+
}
|
|
@@ -7,7 +7,7 @@ import { Select, Control, Range, Checkbox } from './controls';
|
|
|
7
7
|
const defaultOptions = { num: 1, bool: false, select: 'bar' };
|
|
8
8
|
|
|
9
9
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
10
|
-
const meta = {
|
|
10
|
+
const meta: Meta<typeof Control> = {
|
|
11
11
|
title: 'Bootstrap / Controls',
|
|
12
12
|
parameters: {
|
|
13
13
|
layout: 'centered',
|
|
@@ -32,7 +32,7 @@ const meta = {
|
|
|
32
32
|
|
|
33
33
|
return <div style={{ width: '300px' }}>{cloneElement(children as any, { ...safeArgs, ...controlProps })}</div>;
|
|
34
34
|
},
|
|
35
|
-
}
|
|
35
|
+
};
|
|
36
36
|
|
|
37
37
|
export default meta;
|
|
38
38
|
|
|
@@ -4,7 +4,7 @@ const lcFirst = (str) => str[0].toLowerCase() + str.substring(1, str.length);
|
|
|
4
4
|
|
|
5
5
|
const toProperty = (str) => lcFirst(str).split(' ').join('');
|
|
6
6
|
|
|
7
|
-
interface ControlProps {
|
|
7
|
+
export interface ControlProps {
|
|
8
8
|
value?: any;
|
|
9
9
|
setValue?: (v: any) => void;
|
|
10
10
|
defaultValue?: any;
|
|
@@ -22,9 +22,9 @@ interface ControlProps {
|
|
|
22
22
|
hideValue?: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
type LabelProps = Pick<ControlProps, 'name' | 'value' | 'defaultValue'> & { reset: () => void };
|
|
25
|
+
export type LabelProps = Pick<ControlProps, 'name' | 'value' | 'defaultValue'> & { reset: () => void };
|
|
26
26
|
|
|
27
|
-
const Label = memo<LabelProps>(function Label({ name, reset, value, defaultValue }) {
|
|
27
|
+
export const Label = memo<LabelProps>(function Label({ name, reset, value, defaultValue }) {
|
|
28
28
|
return (
|
|
29
29
|
<label className="flex-grow-1 mb-0 d-flex align-items-center justify-content-start gap-2">
|
|
30
30
|
{name}
|
|
@@ -4,7 +4,7 @@ import { fn } from '@storybook/test';
|
|
|
4
4
|
import { ErrorAlert } from './error';
|
|
5
5
|
|
|
6
6
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
7
|
-
const meta = {
|
|
7
|
+
const meta: Meta<typeof ErrorAlert> = {
|
|
8
8
|
title: 'Bootstrap / ErrorAlert',
|
|
9
9
|
component: ErrorAlert,
|
|
10
10
|
parameters: {
|
|
@@ -16,7 +16,7 @@ const meta = {
|
|
|
16
16
|
children: { control: 'text' },
|
|
17
17
|
},
|
|
18
18
|
args: { onRetry: fn() },
|
|
19
|
-
}
|
|
19
|
+
};
|
|
20
20
|
|
|
21
21
|
export default meta;
|
|
22
22
|
type Story = StoryObj<typeof meta>;
|
|
@@ -29,6 +29,6 @@ export const String: Story = {
|
|
|
29
29
|
|
|
30
30
|
export const ErrorObject: Story = {
|
|
31
31
|
args: {
|
|
32
|
-
children: new Error('An error occurred'),
|
|
32
|
+
children: new Error('An error occurred') as any,
|
|
33
33
|
},
|
|
34
34
|
};
|
|
@@ -6,7 +6,7 @@ import { FieldGroup } from './field';
|
|
|
6
6
|
import { FormControl } from 'react-bootstrap';
|
|
7
7
|
|
|
8
8
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
9
|
-
const meta = {
|
|
9
|
+
const meta: Meta<typeof FieldGroup> = {
|
|
10
10
|
title: 'Bootstrap / FieldGroup',
|
|
11
11
|
component: FieldGroup,
|
|
12
12
|
parameters: {
|
|
@@ -19,7 +19,7 @@ const meta = {
|
|
|
19
19
|
horizontal: { control: 'check' },
|
|
20
20
|
},
|
|
21
21
|
args: { onClick: fn() },
|
|
22
|
-
}
|
|
22
|
+
};
|
|
23
23
|
|
|
24
24
|
export default meta;
|
|
25
25
|
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
|
|
3
3
|
import { GlobalLoading } from './globalLoading';
|
|
4
4
|
|
|
5
|
-
const meta = {
|
|
5
|
+
const meta: Meta<typeof GlobalLoading> = {
|
|
6
6
|
title: 'Bootstrap / GlobalLoading',
|
|
7
7
|
component: GlobalLoading,
|
|
8
8
|
parameters: {
|
|
@@ -12,7 +12,7 @@ const meta = {
|
|
|
12
12
|
argTypes: {
|
|
13
13
|
loading: { control: 'check' },
|
|
14
14
|
},
|
|
15
|
-
}
|
|
15
|
+
};
|
|
16
16
|
|
|
17
17
|
export default meta;
|
|
18
18
|
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
|
|
3
3
|
import { Loading } from './loading';
|
|
4
4
|
|
|
5
|
-
const meta = {
|
|
5
|
+
const meta: Meta<typeof Loading> = {
|
|
6
6
|
title: 'Bootstrap / Loading',
|
|
7
7
|
component: Loading,
|
|
8
8
|
parameters: {
|
|
@@ -14,7 +14,7 @@ const meta = {
|
|
|
14
14
|
show: { control: 'check' },
|
|
15
15
|
size: { control: 'select', options: ['sm', 'md'] },
|
|
16
16
|
},
|
|
17
|
-
}
|
|
17
|
+
};
|
|
18
18
|
|
|
19
19
|
export default meta;
|
|
20
20
|
|
|
@@ -45,7 +45,7 @@ const lorem = (
|
|
|
45
45
|
</>
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
const meta = {
|
|
48
|
+
const meta: Meta<typeof Screen> = {
|
|
49
49
|
title: 'Bootstrap / Screen',
|
|
50
50
|
component: Screen,
|
|
51
51
|
parameters: {
|
|
@@ -75,7 +75,7 @@ const meta = {
|
|
|
75
75
|
),
|
|
76
76
|
backCb: fn(),
|
|
77
77
|
},
|
|
78
|
-
}
|
|
78
|
+
};
|
|
79
79
|
|
|
80
80
|
export default meta;
|
|
81
81
|
|
|
@@ -4,7 +4,7 @@ import { fn } from '@storybook/test';
|
|
|
4
4
|
import { Toaster } from './toaster';
|
|
5
5
|
|
|
6
6
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
7
|
-
const meta = {
|
|
7
|
+
const meta: Meta<typeof Toaster> = {
|
|
8
8
|
title: 'Bootstrap / Toaster',
|
|
9
9
|
component: Toaster,
|
|
10
10
|
parameters: {
|
|
@@ -13,7 +13,7 @@ const meta = {
|
|
|
13
13
|
tags: ['autodocs'],
|
|
14
14
|
argTypes: {},
|
|
15
15
|
args: { onClick: fn() },
|
|
16
|
-
}
|
|
16
|
+
};
|
|
17
17
|
|
|
18
18
|
export default meta;
|
|
19
19
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { FC } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import { useModal, UseModalProps } from './useModal';
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from 'react-bootstrap';
|
|
5
5
|
|
|
6
6
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
7
|
-
const meta = {
|
|
7
|
+
const meta: Meta<FC<UseModalProps>> = {
|
|
8
8
|
title: 'Bootstrap / useModal',
|
|
9
9
|
parameters: {
|
|
10
10
|
layout: 'centered',
|
|
@@ -34,7 +34,7 @@ const meta = {
|
|
|
34
34
|
);
|
|
35
35
|
},
|
|
36
36
|
// args: { onSubmit: (e) => e.preventDefault() },
|
|
37
|
-
}
|
|
37
|
+
};
|
|
38
38
|
|
|
39
39
|
export default meta;
|
|
40
40
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# WRPC
|
|
2
|
+
|
|
3
|
+
TODO
|
|
4
|
+
|
|
5
|
+
- [ ] Add to readme https://stackoverflow.com/questions/77727664/how-to-get-returned-value-from-async-generator-when-using-for-await
|
|
6
|
+
- [ ] Value passed to next are ignored https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator/next#sending_values_to_the_generator
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
sequenceDiagram
|
|
10
|
+
box Main
|
|
11
|
+
participant Code
|
|
12
|
+
participant Caller
|
|
13
|
+
end
|
|
14
|
+
box Worker
|
|
15
|
+
participant Worker
|
|
16
|
+
participant Resolver
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
%% autonumber
|
|
20
|
+
|
|
21
|
+
Code->>Caller: caller.generator(payload)
|
|
22
|
+
|
|
23
|
+
Caller-->>Code: iterator & promise
|
|
24
|
+
|
|
25
|
+
Code->>Code: Start main CONSUMER loop
|
|
26
|
+
|
|
27
|
+
Code->>Caller: iterator[Symbol]()
|
|
28
|
+
|
|
29
|
+
Caller->>Caller: Start replica GENERATOR loop
|
|
30
|
+
|
|
31
|
+
Code->>Caller: iterator.next()
|
|
32
|
+
|
|
33
|
+
Caller->>Worker: postMessage(payload)
|
|
34
|
+
|
|
35
|
+
Worker->>Resolver: resolver(payload)
|
|
36
|
+
|
|
37
|
+
Resolver-->>Worker: iterator
|
|
38
|
+
|
|
39
|
+
Worker->>Worker: Start replica CONSUMER loop
|
|
40
|
+
|
|
41
|
+
Worker->>Resolver: iterator[Symbol]()
|
|
42
|
+
|
|
43
|
+
Resolver->>Resolver: Start GENERATOR loop
|
|
44
|
+
|
|
45
|
+
rect rgb(232, 244, 248)
|
|
46
|
+
|
|
47
|
+
loop Synchronized iteration
|
|
48
|
+
Worker->>+Resolver: iterator.next(payload)
|
|
49
|
+
|
|
50
|
+
Resolver-->>-Worker: yield value
|
|
51
|
+
|
|
52
|
+
Worker-->>Caller: postMessage(value, done=false)
|
|
53
|
+
|
|
54
|
+
Note over Worker: Wait for ACK or ABORT
|
|
55
|
+
|
|
56
|
+
Caller-->>Code: yield value
|
|
57
|
+
|
|
58
|
+
Code->>Caller: iterator.next(nextPayload)
|
|
59
|
+
|
|
60
|
+
Caller->>Worker: postMessage(ack, nextPayload)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
break Break or AbortSignal (maybe)
|
|
66
|
+
Code-->>Code: Loop exits
|
|
67
|
+
Code->>Caller: signal.abort()
|
|
68
|
+
|
|
69
|
+
Caller-->>Caller: Loop exits
|
|
70
|
+
|
|
71
|
+
Caller->>Worker: postMessage(abort)
|
|
72
|
+
|
|
73
|
+
Worker-->>Worker: Loop exits via Break
|
|
74
|
+
|
|
75
|
+
Resolver-->>Resolver: Loop exits
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opt Occurs on Error
|
|
79
|
+
Resolver-->>Worker: throw Error
|
|
80
|
+
|
|
81
|
+
Worker-->>Caller: postMessage(error, done=true)
|
|
82
|
+
|
|
83
|
+
Caller-->>Code: throw Error
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Resolver-->>Worker: return value
|
|
87
|
+
|
|
88
|
+
Worker-->>Caller: postMessage(value, done=true)
|
|
89
|
+
|
|
90
|
+
Caller-->>Code: return value
|
|
91
|
+
```
|
package/src/core/cache.ts
CHANGED
|
@@ -64,6 +64,7 @@ export function memo<Key extends any[], Val, SerializedKey extends any[] = Key>(
|
|
|
64
64
|
) {
|
|
65
65
|
const map = new ManyKeysMap<SerializedKey, Val>();
|
|
66
66
|
|
|
67
|
+
//TODO Extend ManyKeysMap
|
|
67
68
|
const clear = (...condition: Key | any[]) => {
|
|
68
69
|
if (!condition?.length) {
|
|
69
70
|
map.forEach((value, serializedKey) => {
|
|
@@ -114,17 +115,12 @@ export function memo<Key extends any[], Val, SerializedKey extends any[] = Key>(
|
|
|
114
115
|
*
|
|
115
116
|
* This makes possible to implement various one-off and subsequent transformations.
|
|
116
117
|
*
|
|
117
|
-
* 1. `write`:
|
|
118
|
-
* 2. `read`:
|
|
119
|
-
* TODO Better names: once, folloing
|
|
120
|
-
* TODO Better names: memoize, recover
|
|
121
|
-
* TODO Better names: writeTransformer, readTransformer
|
|
122
|
-
* TODO Better names: writeModifier, readModifier
|
|
123
|
-
* TODO Better names: write, read
|
|
118
|
+
* 1. `write`: Transform value BEFORE writing to cache, called only once if cache IS NOT present or IS NOT valid
|
|
119
|
+
* 2. `read`: Transform value AFTER it's read from cache, always called if cache IS valid, keep in mind this transform is applied on top of BEFORE
|
|
124
120
|
*
|
|
125
121
|
* Both should return same type or null.
|
|
126
122
|
*
|
|
127
|
-
* If newValue is null, old is returned, and no cache is set.
|
|
123
|
+
* If `newValue` is null, old is returned, and no cache is set.
|
|
128
124
|
*
|
|
129
125
|
* ```ts
|
|
130
126
|
* class InputCache extends TypedCache<string, ImageBitmap> {
|
|
@@ -186,7 +182,7 @@ export function memo<Key extends any[], Val, SerializedKey extends any[] = Key>(
|
|
|
186
182
|
*/
|
|
187
183
|
export abstract class TransformerMap<Key, Val = Key> extends Map<Key, Val> {
|
|
188
184
|
constructor(protected readonly name: string) {
|
|
189
|
-
console.warn('CREATE cache', name);
|
|
185
|
+
// console.warn('CREATE cache', name);
|
|
190
186
|
super();
|
|
191
187
|
}
|
|
192
188
|
|
|
@@ -199,13 +195,17 @@ export abstract class TransformerMap<Key, Val = Key> extends Map<Key, Val> {
|
|
|
199
195
|
}
|
|
200
196
|
|
|
201
197
|
delete(key: Key) {
|
|
202
|
-
const
|
|
203
|
-
|
|
198
|
+
const val = this.get(key);
|
|
199
|
+
const has = typeof val !== 'undefined';
|
|
200
|
+
if (has) this.dispose(val, key);
|
|
204
201
|
super.delete(key);
|
|
205
202
|
return has;
|
|
206
203
|
}
|
|
207
204
|
|
|
208
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Do something with the value and key before removing it from cache
|
|
207
|
+
* For example, close a file or a bitmap
|
|
208
|
+
*/
|
|
209
209
|
protected dispose(value: Val, key: Key) {}
|
|
210
210
|
|
|
211
211
|
clear() {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { saveAs } from 'file-saver';
|
|
2
|
+
|
|
3
|
+
export const downloadFile = async (filename, text, type = 'application/json') => {
|
|
4
|
+
//FIXME https://github.com/eligrey/FileSaver.js/issues/731
|
|
5
|
+
saveAs(new Blob([text], { type }), filename);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const openFile = (): Promise<string> =>
|
|
9
|
+
new Promise((res, rej) => {
|
|
10
|
+
const input = document.createElement('input');
|
|
11
|
+
input.type = 'file';
|
|
12
|
+
input.addEventListener('change', function readFile(e) {
|
|
13
|
+
input.removeEventListener('change', readFile);
|
|
14
|
+
// @ts-expect-error file is always there
|
|
15
|
+
const [file] = e.target.files;
|
|
16
|
+
if (!file) return;
|
|
17
|
+
const reader = new FileReader();
|
|
18
|
+
reader.onload = (e) => res(e.target?.result as string);
|
|
19
|
+
reader.onerror = rej;
|
|
20
|
+
reader.onabort = rej;
|
|
21
|
+
reader.readAsText(file);
|
|
22
|
+
});
|
|
23
|
+
input.click();
|
|
24
|
+
});
|
package/src/core/index.ts
CHANGED
package/src/core/ls.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function getStorage(pref: string) {
|
|
2
|
+
const prefix = (key) => `${pref}-${key}`;
|
|
3
|
+
|
|
4
|
+
const ls = (typeof window !== 'undefined' ? localStorage : null) as Storage;
|
|
5
|
+
|
|
6
|
+
const read = (key: string) => JSON.parse(ls?.getItem(prefix(key)) || 'null');
|
|
7
|
+
const write = (key: string, value: any) => ls?.setItem(prefix(key), JSON.stringify(value));
|
|
8
|
+
const remove = (key: string) => ls?.removeItem(prefix(key));
|
|
9
|
+
|
|
10
|
+
return { read, write, remove };
|
|
11
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { expect, describe, test, vi } from 'vitest';
|
|
2
|
+
import { RespondersBase, WorkerDialog } from './worker';
|
|
3
|
+
import EventEmitter from 'node:events';
|
|
4
|
+
|
|
5
|
+
// Prepare
|
|
6
|
+
|
|
7
|
+
//TODO @vitest/web-worker can ve used, coverage is collected, but this gives more direct access to worker internals
|
|
8
|
+
// class WorkerPeer extends EventEmitter implements WorkerLike {
|
|
9
|
+
class WorkerPeer extends EventEmitter {
|
|
10
|
+
peer: WorkerPeer;
|
|
11
|
+
|
|
12
|
+
setPeer(peer: WorkerPeer) {
|
|
13
|
+
this.peer = peer;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
postMessage(data: any) {
|
|
17
|
+
this.peer?.emit('message', { data });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
addEventListener(event: string, listener: (e: any) => void, options) {
|
|
21
|
+
this[options['once'] ? 'once' : 'on'](event, listener);
|
|
22
|
+
options?.signal?.addEventListener('abort', () => this.off(event, listener));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
removeEventListener(event: string, listener: (e: any) => void) {
|
|
26
|
+
this.off(event, listener);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class Responders extends RespondersBase<Responders> {
|
|
31
|
+
lastContext: any; // for testing purposes
|
|
32
|
+
|
|
33
|
+
test = this.create(
|
|
34
|
+
'test',
|
|
35
|
+
async (
|
|
36
|
+
{ n, abort = false, buf }: { n: number; abort?: boolean; buf?: ArrayBuffer }, // should have all possible combinations for strong typing
|
|
37
|
+
ctx,
|
|
38
|
+
) => {
|
|
39
|
+
this.lastContext = ctx;
|
|
40
|
+
|
|
41
|
+
const res: number[] = [];
|
|
42
|
+
|
|
43
|
+
let aborted = false;
|
|
44
|
+
let progress = 0;
|
|
45
|
+
|
|
46
|
+
ctx.listen(({ abort }) => {
|
|
47
|
+
if (abort) aborted = true;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const i of Array(n).keys()) {
|
|
51
|
+
if (aborted) break;
|
|
52
|
+
progress = (i + 1) / n;
|
|
53
|
+
ctx.send({ progress });
|
|
54
|
+
res.push(i);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { res, progress, aborted };
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
error = this.create(
|
|
62
|
+
'error',
|
|
63
|
+
async (
|
|
64
|
+
_, // should have all possible combinations for strong typing
|
|
65
|
+
ctx,
|
|
66
|
+
) => {
|
|
67
|
+
this.lastContext = ctx;
|
|
68
|
+
throw new Error('Test');
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const createWorker = () => {
|
|
74
|
+
const mainWorker = new WorkerPeer();
|
|
75
|
+
const mainDialog = new WorkerDialog(
|
|
76
|
+
mainWorker as never as Worker,
|
|
77
|
+
{} as WorkerDialog<Responders>['responders'],
|
|
78
|
+
'Main',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const worker = new WorkerPeer();
|
|
82
|
+
const workerDialog = new WorkerDialog(worker as never as Worker, new Responders(), 'Worker');
|
|
83
|
+
|
|
84
|
+
mainWorker.setPeer(worker);
|
|
85
|
+
worker.setPeer(mainWorker);
|
|
86
|
+
|
|
87
|
+
return { mainWorker, mainDialog, worker, workerDialog };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Tests
|
|
91
|
+
|
|
92
|
+
describe('Worker', async () => {
|
|
93
|
+
test('simple', async () => {
|
|
94
|
+
const { mainDialog, workerDialog } = createWorker();
|
|
95
|
+
|
|
96
|
+
const l = vi.fn();
|
|
97
|
+
let mctx;
|
|
98
|
+
|
|
99
|
+
const result = await mainDialog.withMessage('test').fetch({ n: 3, buf: new ArrayBuffer() }, (ctx) => {
|
|
100
|
+
mctx = ctx;
|
|
101
|
+
ctx.listen(l);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(l).toBeCalledTimes(4); // 3x progress, last
|
|
105
|
+
expect(result).toStrictEqual({ res: [0, 1, 2], progress: 1, aborted: false });
|
|
106
|
+
|
|
107
|
+
expect(mainDialog.contexts.size).toBe(0);
|
|
108
|
+
expect(mctx.debugInfo.closed).toBe(true);
|
|
109
|
+
|
|
110
|
+
expect(workerDialog.contexts.size).toBe(2);
|
|
111
|
+
expect(workerDialog.contexts.values().next().value.debugInfo).toStrictEqual({
|
|
112
|
+
id: '*',
|
|
113
|
+
parentId: undefined,
|
|
114
|
+
message: 'test',
|
|
115
|
+
closed: false,
|
|
116
|
+
type: 'response',
|
|
117
|
+
listeners: 1,
|
|
118
|
+
});
|
|
119
|
+
expect(workerDialog.responders.lastContext.debugInfo).toStrictEqual({
|
|
120
|
+
id: '1',
|
|
121
|
+
parentId: '*',
|
|
122
|
+
message: 'test',
|
|
123
|
+
closed: true,
|
|
124
|
+
type: 'response',
|
|
125
|
+
listeners: 0,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('abort', async () => {
|
|
130
|
+
const { mainDialog } = createWorker();
|
|
131
|
+
|
|
132
|
+
const result = await mainDialog.withMessage('test').fetch({ n: 3 }, (ctx) => {
|
|
133
|
+
ctx.listen(() => {
|
|
134
|
+
ctx.send({ abort: true });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result).toStrictEqual({ res: [0], progress: 0.3333333333333333, aborted: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('faulty listener', async () => {
|
|
142
|
+
const { mainDialog } = createWorker();
|
|
143
|
+
|
|
144
|
+
await mainDialog.withMessage('test').fetch({ n: 3 }, (ctx) => {
|
|
145
|
+
ctx.listen(() => {
|
|
146
|
+
throw new Error('Listener');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect.assertions(0); // nothing happened
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('error', async () => {
|
|
154
|
+
const { mainDialog } = createWorker();
|
|
155
|
+
await expect(async () => mainDialog.withMessage('error').fetch()).rejects.toThrowError('Test');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('close', async () => {
|
|
159
|
+
const { mainDialog, workerDialog } = createWorker();
|
|
160
|
+
|
|
161
|
+
mainDialog.close();
|
|
162
|
+
workerDialog.close();
|
|
163
|
+
|
|
164
|
+
expect(mainDialog.contexts.size).toBe(0);
|
|
165
|
+
expect(workerDialog.contexts.size).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('empty message', async () => {
|
|
169
|
+
expect(() => createWorker().mainDialog.withMessage(undefined as any)).toThrowError(
|
|
170
|
+
'New context must have message',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('empty ID', async () => {
|
|
175
|
+
const { mainDialog } = createWorker();
|
|
176
|
+
const first = mainDialog.withMessage('test');
|
|
177
|
+
expect(() => first.withMessage('error', '')).toThrowError('ID is required');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('nesting', async () => {
|
|
181
|
+
const { mainDialog } = createWorker();
|
|
182
|
+
const first = mainDialog.withMessage('test');
|
|
183
|
+
const second = first.withMessage('error', 'x');
|
|
184
|
+
const third = second.withMessage('error');
|
|
185
|
+
expect(mainDialog.contexts.size).toBe(3);
|
|
186
|
+
expect(first.info).toBe('Main:*/1:test[open,request]');
|
|
187
|
+
first.close();
|
|
188
|
+
expect(mainDialog.contexts.size).toBe(0);
|
|
189
|
+
expect(first.debugInfo.closed).toBe(true);
|
|
190
|
+
expect(second.debugInfo.closed).toBe(true);
|
|
191
|
+
expect(third.debugInfo.closed).toBe(true);
|
|
192
|
+
expect(first.info).toBe('Main:*/1:test[closed,request]'); // id bumped from 0
|
|
193
|
+
expect(second.info).toBe('Main:1/x:error[closed,request]'); // new id
|
|
194
|
+
expect(third.info).toBe('Main:x/x:error[closed,request]'); // id kept
|
|
195
|
+
await expect(() => first.fetch({ n: 1 })).rejects.toThrowError(/Context 1 is closed/);
|
|
196
|
+
});
|
|
197
|
+
});
|