@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.
Files changed (107) hide show
  1. package/.ctirc +2 -1
  2. package/.turbo/turbo-build.log +18 -13
  3. package/.turbo/turbo-test.log +78 -25
  4. package/CHANGELOG.md +6 -0
  5. package/README.md +1 -1
  6. package/demo/{cache-demo-clean.ts → worker-demo.ts} +1 -1
  7. package/dist/bootstrap/controls.d.ts +5 -2
  8. package/dist/bootstrap/controls.d.ts.map +1 -1
  9. package/dist/bootstrap/controls.js +1 -0
  10. package/dist/bootstrap/controls.js.map +1 -1
  11. package/dist/bootstrap/controls.stories.d.ts +9 -0
  12. package/dist/bootstrap/controls.stories.d.ts.map +1 -0
  13. package/dist/bootstrap/error.stories.d.ts +8 -0
  14. package/dist/bootstrap/error.stories.d.ts.map +1 -0
  15. package/dist/bootstrap/field.stories.d.ts +8 -0
  16. package/dist/bootstrap/field.stories.d.ts.map +1 -0
  17. package/dist/bootstrap/globalLoading.stories.d.ts +7 -0
  18. package/dist/bootstrap/globalLoading.stories.d.ts.map +1 -0
  19. package/dist/bootstrap/index.js +2 -1
  20. package/dist/bootstrap/loading.stories.d.ts +9 -0
  21. package/dist/bootstrap/loading.stories.d.ts.map +1 -0
  22. package/dist/bootstrap/screen.stories.d.ts +14 -0
  23. package/dist/bootstrap/screen.stories.d.ts.map +1 -0
  24. package/dist/bootstrap/toaster.stories.d.ts +8 -0
  25. package/dist/bootstrap/toaster.stories.d.ts.map +1 -0
  26. package/dist/bootstrap/useModal.stories.d.ts +9 -0
  27. package/dist/bootstrap/useModal.stories.d.ts.map +1 -0
  28. package/dist/core/cache.d.ts +7 -8
  29. package/dist/core/cache.d.ts.map +1 -1
  30. package/dist/core/cache.js +7 -4
  31. package/dist/core/cache.js.map +1 -1
  32. package/dist/core/cache.test.d.ts +2 -0
  33. package/dist/core/cache.test.d.ts.map +1 -0
  34. package/dist/core/files.d.ts +3 -0
  35. package/dist/core/files.d.ts.map +1 -0
  36. package/dist/core/files.js +27 -0
  37. package/dist/core/files.js.map +1 -0
  38. package/dist/core/index.d.ts +4 -0
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +14 -2
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/ls.d.ts +6 -0
  43. package/dist/core/ls.d.ts.map +1 -0
  44. package/dist/core/ls.js +12 -0
  45. package/dist/core/ls.js.map +1 -0
  46. package/dist/core/worker.d.ts +23 -9
  47. package/dist/core/worker.d.ts.map +1 -1
  48. package/dist/core/worker.js +93 -90
  49. package/dist/core/worker.js.map +1 -1
  50. package/dist/core/worker.test.d.ts +2 -0
  51. package/dist/core/worker.test.d.ts.map +1 -0
  52. package/dist/core/workerUtils.d.ts +3 -0
  53. package/dist/core/workerUtils.d.ts.map +1 -0
  54. package/dist/core/workerUtils.js +31 -0
  55. package/dist/core/workerUtils.js.map +1 -0
  56. package/dist/core/wrpc.d.ts +45 -0
  57. package/dist/core/wrpc.d.ts.map +1 -0
  58. package/dist/core/wrpc.fixture.d.ts +29 -0
  59. package/dist/core/wrpc.fixture.d.ts.map +1 -0
  60. package/dist/core/wrpc.js +242 -0
  61. package/dist/core/wrpc.js.map +1 -0
  62. package/dist/core/wrpc.test.d.ts +1 -0
  63. package/dist/core/wrpc.test.d.ts.map +1 -0
  64. package/dist/mui/formControlFieldset.stories.d.ts +7 -0
  65. package/dist/mui/formControlFieldset.stories.d.ts.map +1 -0
  66. package/dist/react/form/form.d.ts +7 -6
  67. package/dist/react/form/form.d.ts.map +1 -1
  68. package/dist/react/form/form.js +14 -20
  69. package/dist/react/form/form.js.map +1 -1
  70. package/dist/react/index.d.ts +1 -0
  71. package/dist/react/index.d.ts.map +1 -1
  72. package/dist/react/index.js +8 -2
  73. package/dist/react/index.js.map +1 -1
  74. package/dist/react/keyboard.d.ts +11 -0
  75. package/dist/react/keyboard.d.ts.map +1 -0
  76. package/dist/react/keyboard.js +38 -0
  77. package/dist/react/keyboard.js.map +1 -0
  78. package/package.json +5 -2
  79. package/src/bootstrap/controls.stories.tsx +2 -2
  80. package/src/bootstrap/controls.tsx +3 -3
  81. package/src/bootstrap/error.stories.tsx +3 -3
  82. package/src/bootstrap/field.stories.tsx +2 -2
  83. package/src/bootstrap/globalLoading.stories.tsx +2 -2
  84. package/src/bootstrap/loading.stories.tsx +2 -2
  85. package/src/bootstrap/screen.stories.tsx +2 -2
  86. package/src/bootstrap/toaster.stories.tsx +2 -2
  87. package/src/bootstrap/useModal.stories.tsx +3 -3
  88. package/src/core/README.md +91 -0
  89. package/src/core/cache.ts +12 -12
  90. package/src/core/files.ts +24 -0
  91. package/src/core/index.ts +4 -0
  92. package/src/core/ls.ts +11 -0
  93. package/src/core/worker.test.ts +197 -0
  94. package/src/core/worker.ts +106 -93
  95. package/src/core/workerUtils.ts +32 -0
  96. package/src/core/wrpc.fixture.ts +72 -0
  97. package/src/core/wrpc.test.ts +225 -0
  98. package/src/core/wrpc.ts +359 -0
  99. package/src/mui/formControlFieldset.stories.tsx +1 -1
  100. package/src/react/form/form.tsx +53 -33
  101. package/src/react/index.ts +1 -0
  102. package/src/react/keyboard.tsx +49 -0
  103. package/src/tailwind/README.md +8 -0
  104. package/tsconfig.json +2 -1
  105. package/vite.config.ts +10 -3
  106. package/vite.exports.ts +3 -3
  107. 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.14",
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
- } satisfies Meta<typeof Control>;
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
- } satisfies Meta<typeof ErrorAlert>;
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
- } satisfies Meta<typeof FieldGroup>;
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
- } satisfies Meta<typeof GlobalLoading>;
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
- } satisfies Meta<typeof Loading>;
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
- } satisfies Meta<typeof Screen>;
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
- } satisfies Meta<typeof Toaster>;
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, ButtonGroup } from 'react-bootstrap';
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
- } satisfies Meta<FC<UseModalProps>>;
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`: Before writing to cache, called only once if cache is present and valid
118
- * 2. `read`: After it's read from cache, always called if cache is valid
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 has = this.has(key);
203
- if (has) this.dispose(this.get(key) as Val, key);
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
- // do something with the value and key before removing it
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
@@ -1,5 +1,9 @@
1
1
  export * from './cache';
2
2
  export * from './errors';
3
+ export * from './files';
4
+ export * from './ls';
3
5
  export * from './measure';
4
6
  export * from './mutex';
5
7
  export * from './worker';
8
+ export * from './workerUtils';
9
+ export * from './wrpc';
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
+ });