@expressive/mvc 0.53.6 → 0.54.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.
@@ -1,305 +0,0 @@
1
- import { Context } from '../context';
2
- import { Model } from '../model';
3
- import { use } from './use';
4
- describe("instruction", () => {
5
- it("will run on create", () => {
6
- class Test extends Model {
7
- constructor() {
8
- super(...arguments);
9
- this.property = use((key) => {
10
- didRunInstruction(key);
11
- });
12
- }
13
- }
14
- const didRunInstruction = jest.fn();
15
- Test.new();
16
- expect(didRunInstruction).toBeCalledWith("property");
17
- });
18
- describe("symbol", () => {
19
- it("will use symbol as placeholder", async () => {
20
- class Test extends Model {
21
- constructor() {
22
- super(...arguments);
23
- this.value = use(() => ({ value: 1 }));
24
- }
25
- }
26
- const test = new Test();
27
- if (typeof test.value !== "symbol")
28
- throw new Error("value is not a symbol");
29
- else
30
- expect(test.value.description).toBe("instruction");
31
- await test.set();
32
- expect(test.value).toBe(1);
33
- });
34
- it("will be deleted prior to instruction", () => {
35
- class Test extends Model {
36
- constructor() {
37
- super(...arguments);
38
- this.value = use(() => {
39
- expect("value" in this).toBe(false);
40
- });
41
- }
42
- }
43
- Test.new();
44
- });
45
- it("will ignore normal symbol", () => {
46
- class Test extends Model {
47
- constructor() {
48
- super(...arguments);
49
- this.value = Symbol("hello");
50
- }
51
- }
52
- const test = Test.new();
53
- expect(typeof test.value).toBe("symbol");
54
- });
55
- });
56
- describe("getter", () => {
57
- it("will run upon access", () => {
58
- const mockAccess = jest.fn((_subscriber) => "foobar");
59
- const mockApply = jest.fn((_key) => mockAccess);
60
- class Test extends Model {
61
- constructor() {
62
- super(...arguments);
63
- this.property = use(mockApply);
64
- }
65
- }
66
- const instance = Test.new();
67
- expect(mockApply).toBeCalledWith("property", expect.any(Test), {});
68
- expect(mockAccess).not.toBeCalled();
69
- expect(instance.property).toBe("foobar");
70
- expect(mockAccess).toBeCalledWith(instance);
71
- });
72
- it("will pass subscriber if within one", () => {
73
- const didGetValue = jest.fn();
74
- class Test extends Model {
75
- constructor() {
76
- super(...arguments);
77
- this.property = use(() => didGetValue);
78
- }
79
- }
80
- const state = Test.new();
81
- state.get(own => {
82
- void own.property;
83
- });
84
- expect(didGetValue).toBeCalledWith(state);
85
- });
86
- it("will not throw suspense if get (required) is false", async () => {
87
- class Test extends Model {
88
- constructor() {
89
- super(...arguments);
90
- this.value = use(() => ({ get: false }));
91
- }
92
- }
93
- const test = Test.new('ID');
94
- const effect = jest.fn((test) => void test.value);
95
- test.get(effect);
96
- test.value = "foo";
97
- await expect(test).toHaveUpdated();
98
- expect(effect).toBeCalledTimes(2);
99
- });
100
- });
101
- describe("setter", () => {
102
- it("will reject update if false", () => {
103
- class Test extends Model {
104
- constructor() {
105
- super(...arguments);
106
- this.value = use(() => ({ set: false }));
107
- }
108
- }
109
- const test = Test.new('ID');
110
- const assign = () => test.value = "foo";
111
- expect(assign).toThrowError(`ID.value is read-only.`);
112
- });
113
- it("will prevent update if returns false", async () => {
114
- const didSetValue = jest.fn((newValue) => {
115
- if (newValue == "ignore")
116
- return false;
117
- });
118
- class Test extends Model {
119
- constructor() {
120
- super(...arguments);
121
- this.property = use(() => {
122
- return {
123
- value: "foobar",
124
- set: didSetValue
125
- };
126
- });
127
- }
128
- }
129
- const test = Test.new();
130
- expect(test.property).toBe("foobar");
131
- test.property = "test";
132
- expect(didSetValue).toBeCalledWith("test", "foobar");
133
- expect(test.property).toBe("test");
134
- await expect(test).toHaveUpdated();
135
- test.property = "ignore";
136
- expect(didSetValue).toBeCalledWith("ignore", "test");
137
- expect(test.property).toBe("test");
138
- await expect(test).not.toHaveUpdated();
139
- });
140
- // duplicate test?
141
- it("will revert update if returns false", async () => {
142
- let ignore = false;
143
- class Test extends Model {
144
- constructor() {
145
- super(...arguments);
146
- this.property = use(() => {
147
- return {
148
- value: 0,
149
- set: (value) => ignore
150
- ? false
151
- : () => value + 10
152
- };
153
- });
154
- }
155
- }
156
- const instance = Test.new();
157
- expect(instance.property).toBe(0);
158
- instance.property = 10;
159
- expect(instance.property).toBe(20);
160
- await expect(instance).toHaveUpdated();
161
- ignore = true;
162
- instance.property = 0;
163
- expect(instance.property).toBe(20);
164
- await expect(instance).not.toHaveUpdated();
165
- });
166
- it("will not duplicate explicit update", () => {
167
- class Test extends Model {
168
- constructor() {
169
- super(...arguments);
170
- this.property = use(() => ({
171
- value: "foobar",
172
- set: (value) => () => value + "!"
173
- }));
174
- }
175
- }
176
- const test = Test.new();
177
- const didUpdate = jest.fn();
178
- test.set(didUpdate);
179
- expect(test.property).toBe("foobar");
180
- test.property = "test";
181
- expect(test.property).toBe("test!");
182
- expect(didUpdate).toBeCalledTimes(1);
183
- });
184
- it("will not update on reassignment", () => {
185
- class Test extends Model {
186
- constructor() {
187
- super(...arguments);
188
- this.property = use((key) => ({
189
- value: "foobar",
190
- set: (value) => {
191
- return () => value + "!";
192
- }
193
- }));
194
- }
195
- }
196
- const test = Test.new();
197
- const didUpdate = jest.fn();
198
- test.set(didUpdate);
199
- expect(test.property).toBe("foobar");
200
- test.property = "test";
201
- expect(test.property).toBe("test!");
202
- expect(didUpdate).toBeCalledTimes(1);
203
- });
204
- });
205
- });
206
- describe("model", () => {
207
- it("will init upon access", () => {
208
- class Child extends Model {
209
- constructor() {
210
- super(...arguments);
211
- this.value = "foo";
212
- }
213
- }
214
- class Test extends Model {
215
- constructor() {
216
- super(...arguments);
217
- this.child = use(Child, mockInit);
218
- }
219
- }
220
- const mockInit = jest.fn();
221
- Test.new();
222
- expect(mockInit).toBeCalledTimes(1);
223
- });
224
- it("will not create base Model", () => {
225
- class Test extends Model {
226
- constructor() {
227
- super(...arguments);
228
- // @ts-expect-error
229
- this.child = use(Model);
230
- }
231
- }
232
- const attempt = () => Test.new();
233
- expect(attempt).toThrowError("Cannot create base Model.");
234
- });
235
- it('will run callback on every assign', () => {
236
- class Child extends Model {
237
- constructor() {
238
- super(...arguments);
239
- this.value = "foo";
240
- }
241
- }
242
- class Parent extends Model {
243
- constructor() {
244
- super(...arguments);
245
- this.child = use(Child, callback);
246
- }
247
- }
248
- const callback = jest.fn();
249
- const parent = Parent.new();
250
- // Initial assignment
251
- expect(callback).toBeCalledTimes(1);
252
- parent.child = new Child();
253
- expect(callback).toBeCalledTimes(2);
254
- });
255
- it.todo("will run callback after assign completes");
256
- it.todo("will run cleanup on reassign");
257
- it('will only reassign a matching model', () => {
258
- class Child extends Model {
259
- }
260
- class Unrelated extends Model {
261
- }
262
- ;
263
- class Parent extends Model {
264
- constructor() {
265
- super(...arguments);
266
- this.child = use(Child);
267
- }
268
- }
269
- const parent = Parent.new("ID");
270
- expect(() => {
271
- parent.child = Unrelated.new("ID");
272
- }).toThrowError(`ID.child expected Model of type Child but got Unrelated.`);
273
- expect(() => {
274
- // @ts-expect-error
275
- parent.child = undefined;
276
- }).toThrowError(`ID.child expected Model of type Child but got undefined.`);
277
- });
278
- it('will allow undefined', () => {
279
- class Child extends Model {
280
- }
281
- class Parent extends Model {
282
- constructor() {
283
- super(...arguments);
284
- this.child = use(Child, false);
285
- }
286
- }
287
- const parent = Parent.new("ID");
288
- expect(parent.child).toBeInstanceOf(Child);
289
- parent.child = undefined;
290
- expect(parent.child).toBeUndefined();
291
- });
292
- it("will be provided by parent", () => {
293
- class Child extends Model {
294
- }
295
- ;
296
- class Test extends Model {
297
- constructor() {
298
- super(...arguments);
299
- this.child = use(Child);
300
- }
301
- }
302
- const context = new Context({ Test });
303
- expect(context.get(Child)).toBeInstanceOf(Child);
304
- });
305
- });
package/dist/esm/mocks.js DELETED
@@ -1,20 +0,0 @@
1
- export function mockPromise() {
2
- const methods = {};
3
- const promise = new Promise((res, rej) => {
4
- methods.resolve = res;
5
- methods.reject = rej;
6
- });
7
- return Object.assign(promise, methods);
8
- }
9
- export function mockWarn() {
10
- const warn = jest.spyOn(console, "warn");
11
- afterEach(() => warn.mockReset());
12
- afterAll(() => warn.mockRestore());
13
- return warn;
14
- }
15
- export function mockError() {
16
- const error = jest.spyOn(console, "error");
17
- afterEach(() => error.mockReset());
18
- afterAll(() => error.mockRestore());
19
- return error;
20
- }
package/dist/esm/model.js DELETED
@@ -1,278 +0,0 @@
1
- import { addListener, createEffect, event, pending, PENDING_KEYS, watch } from './control';
2
- export const define = Object.defineProperty;
3
- /** Register for all active models via string identifiers (usually unique). */
4
- const ID = new WeakMap();
5
- /** Internal state assigned to models. */
6
- const STATE = new WeakMap();
7
- /** External listeners for any given Model. */
8
- const NOTIFY = new WeakMap();
9
- /** Parent-child relationships. */
10
- const PARENT = new WeakMap();
11
- /** List of methods defined by a given type. */
12
- const METHODS = new WeakMap();
13
- /** Reference bound instance methods to real ones. */
14
- const METHOD = new WeakMap();
15
- class Model {
16
- constructor(...args) {
17
- prepare(this);
18
- define(this, "is", { value: this });
19
- init(this, args);
20
- }
21
- get(arg1, arg2) {
22
- const self = this.is;
23
- if (typeof arg1 == "function") {
24
- let pending = new Set();
25
- return createEffect(self, (proxy) => {
26
- const cb = arg1.call(proxy, proxy, pending);
27
- return cb === null ? null : ((update) => {
28
- pending = PENDING_KEYS.get(self);
29
- if (typeof cb == "function")
30
- cb(update);
31
- });
32
- });
33
- }
34
- if (arg1 === undefined)
35
- return snapshot(self);
36
- if (typeof arg2 == "function")
37
- return addListener(self, arg2, arg1);
38
- return arg1 === null
39
- ? Object.isFrozen(STATE.get(self))
40
- : fetch(self, arg1, arg2);
41
- }
42
- set(arg1, arg2) {
43
- const self = this.is;
44
- if (typeof arg1 == "function")
45
- return addListener(self, key => {
46
- if (typeof key == "string")
47
- return arg1.call(self, key, self);
48
- });
49
- if (arg1 && typeof arg1 == "object") {
50
- const methods = METHODS.get(self.constructor);
51
- for (const key in arg1)
52
- if (methods.has(key))
53
- self[key] = arg1[key];
54
- else if (key in self)
55
- update(self, key, arg1[key], arg2 === true);
56
- }
57
- else
58
- event(self, arg1);
59
- return pending(self);
60
- }
61
- /**
62
- * Iterate over managed properties in this instance of Model.
63
- * Yeilds the key and current value for each property.
64
- */
65
- [Symbol.iterator]() {
66
- return Object.entries(STATE.get(this.is))[Symbol.iterator]();
67
- }
68
- /**
69
- * Create and activate a new instance of this model.
70
- *
71
- * **Important** - Unlike `new this(...)` - this method also activates state.
72
- *
73
- * @param args - arguments sent to constructor
74
- */
75
- static new(...args) {
76
- const instance = new this(...args);
77
- event(instance);
78
- return instance;
79
- }
80
- /**
81
- * Static equivalent of `x instanceof this`.
82
- * Determines if provided class is a subtype of this one.
83
- * If so, language server will make available all static
84
- * methods and properties of this class.
85
- */
86
- static is(maybe) {
87
- return maybe === this || typeof maybe == "function" && maybe.prototype instanceof this;
88
- }
89
- /**
90
- * Register a callback to run when any instance of this Model is updated.
91
- */
92
- static on(listener) {
93
- let notify = NOTIFY.get(this);
94
- if (!notify)
95
- NOTIFY.set(this, notify = new Set);
96
- notify.add(listener);
97
- return () => notify.delete(listener);
98
- }
99
- }
100
- define(Model.prototype, "toString", {
101
- value() {
102
- return ID.get(this.is);
103
- }
104
- });
105
- define(Model, "toString", {
106
- value() {
107
- return this.name;
108
- }
109
- });
110
- /** Apply instructions and inherited event listeners. Ensure class metadata is ready. */
111
- function prepare(model) {
112
- let T = model.constructor;
113
- if (T === Model)
114
- throw new Error("Cannot create base Model.");
115
- const chain = [];
116
- let keys = new Set();
117
- ID.set(model, `${T}-${uid()}`);
118
- while (T.name) {
119
- for (const cb of NOTIFY.get(T) || [])
120
- addListener(model, cb);
121
- if (T === Model)
122
- break;
123
- else
124
- chain.unshift(T);
125
- T = Object.getPrototypeOf(T);
126
- }
127
- for (const type of chain) {
128
- if (METHODS.has(type)) {
129
- keys = METHODS.get(type);
130
- continue;
131
- }
132
- METHODS.set(type, keys = new Set(keys));
133
- for (const [key, { value }] of Object.entries(Object.getOwnPropertyDescriptors(type.prototype))) {
134
- if (!value || key == "constructor")
135
- continue;
136
- function bind(method = value) {
137
- const value = method.bind(this.is);
138
- METHOD.set(value, method);
139
- define(this.is, key, { value, writable: true });
140
- return value;
141
- }
142
- keys.add(key);
143
- define(type.prototype, key, { get: bind, set: bind });
144
- }
145
- }
146
- }
147
- /**
148
- * Apply model arguemnts, run callbacks and observe properties.
149
- * Accumulate and handle cleanup events.
150
- **/
151
- function init(model, args) {
152
- const methods = METHODS.get(model.constructor);
153
- const state = {};
154
- STATE.set(model, state);
155
- args = args.flat().filter(arg => {
156
- if (typeof arg != "string")
157
- return true;
158
- ID.set(model, arg);
159
- });
160
- addListener(model, () => {
161
- if (!PARENT.has(model))
162
- PARENT.set(model, null);
163
- args.forEach((arg) => {
164
- const use = typeof arg == "function"
165
- ? arg.call(model, model)
166
- : arg;
167
- if (use instanceof Promise)
168
- use.catch(err => {
169
- console.error(`Async error in constructor for ${model}:`);
170
- console.error(err);
171
- });
172
- else if (typeof use == "function")
173
- addListener(model, use, null);
174
- else if (typeof use == "object")
175
- for (const key in use)
176
- if (key in model || methods.has(key))
177
- model[key] = use[key];
178
- });
179
- for (const key in model) {
180
- const desc = Object.getOwnPropertyDescriptor(model, key);
181
- if ("value" in desc) {
182
- update(model, key, desc.value, true);
183
- define(model, key, {
184
- configurable: false,
185
- get() {
186
- return watch(this, key, state[key]);
187
- },
188
- set(value) {
189
- update(model, key, value);
190
- mayAdopt(model, value);
191
- }
192
- });
193
- }
194
- }
195
- addListener(model, () => {
196
- for (const [_, value] of model)
197
- if (value instanceof Model && PARENT.get(value) === model)
198
- value.set(null);
199
- Object.freeze(state);
200
- }, null);
201
- return null;
202
- });
203
- }
204
- function mayAdopt(parent, child) {
205
- if (child instanceof Model && !PARENT.has(child)) {
206
- PARENT.set(child, parent);
207
- event(child);
208
- }
209
- }
210
- function fetch(subject, property, required) {
211
- const state = STATE.get(subject);
212
- if (property in state || required === false) {
213
- const value = state[property];
214
- if (value !== undefined || !required)
215
- return value;
216
- }
217
- const error = new Error(`${subject}.${property} is not yet available.`);
218
- const promise = new Promise((resolve, reject) => {
219
- addListener(subject, key => {
220
- if (key === property) {
221
- resolve(state[key]);
222
- return null;
223
- }
224
- if (key === null)
225
- reject(new Error(`${subject} is destroyed.`));
226
- });
227
- });
228
- throw Object.assign(promise, {
229
- toString: () => String(error),
230
- name: "Suspense",
231
- message: error.message,
232
- stack: error.stack
233
- });
234
- }
235
- /** Currently accumulating export. Stores real values of placeholder properties such as ref() or child models. */
236
- let EXPORT;
237
- function snapshot(model) {
238
- const values = {};
239
- let isNotRecursive;
240
- if (!EXPORT) {
241
- isNotRecursive = true;
242
- EXPORT = new Map([[model, values]]);
243
- }
244
- for (let [key, value] of model) {
245
- if (EXPORT.has(value))
246
- value = EXPORT.get(value);
247
- else if (value && typeof value == "object" && "get" in value && typeof value.get === "function")
248
- EXPORT.set(value, value = value.get());
249
- values[key] = value;
250
- }
251
- if (isNotRecursive)
252
- EXPORT = undefined;
253
- return Object.freeze(values);
254
- }
255
- function update(subject, key, value, arg) {
256
- const state = STATE.get(subject);
257
- if (Object.isFrozen(state))
258
- throw new Error(`Tried to update ${String(key)} but ${subject} is destroyed.`);
259
- const previous = state[key];
260
- if (typeof arg == "function") {
261
- const out = arg.call(subject, value, previous);
262
- if (out === false)
263
- return false;
264
- if (typeof out == "function")
265
- value = out();
266
- }
267
- if (value === previous)
268
- return;
269
- state[key] = value;
270
- if (arg !== true)
271
- event(subject, key);
272
- return true;
273
- }
274
- /** Random alphanumberic of length 6; always starts with a letter. */
275
- function uid() {
276
- return (0.278 + Math.random() * 0.722).toString(36).substring(2, 8).toUpperCase();
277
- }
278
- export { event, fetch, mayAdopt, METHOD, Model, PARENT, STATE, uid, update, };