@expressive/mvc 1.0.0-beta.44 → 1.0.0-beta.45

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/lib/index.d.ts CHANGED
@@ -18,6 +18,8 @@ declare function act<S>(action: Async<S>): typeof action & {
18
18
  active: boolean;
19
19
  };
20
20
 
21
+ declare function array<T = any>(): T[];
22
+
21
23
  type Callback = () => void;
22
24
  type Class = new (...args: any[]) => any;
23
25
  type InstanceOf<T> = T extends { prototype: infer U } ? U : never;
@@ -34,7 +36,7 @@ declare class Subscriber<T extends Stateful = any> {
34
36
  commit: () => () => void;
35
37
  active: boolean;
36
38
  dependant: Set<Listener>;
37
- watch: { [key in Model.Event<T, Model>]: true | Callback; };
39
+ watch: { [key in Model.Event<T, Model>]: boolean | Callback; };
38
40
  constructor(target: Controller<T> | T, onUpdate: Controller.OnEvent);
39
41
  }
40
42
 
@@ -200,43 +202,31 @@ declare class Controller<T extends Stateful = any> {
200
202
  * Property initializer, will run upon instance creation.
201
203
  * Optional returned callback will run when once upon first access.
202
204
  */
203
- type Instruction<T> = (this: Controller, key: string, thisArg: Controller) =>
204
- | Instruction.Getter<T>
205
- | Instruction.ExplicitDescriptor
206
- | Instruction.Descriptor<T>
207
- | Instruction.RecursiveDescriptor<T>
208
- | boolean
209
- | void;
210
-
205
+ declare type Instruction<T> = (this: Controller, key: string, thisArg: Controller) => Instruction.Getter<T> | Instruction.ExplicitDescriptor | Instruction.Descriptor<T> | Instruction.RecursiveDescriptor<T> | boolean | void;
211
206
  declare namespace Instruction {
212
- type Getter<T> = (state: T, within?: Subscriber) => T;
213
- type Setter<T> = (value: T, state: any) => boolean | void;
214
-
215
- type Runner<T> = (this: Controller, key: string, on: Controller) => Instruction.Descriptor<T> | boolean | undefined;
216
-
217
- interface Descriptor<T> {
218
- configurable?: boolean;
219
- enumerable?: boolean;
220
- value?: T;
221
- writable?: boolean;
222
- get?: Getter<T>;
223
- set?: Setter<T> | false;
224
- suspense?: boolean;
225
- }
226
-
227
- interface ExplicitDescriptor extends PropertyDescriptor {
228
- explicit: true;
229
- }
230
-
231
- interface RecursiveDescriptor<T> {
232
- recursive: true;
233
- enumerable?: boolean;
234
- value?: T;
235
- set?: Setter<T> | false;
236
- suspense?: boolean;
237
- }
207
+ type Getter<T> = (state: T, within?: Subscriber) => T;
208
+ type Setter<T> = (value: T, state: any) => boolean | void;
209
+ type Runner<T> = (this: Controller, key: string, on: Controller) => Instruction.Descriptor<T> | boolean | undefined;
210
+ interface Descriptor<T> {
211
+ configurable?: boolean;
212
+ enumerable?: boolean;
213
+ value?: T;
214
+ writable?: boolean;
215
+ get?: Getter<T>;
216
+ set?: Setter<T> | false;
217
+ suspense?: boolean;
218
+ }
219
+ interface ExplicitDescriptor extends PropertyDescriptor {
220
+ explicit: true;
221
+ }
222
+ interface RecursiveDescriptor<T> {
223
+ recursive: true;
224
+ enumerable?: boolean;
225
+ value?: T;
226
+ set?: Setter<T> | false;
227
+ suspense?: boolean;
228
+ }
238
229
  }
239
-
240
230
  /**
241
231
  * Run instruction as controller sets itself up.
242
232
  * This will specialize the behavior of a given property.
@@ -607,4 +597,4 @@ declare function useTap<T extends Stateful, K extends Model.Field<T>>(source: T
607
597
  declare function useTap<T extends Stateful, R>(source: T | (() => T) | useTap.Tappable, compute: (this: T, from: T) => R, expect: true): Exclude<R, undefined>;
608
598
  declare function useTap<T extends Stateful, R>(source: T | (() => T) | useTap.Tappable, compute: (this: T, from: T) => R, expect?: boolean): R;
609
599
 
610
- export { CONTROL, Consumer, Global, LOCAL, MVC as Model, Provider, STATE, Stateful, WHY, act, apply, MVC as default, from, parent, ref, set, tap, use, useFrom, useInContext, useModel, useTap };
600
+ export { CONTROL, Consumer, Global, LOCAL, MVC as Model, Provider, STATE, Stateful, WHY, act, apply, array, MVC as default, from, parent, ref, set, tap, use, useFrom, useInContext, useModel, useTap };
package/lib/index.esm.js CHANGED
@@ -93,6 +93,149 @@ function applyUpdate(subject, keys) {
93
93
  };
94
94
  }
95
95
 
96
+ const CONTROL = Symbol("CONTROL");
97
+ const WHY = Symbol("UPDATE");
98
+ const LOCAL = Symbol("LOCAL");
99
+ const STATE = Symbol("STATE");
100
+ class Model {
101
+ constructor() {
102
+ define(this, CONTROL, new Controller(this));
103
+ define(this, "get", this);
104
+ define(this, "set", this);
105
+ }
106
+ get [STATE]() {
107
+ return this[CONTROL].state;
108
+ }
109
+ get [WHY]() {
110
+ return UPDATE.get(this);
111
+ }
112
+ on(select, handler, squash, once) {
113
+ return control(this, control => {
114
+ if (typeof select == "function")
115
+ return control.addListener(select);
116
+ const keys = typeof select == "string" ? [select] :
117
+ !select.length ? getOwnPropertyNames(control.state) :
118
+ select;
119
+ ensure(control, keys);
120
+ const callback = squash
121
+ ? handler.bind(this)
122
+ : frame => frame
123
+ .filter(k => keys.includes(k))
124
+ .forEach(k => handler.call(this, control.state[k], k));
125
+ const trigger = once
126
+ ? frame => { remove(); callback(frame); }
127
+ : callback;
128
+ const remove = control.addListener(key => {
129
+ if (keys.includes(key))
130
+ return trigger;
131
+ });
132
+ return remove;
133
+ });
134
+ }
135
+ once(select, callback, squash) {
136
+ if (callback)
137
+ return this.on(select, callback, squash, true);
138
+ return new Promise(resolve => {
139
+ this.on(select, resolve, true, true);
140
+ });
141
+ }
142
+ effect(callback, select) {
143
+ const effect = createEffect(callback);
144
+ return control(this, control => {
145
+ if (!select) {
146
+ const sub = new Subscriber(control, () => invoke);
147
+ const invoke = () => {
148
+ const x = sub.proxy;
149
+ effect.call(x, x);
150
+ };
151
+ invoke();
152
+ return sub.commit();
153
+ }
154
+ const invoke = () => {
155
+ effect.call(this.get, this.get);
156
+ };
157
+ invoke();
158
+ if (!select.length) {
159
+ control.onDestroy.add(invoke);
160
+ return () => {
161
+ control.onDestroy.delete(invoke);
162
+ };
163
+ }
164
+ return control.addListener(key => {
165
+ if (select.includes(key))
166
+ return invoke;
167
+ });
168
+ });
169
+ }
170
+ // TODO: account for exotic properties
171
+ import(source, select) {
172
+ const { subject } = control(this);
173
+ if (!select)
174
+ select = getOwnPropertyNames(subject);
175
+ for (const key of select)
176
+ if (key in source)
177
+ this[key] = source[key];
178
+ }
179
+ export(subset) {
180
+ const { state } = control(this);
181
+ const output = {};
182
+ if (!subset)
183
+ subset = getOwnPropertyNames(state);
184
+ for (const key of subset)
185
+ output[key] = state[key];
186
+ return output;
187
+ }
188
+ update(arg, tag) {
189
+ const target = control(this);
190
+ if (typeof arg == "string") {
191
+ target.update(arg);
192
+ if (1 in arguments && arg in this) {
193
+ const method = this[arg];
194
+ if (typeof method == "function")
195
+ if (typeof tag != "boolean")
196
+ method.call(this, tag);
197
+ else if (tag)
198
+ method.call(this);
199
+ }
200
+ arg = undefined;
201
+ }
202
+ return target.requestUpdate(arg);
203
+ }
204
+ /**
205
+ * Mark this instance for garbage-collection and send `willDestroy` event to all listeners.
206
+ */
207
+ destroy() {
208
+ control(this).stop();
209
+ }
210
+ toString() {
211
+ return this.constructor.name;
212
+ }
213
+ /**
214
+ * Creates a new instance of this controller.
215
+ *
216
+ * Beyond `new this(...)`, method will activate managed-state.
217
+ *
218
+ * @param args - arguments sent to constructor
219
+ */
220
+ static create(...args) {
221
+ const instance = new this(...args);
222
+ control(instance);
223
+ return instance;
224
+ }
225
+ /**
226
+ * Static equivalent of `x instanceof this`.
227
+ *
228
+ * Will determine if provided class is a subtype of this one.
229
+ */
230
+ static isTypeof(maybe) {
231
+ return (typeof maybe == "function" &&
232
+ maybe.prototype instanceof this);
233
+ }
234
+ }
235
+ defineLazy(Model, CONTROL, function () {
236
+ return new Controller(this);
237
+ });
238
+
96
239
  class Subscriber {
97
240
  constructor(target, onUpdate) {
98
241
  this.onUpdate = onUpdate;
@@ -304,6 +447,7 @@ const Oops$9 = issues({
304
447
  StrictUpdate: () => `Strict update() did not find pending updates.`,
305
448
  NoChaining: () => `Then called with undefined; update promise will never catch nor supports chaining.`
306
449
  });
450
+ const PENDING = new Map();
307
451
  const READY = new WeakSet();
308
452
  class Controller {
309
453
  constructor(subject) {
@@ -329,52 +473,28 @@ class Controller {
329
473
  const { value: entry } = getOwnPropertyDescriptor(subject, key);
330
474
  if (typeof entry == "function")
331
475
  return;
332
- let onGet;
333
- let onSet;
334
- let set;
335
- let enumerable;
336
- let suspense;
337
- const instruction = Pending.get(entry);
338
- if (instruction) {
339
- Pending.delete(entry);
340
- delete subject[key];
341
- const desc = instruction.call(this, key, this);
342
- if (desc === false)
343
- return;
344
- if (typeof desc == "object") {
345
- if ("value" in desc)
346
- state[key] = desc.value;
347
- onSet = desc.set;
348
- onGet = desc.get;
349
- suspense = desc.suspense;
350
- enumerable = desc.enumerable;
351
- }
476
+ if (typeof entry == "symbol") {
477
+ const instruction = PENDING.get(entry);
478
+ if (instruction)
479
+ instruction.call(this, key, this);
480
+ return;
352
481
  }
353
- else
354
- state[key] = entry;
355
- if (onSet !== false)
356
- set = this.ref(key, onSet);
357
- const get = (local) => {
358
- if (!(key in state) && suspense)
359
- throw suspend(this, key);
360
- const value = state[key];
361
- if (onGet)
362
- return local
363
- ? onGet(value, local)
364
- : onGet(value);
365
- return value;
366
- };
482
+ state[key] = entry;
483
+ const set = this.ref(key);
367
484
  defineProperty(subject, key, {
368
- enumerable, set, get
485
+ enumerable: false,
486
+ get: () => state[key],
487
+ set
369
488
  });
370
489
  defineProperty(proxy, key, {
371
- enumerable, set,
490
+ enumerable: false,
372
491
  get() {
373
492
  const local = this[LOCAL];
374
493
  if (local && !local.watch[key])
375
494
  local.watch[key] = true;
376
- return get(local);
377
- }
495
+ return state[key];
496
+ },
497
+ set
378
498
  });
379
499
  }
380
500
  ref(key, handler) {
@@ -473,179 +593,55 @@ function control(subject, cb) {
473
593
  return cb(control);
474
594
  }
475
595
 
476
- const CONTROL = Symbol("CONTROL");
477
- const WHY = Symbol("UPDATE");
478
- const LOCAL = Symbol("LOCAL");
479
- const STATE = Symbol("STATE");
480
- class Model {
481
- constructor() {
482
- define(this, CONTROL, new Controller(this));
483
- define(this, "get", this);
484
- define(this, "set", this);
485
- }
486
- get [STATE]() {
487
- return this[CONTROL].state;
488
- }
489
- get [WHY]() {
490
- return UPDATE.get(this);
491
- }
492
- on(select, handler, squash, once) {
493
- return control(this, control => {
494
- if (typeof select == "function")
495
- return control.addListener(select);
496
- const keys = typeof select == "string" ? [select] :
497
- !select.length ? getOwnPropertyNames(control.state) :
498
- select;
499
- ensure(control, keys);
500
- const callback = squash
501
- ? handler.bind(this)
502
- : frame => frame
503
- .filter(k => keys.includes(k))
504
- .forEach(k => handler.call(this, control.state[k], k));
505
- const trigger = once
506
- ? frame => { remove(); callback(frame); }
507
- : callback;
508
- const remove = control.addListener(key => {
509
- if (keys.includes(key))
510
- return trigger;
511
- });
512
- return remove;
513
- });
514
- }
515
- once(select, callback, squash) {
516
- if (callback)
517
- return this.on(select, callback, squash, true);
518
- return new Promise(resolve => {
519
- this.on(select, resolve, true, true);
520
- });
521
- }
522
- effect(callback, select) {
523
- const effect = createEffect(callback);
524
- return control(this, control => {
525
- if (!select) {
526
- const sub = new Subscriber(control, () => invoke);
527
- const invoke = () => {
528
- const x = sub.proxy;
529
- effect.call(x, x);
530
- };
531
- invoke();
532
- return sub.commit();
533
- }
534
- const invoke = () => {
535
- effect.call(this.get, this.get);
536
- };
537
- invoke();
538
- if (!select.length) {
539
- control.onDestroy.add(invoke);
540
- return () => {
541
- control.onDestroy.delete(invoke);
542
- };
543
- }
544
- return control.addListener(key => {
545
- if (select.includes(key))
546
- return invoke;
547
- });
548
- });
549
- }
550
- // TODO: account for exotic properties
551
- import(source, select) {
552
- const { subject } = control(this);
553
- if (!select)
554
- select = getOwnPropertyNames(subject);
555
- for (const key of select)
556
- if (key in source)
557
- this[key] = source[key];
558
- }
559
- export(subset) {
560
- const { state } = control(this);
561
- const output = {};
562
- if (!subset)
563
- subset = getOwnPropertyNames(state);
564
- for (const key of subset)
565
- output[key] = state[key];
566
- return output;
567
- }
568
- update(arg, tag) {
569
- const target = control(this);
570
- if (typeof arg == "string") {
571
- target.update(arg);
572
- if (1 in arguments && arg in this) {
573
- const method = this[arg];
574
- if (typeof method == "function")
575
- if (typeof tag != "boolean")
576
- method.call(this, tag);
577
- else if (tag)
578
- method.call(this);
579
- }
580
- arg = undefined;
581
- }
582
- return target.requestUpdate(arg);
583
- }
584
- /**
585
- * Mark this instance for garbage-collection and send `willDestroy` event to all listeners.
586
- */
587
- destroy() {
588
- control(this).stop();
589
- }
590
- toString() {
591
- return this.constructor.name;
592
- }
593
- /**
594
- * Creates a new instance of this controller.
595
- *
596
- * Beyond `new this(...)`, method will activate managed-state.
597
- *
598
- * @param args - arguments sent to constructor
599
- */
600
- static create(...args) {
601
- const instance = new this(...args);
602
- control(instance);
603
- return instance;
604
- }
605
- /**
606
- * Static equivalent of `x instanceof this`.
607
- *
608
- * Will determine if provided class is a subtype of this one.
609
- */
610
- static isTypeof(maybe) {
611
- return (typeof maybe == "function" &&
612
- maybe.prototype instanceof this);
613
- }
614
- }
615
- defineLazy(Model, CONTROL, function () {
616
- return new Controller(this);
617
- });
618
-
619
- const Pending = new Map();
620
596
  function apply(fn, label) {
621
597
  const name = label || fn.name || "pending";
622
598
  const placeholder = Symbol(`${name} instruction`);
623
599
  function setup(key) {
600
+ const { proxy, state, subject } = this;
601
+ const control = this;
602
+ PENDING.delete(placeholder);
603
+ delete subject[key];
624
604
  let output = fn.call(this, key, this);
625
- switch (typeof output) {
626
- case "function":
627
- return { get: output };
628
- case "object": {
629
- if ("explicit" in output && output.explicit) {
630
- defineProperty(this.subject, key, output);
631
- return false;
632
- }
633
- if ("recursive" in output && output.recursive)
634
- return Object.assign(Object.assign({}, output), { get: recursive(this, key) });
635
- }
636
- case "boolean":
637
- return output;
605
+ if (typeof output == "function")
606
+ output = { get: output };
607
+ if (typeof output != "object")
608
+ return;
609
+ if ("explicit" in output && output.explicit) {
610
+ defineProperty(subject, key, output);
611
+ return false;
638
612
  }
639
- return;
613
+ if ("recursive" in output && output.recursive)
614
+ output = Object.assign(Object.assign({}, output), { get: recursive(this, key) });
615
+ const desc = output;
616
+ if ("value" in desc)
617
+ state[key] = desc.value;
618
+ const { get: onGet, set: onSet, enumerable, suspense } = desc;
619
+ let set = onSet === false
620
+ ? undefined
621
+ : this.ref(key, onSet);
622
+ function get() {
623
+ if (!(key in state) && suspense)
624
+ throw suspend(control, key);
625
+ const local = this[LOCAL];
626
+ if (local && !(key in local.watch))
627
+ local.watch[key] = true;
628
+ const value = state[key];
629
+ if (!onGet)
630
+ return value;
631
+ return local
632
+ ? onGet(value, local)
633
+ : onGet(value);
634
+ }
635
+ for (const x of [subject, proxy])
636
+ defineProperty(x, key, { enumerable, get, set });
640
637
  }
641
- Pending.set(placeholder, setup);
638
+ PENDING.set(placeholder, setup);
642
639
  return placeholder;
643
640
  }
644
641
  function recursive(source, key) {
645
- const context = new WeakMap();
646
- const subscribe = (parent) => {
642
+ return forSubscriber((parent, context) => {
647
643
  let child;
648
- const init = () => {
644
+ function init() {
649
645
  if (child) {
650
646
  child.release();
651
647
  parent.dependant.delete(child);
@@ -660,15 +656,23 @@ function recursive(source, key) {
660
656
  parent.dependant.add(child);
661
657
  context.set(parent, child.proxy);
662
658
  }
663
- };
659
+ }
664
660
  init();
665
661
  parent.watch[key] = init;
666
- };
662
+ });
663
+ }
664
+ function forSubscriber(fn) {
665
+ const context = new WeakMap();
667
666
  return (value, local) => {
668
667
  if (!local)
669
668
  return value;
670
- if (!context.has(local))
671
- subscribe(local);
669
+ if (!context.has(local)) {
670
+ const output = fn(local, context);
671
+ if (output) {
672
+ context.set(local, output);
673
+ return output;
674
+ }
675
+ }
672
676
  return context.get(local);
673
677
  };
674
678
  }
@@ -702,6 +706,65 @@ function act(task) {
702
706
  });
703
707
  }
704
708
 
709
+ class Managed extends Array {
710
+ constructor(onUpdate) {
711
+ super();
712
+ this.lastUpdate = [0, 0];
713
+ this.update = (from, to) => {
714
+ this.lastUpdate = [from, to];
715
+ onUpdate();
716
+ };
717
+ }
718
+ push(...items) {
719
+ const length = this.length;
720
+ const end = super.push(...items);
721
+ this.update(length, end);
722
+ return end;
723
+ }
724
+ unshift(...items) {
725
+ const end = super.push(...items);
726
+ this.update(0, end);
727
+ return end;
728
+ }
729
+ splice(start, deleteCount, ...rest) {
730
+ this.update(start, deleteCount !== rest.length ? Infinity : deleteCount);
731
+ return super.splice(start, deleteCount, ...rest);
732
+ }
733
+ }
734
+ function array() {
735
+ return apply(function array(key) {
736
+ const array = new Managed(() => this.update(key));
737
+ const access = forSubscriber(context => {
738
+ const local = Object.create(array);
739
+ const update = context.onUpdate(key, this);
740
+ let rangeA = 0;
741
+ let rangeB = 0;
742
+ function onUpdate() {
743
+ const [lastA, lastB] = array.lastUpdate;
744
+ if (rangeA <= lastB && rangeB >= lastA)
745
+ return update;
746
+ }
747
+ function listen(eventA, eventB) {
748
+ if (context.active)
749
+ return;
750
+ context.watch[key] = onUpdate;
751
+ rangeA = Math.min(rangeA, eventA);
752
+ rangeB = Math.max(rangeB, eventB);
753
+ }
754
+ local[Symbol.iterator] = () => {
755
+ listen(0, Infinity);
756
+ return array[Symbol.iterator]();
757
+ };
758
+ return local;
759
+ });
760
+ return {
761
+ get: access,
762
+ set: false,
763
+ value: array
764
+ };
765
+ });
766
+ }
767
+
705
768
  function ref(arg) {
706
769
  return apply(function ref(key) {
707
770
  let value = {};
@@ -1287,5 +1350,5 @@ class Global extends MVC {
1287
1350
  }
1288
1351
  }
1289
1352
 
1290
- export { CONTROL, Consumer, Global, LOCAL, MVC as Model, Provider, STATE, WHY, act, apply, MVC as default, from, parent, ref, set, tap, use$1 as use, useFrom, useInContext, useModel, useTap };
1353
+ export { CONTROL, Consumer, Global, LOCAL, MVC as Model, Provider, STATE, WHY, act, apply, array, MVC as default, from, parent, ref, set, tap, use$1 as use, useFrom, useInContext, useModel, useTap };
1291
1354
  //# sourceMappingURL=index.esm.js.map