tinywidgets 1.0.16 → 1.1.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/bun.lockb +0 -0
- package/package.json +12 -12
- package/src/components/TasksProvider/index.tsx +287 -0
- package/src/index.ts +4 -0
package/bun.lockb
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinywidgets",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"author": "jamesgpearce",
|
|
5
5
|
"repository": "github:tinyplex/tinywidgets",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,26 +21,26 @@
|
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/react": "^18.3.11",
|
|
23
23
|
"@types/react-dom": "^18.3.0",
|
|
24
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
25
|
-
"@typescript-eslint/parser": "^8.
|
|
26
|
-
"cspell": "^8.
|
|
24
|
+
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
|
25
|
+
"@typescript-eslint/parser": "^8.17.0",
|
|
26
|
+
"cspell": "^8.16.1",
|
|
27
27
|
"eslint": "^8.57.0",
|
|
28
28
|
"eslint-config-prettier": "^9.1.0",
|
|
29
|
-
"eslint-plugin-react": "^7.37.
|
|
30
|
-
"eslint-plugin-react-hooks": "^
|
|
31
|
-
"eslint-plugin-react-refresh": "^0.4.
|
|
32
|
-
"prettier": "^3.
|
|
33
|
-
"typescript": "^5.
|
|
29
|
+
"eslint-plugin-react": "^7.37.2",
|
|
30
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
31
|
+
"eslint-plugin-react-refresh": "^0.4.16",
|
|
32
|
+
"prettier": "^3.4.2",
|
|
33
|
+
"typescript": "^5.7.2"
|
|
34
34
|
},
|
|
35
35
|
"exports": {
|
|
36
36
|
".": "./src/index.ts",
|
|
37
37
|
"./css": "./src/index.css.ts"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@vanilla-extract/css": "^1.16.
|
|
41
|
-
"lucide-react": "^0.
|
|
40
|
+
"@vanilla-extract/css": "^1.16.1",
|
|
41
|
+
"lucide-react": "^0.468.0",
|
|
42
42
|
"react": "^18.3.1",
|
|
43
43
|
"react-dom": "^18.3.1",
|
|
44
|
-
"tinybase": "^5.
|
|
44
|
+
"tinybase": "^5.4.2"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as UiReact from 'tinybase/ui-react/with-schemas';
|
|
2
|
+
import {
|
|
3
|
+
Id,
|
|
4
|
+
Store as StoreWithSchemas,
|
|
5
|
+
createStore,
|
|
6
|
+
} from 'tinybase/with-schemas';
|
|
7
|
+
import React, {
|
|
8
|
+
ReactNode,
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
11
|
+
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useRef,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import {
|
|
16
|
+
Provider as UiReactProvider,
|
|
17
|
+
useStore,
|
|
18
|
+
useStores,
|
|
19
|
+
} from 'tinybase/ui-react';
|
|
20
|
+
import {Persister} from 'tinybase/persisters/with-schemas';
|
|
21
|
+
import {Store} from 'tinybase';
|
|
22
|
+
import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';
|
|
23
|
+
|
|
24
|
+
export type Task =
|
|
25
|
+
| ((
|
|
26
|
+
arg: string,
|
|
27
|
+
store: Store | undefined,
|
|
28
|
+
scheduleTask: ScheduleTask,
|
|
29
|
+
) => void)
|
|
30
|
+
| ((
|
|
31
|
+
arg: string,
|
|
32
|
+
store: StoreWithSchemas<any> | undefined,
|
|
33
|
+
scheduleTask: ScheduleTask,
|
|
34
|
+
) => void)
|
|
35
|
+
| ((
|
|
36
|
+
arg: string,
|
|
37
|
+
store: Store | undefined,
|
|
38
|
+
scheduleTask: ScheduleTask,
|
|
39
|
+
) => Promise<void>)
|
|
40
|
+
| ((
|
|
41
|
+
arg: string,
|
|
42
|
+
store: StoreWithSchemas<any> | undefined,
|
|
43
|
+
scheduleTask: ScheduleTask,
|
|
44
|
+
) => Promise<void>);
|
|
45
|
+
export type Tasks = {
|
|
46
|
+
[taskId: string]: Task;
|
|
47
|
+
};
|
|
48
|
+
export type ScheduleTask = (taskId: Id, arg?: string, storeId?: string) => void;
|
|
49
|
+
|
|
50
|
+
const STORE_ID = 'tinywidgets/Tasks';
|
|
51
|
+
const TABLE_ID = 'tasks';
|
|
52
|
+
|
|
53
|
+
const TABLES_SCHEMA = {
|
|
54
|
+
tasks: {
|
|
55
|
+
taskId: {type: 'string', default: ''},
|
|
56
|
+
arg: {type: 'string'},
|
|
57
|
+
storeId: {type: 'string'},
|
|
58
|
+
running: {type: 'boolean'},
|
|
59
|
+
expires: {type: 'number', default: 0},
|
|
60
|
+
},
|
|
61
|
+
} as const;
|
|
62
|
+
const VALUES_SCHEMA = {} as const;
|
|
63
|
+
type Schemas = [typeof TABLES_SCHEMA, typeof VALUES_SCHEMA];
|
|
64
|
+
const {useCreateStore, useCreatePersister} =
|
|
65
|
+
UiReact as UiReact.WithSchemas<Schemas>;
|
|
66
|
+
|
|
67
|
+
const Context = createContext<ScheduleTask>(() => {});
|
|
68
|
+
|
|
69
|
+
const TaskRunner = ({
|
|
70
|
+
tasks,
|
|
71
|
+
tasksStore,
|
|
72
|
+
interval,
|
|
73
|
+
}: {
|
|
74
|
+
readonly tasks: Tasks;
|
|
75
|
+
readonly tasksStore: StoreWithSchemas<Schemas>;
|
|
76
|
+
readonly interval: number;
|
|
77
|
+
}) => {
|
|
78
|
+
const store = useStore();
|
|
79
|
+
const stores = useStores();
|
|
80
|
+
const scheduleTask = useContext(Context);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const nextJob = async () => {
|
|
83
|
+
const jobId = tasksStore.getRowIds(TABLE_ID)[0];
|
|
84
|
+
if (jobId != null) {
|
|
85
|
+
const {taskId, arg, storeId, running, expires} = tasksStore.getRow(
|
|
86
|
+
TABLE_ID,
|
|
87
|
+
jobId,
|
|
88
|
+
);
|
|
89
|
+
if (expires > Date.now()) {
|
|
90
|
+
const task = tasks[taskId];
|
|
91
|
+
if (task && !running) {
|
|
92
|
+
tasksStore.setCell(TABLE_ID, jobId, 'running', true);
|
|
93
|
+
try {
|
|
94
|
+
await task(
|
|
95
|
+
arg ?? '',
|
|
96
|
+
storeId == null ? store : (stores[storeId] as any),
|
|
97
|
+
scheduleTask,
|
|
98
|
+
);
|
|
99
|
+
} catch {}
|
|
100
|
+
tasksStore.delRow(TABLE_ID, jobId);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
tasksStore.delRow(TABLE_ID, jobId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const intervalHandle = setInterval(nextJob, interval * 1000);
|
|
108
|
+
return () => clearInterval(intervalHandle);
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
}, [tasksStore, JSON.stringify(Object.keys(stores))]);
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The useScheduleTask hook returns a function that can be used to schedule a
|
|
117
|
+
* new task.
|
|
118
|
+
*
|
|
119
|
+
* The returned function takes the following arguments:
|
|
120
|
+
*
|
|
121
|
+
* - `taskId`: a required string to identify the task by Id.
|
|
122
|
+
* - `arg`: an optional string that will be passed to the task when run.
|
|
123
|
+
* - `storeId`: an optional string that will be used to look up a Store by Id
|
|
124
|
+
* in the current TinyBase Provider context and be passed to the task.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* const TaskButton = () => {
|
|
129
|
+
* const scheduleTask = useScheduleTask();
|
|
130
|
+
* return (<Button
|
|
131
|
+
* title="Hello"
|
|
132
|
+
* onClick={() => scheduleTask('greet', 'Hello')}
|
|
133
|
+
* />);
|
|
134
|
+
* };
|
|
135
|
+
* // ...
|
|
136
|
+
* <TasksProvider tasks={{greet: (arg) => alert(arg)}}>
|
|
137
|
+
* <TaskButton />
|
|
138
|
+
* </TasksProvider>
|
|
139
|
+
* ```
|
|
140
|
+
* This example shows the hook returning a function that will schedule a task to
|
|
141
|
+
* be run by the provider. Note that it may take as long as a second (the
|
|
142
|
+
* default interval between task executions) for the alert to appear.
|
|
143
|
+
* @example
|
|
144
|
+
* ```tsx
|
|
145
|
+
* const TaskButton2 = () => {
|
|
146
|
+
* const scheduleTask = useScheduleTask();
|
|
147
|
+
* return (<Button
|
|
148
|
+
* iconRight={Lucide.Globe}
|
|
149
|
+
* title="Hello"
|
|
150
|
+
* onClick={() => scheduleTask('greet1', 'Hello')}
|
|
151
|
+
* />);
|
|
152
|
+
* };
|
|
153
|
+
* // ...
|
|
154
|
+
* <TasksProvider tasks={{
|
|
155
|
+
* greet1: async (arg, store, scheduleTask) => {
|
|
156
|
+
* scheduleTask('greet2', 'World');
|
|
157
|
+
* alert(arg);
|
|
158
|
+
* },
|
|
159
|
+
* greet2: (arg) => alert(arg)
|
|
160
|
+
* }}>
|
|
161
|
+
* <TaskButton2 />
|
|
162
|
+
* </TasksProvider>
|
|
163
|
+
* ```
|
|
164
|
+
* This example shows the hook returning a function that will schedule a task to
|
|
165
|
+
* be run by the provider. That task in turn will call another.
|
|
166
|
+
*/
|
|
167
|
+
export const useScheduleTask = () => useContext(Context);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The `TasksProvider` component is a non-visual component that makes it easy to
|
|
171
|
+
* manage sequential tasks in your TinyWidget application.
|
|
172
|
+
*
|
|
173
|
+
* @param props The props for the component.
|
|
174
|
+
* @returns The TasksProvider component.
|
|
175
|
+
* @icon Lucide.FileClock
|
|
176
|
+
* @example
|
|
177
|
+
* ```tsx
|
|
178
|
+
* const TaskButton1 = () => {
|
|
179
|
+
* const scheduleTask = useScheduleTask();
|
|
180
|
+
* return (<Button
|
|
181
|
+
* iconRight={Lucide.Hand}
|
|
182
|
+
* title="Hello"
|
|
183
|
+
* onClick={() => scheduleTask('greet1')}
|
|
184
|
+
* />);
|
|
185
|
+
* };
|
|
186
|
+
* const TaskButton2 = () => {
|
|
187
|
+
* const scheduleTask = useScheduleTask();
|
|
188
|
+
* return (<Button
|
|
189
|
+
* iconRight={Lucide.Globe}
|
|
190
|
+
* title="World"
|
|
191
|
+
* onClick={() => scheduleTask('greet2')}
|
|
192
|
+
* />);
|
|
193
|
+
* };
|
|
194
|
+
* // ...
|
|
195
|
+
* <TasksProvider tasks={{
|
|
196
|
+
* greet1: () => alert('Hello'),
|
|
197
|
+
* greet2: () => alert('World'),
|
|
198
|
+
* }}>
|
|
199
|
+
* <TaskButton1 />
|
|
200
|
+
* <TaskButton2 />
|
|
201
|
+
* </TasksProvider>
|
|
202
|
+
* ```
|
|
203
|
+
* This example shows the hook returning a function that will schedule a task to
|
|
204
|
+
* be run by the provider. That task in turn will call another.
|
|
205
|
+
*/
|
|
206
|
+
export const TasksProvider = ({
|
|
207
|
+
tasks,
|
|
208
|
+
interval = 1,
|
|
209
|
+
expiry = 5,
|
|
210
|
+
children,
|
|
211
|
+
}: {
|
|
212
|
+
/**
|
|
213
|
+
* An object listing all the tasks that can be executed, keyed by Id.
|
|
214
|
+
*
|
|
215
|
+
* Each task is a function that receives a string argument, a reference to a
|
|
216
|
+
* Store (each as specified when `scheduleTask` was called), and a reference
|
|
217
|
+
* to the `scheduleTask` function again so that tasks can be chained.
|
|
218
|
+
*
|
|
219
|
+
* A task can be asynchronous.
|
|
220
|
+
*/
|
|
221
|
+
readonly tasks: Tasks;
|
|
222
|
+
/**
|
|
223
|
+
* The interval in seconds between each task execution, defaulting to 1.
|
|
224
|
+
*/
|
|
225
|
+
readonly interval?: number;
|
|
226
|
+
/**
|
|
227
|
+
* The time in seconds from scheduling until a task expires and will not be
|
|
228
|
+
* started, defaulting to 5.
|
|
229
|
+
*/
|
|
230
|
+
readonly expiry?: number;
|
|
231
|
+
/**
|
|
232
|
+
* The children of the component to be rendered.
|
|
233
|
+
*/
|
|
234
|
+
readonly children: ReactNode;
|
|
235
|
+
}) => {
|
|
236
|
+
const pendingAddedJobs = useRef<
|
|
237
|
+
[taskId: string, arg?: string, storeId?: string][]
|
|
238
|
+
>([]);
|
|
239
|
+
const activePersister = useRef<Persister<Schemas> | undefined>();
|
|
240
|
+
|
|
241
|
+
const tasksStore = useCreateStore(() =>
|
|
242
|
+
createStore().setSchema(TABLES_SCHEMA, VALUES_SCHEMA),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
useCreatePersister(
|
|
246
|
+
tasksStore,
|
|
247
|
+
(tasksStore) => createLocalPersister(tasksStore, STORE_ID),
|
|
248
|
+
[],
|
|
249
|
+
async (persister) => {
|
|
250
|
+
await persister.startAutoLoad();
|
|
251
|
+
tasksStore.forEachRow('tasks', (taskId, _) =>
|
|
252
|
+
tasksStore.delCell('tasks', taskId, 'running'),
|
|
253
|
+
);
|
|
254
|
+
await persister.startAutoSave();
|
|
255
|
+
activePersister.current = persister;
|
|
256
|
+
pendingAddedJobs.current.forEach((pendingAddedJob) =>
|
|
257
|
+
scheduleTask(...pendingAddedJob),
|
|
258
|
+
);
|
|
259
|
+
pendingAddedJobs.current.splice(0);
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const scheduleTask: ScheduleTask = useCallback(
|
|
264
|
+
(taskId: string, arg?: string, storeId?: string) => {
|
|
265
|
+
if (activePersister.current) {
|
|
266
|
+
tasksStore.addRow(TABLE_ID, {
|
|
267
|
+
taskId,
|
|
268
|
+
expires: Date.now() + expiry * 1000,
|
|
269
|
+
arg,
|
|
270
|
+
storeId,
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
pendingAddedJobs.current.push([taskId, arg, storeId]);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
[tasksStore, expiry],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<Context.Provider value={scheduleTask}>
|
|
281
|
+
<UiReactProvider>
|
|
282
|
+
{children}
|
|
283
|
+
<TaskRunner tasks={tasks} tasksStore={tasksStore} interval={interval} />
|
|
284
|
+
</UiReactProvider>
|
|
285
|
+
</Context.Provider>
|
|
286
|
+
);
|
|
287
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,10 @@ export {Metric} from './components/Metric/index.tsx';
|
|
|
10
10
|
export {Row} from './components/Row/index.tsx';
|
|
11
11
|
export {Summary} from './components/Summary/index.tsx';
|
|
12
12
|
export {Tag} from './components/Tag/index.tsx';
|
|
13
|
+
export {
|
|
14
|
+
TasksProvider,
|
|
15
|
+
useScheduleTask,
|
|
16
|
+
} from './components/TasksProvider/index.tsx';
|
|
13
17
|
|
|
14
18
|
export {useRoute, useSetRouteCallback} from './stores/RouteStore.tsx';
|
|
15
19
|
export {useDark} from './stores/LocalStore.tsx';
|