@converse/skeletor 4.0.0 → 4.0.1
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 +5 -0
- package/dist/skeletor.d.ts +922 -953
- package/dist/skeletor.esm.js +75 -72
- package/dist/skeletor.esm.js.map +1 -1
- package/dist/skeletor.js +75 -72
- package/dist/skeletor.js.map +1 -1
- package/dist/skeletor.min.js +5 -5
- package/dist/skeletor.node.d.ts +947 -978
- package/dist/skeletor.node.js +75 -72
- package/dist/skeletor.node.mjs +75 -72
- package/package.json +7 -10
- package/src/collection.ts +14 -9
- package/src/model.ts +85 -52
package/src/model.ts
CHANGED
|
@@ -36,6 +36,72 @@ import {
|
|
|
36
36
|
Options,
|
|
37
37
|
} from './types';
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Per-instance state that is private to this module.
|
|
41
|
+
*
|
|
42
|
+
* It lives in a WeakMap rather than in ES `#private` fields so that the
|
|
43
|
+
* published declarations carry no private members. TypeScript 6 refuses to
|
|
44
|
+
* emit an anonymous class type whose base has private members (TS4094), and
|
|
45
|
+
* that is exactly what the usual mixin pattern (`class extends Base`) over
|
|
46
|
+
* `Model` produces in consumers such as Converse.
|
|
47
|
+
*/
|
|
48
|
+
type ModelInternals = {
|
|
49
|
+
state: 'constructing' | 'hydrating' | 'ready';
|
|
50
|
+
computedCache?: Record<string, any>;
|
|
51
|
+
computedDefs?: ComputedProperties<any>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const INTERNALS = new WeakMap<Model<any>, ModelInternals>();
|
|
55
|
+
|
|
56
|
+
function internals(model: Model<any>): ModelInternals {
|
|
57
|
+
let priv = INTERNALS.get(model);
|
|
58
|
+
if (!priv) {
|
|
59
|
+
priv = { state: 'constructing' };
|
|
60
|
+
INTERNALS.set(model, priv);
|
|
61
|
+
}
|
|
62
|
+
return priv;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Load the model from storage, marking it ready whether or not the read succeeds. */
|
|
66
|
+
async function hydrate(model: Model<any>, priv: ModelInternals): Promise<void> {
|
|
67
|
+
ensureUnloadListener(PersistentStorage);
|
|
68
|
+
try {
|
|
69
|
+
await model.fetch({ fromStorage: true, promise: true });
|
|
70
|
+
} finally {
|
|
71
|
+
priv.state = 'ready';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Evaluate the model's declared computed properties once, seeding the cache. */
|
|
76
|
+
function initComputed(model: Model<any>, priv: ModelInternals): void {
|
|
77
|
+
const defs = result(model, 'computed') as ComputedProperties<any>;
|
|
78
|
+
if (!defs || isEmpty(defs)) return;
|
|
79
|
+
priv.computedDefs = defs;
|
|
80
|
+
priv.computedCache = {};
|
|
81
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
82
|
+
priv.computedCache[key] = def.fn(model);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Recompute one computed property, firing `change:key` when its value changed. */
|
|
87
|
+
function updateComputed(
|
|
88
|
+
model: Model<any>,
|
|
89
|
+
priv: ModelInternals,
|
|
90
|
+
key: string,
|
|
91
|
+
def: ComputedProperty<any>,
|
|
92
|
+
silent: boolean,
|
|
93
|
+
options: ModelOptions,
|
|
94
|
+
): void {
|
|
95
|
+
const newVal = def.fn(model);
|
|
96
|
+
if (!isEqual(priv.computedCache[key], newVal)) {
|
|
97
|
+
priv.computedCache[key] = newVal;
|
|
98
|
+
if (!silent) {
|
|
99
|
+
(model.changed as any)[key] = newVal;
|
|
100
|
+
model.trigger('change:' + key, model, newVal, options);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
39
105
|
/**
|
|
40
106
|
* @public
|
|
41
107
|
* **Models** are the basic data object in the framework --
|
|
@@ -44,9 +110,6 @@ import {
|
|
|
44
110
|
* performing computations and transformations on that data.
|
|
45
111
|
*/
|
|
46
112
|
export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmitterObject {
|
|
47
|
-
#computedCache?: Record<string, any>;
|
|
48
|
-
#computedDefs?: Record<string, ComputedProperty<this>>;
|
|
49
|
-
#state: 'constructing' | 'hydrating' | 'ready' = 'constructing';
|
|
50
113
|
_storage?: PersistentStorage;
|
|
51
114
|
_changing = false;
|
|
52
115
|
_pending: boolean | ModelOptions = false;
|
|
@@ -68,6 +131,7 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
68
131
|
*/
|
|
69
132
|
constructor(attributes?: Partial<T>, options?: ModelOptions) {
|
|
70
133
|
super();
|
|
134
|
+
const priv = internals(this);
|
|
71
135
|
let attrs: Partial<T> = attributes || ({} as Partial<T>);
|
|
72
136
|
options = options || {};
|
|
73
137
|
|
|
@@ -83,12 +147,13 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
83
147
|
|
|
84
148
|
this.set(attrs, options);
|
|
85
149
|
|
|
86
|
-
|
|
150
|
+
initComputed(this, priv);
|
|
87
151
|
|
|
88
152
|
this.attrs = new Proxy(this.attributes as T, {
|
|
89
153
|
get: (_target, key) => {
|
|
90
|
-
|
|
91
|
-
|
|
154
|
+
const cache = priv.computedCache;
|
|
155
|
+
if (typeof key !== 'symbol' && cache && (key as string) in cache) {
|
|
156
|
+
return cache[key as string];
|
|
92
157
|
}
|
|
93
158
|
return (this.attributes as any)[key];
|
|
94
159
|
},
|
|
@@ -109,14 +174,14 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
109
174
|
// otherwise it's already ready and resolves immediately.
|
|
110
175
|
if (this.autoSync) {
|
|
111
176
|
if (getStorage(this) && !this.isNew()) {
|
|
112
|
-
|
|
113
|
-
this.hydrated =
|
|
177
|
+
priv.state = 'hydrating';
|
|
178
|
+
this.hydrated = hydrate(this, priv);
|
|
114
179
|
} else {
|
|
115
|
-
|
|
180
|
+
priv.state = 'ready';
|
|
116
181
|
this.hydrated = Promise.resolve();
|
|
117
182
|
}
|
|
118
183
|
} else {
|
|
119
|
-
|
|
184
|
+
priv.state = 'ready';
|
|
120
185
|
}
|
|
121
186
|
}
|
|
122
187
|
|
|
@@ -161,19 +226,6 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
161
226
|
return 100;
|
|
162
227
|
}
|
|
163
228
|
|
|
164
|
-
async #hydrate(): Promise<void> {
|
|
165
|
-
ensureUnloadListener(PersistentStorage);
|
|
166
|
-
try {
|
|
167
|
-
await this.fetch({ fromStorage: true, promise: true });
|
|
168
|
-
} finally {
|
|
169
|
-
this.#state = 'ready';
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
#doAutoSave(): unknown {
|
|
174
|
-
return this.save(null, { noAutoSave: true });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
229
|
/**
|
|
178
230
|
* The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
|
179
231
|
* CouchDB users may want to set this to `"_id"` (by overriding this getter
|
|
@@ -226,27 +278,6 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
226
278
|
return {};
|
|
227
279
|
}
|
|
228
280
|
|
|
229
|
-
#updateComputed(key: string, def: ComputedProperty<this>, silent: boolean, options: ModelOptions): void {
|
|
230
|
-
const newVal = def.fn(this);
|
|
231
|
-
if (!isEqual(this.#computedCache[key], newVal)) {
|
|
232
|
-
this.#computedCache[key] = newVal;
|
|
233
|
-
if (!silent) {
|
|
234
|
-
(this.changed as any)[key] = newVal;
|
|
235
|
-
this.trigger('change:' + key, this, newVal, options);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
#initComputed(): void {
|
|
241
|
-
const defs = result(this, 'computed') as ComputedProperties<this>;
|
|
242
|
-
if (!defs || isEmpty(defs)) return;
|
|
243
|
-
this.#computedDefs = defs;
|
|
244
|
-
this.#computedCache = {};
|
|
245
|
-
for (const [key, def] of Object.entries(defs)) {
|
|
246
|
-
this.#computedCache[key] = def.fn(this);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
281
|
/**
|
|
251
282
|
* Return a copy of the model's `attributes` object.
|
|
252
283
|
*/
|
|
@@ -265,8 +296,9 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
265
296
|
* Get the value of an attribute, or a computed property.
|
|
266
297
|
*/
|
|
267
298
|
get<K extends keyof T>(attr: K): T[K] {
|
|
268
|
-
|
|
269
|
-
|
|
299
|
+
const cache = internals(this).computedCache;
|
|
300
|
+
if (cache && (attr as string) in cache) {
|
|
301
|
+
return cache[attr as string];
|
|
270
302
|
}
|
|
271
303
|
return this.attributes[attr];
|
|
272
304
|
}
|
|
@@ -352,10 +384,11 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
352
384
|
}
|
|
353
385
|
|
|
354
386
|
options = options || {};
|
|
387
|
+
const priv = internals(this);
|
|
355
388
|
|
|
356
389
|
// Guard must come after arg normalization: when called as set(obj, opts),
|
|
357
390
|
// fromStorage lives in `val` (second arg), not `options` (third arg).
|
|
358
|
-
if (this.autoSync &&
|
|
391
|
+
if (this.autoSync && priv.state === 'hydrating' && !options.fromStorage) {
|
|
359
392
|
throw new Error(
|
|
360
393
|
`Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`,
|
|
361
394
|
);
|
|
@@ -404,10 +437,10 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
404
437
|
}
|
|
405
438
|
|
|
406
439
|
// Recalculate computed properties whose deps changed.
|
|
407
|
-
if (changes.length &&
|
|
408
|
-
for (const [key, def] of Object.entries(
|
|
440
|
+
if (changes.length && priv.computedDefs) {
|
|
441
|
+
for (const [key, def] of Object.entries(priv.computedDefs)) {
|
|
409
442
|
if (def.deps.some((dep) => changes.includes(dep))) {
|
|
410
|
-
|
|
443
|
+
updateComputed(this, priv, key, def, silent, options);
|
|
411
444
|
}
|
|
412
445
|
}
|
|
413
446
|
}
|
|
@@ -430,14 +463,14 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
430
463
|
// were actual changes that originated from application code (not a
|
|
431
464
|
// storage read or an explicit noAutoSave call).
|
|
432
465
|
if (
|
|
433
|
-
|
|
466
|
+
priv.state === 'ready' &&
|
|
434
467
|
this.autoSync &&
|
|
435
468
|
getStorage(this) &&
|
|
436
469
|
changes.length &&
|
|
437
470
|
!options.fromStorage &&
|
|
438
471
|
!options.noAutoSave
|
|
439
472
|
) {
|
|
440
|
-
scheduleAutoSave(this, () => this
|
|
473
|
+
scheduleAutoSave(this, () => this.save(null, { noAutoSave: true }), this.autoSyncDelay);
|
|
441
474
|
}
|
|
442
475
|
|
|
443
476
|
return this;
|