@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/dist/skeletor.node.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
8090
|
+
caughtError = new Error('Private browsing is unsupported');
|
|
8091
8091
|
}
|
|
8092
8092
|
else {
|
|
8093
|
-
|
|
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
|
-
|
|
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(
|
|
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, `
|
|
11050
|
-
// uniformly `await model.
|
|
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.
|
|
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.
|
|
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
|
|
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, `
|
|
11680
|
-
// uniformly `await collection.
|
|
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.
|
|
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 `
|
|
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
|
-
//
|
|
12390
|
-
//
|
|
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
|
package/dist/skeletor.node.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
8086
|
+
caughtError = new Error('Private browsing is unsupported');
|
|
8087
8087
|
}
|
|
8088
8088
|
else {
|
|
8089
|
-
|
|
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
|
-
|
|
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(
|
|
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, `
|
|
11046
|
-
// uniformly `await model.
|
|
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.
|
|
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.
|
|
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
|
|
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, `
|
|
11676
|
-
// uniformly `await collection.
|
|
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.
|
|
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 `
|
|
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
|
-
//
|
|
12386
|
-
//
|
|
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
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
|
-
|
|
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, `
|
|
65
|
-
// uniformly `await collection.
|
|
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.
|
|
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 `
|
|
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
|
-
//
|
|
123
|
-
//
|
|
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
|
-
|
|
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, `
|
|
106
|
-
// uniformly `await model.
|
|
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.
|
|
112
|
+
this.hydrated = this.#hydrate();
|
|
113
113
|
} else {
|
|
114
114
|
this.#state = 'ready';
|
|
115
|
-
this.
|
|
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
|
|
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,
|
|
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
|
-
|
|
226
|
+
caughtError = new Error('Private browsing is unsupported');
|
|
227
227
|
} else {
|
|
228
|
-
|
|
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
|
-
|
|
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(
|
|
257
|
+
options.error(caughtError ?? new Error('Record Not Found'));
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
260
|
}
|