@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.
@@ -8030,7 +8030,7 @@ class PersistentStorage {
8030
8030
  // eslint-disable-next-line @typescript-eslint/no-this-alias
8031
8031
  const that = this;
8032
8032
  async function localSync(method, model, options) {
8033
- let resp, errorMessage, promise, new_attributes;
8033
+ let resp, caughtError, promise, new_attributes;
8034
8034
  // We get the collection (and if necessary the model attribute.
8035
8035
  // Waiting for storeInitialized will cause another iteration of
8036
8036
  // the event loop, after which the collection reference will
@@ -8087,10 +8087,12 @@ class PersistentStorage {
8087
8087
  catch (error) {
8088
8088
  const storageSize = await that.getStorageSize();
8089
8089
  if (error.code === 22 && storageSize === 0) {
8090
- errorMessage = 'Private browsing is unsupported';
8090
+ caughtError = new Error('Private browsing is unsupported');
8091
8091
  }
8092
8092
  else {
8093
- errorMessage = error.message;
8093
+ // Preserve a real Error (stack/name); wrap anything else so the
8094
+ // error path always surfaces an Error, never a bare string.
8095
+ caughtError = error instanceof Error ? error : new Error(error?.message ?? String(error));
8094
8096
  }
8095
8097
  }
8096
8098
  if (resp) {
@@ -8104,10 +8106,21 @@ class PersistentStorage {
8104
8106
  options.success(data, options);
8105
8107
  }
8106
8108
  }
8109
+ else if (method === 'read' && !caughtError) {
8110
+ // A read that finds no record is a normal empty result (first run /
8111
+ // empty storage), not an error. Resolve with `null` so the callback
8112
+ // and promise forms of fetch() agree and a first-run read never
8113
+ // rejects.
8114
+ if (options && options.success) {
8115
+ options.success(null, options);
8116
+ }
8117
+ }
8107
8118
  else {
8108
- errorMessage = errorMessage ? errorMessage : 'Record Not Found';
8119
+ // A genuine failure (storage threw, or a write produced no result):
8120
+ // surface a real Error so consumers get a stack and `instanceof Error`
8121
+ // holds.
8109
8122
  if (options && options.error) {
8110
- options.error(errorMessage);
8123
+ options.error(caughtError ?? new Error('Record Not Found'));
8111
8124
  }
8112
8125
  }
8113
8126
  }
@@ -11046,18 +11059,18 @@ class Model extends EventEmitterObject {
11046
11059
  this.initialize.apply(this, arguments);
11047
11060
  // Reset changed after initial set
11048
11061
  this.changed = {};
11049
- // When autoSync is on, `initialized` is always a Promise so callers can
11050
- // uniformly `await model.initialized`. It only hydrates from storage when
11062
+ // When autoSync is on, `hydrated` is always a Promise so callers can
11063
+ // uniformly `await model.hydrated`. It only hydrates from storage when
11051
11064
  // there's something to load (storage configured and the model isn't new);
11052
11065
  // otherwise it's already ready and resolves immediately.
11053
11066
  if (this.autoSync) {
11054
11067
  if (getStorage(this) && !this.isNew()) {
11055
11068
  __classPrivateFieldSet(this, _Model_state, 'hydrating', "f");
11056
- this.initialized = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
11069
+ this.hydrated = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
11057
11070
  }
11058
11071
  else {
11059
11072
  __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11060
- this.initialized = Promise.resolve();
11073
+ this.hydrated = Promise.resolve();
11061
11074
  }
11062
11075
  }
11063
11076
  else {
@@ -11230,7 +11243,7 @@ class Model extends EventEmitterObject {
11230
11243
  // Guard must come after arg normalization: when called as set(obj, opts),
11231
11244
  // fromStorage lives in `val` (second arg), not `options` (third arg).
11232
11245
  if (this.autoSync && __classPrivateFieldGet(this, _Model_state, "f") === 'hydrating' && !options.fromStorage) {
11233
- throw new Error(`Skeletor: set() called on an autoSync model before initialized resolved. Await model.initialized first.`);
11246
+ throw new Error(`Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`);
11234
11247
  }
11235
11248
  // Run validation.
11236
11249
  if (!this._validate(attrs, options))
@@ -11606,12 +11619,6 @@ _Model_computedCache = new WeakMap(), _Model_computedDefs = new WeakMap(), _Mode
11606
11619
  try {
11607
11620
  await this.fetch({ fromStorage: true, promise: true });
11608
11621
  }
11609
- catch (e) {
11610
- // 'Record Not Found' is a normal first-run state (nothing stored yet);
11611
- // keep the initial attributes and resolve. Re-throw anything else.
11612
- if (e !== 'Record Not Found')
11613
- throw e;
11614
- }
11615
11622
  finally {
11616
11623
  __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11617
11624
  }
@@ -11676,11 +11683,11 @@ class Collection extends EventEmitterObject {
11676
11683
  if (models)
11677
11684
  this.reset(models, Object.assign({ silent: true }, options));
11678
11685
  this[Symbol.iterator] = this.values;
11679
- // When autoSync is on, `initialized` is always a Promise so callers can
11680
- // uniformly `await collection.initialized`; it resolves immediately when
11686
+ // When autoSync is on, `hydrated` is always a Promise so callers can
11687
+ // uniformly `await collection.hydrated`; it resolves immediately when
11681
11688
  // there's no storage to hydrate from.
11682
11689
  if (this.autoSync) {
11683
- this.initialized = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
11690
+ this.hydrated = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
11684
11691
  }
11685
11692
  }
11686
11693
  get comparator() {
@@ -11712,7 +11719,7 @@ class Collection extends EventEmitterObject {
11712
11719
  }
11713
11720
  /**
11714
11721
  * Override to return `true` to automatically load the collection from
11715
- * storage on construction. Await `initialized` to know when hydration
11722
+ * storage on construction. Await `hydrated` to know when hydration
11716
11723
  * is complete.
11717
11724
  *
11718
11725
  * Note: collection `autoSync` is hydrate-only. Unlike `Model`, it does
@@ -12386,8 +12393,9 @@ class Collection extends EventEmitterObject {
12386
12393
  }
12387
12394
  _Collection_instances = new WeakSet(), _Collection_hydrate = async function _Collection_hydrate() {
12388
12395
  ensureUnloadListener(PersistentStorage);
12389
- // findAll() returns [] for an empty store, so a first-run collection
12390
- // resolves rather than erroring the way a model fetch-by-id would.
12396
+ // An empty store is a normal first-run state: findAll() returns [] and a
12397
+ // missing model record resolves to null, so hydration resolves rather
12398
+ // than rejecting.
12391
12399
  await this.fetch({ fromStorage: true, promise: true });
12392
12400
  }, _a = Symbol.iterator;
12393
12401
  // This "enum" defines the three possible kinds of values which can be emitted
@@ -8026,7 +8026,7 @@ class PersistentStorage {
8026
8026
  // eslint-disable-next-line @typescript-eslint/no-this-alias
8027
8027
  const that = this;
8028
8028
  async function localSync(method, model, options) {
8029
- let resp, errorMessage, promise, new_attributes;
8029
+ let resp, caughtError, promise, new_attributes;
8030
8030
  // We get the collection (and if necessary the model attribute.
8031
8031
  // Waiting for storeInitialized will cause another iteration of
8032
8032
  // the event loop, after which the collection reference will
@@ -8083,10 +8083,12 @@ class PersistentStorage {
8083
8083
  catch (error) {
8084
8084
  const storageSize = await that.getStorageSize();
8085
8085
  if (error.code === 22 && storageSize === 0) {
8086
- errorMessage = 'Private browsing is unsupported';
8086
+ caughtError = new Error('Private browsing is unsupported');
8087
8087
  }
8088
8088
  else {
8089
- errorMessage = error.message;
8089
+ // Preserve a real Error (stack/name); wrap anything else so the
8090
+ // error path always surfaces an Error, never a bare string.
8091
+ caughtError = error instanceof Error ? error : new Error(error?.message ?? String(error));
8090
8092
  }
8091
8093
  }
8092
8094
  if (resp) {
@@ -8100,10 +8102,21 @@ class PersistentStorage {
8100
8102
  options.success(data, options);
8101
8103
  }
8102
8104
  }
8105
+ else if (method === 'read' && !caughtError) {
8106
+ // A read that finds no record is a normal empty result (first run /
8107
+ // empty storage), not an error. Resolve with `null` so the callback
8108
+ // and promise forms of fetch() agree and a first-run read never
8109
+ // rejects.
8110
+ if (options && options.success) {
8111
+ options.success(null, options);
8112
+ }
8113
+ }
8103
8114
  else {
8104
- errorMessage = errorMessage ? errorMessage : 'Record Not Found';
8115
+ // A genuine failure (storage threw, or a write produced no result):
8116
+ // surface a real Error so consumers get a stack and `instanceof Error`
8117
+ // holds.
8105
8118
  if (options && options.error) {
8106
- options.error(errorMessage);
8119
+ options.error(caughtError ?? new Error('Record Not Found'));
8107
8120
  }
8108
8121
  }
8109
8122
  }
@@ -11042,18 +11055,18 @@ class Model extends EventEmitterObject {
11042
11055
  this.initialize.apply(this, arguments);
11043
11056
  // Reset changed after initial set
11044
11057
  this.changed = {};
11045
- // When autoSync is on, `initialized` is always a Promise so callers can
11046
- // uniformly `await model.initialized`. It only hydrates from storage when
11058
+ // When autoSync is on, `hydrated` is always a Promise so callers can
11059
+ // uniformly `await model.hydrated`. It only hydrates from storage when
11047
11060
  // there's something to load (storage configured and the model isn't new);
11048
11061
  // otherwise it's already ready and resolves immediately.
11049
11062
  if (this.autoSync) {
11050
11063
  if (getStorage(this) && !this.isNew()) {
11051
11064
  __classPrivateFieldSet(this, _Model_state, 'hydrating', "f");
11052
- this.initialized = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
11065
+ this.hydrated = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
11053
11066
  }
11054
11067
  else {
11055
11068
  __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11056
- this.initialized = Promise.resolve();
11069
+ this.hydrated = Promise.resolve();
11057
11070
  }
11058
11071
  }
11059
11072
  else {
@@ -11226,7 +11239,7 @@ class Model extends EventEmitterObject {
11226
11239
  // Guard must come after arg normalization: when called as set(obj, opts),
11227
11240
  // fromStorage lives in `val` (second arg), not `options` (third arg).
11228
11241
  if (this.autoSync && __classPrivateFieldGet(this, _Model_state, "f") === 'hydrating' && !options.fromStorage) {
11229
- throw new Error(`Skeletor: set() called on an autoSync model before initialized resolved. Await model.initialized first.`);
11242
+ throw new Error(`Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`);
11230
11243
  }
11231
11244
  // Run validation.
11232
11245
  if (!this._validate(attrs, options))
@@ -11602,12 +11615,6 @@ _Model_computedCache = new WeakMap(), _Model_computedDefs = new WeakMap(), _Mode
11602
11615
  try {
11603
11616
  await this.fetch({ fromStorage: true, promise: true });
11604
11617
  }
11605
- catch (e) {
11606
- // 'Record Not Found' is a normal first-run state (nothing stored yet);
11607
- // keep the initial attributes and resolve. Re-throw anything else.
11608
- if (e !== 'Record Not Found')
11609
- throw e;
11610
- }
11611
11618
  finally {
11612
11619
  __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11613
11620
  }
@@ -11672,11 +11679,11 @@ class Collection extends EventEmitterObject {
11672
11679
  if (models)
11673
11680
  this.reset(models, Object.assign({ silent: true }, options));
11674
11681
  this[Symbol.iterator] = this.values;
11675
- // When autoSync is on, `initialized` is always a Promise so callers can
11676
- // uniformly `await collection.initialized`; it resolves immediately when
11682
+ // When autoSync is on, `hydrated` is always a Promise so callers can
11683
+ // uniformly `await collection.hydrated`; it resolves immediately when
11677
11684
  // there's no storage to hydrate from.
11678
11685
  if (this.autoSync) {
11679
- this.initialized = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
11686
+ this.hydrated = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
11680
11687
  }
11681
11688
  }
11682
11689
  get comparator() {
@@ -11708,7 +11715,7 @@ class Collection extends EventEmitterObject {
11708
11715
  }
11709
11716
  /**
11710
11717
  * Override to return `true` to automatically load the collection from
11711
- * storage on construction. Await `initialized` to know when hydration
11718
+ * storage on construction. Await `hydrated` to know when hydration
11712
11719
  * is complete.
11713
11720
  *
11714
11721
  * Note: collection `autoSync` is hydrate-only. Unlike `Model`, it does
@@ -12382,8 +12389,9 @@ class Collection extends EventEmitterObject {
12382
12389
  }
12383
12390
  _Collection_instances = new WeakSet(), _Collection_hydrate = async function _Collection_hydrate() {
12384
12391
  ensureUnloadListener(PersistentStorage);
12385
- // findAll() returns [] for an empty store, so a first-run collection
12386
- // resolves rather than erroring the way a model fetch-by-id would.
12392
+ // An empty store is a normal first-run state: findAll() returns [] and a
12393
+ // missing model record resolves to null, so hydration resolves rather
12394
+ // than rejecting.
12387
12395
  await this.fetch({ fromStorage: true, promise: true });
12388
12396
  }, _a = Symbol.iterator;
12389
12397
  // This "enum" defines the three possible kinds of values which can be emitted
package/package.json CHANGED
@@ -36,7 +36,7 @@
36
36
  "contributors": [
37
37
  "JC Brand"
38
38
  ],
39
- "version": "3.1.2",
39
+ "version": "3.1.4",
40
40
  "engines": {
41
41
  "node": ">=22.0.0"
42
42
  },
package/src/collection.ts CHANGED
@@ -40,7 +40,7 @@ export class Collection<T extends Model = Model> extends EventEmitterObject {
40
40
  _storage?: PersistentStorage;
41
41
  _comparator?: Comparator<T>;
42
42
  _url: string = '';
43
- initialized?: Promise<void>;
43
+ hydrated?: Promise<void>;
44
44
  models: T[];
45
45
  protected _byId: Record<string, T>;
46
46
  protected _model?: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T;
@@ -61,11 +61,11 @@ export class Collection<T extends Model = Model> extends EventEmitterObject {
61
61
  if (models) this.reset(models, Object.assign({ silent: true }, options));
62
62
 
63
63
  this[Symbol.iterator] = this.values;
64
- // When autoSync is on, `initialized` is always a Promise so callers can
65
- // uniformly `await collection.initialized`; it resolves immediately when
64
+ // When autoSync is on, `hydrated` is always a Promise so callers can
65
+ // uniformly `await collection.hydrated`; it resolves immediately when
66
66
  // there's no storage to hydrate from.
67
67
  if (this.autoSync) {
68
- this.initialized = getStorage(this) ? this.#hydrate() : Promise.resolve();
68
+ this.hydrated = getStorage(this) ? this.#hydrate() : Promise.resolve();
69
69
  }
70
70
  }
71
71
 
@@ -104,7 +104,7 @@ export class Collection<T extends Model = Model> extends EventEmitterObject {
104
104
 
105
105
  /**
106
106
  * Override to return `true` to automatically load the collection from
107
- * storage on construction. Await `initialized` to know when hydration
107
+ * storage on construction. Await `hydrated` to know when hydration
108
108
  * is complete.
109
109
  *
110
110
  * Note: collection `autoSync` is hydrate-only. Unlike `Model`, it does
@@ -119,8 +119,9 @@ export class Collection<T extends Model = Model> extends EventEmitterObject {
119
119
 
120
120
  async #hydrate(): Promise<void> {
121
121
  ensureUnloadListener(PersistentStorage);
122
- // findAll() returns [] for an empty store, so a first-run collection
123
- // resolves rather than erroring the way a model fetch-by-id would.
122
+ // An empty store is a normal first-run state: findAll() returns [] and a
123
+ // missing model record resolves to null, so hydration resolves rather
124
+ // than rejecting.
124
125
  await this.fetch({ fromStorage: true, promise: true });
125
126
  }
126
127
 
package/src/model.ts CHANGED
@@ -58,7 +58,7 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
58
58
  cid: string;
59
59
  collection?: Collection;
60
60
  id: string | number;
61
- initialized?: Promise<void>;
61
+ hydrated?: Promise<void>;
62
62
  validationError: string | number | null = null;
63
63
 
64
64
  /**
@@ -102,17 +102,17 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
102
102
  // Reset changed after initial set
103
103
  this.changed = {};
104
104
 
105
- // When autoSync is on, `initialized` is always a Promise so callers can
106
- // uniformly `await model.initialized`. It only hydrates from storage when
105
+ // When autoSync is on, `hydrated` is always a Promise so callers can
106
+ // uniformly `await model.hydrated`. It only hydrates from storage when
107
107
  // there's something to load (storage configured and the model isn't new);
108
108
  // otherwise it's already ready and resolves immediately.
109
109
  if (this.autoSync) {
110
110
  if (getStorage(this) && !this.isNew()) {
111
111
  this.#state = 'hydrating';
112
- this.initialized = this.#hydrate();
112
+ this.hydrated = this.#hydrate();
113
113
  } else {
114
114
  this.#state = 'ready';
115
- this.initialized = Promise.resolve();
115
+ this.hydrated = Promise.resolve();
116
116
  }
117
117
  } else {
118
118
  this.#state = 'ready';
@@ -164,10 +164,6 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
164
164
  ensureUnloadListener(PersistentStorage);
165
165
  try {
166
166
  await this.fetch({ fromStorage: true, promise: true });
167
- } catch (e) {
168
- // 'Record Not Found' is a normal first-run state (nothing stored yet);
169
- // keep the initial attributes and resolve. Re-throw anything else.
170
- if (e !== 'Record Not Found') throw e;
171
167
  } finally {
172
168
  this.#state = 'ready';
173
169
  }
@@ -351,7 +347,7 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
351
347
  // fromStorage lives in `val` (second arg), not `options` (third arg).
352
348
  if (this.autoSync && this.#state === 'hydrating' && !options.fromStorage) {
353
349
  throw new Error(
354
- `Skeletor: set() called on an autoSync model before initialized resolved. Await model.initialized first.`,
350
+ `Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`,
355
351
  );
356
352
  }
357
353
 
package/src/storage.ts CHANGED
@@ -125,7 +125,7 @@ class PersistentStorage {
125
125
  if (!NodeSQLiteStorage) {
126
126
  throw new Error(
127
127
  "Skeletor.storage: the 'node' storage type is only available in Node.js builds. " +
128
- "Import from '@converse/skeletor/node' instead of '@converse/skeletor'."
128
+ "Import from '@converse/skeletor/node' instead of '@converse/skeletor'.",
129
129
  );
130
130
  }
131
131
  this.store = new NodeSQLiteStorage(this.name);
@@ -166,7 +166,7 @@ class PersistentStorage {
166
166
  const that = this;
167
167
 
168
168
  async function localSync(method: SyncOperation, model: Model, options: SyncOptions) {
169
- let resp: any, errorMessage: string | undefined, promise: Promise<any>, new_attributes: any;
169
+ let resp: any, caughtError: Error | undefined, promise: Promise<any>, new_attributes: any;
170
170
 
171
171
  // We get the collection (and if necessary the model attribute.
172
172
  // Waiting for storeInitialized will cause another iteration of
@@ -223,9 +223,11 @@ class PersistentStorage {
223
223
  } catch (error: any) {
224
224
  const storageSize = await that.getStorageSize();
225
225
  if (error.code === 22 && storageSize === 0) {
226
- errorMessage = 'Private browsing is unsupported';
226
+ caughtError = new Error('Private browsing is unsupported');
227
227
  } else {
228
- errorMessage = error.message;
228
+ // Preserve a real Error (stack/name); wrap anything else so the
229
+ // error path always surfaces an Error, never a bare string.
230
+ caughtError = error instanceof Error ? error : new Error(error?.message ?? String(error));
229
231
  }
230
232
  }
231
233
 
@@ -239,10 +241,20 @@ class PersistentStorage {
239
241
  const data = method === 'read' ? resp : null;
240
242
  options.success(data, options);
241
243
  }
244
+ } else if (method === 'read' && !caughtError) {
245
+ // A read that finds no record is a normal empty result (first run /
246
+ // empty storage), not an error. Resolve with `null` so the callback
247
+ // and promise forms of fetch() agree and a first-run read never
248
+ // rejects.
249
+ if (options && options.success) {
250
+ options.success(null, options);
251
+ }
242
252
  } else {
243
- errorMessage = errorMessage ? errorMessage : 'Record Not Found';
253
+ // A genuine failure (storage threw, or a write produced no result):
254
+ // surface a real Error so consumers get a stack and `instanceof Error`
255
+ // holds.
244
256
  if (options && options.error) {
245
- options.error(errorMessage);
257
+ options.error(caughtError ?? new Error('Record Not Found'));
246
258
  }
247
259
  }
248
260
  }