@nuxodin/item 0.6.19 → 0.6.20
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 +1 -1
- package/src/AsyncDataPoint.js +36 -29
package/package.json
CHANGED
package/src/AsyncDataPoint.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
set: value => fetch('https://example.com/todos/1', {method: 'PUT', body: JSON.stringify(value)}).then(res => res.json())
|
|
6
6
|
});
|
|
7
7
|
datapoint.set({title: 'foo', completed: true}); // Returns promise - or undefined if value is already the same - do not rely on return value
|
|
8
|
+
// a write overtaken by a newer one settles as that one does: "this value or a newer one is saved"
|
|
8
9
|
datapoint.get().then(value => console.log(value));
|
|
9
10
|
|
|
10
11
|
// API
|
|
11
12
|
datapoint.onchange = ({value}) => console.log('value changed', value);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// options, passed to the constructor (or changed in datapoint.options):
|
|
14
|
+
// ttl: 5000 (cache in ms), debounceMs: 5 (setter delay), getRetry / setRetry, getRetryDelay / setRetryDelay,
|
|
15
|
+
// optimistic: true (until the sending is done, the value is the new one, though the server may fail)
|
|
15
16
|
datapoint.setLocal({title: 'foo', completed: true}); // set value without saving it to the master (the value comes from the master through an other channel / trusted source)
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
createSetter;
|
|
22
23
|
#expectedValue = null; // value while saving
|
|
23
24
|
#setter = null; // current setter promise
|
|
25
|
+
#writing = Promise.resolve(); // the write running, the next one starts after it
|
|
24
26
|
#getter = null; // current/cached getter promise
|
|
25
27
|
#getterExpiry = Infinity; // ttl is checked lazily on access — no timer per cache entry
|
|
26
28
|
// expired getters count as gone (lazy ttl)
|
|
@@ -62,11 +64,15 @@
|
|
|
62
64
|
#createSetter(value) {
|
|
63
65
|
const {setRetry, setRetryDelay, debounceMs} = this.options;
|
|
64
66
|
const promise = abortablePromise((resolve, reject) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const {signal} = promise.controller;
|
|
68
|
+
// one write at a time: a slow one landing last would leave the remote with an old value
|
|
69
|
+
const run = this.#writing.then(() => signal.aborted || retryAsync(
|
|
70
|
+
() => this.createSetter(value, signal),
|
|
67
71
|
setRetry,
|
|
68
72
|
setRetryDelay
|
|
69
|
-
)
|
|
73
|
+
));
|
|
74
|
+
this.#writing = run.catch(() => {});
|
|
75
|
+
run.then(resolve, reject);
|
|
70
76
|
}, debounceMs);
|
|
71
77
|
|
|
72
78
|
makePromiseTransparent(promise);
|
|
@@ -83,18 +89,12 @@
|
|
|
83
89
|
promise.then(
|
|
84
90
|
value => {
|
|
85
91
|
if (this.#getter !== promise) return;
|
|
86
|
-
if (!isEqual(oldValue, value)) {
|
|
87
|
-
try {
|
|
88
|
-
this.onchange?.({value, oldValue});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error('[item.js] AsyncDataPoint onchange error:', err);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
92
|
+
if (!isEqual(oldValue, value)) this.#notify({value, oldValue});
|
|
93
93
|
},
|
|
94
94
|
error => {
|
|
95
95
|
if (this.#getter !== promise) return;
|
|
96
96
|
if (error?.name === 'AbortError') return;
|
|
97
|
-
this
|
|
97
|
+
this.#notify({error, oldValue});
|
|
98
98
|
}
|
|
99
99
|
);
|
|
100
100
|
|
|
@@ -103,6 +103,11 @@
|
|
|
103
103
|
this.#getterExpiry = duration > 0 ? Date.now() + duration : Infinity;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
#notify(event) {
|
|
107
|
+
try { this.onchange?.(event); }
|
|
108
|
+
catch (err) { console.error('[item.js] AsyncDataPoint onchange error:', err); }
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
setLocal(value) { this.#cacheGetter(transparentPromiseResolve(value)); }
|
|
107
112
|
setFromPromise(promise) { this.#cacheGetter(makePromiseTransparent(promise)); }
|
|
108
113
|
get isPending() { return this.#cachedGetter?.state === 'pending' || this.#setter?.state === 'pending'; }
|
|
@@ -124,18 +129,18 @@
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
set(value) {
|
|
132
|
+
const previous = this.#setter, writing = previous?.state === 'pending';
|
|
127
133
|
// Already setting this value
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
this.#setter?.controller.abort(); // abort previous setter
|
|
134
|
+
if (writing && isEqual(this.#expectedValue, value)) return previous;
|
|
131
135
|
|
|
132
|
-
// ignore if latest getter value is the same
|
|
133
136
|
// Value already set (no-op). Returns undefined to avoid falsy issues (value could be 0, false, etc.)
|
|
137
|
+
// While a write runs the loaded value is outdated: setting it back is a change.
|
|
134
138
|
const cached = this.#cachedGetter;
|
|
135
|
-
if (cached?.state === 'fulfilled' && isEqual(cached.value, value)) return;
|
|
139
|
+
if (!writing && cached?.state === 'fulfilled' && isEqual(cached.value, value)) return;
|
|
136
140
|
|
|
137
141
|
this.#expectedValue = value;
|
|
138
142
|
const promise = this.#createSetter(value);
|
|
143
|
+
previous?.controller.abort(promise); // the previous one settles as this one does
|
|
139
144
|
|
|
140
145
|
this.onpending?.();
|
|
141
146
|
|
|
@@ -146,13 +151,12 @@
|
|
|
146
151
|
this.#setter = null;
|
|
147
152
|
this.#expectedValue = null;
|
|
148
153
|
},
|
|
149
|
-
error => {
|
|
154
|
+
error => { // the failed setter stays: lastError reports it until the next write
|
|
150
155
|
if (this.#setter !== promise) return;
|
|
151
156
|
if (error?.name === 'AbortError') return;
|
|
152
157
|
this.#getter = null;
|
|
153
|
-
this.#setter = null;
|
|
154
158
|
this.#expectedValue = null;
|
|
155
|
-
this
|
|
159
|
+
this.#notify({ error }); }
|
|
156
160
|
);
|
|
157
161
|
|
|
158
162
|
return this.#setter = promise;
|
|
@@ -223,18 +227,21 @@
|
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
/**
|
|
226
|
-
* Returns a delayed Promise that can be aborted
|
|
227
|
-
*
|
|
228
|
-
* @param {
|
|
230
|
+
* Returns a delayed Promise that can be aborted. Aborted with a promise as reason, it settles as
|
|
231
|
+
* that one does (a newer write taking over), else it rejects with the AbortError.
|
|
232
|
+
* @param {Function} fn - Called with (resolve, reject) after the delay.
|
|
233
|
+
* @param {Number} [ms=1] - The time in milliseconds to delay before calling fn.
|
|
229
234
|
* @returns {Promise<any>} - A delayed Promise that can be aborted.
|
|
230
235
|
*/
|
|
231
236
|
function abortablePromise(fn, ms=1) { // delayed and therfore abortable within ms
|
|
232
237
|
const controller = new AbortController();
|
|
238
|
+
const {signal} = controller;
|
|
233
239
|
const promise = new Promise((resolve, reject) => {
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
}
|
|
240
|
+
const settle = (ok) => (v) => {
|
|
241
|
+
if (!signal.aborted) return ok(v);
|
|
242
|
+
signal.reason?.then ? resolve(signal.reason) : reject(signal.reason);
|
|
243
|
+
};
|
|
244
|
+
setTimeout(() => signal.aborted ? settle()() : fn(settle(resolve), settle(reject)), ms);
|
|
238
245
|
});
|
|
239
246
|
promise.controller = controller;
|
|
240
247
|
return promise;
|