@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/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
- this.#initComputed();
150
+ initComputed(this, priv);
87
151
 
88
152
  this.attrs = new Proxy(this.attributes as T, {
89
153
  get: (_target, key) => {
90
- if (typeof key !== 'symbol' && this.#computedCache && (key as string) in this.#computedCache) {
91
- return this.#computedCache[key as string];
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
- this.#state = 'hydrating';
113
- this.hydrated = this.#hydrate();
177
+ priv.state = 'hydrating';
178
+ this.hydrated = hydrate(this, priv);
114
179
  } else {
115
- this.#state = 'ready';
180
+ priv.state = 'ready';
116
181
  this.hydrated = Promise.resolve();
117
182
  }
118
183
  } else {
119
- this.#state = 'ready';
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
- if (this.#computedCache && (attr as string) in this.#computedCache) {
269
- return this.#computedCache[attr as string];
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 && this.#state === 'hydrating' && !options.fromStorage) {
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 && this.#computedDefs) {
408
- for (const [key, def] of Object.entries(this.#computedDefs)) {
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
- this.#updateComputed(key, def, silent, options);
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
- this.#state === 'ready' &&
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.#doAutoSave(), this.autoSyncDelay);
473
+ scheduleAutoSave(this, () => this.save(null, { noAutoSave: true }), this.autoSyncDelay);
441
474
  }
442
475
 
443
476
  return this;