@expressive/mvc 0.86.0 → 0.88.0

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/dist/state.js CHANGED
@@ -1,6 +1,6 @@
1
- import { Caught } from "./caught.js";
1
+ import { REPORT } from "./dispatch.js";
2
2
  import { capture, event, listener, observer, queued, touch, watch } from "./observable.js";
3
- import { Context, join } from "./context.js";
3
+ import { Context, host, join } from "./context.js";
4
4
 
5
5
  //#region src/state.ts
6
6
  const define = Object.defineProperty;
@@ -11,7 +11,6 @@ const STORE = /* @__PURE__ */ new WeakMap();
11
11
  /** States under construction - added on `new`, removed when activated or released.
12
12
  * Also carries work to run at the deadline, which `def` uses to bound its registry. */
13
13
  const PENDING = /* @__PURE__ */ new Set();
14
- /** External lifecycle listeners for any given State class. */
15
14
  const SETUP = /* @__PURE__ */ new WeakMap();
16
15
  /** Parent-child relationships. */
17
16
  const PARENT = /* @__PURE__ */ new WeakMap();
@@ -21,6 +20,10 @@ const UNBIND = /* @__PURE__ */ new WeakMap();
21
20
  const METHODS = /* @__PURE__ */ new WeakMap();
22
21
  /** List of reactive getters defined by a given type. */
23
22
  const GETTERS = /* @__PURE__ */ new WeakMap();
23
+ /** Replacement for a getter captured by live computed properties, for hot patching. */
24
+ const LATEST = /* @__PURE__ */ new WeakMap();
25
+ /** Event every observer treats as watched, forcing a refresh after a hot patch. */
26
+ const REFRESH = Symbol.for("@expressive/mvc.refresh");
24
27
  /** Stale flags for compute closures awaiting refresh on next access. */
25
28
  const STALE = /* @__PURE__ */ new WeakSet();
26
29
  /** Whether the deadline which drains `PENDING` is already queued for this tick. */
@@ -31,6 +34,8 @@ let ANNOUNCED = false;
31
34
  /** Adopters for managed properties which have held a child State. */
32
35
  const ADOPT = /* @__PURE__ */ new WeakMap();
33
36
  const CHILDREN = /* @__PURE__ */ new WeakMap();
