@pistonite/pure 0.0.12 → 0.0.14
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/package.json +5 -3
- package/src/pref/dark.ts +21 -47
- package/src/pref/locale.ts +20 -44
- package/src/sync/batch.ts +233 -0
- package/src/sync/cell.ts +60 -0
- package/src/sync/debounce.ts +179 -0
- package/src/sync/index.ts +7 -4
- package/src/sync/latest.ts +85 -0
- package/src/sync/persist.ts +122 -0
- package/src/sync/serial.ts +220 -0
- package/src/sync/util.ts +23 -0
- package/src/sync/Debounce.ts +0 -35
- package/src/sync/Latest.ts +0 -75
- package/src/sync/Serial.ts +0 -170
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pistonite/pure",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "Pure TypeScript libraries for my projects",
|
|
5
5
|
"homepage": "https://github.com/Pistonite/pure",
|
|
6
6
|
"bugs": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"author": "Pistonight <pistonknight@outlook.com>",
|
|
11
11
|
"files": [
|
|
12
|
-
"src/**/*"
|
|
12
|
+
"src/**/*",
|
|
13
|
+
"!src/**/*.test.ts"
|
|
13
14
|
],
|
|
14
15
|
"exports": {
|
|
15
16
|
"./fs": "./src/fs/index.ts",
|
|
@@ -24,10 +25,11 @@
|
|
|
24
25
|
"directory": "packages/pure"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
28
|
+
"@types/file-saver": "^2.0.7",
|
|
27
29
|
"denque": "2.1.0",
|
|
28
30
|
"file-saver": "2.0.5"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
|
-
"
|
|
33
|
+
"vitest": "^2.1.8"
|
|
32
34
|
}
|
|
33
35
|
}
|
package/src/pref/dark.ts
CHANGED
|
@@ -50,12 +50,16 @@
|
|
|
50
50
|
* @module
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
+
import { persist } from "../sync/persist.ts";
|
|
53
54
|
import { injectStyle } from "./injectStyle.ts";
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const dark = persist({
|
|
57
|
+
initial: false,
|
|
58
|
+
key: "Pure.Dark",
|
|
59
|
+
storage: localStorage,
|
|
60
|
+
serialize: (value) => (value ? "1" : ""),
|
|
61
|
+
deserialize: (value) => !!value,
|
|
62
|
+
});
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
65
|
* Returns if dark mode is prefered in the browser environment
|
|
@@ -96,20 +100,6 @@ export type DarkOptions = {
|
|
|
96
100
|
export const initDark = (options: DarkOptions = {}): void => {
|
|
97
101
|
let _dark = options.initial || prefersDarkMode();
|
|
98
102
|
|
|
99
|
-
if (options.persist) {
|
|
100
|
-
const value = localStorage.getItem(KEY);
|
|
101
|
-
if (value !== null) {
|
|
102
|
-
_dark = !!value;
|
|
103
|
-
}
|
|
104
|
-
addDarkSubscriber((dark: boolean) => {
|
|
105
|
-
localStorage.setItem(KEY, dark ? "1" : "");
|
|
106
|
-
});
|
|
107
|
-
} else {
|
|
108
|
-
localStorage.removeItem(KEY);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
setDark(_dark);
|
|
112
|
-
|
|
113
103
|
const selector = options.selector ?? ":root";
|
|
114
104
|
if (selector) {
|
|
115
105
|
// notify immediately to update the style initially
|
|
@@ -117,6 +107,12 @@ export const initDark = (options: DarkOptions = {}): void => {
|
|
|
117
107
|
updateStyle(dark, selector);
|
|
118
108
|
}, true /* notify */);
|
|
119
109
|
}
|
|
110
|
+
|
|
111
|
+
if (options.persist) {
|
|
112
|
+
dark.init(_dark);
|
|
113
|
+
} else {
|
|
114
|
+
dark.disable();
|
|
115
|
+
}
|
|
120
116
|
};
|
|
121
117
|
|
|
122
118
|
/**
|
|
@@ -130,55 +126,33 @@ export const initDark = (options: DarkOptions = {}): void => {
|
|
|
130
126
|
* subsequence `setDark` calls will still persist the value.
|
|
131
127
|
*/
|
|
132
128
|
export const clearPersistedDarkPerference = (): void => {
|
|
133
|
-
|
|
129
|
+
dark.clear();
|
|
134
130
|
};
|
|
135
131
|
|
|
136
132
|
/**
|
|
137
133
|
* Gets the current value of dark mode
|
|
138
134
|
*/
|
|
139
|
-
export const isDark = (): boolean => dark;
|
|
135
|
+
export const isDark = (): boolean => dark.get();
|
|
140
136
|
|
|
141
137
|
/**
|
|
142
138
|
* Set the value of dark mode
|
|
143
139
|
*/
|
|
144
140
|
export const setDark = (value: boolean): void => {
|
|
145
|
-
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
dark = value;
|
|
149
|
-
const len = subscribers.length;
|
|
150
|
-
for (let i = 0; i < len; i++) {
|
|
151
|
-
subscribers[i](dark);
|
|
152
|
-
}
|
|
141
|
+
dark.set(value);
|
|
153
142
|
};
|
|
154
143
|
/**
|
|
155
|
-
* Add a subscriber to dark mode changes
|
|
144
|
+
* Add a subscriber to dark mode changes and return a function to remove the subscriber
|
|
156
145
|
*
|
|
157
146
|
* If `notifyImmediately` is `true`, the subscriber will be called immediately with the current value
|
|
158
147
|
*/
|
|
159
148
|
export const addDarkSubscriber = (
|
|
160
149
|
subscriber: (dark: boolean) => void,
|
|
161
150
|
notifyImmediately?: boolean,
|
|
162
|
-
): void => {
|
|
163
|
-
|
|
164
|
-
if (notifyImmediately) {
|
|
165
|
-
subscriber(dark);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Remove a subscriber from dark mode changes
|
|
171
|
-
*/
|
|
172
|
-
export const removeDarkSubscriber = (
|
|
173
|
-
subscriber: (dark: boolean) => void,
|
|
174
|
-
): void => {
|
|
175
|
-
const index = subscribers.indexOf(subscriber);
|
|
176
|
-
if (index >= 0) {
|
|
177
|
-
subscribers.splice(index, 1);
|
|
178
|
-
}
|
|
151
|
+
): (() => void) => {
|
|
152
|
+
return dark.subscribe(subscriber, notifyImmediately);
|
|
179
153
|
};
|
|
180
154
|
|
|
181
155
|
const updateStyle = (dark: boolean, selector: string) => {
|
|
182
156
|
const text = `${selector} { color-scheme: ${dark ? "dark" : "light"}; }`;
|
|
183
|
-
injectStyle(
|
|
157
|
+
injectStyle("pure-pref-dark", text);
|
|
184
158
|
};
|
package/src/pref/locale.ts
CHANGED
|
@@ -53,12 +53,20 @@
|
|
|
53
53
|
* @module
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
import { persist } from "../sync/persist.ts";
|
|
57
57
|
|
|
58
58
|
let supportedLocales: readonly string[] = [];
|
|
59
|
-
let locale: string = "";
|
|
60
59
|
let defaultLocale: string = "";
|
|
61
|
-
const
|
|
60
|
+
const locale = persist<string>({
|
|
61
|
+
initial: "",
|
|
62
|
+
key: "Pure.Locale",
|
|
63
|
+
storage: localStorage,
|
|
64
|
+
serialize: (value) => value,
|
|
65
|
+
deserialize: (value) => {
|
|
66
|
+
const supported = convertToSupportedLocale(value);
|
|
67
|
+
return supported || null;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
62
70
|
|
|
63
71
|
/**
|
|
64
72
|
* Use browser API to guess user's preferred locale
|
|
@@ -120,21 +128,10 @@ export const initLocale = <TLocale extends string>(
|
|
|
120
128
|
}
|
|
121
129
|
defaultLocale = options.default;
|
|
122
130
|
if (options.persist) {
|
|
123
|
-
|
|
124
|
-
if (value !== null) {
|
|
125
|
-
const supported = convertToSupportedLocale(value);
|
|
126
|
-
if (supported) {
|
|
127
|
-
_locale = supported;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
addLocaleSubscriber((locale: string) => {
|
|
131
|
-
localStorage.setItem(KEY, locale);
|
|
132
|
-
});
|
|
131
|
+
locale.init(_locale);
|
|
133
132
|
} else {
|
|
134
|
-
|
|
133
|
+
locale.disable();
|
|
135
134
|
}
|
|
136
|
-
|
|
137
|
-
setLocale(_locale);
|
|
138
135
|
};
|
|
139
136
|
|
|
140
137
|
/**
|
|
@@ -149,13 +146,11 @@ export const initLocale = <TLocale extends string>(
|
|
|
149
146
|
* subsequence `setLocale` calls will still persist the value.
|
|
150
147
|
*/
|
|
151
148
|
export const clearPersistedLocalePreference = (): void => {
|
|
152
|
-
|
|
149
|
+
locale.clear();
|
|
153
150
|
};
|
|
154
151
|
|
|
155
152
|
/** Get the current selected locale */
|
|
156
|
-
export const getLocale = (): string =>
|
|
157
|
-
return locale;
|
|
158
|
-
};
|
|
153
|
+
export const getLocale = (): string => locale.get();
|
|
159
154
|
|
|
160
155
|
/** Get the default locale when initialized */
|
|
161
156
|
export const getDefaultLocale = (): string => {
|
|
@@ -172,14 +167,7 @@ export const setLocale = (newLocale: string): boolean => {
|
|
|
172
167
|
if (!supported) {
|
|
173
168
|
return false;
|
|
174
169
|
}
|
|
175
|
-
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
locale = supported;
|
|
179
|
-
const len = subscribers.length;
|
|
180
|
-
for (let i = 0; i < len; i++) {
|
|
181
|
-
subscribers[i](locale);
|
|
182
|
-
}
|
|
170
|
+
locale.set(supported);
|
|
183
171
|
return true;
|
|
184
172
|
};
|
|
185
173
|
|
|
@@ -235,28 +223,16 @@ export const convertToSupportedLocaleOrDefault = (
|
|
|
235
223
|
};
|
|
236
224
|
|
|
237
225
|
/**
|
|
238
|
-
* Add a subscriber to be notified when the locale changes
|
|
226
|
+
* Add a subscriber to be notified when the locale changes.
|
|
227
|
+
* Returns a function to remove the subscriber
|
|
239
228
|
*
|
|
240
229
|
* If `notifyImmediately` is `true`, the subscriber will be called immediately with the current locale
|
|
241
230
|
*/
|
|
242
231
|
export const addLocaleSubscriber = (
|
|
243
232
|
fn: (locale: string) => void,
|
|
244
233
|
notifyImmediately?: boolean,
|
|
245
|
-
): void => {
|
|
246
|
-
|
|
247
|
-
if (notifyImmediately) {
|
|
248
|
-
fn(locale);
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Remove a subscriber from locale changes
|
|
254
|
-
*/
|
|
255
|
-
export const removeLocaleSubscriber = (fn: (locale: string) => void): void => {
|
|
256
|
-
const index = subscribers.indexOf(fn);
|
|
257
|
-
if (index !== -1) {
|
|
258
|
-
subscribers.splice(index, 1);
|
|
259
|
-
}
|
|
234
|
+
): (() => void) => {
|
|
235
|
+
return locale.subscribe(fn, notifyImmediately);
|
|
260
236
|
};
|
|
261
237
|
|
|
262
238
|
/**
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { makePromise, type AwaitRet } from "./util.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An async event wrapper that allows multiple calls in an interval
|
|
5
|
+
* to be batched together, and only call the underlying function once.
|
|
6
|
+
*
|
|
7
|
+
* Optionally, the output can be unbatched to match the inputs.
|
|
8
|
+
*
|
|
9
|
+
* ## Example
|
|
10
|
+
* The API is a lot like `debounce`, but with an additional `batch` function
|
|
11
|
+
* and an optional `unbatch` function.
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { batch } from "@pistonite/pure/sync";
|
|
14
|
+
*
|
|
15
|
+
* const execute = batch({
|
|
16
|
+
* fn: (n: number) => {
|
|
17
|
+
* console.log(n);
|
|
18
|
+
* },
|
|
19
|
+
* interval: 100,
|
|
20
|
+
* // batch receives all the inputs and returns a single input
|
|
21
|
+
* // here we just sums the inputs
|
|
22
|
+
* batch: (args: [number][]): [number] => [args.reduce((acc, [n]) => acc + n, 0)],
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await execute(1); // logs 1 immediately
|
|
26
|
+
* const p1 = execute(2); // will be resolved at 100ms
|
|
27
|
+
* const p2 = execute(3); // will be resolved at 100ms
|
|
28
|
+
* await Promise.all([p1, p2]); // logs 5 after 100ms
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* ## Unbatching
|
|
32
|
+
* The optional `unbatch` function allows the output to be unbatched,
|
|
33
|
+
* so the promises are resolved as if the underlying function is called
|
|
34
|
+
* directly.
|
|
35
|
+
*
|
|
36
|
+
* Note that unbatching is usually slow and not required.
|
|
37
|
+
*
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { batch } from "@pistonite/pure/sync";
|
|
40
|
+
*
|
|
41
|
+
* type Message = {
|
|
42
|
+
* id: number;
|
|
43
|
+
* payload: string;
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* const execute = batch({
|
|
47
|
+
* fn: (messages: Message[]): Message[] => {
|
|
48
|
+
* console.log(messages.length);
|
|
49
|
+
* return messages.map((m) => ({
|
|
50
|
+
* id: m.id,
|
|
51
|
+
* payload: m.payload + "out",
|
|
52
|
+
* }));
|
|
53
|
+
* },
|
|
54
|
+
* batch: (args: [Message[]][]): [Message[]] => {
|
|
55
|
+
* const out: Message[] = [];
|
|
56
|
+
* for (const [messages] of args) {
|
|
57
|
+
* out.push(...messages);
|
|
58
|
+
* }
|
|
59
|
+
* return [out];
|
|
60
|
+
* },
|
|
61
|
+
* unbatch: (inputs: [Message[]][], output: Message[]): Message[][] => {
|
|
62
|
+
* // not efficient, but just for demonstration
|
|
63
|
+
* const idToOutput = new Map();
|
|
64
|
+
* for (const o of output) {
|
|
65
|
+
* idToOutput.set(o.id, o);
|
|
66
|
+
* }
|
|
67
|
+
* return inputs.map(([messages]) => {
|
|
68
|
+
* return messages.map(({id}) => {
|
|
69
|
+
* return idToOutput.get(m.id)!;
|
|
70
|
+
* });
|
|
71
|
+
* });
|
|
72
|
+
* },
|
|
73
|
+
* interval: 100,
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* const r1 = await execute([{id: 1, payload: "a"}]); // logs 1 immediately
|
|
77
|
+
* // r1 is [ {id: 1, payload: "aout"} ]
|
|
78
|
+
*
|
|
79
|
+
* const p1 = execute([{id: 2, payload: "b"}]); // will be resolved at 100ms
|
|
80
|
+
* const p2 = execute([{id: 3, payload: "c"}]); // will be resolved at 100ms
|
|
81
|
+
*
|
|
82
|
+
* const r2 = await p2; // 2 is logged
|
|
83
|
+
* // r1 is [ {id: 2, payload: "bout"} ]
|
|
84
|
+
* const r3 = await p3; // nothing is logged, as it's already resolved
|
|
85
|
+
* // r2 is [ {id: 3, payload: "cout"} ]
|
|
86
|
+
*
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
export function batch<TFn extends (...args: any[]) => any>({
|
|
91
|
+
fn,
|
|
92
|
+
batch,
|
|
93
|
+
unbatch,
|
|
94
|
+
interval,
|
|
95
|
+
disregardExecutionTime,
|
|
96
|
+
}: BatchConstructor<TFn>) {
|
|
97
|
+
const impl = new BatchImpl(
|
|
98
|
+
fn,
|
|
99
|
+
batch,
|
|
100
|
+
unbatch,
|
|
101
|
+
interval,
|
|
102
|
+
!!disregardExecutionTime,
|
|
103
|
+
);
|
|
104
|
+
return (...args: Parameters<TFn>) => impl.invoke(...args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Options to construct a `batch` function
|
|
109
|
+
*/
|
|
110
|
+
export type BatchConstructor<TFn extends (...args: any[]) => any> = {
|
|
111
|
+
/** Function to be wrapped */
|
|
112
|
+
fn: TFn;
|
|
113
|
+
/** Function to batch the inputs across multiple calls */
|
|
114
|
+
batch: (args: Parameters<TFn>[]) => Parameters<TFn>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* If provided, unbatch the output according to the inputs,
|
|
118
|
+
* so each call receives its own output.
|
|
119
|
+
*
|
|
120
|
+
* By default, each input will receive the same output from the batched call
|
|
121
|
+
*/
|
|
122
|
+
unbatch?: (
|
|
123
|
+
inputs: Parameters<TFn>[],
|
|
124
|
+
output: AwaitRet<TFn>,
|
|
125
|
+
) => AwaitRet<TFn>[];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Interval between each batched call
|
|
129
|
+
*/
|
|
130
|
+
interval: number;
|
|
131
|
+
|
|
132
|
+
/** See `debounce` for more information */
|
|
133
|
+
disregardExecutionTime?: boolean;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
class BatchImpl<TFn extends (...args: any[]) => any> {
|
|
137
|
+
private idle: boolean;
|
|
138
|
+
private scheduled: {
|
|
139
|
+
input: Parameters<TFn>;
|
|
140
|
+
promise: Promise<AwaitRet<TFn>>;
|
|
141
|
+
resolve: (value: AwaitRet<TFn>) => void;
|
|
142
|
+
reject: (error: unknown) => void;
|
|
143
|
+
}[];
|
|
144
|
+
|
|
145
|
+
constructor(
|
|
146
|
+
private fn: TFn,
|
|
147
|
+
private batch: (inputs: Parameters<TFn>[]) => Parameters<TFn>,
|
|
148
|
+
private unbatch:
|
|
149
|
+
| ((
|
|
150
|
+
input: Parameters<TFn>[],
|
|
151
|
+
output: AwaitRet<TFn>,
|
|
152
|
+
) => AwaitRet<TFn>[])
|
|
153
|
+
| undefined,
|
|
154
|
+
private interval: number,
|
|
155
|
+
private disregardExecutionTime: boolean,
|
|
156
|
+
) {
|
|
157
|
+
this.idle = true;
|
|
158
|
+
this.scheduled = [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public invoke(...args: Parameters<TFn>): Promise<AwaitRet<TFn>> {
|
|
162
|
+
if (this.idle) {
|
|
163
|
+
this.idle = false;
|
|
164
|
+
return this.execute(...args);
|
|
165
|
+
}
|
|
166
|
+
const { promise, reject, resolve } = makePromise<AwaitRet<TFn>>();
|
|
167
|
+
this.scheduled.push({
|
|
168
|
+
input: args,
|
|
169
|
+
promise,
|
|
170
|
+
resolve,
|
|
171
|
+
reject,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return promise;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async scheduleNext() {
|
|
178
|
+
const next = this.scheduled;
|
|
179
|
+
if (next.length) {
|
|
180
|
+
this.scheduled = [];
|
|
181
|
+
const batch = this.batch;
|
|
182
|
+
const inputs = next.map(({ input }) => input);
|
|
183
|
+
const input = next.length > 1 ? batch(inputs) : inputs[0];
|
|
184
|
+
try {
|
|
185
|
+
const output = await this.execute(...input);
|
|
186
|
+
const unbatch = this.unbatch;
|
|
187
|
+
if (unbatch && inputs.length > 1) {
|
|
188
|
+
const outputs = unbatch(inputs, output);
|
|
189
|
+
for (let i = 0; i < next.length; i++) {
|
|
190
|
+
const { resolve } = next[i];
|
|
191
|
+
resolve(outputs[i]);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
for (let i = 0; i < next.length; i++) {
|
|
195
|
+
const { resolve } = next[i];
|
|
196
|
+
resolve(output);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
for (let i = 0; i < next.length; i++) {
|
|
201
|
+
const { reject } = next[i];
|
|
202
|
+
reject(e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.idle = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async execute(...args: Parameters<TFn>): Promise<AwaitRet<TFn>> {
|
|
211
|
+
const fn = this.fn;
|
|
212
|
+
let done = this.disregardExecutionTime;
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (done) {
|
|
215
|
+
this.scheduleNext();
|
|
216
|
+
} else {
|
|
217
|
+
done = true;
|
|
218
|
+
}
|
|
219
|
+
}, this.interval);
|
|
220
|
+
try {
|
|
221
|
+
return await fn(...args);
|
|
222
|
+
} finally {
|
|
223
|
+
if (!this.disregardExecutionTime) {
|
|
224
|
+
if (done) {
|
|
225
|
+
// interval already passed, we need to call it
|
|
226
|
+
this.scheduleNext();
|
|
227
|
+
} else {
|
|
228
|
+
done = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
package/src/sync/cell.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a light weight storage wrapper around a value
|
|
3
|
+
* that can be subscribed to for changes
|
|
4
|
+
*/
|
|
5
|
+
export function cell<T>({ initial }: CellConstructor<T>): Cell<T> {
|
|
6
|
+
return new CellImpl(initial);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type CellConstructor<T> = {
|
|
10
|
+
/** Initial value */
|
|
11
|
+
initial: T;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type Cell<T> = {
|
|
15
|
+
get(): T;
|
|
16
|
+
set(value: T): void;
|
|
17
|
+
subscribe(
|
|
18
|
+
callback: (value: T) => void,
|
|
19
|
+
notifyImmediately?: boolean,
|
|
20
|
+
): () => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
class CellImpl<T> implements Cell<T> {
|
|
24
|
+
private subscribers: ((value: T) => void)[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(private value: T) {}
|
|
27
|
+
|
|
28
|
+
public get(): T {
|
|
29
|
+
return this.value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public set(value: T): void {
|
|
33
|
+
if (this.value === value) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.value = value;
|
|
37
|
+
const len = this.subscribers.length;
|
|
38
|
+
for (let i = 0; i < len; i++) {
|
|
39
|
+
const callback = this.subscribers[i];
|
|
40
|
+
callback(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public subscribe(
|
|
45
|
+
callback: (value: T) => void,
|
|
46
|
+
notifyImmediately?: boolean,
|
|
47
|
+
): () => void {
|
|
48
|
+
this.subscribers.push(callback);
|
|
49
|
+
const unsubscribe = () => {
|
|
50
|
+
const index = this.subscribers.indexOf(callback);
|
|
51
|
+
if (index >= 0) {
|
|
52
|
+
this.subscribers.splice(index, 1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
if (notifyImmediately) {
|
|
56
|
+
callback(this.value);
|
|
57
|
+
}
|
|
58
|
+
return unsubscribe;
|
|
59
|
+
}
|
|
60
|
+
}
|