@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxodin/item",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "description": "Reactive state library",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- datapoint.ttl = 1000; // cache for 1 second
13
- datapoint.optimistic = true; // trust sending value: until the sending is done, the value is the new value, despite the uncertainty that the server will fail
14
- datapoint.debounceMs = 5; // debounce period for setter in ms, default 5
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
- retryAsync(
66
- () => this.createSetter(value, promise.controller.signal),
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
- ).then(resolve, reject);
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.onchange?.({error, oldValue});
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 (this.#setter?.state === 'pending' && isEqual(this.#expectedValue, value)) return this.#setter;
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.onchange?.({ error }); }
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 within the specified time (ms).
227
- * @param {Promise<any>} fn - A function that returns a Promise.
228
- * @param {Number} [ms=1] - The time in milliseconds to delay before resolving the Promise.
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
- setTimeout(() => {
235
- if (controller.signal.aborted) return reject(new DOMException('aborted', 'AbortError'));
236
- fn(resolve, reject);
237
- }, ms);
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;