37
+ const OWNS = /* @__PURE__ */ new WeakMap();
38
+ const OWNED = /* @__PURE__ */ new WeakMap();
34
39
  var State = class State {
35
40
  /**
36
41
  * Whether an instance activated with no enclosing context registers itself
@@ -51,7 +56,8 @@ var State = class State {
51
56
  get(arg1, arg2, arg3) {
52
57
  const self = this.is;
53
58
  if (arg1 === void 0) return values(self);
54
- if (State.is(arg1)) return Context.get(self).get(arg1, arg2, arg3, self);
59
+ if (arg1 === State) return typeof arg2 == "function" ? owned(self, arg2) : observed(this, arg1, owner(self, arg2));
60
+ if (State.is(arg1)) return observed(this, arg1, Context.get(self).get(arg1, arg2, arg3, self));
55
61
  if (typeof arg1 == "function") return watch(self, unbind(arg1));
56
62
  if (typeof arg2 == "function") return callback(self, arg2, arg1);
57
63
  if (arg1 === null) return observer(self) === null;
@@ -60,7 +66,7 @@ var State = class State {
60
66
  set(arg1, arg2) {
61
67
  const self = this.is;
62
68
  if (typeof arg1 == "function") return listener(self, (key) => {
63
- if (typeof key == "string" || typeof key == "number" || typeof key == "symbol") return arg1.call(self, key, self);
69
+ if (typeof key == "string" || typeof key == "number" || typeof key == "symbol" && key !== REFRESH) return arg1.call(self, key, self);
64
70
  });
65
71
  if (typeof arg2 == "function") return callback(self, arg2, arg1);
66
72
  if (arg1 && typeof arg1 == "object") assign(self, arg1, arg2 === true);
@@ -96,18 +102,8 @@ var State = class State {
96
102
  static is(maybe) {
97
103
  return maybe === this || typeof maybe == "function" && maybe.prototype instanceof this;
98
104
  }
99
- /**
100
- * Register a lifecycle handler for this State and its subclasses.
101
- *
102
- * A bare function is per-instance setup run in the `prepare` phase (sugar for
103
- * `{ before }`); if it returns a function, that runs when the instance is
104
- * destroyed. Pass a {@link State.On} object to hook by cadence - `type`
105
- * (per-class, at bootstrap), `before` (per-instance, before `new()`), and
106
- * `after` (per-instance, at the `new()` slot).
107
- *
108
- * @returns Function to remove the handler.
109
- */
110
105
  static on(handler) {
106
+ if (typeof handler == "function") handler = { setup: handler };
111
107
  let setup = SETUP.get(this);
112
108
  if (!setup) SETUP.set(this, setup = /* @__PURE__ */ new Set());
113
109
  setup.add(handler);
@@ -120,6 +116,9 @@ define(State.prototype, "toString", { value() {
120
116
  define(State, "toString", { value() {
121
117
  return this.name;
122
118
  } });
119
+ function observed(from, key, value) {
120
+ return value instanceof State ? touch(from, key, value) : value;
121
+ }
123
122
  /** Register a user OnEvent callback, preserving `this` and `source`. */
124
123
  function callback(self, cb, select) {
125
124
  return listener(self, (key) => cb.call(self, key, self), select);
@@ -141,7 +140,9 @@ function init(state, ...args) {
141
140
  function observe() {
142
141
  for (const key in state) {
143
142
  const desc = Object.getOwnPropertyDescriptor(state, key) || {};
144
- if ("value" in desc && desc.configurable) apply(state, key, desc, true);
143
+ if (!("value" in desc) || !desc.configurable) continue;
144
+ if (key[0] == "_") unmanaged(state, key, desc.value);
145
+ else apply(state, key, desc, true);
145
146
  }
146
147
  }
147
148
  function register() {
@@ -150,13 +151,20 @@ function init(state, ...args) {
150
151
  const g = type.global;
151
152
  if (!(typeof g == "function" ? g(state) : g)) return;
152
153
  if (!Object.prototype.hasOwnProperty.call(type, "global")) throw new Error(`${state} would register as a global by inheritance alone - re-declare \`static global\` on ${type.name} (\`true\` to keep it, \`false\` to opt out).`);
154
+ for (let T = type; T !== State; T = Object.getPrototypeOf(T)) {
155
+ const found = Context.root.get(T, false);
156
+ if (found && (T === type || found.constructor === T)) throw new Error(`Cannot register ${state} as a global - ${found} already exists in root. Destroy the existing instance first, or provide additional ones via explicit context.`);
157
+ }
153
158
  return Context.root.add(state);
154
159
  }
155
160
  listener(state, (key) => {
156
161
  const rest = trailing(state);
157
162
  PENDING.delete(state);
158
163
  if (key === null) return null;
159
- parent(state, null);
164
+ if (!PARENT.has(state)) parent(state, host(state) || null);
165
+ const above = PARENT.get(state);
166
+ const owns = above && (OWNS.get(above) || OWNS.set(above, /* @__PURE__ */ new Set()).get(above));
167
+ if (owns) listener(state, () => void owns.delete(state), null);
160
168
  const queue = [
161
169
  ...before,
162
170
  observe,
@@ -169,12 +177,16 @@ function init(state, ...args) {
169
177
  for (let i = 0; i < queue.length; i++) {
170
178
  const arg = queue[i];
171
179
  const out = typeof arg == "function" ? arg.call(state, state) : arg;
172
- if (out instanceof Promise) out.catch((err) => report(new Caught.Init(state, err)));
180
+ if (out instanceof Promise) out.catch((err) => report("setup", state, err));
173
181
  else if (Array.isArray(out)) queue.splice(i + 1, 0, ...out);
174
182
  else if (typeof out == "function") listener(state, out, null);
175
183
  else if (typeof out == "object") assign(state, out, true);
176
184
  }
177
185
  });
186
+ if (owns) {
187
+ owns.add(state);
188
+ OWNED.get(above)?.forEach((cb) => cb(state));
189
+ }
178
190
  let end = rest.length;
179
191
  while (end-- > 0 && PENDING.has(rest[end]));
180
192
  for (let i = 0; i < end; i++) PENDING.delete(rest[i]);
@@ -185,12 +197,22 @@ function init(state, ...args) {
185
197
  queueMicrotask(() => {
186
198
  QUEUED = false;
187
199
  for (const item of PENDING) if (typeof item == "function") item();
188
- else report(new Caught.Inactive(item));
200
+ else report("unused", item, /* @__PURE__ */ new Error(`${item} was constructed but never activated.`));
189
201
  PENDING.clear();
190
202
  });
191
203
  }
192
204
  PENDING.add(state);
193
205
  }
206
+ function unmanaged(state, key, value) {
207
+ define(state, key, {
208
+ configurable: true,
209
+ enumerable: false,
210
+ get: () => value,
211
+ set: (next) => {
212
+ value = next;
213
+ }
214
+ });
215
+ }
194
216
  /**
195
217
  * Apply instructions and inherited event listeners. Ensure class metadata is ready.
196
218
  *
@@ -210,10 +232,9 @@ function bootstrap(T) {
210
232
  T = Object.getPrototypeOf(T);
211
233
  }
212
234
  for (const type of chain) {
213
- for (const handler of SETUP.get(type) || []) if (typeof handler == "function") before.add(handler);
214
- else {
215
- if (handler.before) before.add(handler.before);
216
- if (handler.after) after.add(handler.after);
235
+ for (const handler of SETUP.get(type) || []) {
236
+ if (handler.setup) before.add(handler.setup);
237
+ if (handler.ready) after.add(handler.ready);
217
238
  if (handler.type) onType.add(handler.type);
218
239
  }
219
240
  if (type === State) continue;
@@ -225,34 +246,7 @@ function bootstrap(T) {
225
246
  for (const setupType of onType) setupType(type);
226
247
  METHODS.set(type, keys = new Map(keys));
227
248
  GETTERS.set(type, getters = new Map(getters));
228
- for (const [key, desc] of Object.entries(Object.getOwnPropertyDescriptors(type.prototype))) {
229
- if (key == "constructor" || !desc.configurable) continue;
230
- if (typeof desc.get == "function") {
231
- if (desc.configurable && typeof desc.set != "function") getters.set(key, desc.get);
232
- continue;
233
- }
234
- const { value } = desc;
235
- if (typeof value !== "function") continue;
236
- function bind(original) {
237
- const { is } = this;
238
- if (is.hasOwnProperty(key) && !original) return value;
239
- const fn = original || value;
240
- const bound = fn.bind(is);
241
- UNBIND.set(bound, fn);
242
- define(is, key, {
243
- value: bound,
244
- writable: true,
245
- configurable: true
246
- });
247
- return bound;
248
- }
249
- UNBIND.set(bind, value);
250
- keys.set(key, bind);
251
- define(type.prototype, key, {
252
- get: bind,
253
- set: bind
254
- });
255
- }
249
+ classify(type, keys, getters);
256
250
  }
257
251
  if (getters.size) before.add((self) => getters.forEach(compute, self));
258
252
  return {
@@ -260,6 +254,57 @@ function bootstrap(T) {
260
254
  after
261
255
  };
262
256
  }
257
+ /** Bind own configurable methods of a type and collect its reactive getters. */
258
+ function classify(type, keys, getters) {
259
+ for (const [key, desc] of Object.entries(Object.getOwnPropertyDescriptors(type.prototype))) {
260
+ if (key == "constructor" || !desc.configurable) continue;
261
+ const { get, set } = desc;
262
+ if (key[0] == "_" && (get || set)) {
263
+ define(type.prototype, key, {
264
+ ...desc,
265
+ get: get && function() {
266
+ return get.call(this.is || this);
267
+ },
268
+ set: set && function(value) {
269
+ set.call(this.is || this, value);
270
+ }
271
+ });
272
+ continue;
273
+ }
274
+ if (typeof get == "function") {
275
+ if (typeof set != "function") getters.set(key, get);
276
+ continue;
277
+ }
278
+ const { value } = desc;
279
+ if (typeof value !== "function") continue;
280
+ function bind(original) {
281
+ const { is } = this;
282
+ if (is.hasOwnProperty(key) && !original) return value;
283
+ const fn = original || value;
284
+ const bound = fn.bind(is);
285
+ UNBIND.set(bound, fn);
286
+ define(is, key, {
287
+ value: bound,
288
+ writable: true,
289
+ configurable: true
290
+ });
291
+ for (const handler of stages(is.constructor, "method")) handler.call(is, key, bound, is);
292
+ return bound;
293
+ }
294
+ UNBIND.set(bind, value);
295
+ keys.set(key, bind);
296
+ define(type.prototype, key, {
297
+ get: bind,
298
+ set: bind
299
+ });
300
+ }
301
+ }
302
+ /** Handlers of one stage along the class chain - ancestor first, in registration order, each once. */
303
+ function stages(T, key) {
304
+ const found = T === State ? /* @__PURE__ */ new Set() : stages(Object.getPrototypeOf(T), key);
305
+ for (const handler of SETUP.get(T) || []) if (handler[key]) found.add(handler[key]);
306
+ return found;
307
+ }
263
308
  /**
264
309
  * Install a reactive computed property on a state instance, derived from a prototype
265
310
  * getter or an arity-bearing `set` factory.
@@ -288,13 +333,13 @@ function compute(getter, key) {
288
333
  const run = (initial) => {
289
334
  let next;
290
335
  try {
291
- next = getter.call(proxy, proxy);
336
+ next = latest(getter).call(proxy, proxy);
292
337
  } catch (err) {
293
338
  if (initial) {
294
339
  console.warn(`An exception was thrown while initializing ${this}.${key}.`);
295
340
  throw err;
296
341
  }
297
- if (!(err instanceof Promise)) report(new Caught.Getter(this, key, err));
342
+ if (!(err instanceof Promise)) report("getter", this, err, key);
298
343
  }
299
344
  update(this, key, next, !isAsync);
300
345
  };
@@ -416,6 +461,26 @@ function children(state, callback) {
416
461
  set.add(callback);
417
462
  return () => set.delete(callback);
418
463
  }
464
+ function owner(state, required) {
465
+ const found = PARENT.get(state);
466
+ if (found || required === false) return found || void 0;
467
+ throw new Error(`${state} has no owner.`);
468
+ }
469
+ /**
470
+ * Report States owned by this one - those active now, then each one as it
471
+ * activates. A function returned by `callback` runs when that State is destroyed.
472
+ */
473
+ function owned(state, callback) {
474
+ function each(child) {
475
+ const done = callback(child);
476
+ if (typeof done == "function") listener(child, () => void done(), null);
477
+ }
478
+ OWNS.get(state)?.forEach(each);
479
+ let set = OWNED.get(state);
480
+ if (!set) OWNED.set(state, set = /* @__PURE__ */ new Set());
481
+ set.add(each);
482
+ return () => void set.delete(each);
483
+ }
419
484
  /** Currently accumulating export. Stores real values of placeholder properties such as ref() or child states. */
420
485
  let EXPORT;
421
486
  function values(state) {
@@ -470,7 +535,7 @@ function assign(state, data, silent) {
470
535
  else if (key in state && key !== "is") {
471
536
  const set = (Object.getOwnPropertyDescriptor(state, key) || {}).set;
472
537
  if (set) set.call(state, data[key], silent);
473
- else state[key] = data[key];
538
+ else Reflect.set(state, key, data[key]);
474
539
  }
475
540
  }
476
541
  }
@@ -479,15 +544,16 @@ function assign(state, data, silent) {
479
544
  *
480
545
  * This is used internally to update properties, but can also be used to update properties which are not managed by state, or to update values without triggering setters.
481
546
  *
482
- * A destroyed state reports the write - thrown to the writer unless a `catch` handler takes it - and returns `false`; `silent` skips both, and dispatch.
547
+ * A destroyed state stores the write without dispatch, reports it unless `silent`, and returns `false`.
483
548
  */
484
549
  function update(state, key, value, silent, own) {
550
+ const store = STORE.get(state);
551
+ if (value instanceof State) value = value.is;
485
552
  if (observer(state) === null) {
486
- if (!silent) report(new Caught.Destroyed(state, String(key)), true);
553
+ if (!silent) report("dead", state, /* @__PURE__ */ new Error(`Tried to update ${state}.${String(key)} but state is destroyed.`), String(key));
554
+ store[key] = value;
487
555
  return false;
488
556
  }
489
- const store = STORE.get(state);
490
- if (value instanceof State) value = value.is;
491
557
  if (key in store && value === store[key]) return false;
492
558
  store[key] = value;
493
559
  if (!silent) event(state, key);
@@ -495,40 +561,23 @@ function update(state, key, value, silent, own) {
495
561
  return true;
496
562
  }
497
563
  /**
498
- * Pass a caught error along `catch` handlers - most-derived class first, last registered
499
- * first - until one returns nothing. Passed off the end, a warning logs and anything
500
- * else is thrown - to the caller when `sync`, else uncaught. So is a handler's throw,
501
- * and one wrapping an error a handler already threw escapes that error unreported.
564
+ * Pass an error along `catch` handlers - most-derived class first, last registered
565
+ * first, one on several classes once at the outermost - until one returns nothing. Passed off
566
+ * the end, a destroyed write outputs nothing, `unused` warns, anything else escapes uncaught;
567
+ * so does a handler's throw.
502
568
  */
503
- function report(caught, sync) {
504
- const handlers = /* @__PURE__ */ new Set();
505
- let error = caught;
506
- if (caught.cause instanceof Caught) return escape(caught.cause);
507
- for (let T = caught.state.constructor;; T = Object.getPrototypeOf(T)) {
508
- for (const handler of [...SETUP.get(T) || []].reverse()) if (typeof handler == "object" && handler.catch) handlers.add(handler.catch);
509
- if (T === State) break;
510
- }
569
+ function report(kind, owner, error, key) {
570
+ const state = owner instanceof State ? owner : owner && PARENT.get(owner);
571
+ if (!state) return REPORT.error(error);
511
572
  try {
512
- for (const handler of handlers) if (!(error = handler.call(caught.state, error))) return;
573
+ for (const handler of [...stages(state.constructor, "catch")].reverse()) if ((error = handler.call(state, error, kind, key)) === void 0) return;
574
+ if (kind == "unused") console.warn(error);
575
+ else if (kind != "dead") REPORT.error(error);
513
576
  } catch (err) {
514
- if (sync) throw err;
515
- return escape(err);
577
+ REPORT.error(err);
516
578
  }
517
- if (error.warning) console.warn(error);
518
- else if (sync) throw error;
519
- else escape(error);
520
- }
521
- function escape(err) {
522
- queueMicrotask(() => {
523
- throw err;
524
- });
525
- }
526
- /** Error thrown by a handler replaying in dispatch, attributed to the State which queued it. */
527
- function fault(err, owner) {
528
- const state = owner instanceof State ? owner : owner && PARENT.get(owner);
529
- if (state) report(new Caught.Effect(state, err));
530
- else escape(err);
531
579
  }
580
+ REPORT.as = report;
532
581
  /**
533
582
  * Hand a value stored by an owning writer to that property's adopter,
534
583
  * creating one the first time a State is stored there. A property which has
@@ -548,6 +597,11 @@ function adopt(state, key, value) {
548
597
  }
549
598
  claim(value);
550
599
  }
600
+ /** Resolve a getter to its latest hot-patched replacement. */
601
+ function latest(getter) {
602
+ for (let next; next = LATEST.get(getter);) getter = next;
603
+ return getter;
604
+ }
551
605
  /** Random alphanumberic of length 6; always starts with a letter. */
552
606
  function uid() {
553
607
  return (.278 + Math.random() * .722).toString(36).substring(2, 8).toUpperCase();
@@ -563,5 +617,5 @@ function parent(child, value) {
563
617
  }
564
618
 
565
619
  //#endregion
566
- export { PENDING, STORE, State, access, apply, children, compute, event, fault, parent, uid, unbind, update };
620
+ export { GETTERS, LATEST, METHODS, PENDING, SETUP, STORE, State, UNBIND, access, adopt, apply, children, classify, compute, event, owned, parent, trailing, uid, unbind, update };
567
621
  //# sourceMappingURL=state.js.map