@converse/skeletor 3.1.2 → 3.1.4
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/CHANGES.md +27 -0
- package/README.md +3 -2
- package/dist/skeletor.d.ts +3 -3
- package/dist/skeletor.esm.js +30 -22
- package/dist/skeletor.esm.js.map +1 -1
- package/dist/skeletor.js +30 -22
- package/dist/skeletor.js.map +1 -1
- package/dist/skeletor.min.js +3 -3
- package/dist/skeletor.node.js +30 -22
- package/dist/skeletor.node.mjs +30 -22
- package/package.json +1 -1
- package/src/collection.ts +8 -7
- package/src/model.ts +6 -10
- package/src/storage.ts +18 -6
package/CHANGES.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.1.4 (2026-06-23)
|
|
4
|
+
|
|
5
|
+
- Fix the `autoSync` hydration promise clobbering a consumer's own `initialized` promise.
|
|
6
|
+
The promise that resolves once hydration is complete is now exposed as `hydrated` (on both
|
|
7
|
+
`Model` and `Collection`) instead of `initialized`. The old name collided with the common
|
|
8
|
+
convention (used by Converse.js) of setting a `model.initialized` promise inside
|
|
9
|
+
`initialize()` to track a consumer's own async setup.
|
|
10
|
+
|
|
11
|
+
Skeletor no longer sets or reads `initialized`. Await `model.hydrated` / `collection.hydrated`
|
|
12
|
+
to know when hydration is complete. If you used the autoSync `initialized` promise added
|
|
13
|
+
in 3.1.0, rename those reads to `hydrated`.
|
|
14
|
+
|
|
15
|
+
## 3.1.3 (2026-06-23)
|
|
16
|
+
|
|
17
|
+
- Fix a regression (since 3.1.0) where a missing record on a `read` was treated as an error.
|
|
18
|
+
`model.fetch({ promise: true })` rejected with the bare string `'Record Not Found'` on a
|
|
19
|
+
first-run read of a non-existent record (and the `error` callback / `'error'` event fired),
|
|
20
|
+
where previously 3.0.x had resolved. A read miss is a normal empty state, so it now
|
|
21
|
+
resolves with `null` on both the callback (`success`) and promise forms.
|
|
22
|
+
|
|
23
|
+
**Behaviour change:** a read miss no longer fires the `error` callback or the `'error'` event,
|
|
24
|
+
it fires `success(null)` instead.
|
|
25
|
+
|
|
26
|
+
- Storage-layer `sync` failures now surface a real `Error` (with a stack) through the `error`
|
|
27
|
+
callback / rejected promise instead of a bare string. The internal `Model#hydrate`
|
|
28
|
+
`'Record Not Found'` string-match workaround is removed accordingly.
|
|
29
|
+
|
|
3
30
|
## 3.1.2 (2026-06-22)
|
|
4
31
|
|
|
5
32
|
- Fix the published package missing its `dist/` directory. `dist/` is gitignored and nothing built
|
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ class Settings extends Model {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const settings = new Settings({ id: 'main' });
|
|
162
|
-
await settings.
|
|
162
|
+
await settings.hydrated; // resolves when prior data has been loaded
|
|
163
163
|
|
|
164
164
|
settings.attrs.theme = 'dark'; // persisted automatically (debounced)
|
|
165
165
|
settings.set('lang', 'en'); // also auto-saved
|
|
@@ -171,7 +171,8 @@ settings.set('transient', true, { noAutoSave: true });
|
|
|
171
171
|
Pending writes are debounced (see `autoSyncDelay`, default 100ms) and flushed when the
|
|
172
172
|
page is hidden (`visibilitychange`) or unloaded (`pagehide`).
|
|
173
173
|
|
|
174
|
-
When `autoSync` is off, `
|
|
174
|
+
When `autoSync` is off, `hydrated` is `undefined`, but `await model.hydrated` is still safe
|
|
175
|
+
(awaiting `undefined` is a no-op), so code can await it uniformly.
|
|
175
176
|
|
|
176
177
|
On a `Collection`, `autoSync` is **hydrate-only**: the collection loads its stored
|
|
177
178
|
state on construction but does not auto-save on `add`/`remove`/`reset`. To persist
|
package/dist/skeletor.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class Collection<T extends Model = Model> extends EventEmitterObj
|
|
|
26
26
|
_storage?: PersistentStorage;
|
|
27
27
|
_comparator?: Comparator<T>;
|
|
28
28
|
_url: string;
|
|
29
|
-
|
|
29
|
+
hydrated?: Promise<void>;
|
|
30
30
|
models: T[];
|
|
31
31
|
protected _byId: Record<string, T>;
|
|
32
32
|
protected _model?: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T;
|
|
@@ -51,7 +51,7 @@ export declare class Collection<T extends Model = Model> extends EventEmitterObj
|
|
|
51
51
|
set browserStorage(s: PersistentStorage);
|
|
52
52
|
/**
|
|
53
53
|
* Override to return `true` to automatically load the collection from
|
|
54
|
-
* storage on construction. Await `
|
|
54
|
+
* storage on construction. Await `hydrated` to know when hydration
|
|
55
55
|
* is complete.
|
|
56
56
|
*
|
|
57
57
|
* Note: collection `autoSync` is hydrate-only. Unlike `Model`, it does
|
|
@@ -545,7 +545,7 @@ export declare class Model<T extends ModelAttributes = ModelAttributes> extends
|
|
|
545
545
|
cid: string;
|
|
546
546
|
collection?: Collection;
|
|
547
547
|
id: string | number;
|
|
548
|
-
|
|
548
|
+
hydrated?: Promise<void>;
|
|
549
549
|
validationError: string | number | null;
|
|
550
550
|
/**
|
|
551
551
|
* Create a new model with the specified attributes. A client id (`cid`)
|
package/dist/skeletor.esm.js
CHANGED
|
@@ -8705,7 +8705,7 @@ class PersistentStorage {
|
|
|
8705
8705
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
8706
8706
|
const that = this;
|
|
8707
8707
|
async function localSync(method, model, options) {
|
|
8708
|
-
let resp,
|
|
8708
|
+
let resp, caughtError, promise, new_attributes;
|
|
8709
8709
|
// We get the collection (and if necessary the model attribute.
|
|
8710
8710
|
// Waiting for storeInitialized will cause another iteration of
|
|
8711
8711
|
// the event loop, after which the collection reference will
|
|
@@ -8762,10 +8762,12 @@ class PersistentStorage {
|
|
|
8762
8762
|
catch (error) {
|
|
8763
8763
|
const storageSize = await that.getStorageSize();
|
|
8764
8764
|
if (error.code === 22 && storageSize === 0) {
|
|
8765
|
-
|
|
8765
|
+
caughtError = new Error('Private browsing is unsupported');
|
|
8766
8766
|
}
|
|
8767
8767
|
else {
|
|
8768
|
-
|
|
8768
|
+
// Preserve a real Error (stack/name); wrap anything else so the
|
|
8769
|
+
// error path always surfaces an Error, never a bare string.
|
|
8770
|
+
caughtError = error instanceof Error ? error : new Error(error?.message ?? String(error));
|
|
8769
8771
|
}
|
|
8770
8772
|
}
|
|
8771
8773
|
if (resp) {
|
|
@@ -8779,10 +8781,21 @@ class PersistentStorage {
|
|
|
8779
8781
|
options.success(data, options);
|
|
8780
8782
|
}
|
|
8781
8783
|
}
|
|
8784
|
+
else if (method === 'read' && !caughtError) {
|
|
8785
|
+
// A read that finds no record is a normal empty result (first run /
|
|
8786
|
+
// empty storage), not an error. Resolve with `null` so the callback
|
|
8787
|
+
// and promise forms of fetch() agree and a first-run read never
|
|
8788
|
+
// rejects.
|
|
8789
|
+
if (options && options.success) {
|
|
8790
|
+
options.success(null, options);
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8782
8793
|
else {
|
|
8783
|
-
|
|
8794
|
+
// A genuine failure (storage threw, or a write produced no result):
|
|
8795
|
+
// surface a real Error so consumers get a stack and `instanceof Error`
|
|
8796
|
+
// holds.
|
|
8784
8797
|
if (options && options.error) {
|
|
8785
|
-
options.error(
|
|
8798
|
+
options.error(caughtError ?? new Error('Record Not Found'));
|
|
8786
8799
|
}
|
|
8787
8800
|
}
|
|
8788
8801
|
}
|
|
@@ -10894,18 +10907,18 @@ class Model extends EventEmitterObject {
|
|
|
10894
10907
|
this.initialize.apply(this, arguments);
|
|
10895
10908
|
// Reset changed after initial set
|
|
10896
10909
|
this.changed = {};
|
|
10897
|
-
// When autoSync is on, `
|
|
10898
|
-
// uniformly `await model.
|
|
10910
|
+
// When autoSync is on, `hydrated` is always a Promise so callers can
|
|
10911
|
+
// uniformly `await model.hydrated`. It only hydrates from storage when
|
|
10899
10912
|
// there's something to load (storage configured and the model isn't new);
|
|
10900
10913
|
// otherwise it's already ready and resolves immediately.
|
|
10901
10914
|
if (this.autoSync) {
|
|
10902
10915
|
if (getStorage(this) && !this.isNew()) {
|
|
10903
10916
|
__classPrivateFieldSet(this, _Model_state, 'hydrating', "f");
|
|
10904
|
-
this.
|
|
10917
|
+
this.hydrated = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
|
|
10905
10918
|
}
|
|
10906
10919
|
else {
|
|
10907
10920
|
__classPrivateFieldSet(this, _Model_state, 'ready', "f");
|
|
10908
|
-
this.
|
|
10921
|
+
this.hydrated = Promise.resolve();
|
|
10909
10922
|
}
|
|
10910
10923
|
}
|
|
10911
10924
|
else {
|
|
@@ -11078,7 +11091,7 @@ class Model extends EventEmitterObject {
|
|
|
11078
11091
|
// Guard must come after arg normalization: when called as set(obj, opts),
|
|
11079
11092
|
// fromStorage lives in `val` (second arg), not `options` (third arg).
|
|
11080
11093
|
if (this.autoSync && __classPrivateFieldGet(this, _Model_state, "f") === 'hydrating' && !options.fromStorage) {
|
|
11081
|
-
throw new Error(`Skeletor: set() called on an autoSync model before
|
|
11094
|
+
throw new Error(`Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`);
|
|
11082
11095
|
}
|
|
11083
11096
|
// Run validation.
|
|
11084
11097
|
if (!this._validate(attrs, options))
|
|
@@ -11454,12 +11467,6 @@ _Model_computedCache = new WeakMap(), _Model_computedDefs = new WeakMap(), _Mode
|
|
|
11454
11467
|
try {
|
|
11455
11468
|
await this.fetch({ fromStorage: true, promise: true });
|
|
11456
11469
|
}
|
|
11457
|
-
catch (e) {
|
|
11458
|
-
// 'Record Not Found' is a normal first-run state (nothing stored yet);
|
|
11459
|
-
// keep the initial attributes and resolve. Re-throw anything else.
|
|
11460
|
-
if (e !== 'Record Not Found')
|
|
11461
|
-
throw e;
|
|
11462
|
-
}
|
|
11463
11470
|
finally {
|
|
11464
11471
|
__classPrivateFieldSet(this, _Model_state, 'ready', "f");
|
|
11465
11472
|
}
|
|
@@ -11524,11 +11531,11 @@ class Collection extends EventEmitterObject {
|
|
|
11524
11531
|
if (models)
|
|
11525
11532
|
this.reset(models, Object.assign({ silent: true }, options));
|
|
11526
11533
|
this[Symbol.iterator] = this.values;
|
|
11527
|
-
// When autoSync is on, `
|
|
11528
|
-
// uniformly `await collection.
|
|
11534
|
+
// When autoSync is on, `hydrated` is always a Promise so callers can
|
|
11535
|
+
// uniformly `await collection.hydrated`; it resolves immediately when
|
|
11529
11536
|
// there's no storage to hydrate from.
|
|
11530
11537
|
if (this.autoSync) {
|
|
11531
|
-
this.
|
|
11538
|
+
this.hydrated = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
|
|
11532
11539
|
}
|
|
11533
11540
|
}
|
|
11534
11541
|
get comparator() {
|
|
@@ -11560,7 +11567,7 @@ class Collection extends EventEmitterObject {
|
|
|
11560
11567
|
}
|
|
11561
11568
|
/**
|
|
11562
11569
|
* Override to return `true` to automatically load the collection from
|
|
11563
|
-
* storage on construction. Await `
|
|
11570
|
+
* storage on construction. Await `hydrated` to know when hydration
|
|
11564
11571
|
* is complete.
|
|
11565
11572
|
*
|
|
11566
11573
|
* Note: collection `autoSync` is hydrate-only. Unlike `Model`, it does
|
|
@@ -12234,8 +12241,9 @@ class Collection extends EventEmitterObject {
|
|
|
12234
12241
|
}
|
|
12235
12242
|
_Collection_instances = new WeakSet(), _Collection_hydrate = async function _Collection_hydrate() {
|
|
12236
12243
|
ensureUnloadListener(PersistentStorage);
|
|
12237
|
-
//
|
|
12238
|
-
//
|
|
12244
|
+
// An empty store is a normal first-run state: findAll() returns [] and a
|
|
12245
|
+
// missing model record resolves to null, so hydration resolves rather
|
|
12246
|
+
// than rejecting.
|
|
12239
12247
|
await this.fetch({ fromStorage: true, promise: true });
|
|
12240
12248
|
}, _a = Symbol.iterator;
|
|
12241
12249
|
// This "enum" defines the three possible kinds of values which can be emitted
|