@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.js CHANGED
@@ -101,6 +101,149 @@ function applyUpdate(subject, keys) {
101
101
  };
102
102
  }
103
103
 
104
+ const CONTROL = Symbol("CONTROL");
105
+ const WHY = Symbol("UPDATE");
106
+ const LOCAL = Symbol("LOCAL");
107
+ const STATE = Symbol("STATE");
108
+ class Model {
109
+ constructor() {
110
+ define(this, CONTROL, new Controller(this));
111
+ define(this, "get", this);
112
+ define(this, "set", this);
113
+ }
114
+ get [STATE]() {
115
+ return this[CONTROL].state;
116
+ }
117
+ get [WHY]() {
118
+ return UPDATE.get(this);
119
+ }
120
+ on(select, handler, squash, once) {
121
+ return control(this, control => {
122
+ if (typeof select == "function")
123
+ return control.addListener(select);
124
+ const keys = typeof select == "string" ? [select] :
125
+ !select.length ? getOwnPropertyNames(control.state) :
126
+ select;
127
+ ensure(control, keys);
128
+ const callback = squash
129
+ ? handler.bind(this)
130
+ : frame => frame
131
+ .filter(k => keys.includes(k))
132
+ .forEach(k => handler.call(this, control.state[k], k));
133
+ const trigger = once
134
+ ? frame => { remove(); callback(frame); }
135
+ : callback;
136
+ const remove = control.addListener(key => {
137
+ if (keys.includes(key))
138
+ return trigger;
139
+ });
140
+ return remove;
141
+ });
142
+ }
143
+ once(select, callback, squash) {
144
+ if (callback)
145
+ return this.on(select, callback, squash, true);
146
+ return new Promise(resolve => {
147
+ this.on(select, resolve, true, true);
148
+ });
149
+ }
150
+ effect(callback, select) {
151
+ const effect = createEffect(callback);
152
+ return control(this, control => {
153
+ if (!select) {
154
+ const sub = new Subscriber(control, () => invoke);
155
+ const invoke = () => {
156
+ const x = sub.proxy;
157
+ effect.call(x, x);
158
+ };
159
+ invoke();
160
+ return sub.commit();
161
+ }
162
+ const invoke = () => {
163
+ effect.call(this.get, this.get);
164
+ };
165
+ invoke();
166
+ if (!select.length) {
167
+ control.onDestroy.add(invoke);
168
+ return () => {
169
+ control.onDestroy.delete(invoke);
170
+ };
171
+ }
172
+ return control.addListener(key => {
173
+ if (select.includes(key))
174
+ return invoke;
175
+ });
176
+ });
177
+ }
178
+ // TODO: account for exotic properties
179
+ import(source, select) {
180
+ const { subject } = control(this);
181
+ if (!select)
182
+ select = getOwnPropertyNames(subject);
183
+ for (const key of select)
184
+ if (key in source)
185
+ this[key] = source[key];
186
+ }
187
+ export(subset) {
188
+ const { state } = control(this);
189
+ const output = {};
190
+ if (!subset)
191
+ subset = getOwnPropertyNames(state);
192
+ for (const key of subset)
193
+ output[key] = state[key];
194
+ return output;
195
+ }
196
+ update(arg, tag) {
197
+ const target = control(this);
198
+ if (typeof arg == "string") {
199
+ target.update(arg);
200
+ if (1 in arguments && arg in this) {
201
+ const method = this[arg];
202
+ if (typeof method == "function")
203
+ if (typeof tag != "boolean")
204
+ method.call(this, tag);
205
+ else if (tag)
206
+ method.call(this);
207
+ }
208
+ arg = undefined;
209
+ }
210
+ return target.requestUpdate(arg);
211
+ }
212
+ /**
213
+ * Mark this instance for garbage-collection and send `willDestroy` event to all listeners.
214
+ */
215
+ destroy() {
216
+ control(this).stop();
217
+ }
218
+ toString() {
219
+ return this.constructor.name;
220
+ }
221
+ /**
222
+ * Creates a new instance of this controller.
223
+ *
224
+ * Beyond `new this(...)`, method will activate managed-state.
225
+ *
226
+ * @param args - arguments sent to constructor
227
+ */
228
+ static create(...args) {
229
+ const instance = new this(...args);
230
+ control(instance);
231
+ return instance;
232
+ }
233
+ /**
234
+ * Static equivalent of `x instanceof this`.
235
+ *
236
+ * Will determine if provided class is a subtype of this one.
237
+ */
238
+ static isTypeof(maybe) {
239
+ return (typeof maybe == "function" &&
240
+ maybe.prototype instanceof this);
241
+ }
242
+ }
243
+ defineLazy(Model, CONTROL, function () {
244
+ return new Controller(this);
245
+ });
246
+
104
247
  class Subscriber {
105
248
  constructor(target, onUpdate) {
106
249
  this.onUpdate = onUpdate;
@@ -312,6 +455,7 @@ const Oops$9 = issues({
312
455
  StrictUpdate: () => `Strict update() did not find pending updates.`,
313
456
  NoChaining: () => `Then called with undefined; update promise will never catch nor supports chaining.`
314
457
  });
458
+ const PENDING = new Map();
315
459
  const READY = new WeakSet();
316
460
  class Controller {
317
461
  constructor(subject) {
@@ -337,52 +481,28 @@ class Controller {
337
481
  const { value: entry } = getOwnPropertyDescriptor(subject, key);
338
482
  if (typeof entry == "function")
339
483
  return;
340
- let onGet;
341
- let onSet;
342
- let set;
343
- let enumerable;
344
- let suspense;
345
- const instruction = Pending.get(entry);
346
- if (instruction) {
347
- Pending.delete(entry);
348
- delete subject[key];
349
- const desc = instruction.call(this, key, this);
350
- if (desc === false)
351
- return;
352
- if (typeof desc == "object") {
353
- if ("value" in desc)
354
- state[key] = desc.value;
355
- onSet = desc.set;
356
- onGet = desc.get;
357
- suspense = desc.suspense;
358
- enumerable = desc.enumerable;
359
- }
484
+ if (typeof entry == "symbol") {
485
+ const instruction = PENDING.get(entry);
486
+ if (instruction)
487
+ instruction.call(this, key, this);
488
+ return;
360
489
  }
361
- else
362
- state[key] = entry;
363
- if (onSet !== false)
364
- set = this.ref(key, onSet);
365
- const get = (local) => {
366
- if (!(key in state) && suspense)
367
- throw suspend(this, key);
368
- const value = state[key];
369
- if (onGet)
370
- return local
371
- ? onGet(value, local)
372
- : onGet(value);
373
- return value;
374
- };
490
+ state[key] = entry;
491
+ const set = this.ref(key);
375
492
  defineProperty(subject, key, {
376
- enumerable, set, get
493
+ enumerable: false,
494
+ get: () => state[key],
495
+ set
377
496
  });
378
497
  defineProperty(proxy, key, {
379
- enumerable, set,
498
+ enumerable: false,
380
499
  get() {
381
500
  const local = this[LOCAL];
382
501
  if (local && !local.watch[key])
383
502
  local.watch[key] = true;
384
- return get(local);
385
- }
503
+ return state[key];
504
+ },
505
+ set
386
506
  });
387
507
  }
388
508
  ref(key, handler) {
@@ -481,179 +601,55 @@ function control(subject, cb) {
481
601
  return cb(control);
482
602
  }
483
603
 
484
- const CONTROL = Symbol("CONTROL");
485
- const WHY = Symbol("UPDATE");
486
- const LOCAL = Symbol("LOCAL");
487
- const STATE = Symbol("STATE");
488
- class Model {
489
- constructor() {
490
- define(this, CONTROL, new Controller(this));
491
- define(this, "get", this);
492
- define(this, "set", this);
493
- }
494
- get [STATE]() {
495
- return this[CONTROL].state;
496
- }
497
- get [WHY]() {
498
- return UPDATE.get(this);
499
- }
500
- on(select, handler, squash, once) {
501
- return control(this, control => {
502
- if (typeof select == "function")
503
- return control.addListener(select);
504
- const keys = typeof select == "string" ? [select] :
505
- !select.length ? getOwnPropertyNames(control.state) :
506
- select;
507
- ensure(control, keys);
508
- const callback = squash
509
- ? handler.bind(this)
510
- : frame => frame
511
- .filter(k => keys.includes(k))
512
- .forEach(k => handler.call(this, control.state[k], k));
513
- const trigger = once
514
- ? frame => { remove(); callback(frame); }
515
- : callback;
516
- const remove = control.addListener(key => {
517
- if (keys.includes(key))
518
- return trigger;
519
- });
520
- return remove;
521
- });
522
- }
523
- once(select, callback, squash) {
524
- if (callback)
525
- return this.on(select, callback, squash, true);
526
- return new Promise(resolve => {
527
- this.on(select, resolve, true, true);
528
- });
529
- }
530
- effect(callback, select) {
531
- const effect = createEffect(callback);
532
- return control(this, control => {
533
- if (!select) {
534
- const sub = new Subscriber(control, () => invoke);
535
- const invoke = () => {
536
- const x = sub.proxy;
537
- effect.call(x, x);
538
- };
539
- invoke();
540
- return sub.commit();
541
- }
542
- const invoke = () => {
543
- effect.call(this.get, this.get);
544
- };
545
- invoke();
546
- if (!select.length) {
547
- control.onDestroy.add(invoke);
548
- return () => {
549
- control.onDestroy.delete(invoke);
550
- };
551
- }
552
- return control.addListener(key => {
553
- if (select.includes(key))
554
- return invoke;
555
- });
556
- });
557
- }
558
- // TODO: account for exotic properties
559
- import(source, select) {
560
- const { subject } = control(this);
561
- if (!select)
562
- select = getOwnPropertyNames(subject);
563
- for (const key of select)
564
- if (key in source)
565
- this[key] = source[key];
566
- }
567
- export(subset) {
568
- const { state } = control(this);
569
- const output = {};
570
- if (!subset)
571
- subset = getOwnPropertyNames(state);
572
- for (const key of subset)
573
- output[key] = state[key];
574
- return output;
575
- }
576
- update(arg, tag) {
577
- const target = control(this);
578
- if (typeof arg == "string") {
579
- target.update(arg);
580
- if (1 in arguments && arg in this) {
581
- const method = this[arg];
582
- if (typeof method == "function")
583
- if (typeof tag != "boolean")
584
- method.call(this, tag);
585
- else if (tag)
586
- method.call(this);
587
- }
588
- arg = undefined;
589
- }
590
- return target.requestUpdate(arg);
591
- }
592
- /**
593
- * Mark this instance for garbage-collection and send `willDestroy` event to all listeners.
594
- */
595
- destroy() {
596
- control(this).stop();
597
- }
598
- toString() {
599
- return this.constructor.name;
600
- }
601
- /**
602
- * Creates a new instance of this controller.
603
- *
604
- * Beyond `new this(...)`, method will activate managed-state.
605
- *
606
- * @param args - arguments sent to constructor
607
- */
608
- static create(...args) {
609
- const instance = new this(...args);
610
- control(instance);
611
- return instance;
612
- }
613
- /**
614
- * Static equivalent of `x instanceof this`.
615
- *
616
- * Will determine if provided class is a subtype of this one.
617
- */
618
- static isTypeof(maybe) {
619
- return (typeof maybe == "function" &&
620
- maybe.prototype instanceof this);
621
- }
622
- }
623
- defineLazy(Model, CONTROL, function () {
624
- return new Controller(this);
625
- });
626
-
627
- const Pending = new Map();
628
604
  function apply(fn, label) {
629
605
  const name = label || fn.name || "pending";
630
606
  const placeholder = Symbol(`${name} instruction`);
631
607
  function setup(key) {
608
+ const { proxy, state, subject } = this;
609
+ const control = this;
610
+ PENDING.delete(placeholder);
611
+ delete subject[key];
632
612
  let output = fn.call(this, key, this);
633
- switch (typeof output) {
634
- case "function":
635
- return { get: output };
636
- case "object": {
637
- if ("explicit" in output && output.explicit) {
638
- defineProperty(this.subject, key, output);
639
- return false;
640
- }
641
- if ("recursive" in output && output.recursive)
642
- return Object.assign(Object.assign({}, output), { get: recursive(this, key) });
643
- }
644
- case "boolean":
645
- return output;
613
+ if (typeof output == "function")
614
+ output = { get: output };
615
+ if (typeof output != "object")
616
+ return;
617
+ if ("explicit" in output && output.explicit) {
618
+ defineProperty(subject, key, output);
619
+ return false;
646
620
  }
647
- return;
621
+ if ("recursive" in output && output.recursive)
622
+ output = Object.assign(Object.assign({}, output), { get: recursive(this, key) });
623
+ const desc = output;
624
+ if ("value" in desc)
625
+ state[key] = desc.value;
626
+ const { get: onGet, set: onSet, enumerable, suspense } = desc;
627
+ let set = onSet === false
628
+ ? undefined
629
+ : this.ref(key, onSet);
630
+ function get() {
631
+ if (!(key in state) && suspense)
632
+ throw suspend(control, key);
633
+ const local = this[LOCAL];
634
+ if (local && !(key in local.watch))
635
+ local.watch[key] = true;
636
+ const value = state[key];
637
+ if (!onGet)
638
+ return value;
639
+ return local
640
+ ? onGet(value, local)
641
+ : onGet(value);
642
+ }
643
+ for (const x of [subject, proxy])
644
+ defineProperty(x, key, { enumerable, get, set });
648
645
  }
649
- Pending.set(placeholder, setup);
646
+ PENDING.set(placeholder, setup);
650
647
  return placeholder;
651
648
  }
652
649
  function recursive(source, key) {
653
- const context = new WeakMap();
654
- const subscribe = (parent) => {
650
+ return forSubscriber((parent, context) => {
655
651
  let child;
656
- const init = () => {
652
+ function init() {
657
653
  if (child) {
658
654
  child.release();
659
655
  parent.dependant.delete(child);
@@ -668,15 +664,23 @@ function recursive(source, key) {
668
664
  parent.dependant.add(child);
669
665
  context.set(parent, child.proxy);
670
666
  }
671
- };
667
+ }
672
668
  init();
673
669
  parent.watch[key] = init;
674
- };
670
+ });
671
+ }
672
+ function forSubscriber(fn) {
673
+ const context = new WeakMap();
675
674
  return (value, local) => {
676
675
  if (!local)
677
676
  return value;
678
- if (!context.has(local))
679
- subscribe(local);
677
+ if (!context.has(local)) {
678
+ const output = fn(local, context);
679
+ if (output) {
680
+ context.set(local, output);
681
+ return output;
682
+ }
683
+ }
680
684
  return context.get(local);
681
685
  };
682
686
  }
@@ -710,6 +714,65 @@ function act(task) {
710
714
  });
711
715
  }
712
716
 
717
+ class Managed extends Array {
718
+ constructor(onUpdate) {
719
+ super();
720
+ this.lastUpdate = [0, 0];
721
+ this.update = (from, to) => {
722
+ this.lastUpdate = [from, to];
723
+ onUpdate();
724
+ };
725
+ }
726
+ push(...items) {
727
+ const length = this.length;
728
+ const end = super.push(...items);
729
+ this.update(length, end);
730
+ return end;
731
+ }
732
+ unshift(...items) {
733
+ const end = super.push(...items);
734
+ this.update(0, end);
735
+ return end;
736
+ }
737
+ splice(start, deleteCount, ...rest) {
738
+ this.update(start, deleteCount !== rest.length ? Infinity : deleteCount);
739
+ return super.splice(start, deleteCount, ...rest);
740
+ }
741
+ }
742
+ function array() {
743
+ return apply(function array(key) {
744
+ const array = new Managed(() => this.update(key));
745
+ const access = forSubscriber(context => {
746
+ const local = Object.create(array);
747
+ const update = context.onUpdate(key, this);
748
+ let rangeA = 0;
749
+ let rangeB = 0;
750
+ function onUpdate() {
751
+ const [lastA, lastB] = array.lastUpdate;
752
+ if (rangeA <= lastB && rangeB >= lastA)
753
+ return update;
754
+ }
755
+ function listen(eventA, eventB) {
756
+ if (context.active)
757
+ return;
758
+ context.watch[key] = onUpdate;
759
+ rangeA = Math.min(rangeA, eventA);
760
+ rangeB = Math.max(rangeB, eventB);
761
+ }
762
+ local[Symbol.iterator] = () => {
763
+ listen(0, Infinity);
764
+ return array[Symbol.iterator]();
765
+ };
766
+ return local;
767
+ });
768
+ return {
769
+ get: access,
770
+ set: false,
771
+ value: array
772
+ };
773
+ });
774
+ }
775
+
713
776
  function ref(arg) {
714
777
  return apply(function ref(key) {
715
778
  let value = {};
@@ -1305,6 +1368,7 @@ exports.STATE = STATE;
1305
1368
  exports.WHY = WHY;
1306
1369
  exports.act = act;
1307
1370
  exports.apply = apply;
1371
+ exports.array = array;
1308
1372
  exports["default"] = MVC;
1309
1373
  exports.from = from;
1310
1374
  exports.parent = parent;