arrmatura 6.3.1 → 6.3.2

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,45 +1,44 @@
1
- import { areEqual, capitalize, identity } from "ultimus";
2
- import { stringify } from "../utils/stringify";
3
- import type { Delta, Fn, Hash, Proc, Uid } from "ultimus/types";
4
- import type { IEntitron, IManifestNode, IComponent, IPlatform } from "arrmatura/types";
1
+ import { capitalize, identity } from "ultimus";
2
+ import type { Delta, Fn, Hash, LogEntry, Proc, Uid } from "ultimus/types";
5
3
 
6
- type EProcedure = (c: IEntitron) => void;
4
+ import type { IArrmatron, IManifestNode, IComponent, IPlatform } from "../../types";
5
+ import { objectFingerprint } from "../utils/FingerprintMashine";
6
+ import { applyStateChangedToImpl } from "../utils/applyStateChangedToImpl";
7
+ import { stringify } from "../utils/stringify";
7
8
 
8
9
  /**
9
- * The Entitron is the essential the framework concept.
10
- *
11
- * It creates a own component instance,
12
- * manages its state and behavior,
13
- * and ties it up with others.
10
+ * The Arrmatron is the essential concept of the framework.
11
+ * It creates a component instance, manages its state and keeps in touch with others.
14
12
  */
15
- export class Entitron<T extends IManifestNode = IManifestNode> implements IEntitron {
16
- readonly component: IComponent;
17
- $children?: Map<Uid, IEntitron>;
18
- readonly root: IEntitron;
19
- readonly scope: IEntitron;
20
- isDone = false;
13
+ export class Arrmatron<T extends IManifestNode = IManifestNode> implements IArrmatron {
14
+ readonly $component: IComponent;
15
+ readonly root: IArrmatron;
16
+ readonly scope: Arrmatron;
21
17
 
18
+ $children?: Map<Uid, Arrmatron>;
19
+ isDone = false;
22
20
  #isInited = false;
23
- #state: Hash = {};
21
+ #fprints: Hash = {};
24
22
  #initialState: Hash = {};
25
- #listeners?: Set<(c: Entitron) => void>;
26
- #defered?: EProcedure[] = [];
23
+ #listeners?: Set<(c: Arrmatron) => void>;
24
+ #defered?: Array<(c: IArrmatron) => void> = [];
27
25
  #weak?: Map<string, number>;
28
- #refs?: Hash<IEntitron>;
26
+ #refs?: Hash<IArrmatron>;
29
27
  #propFnMap?: Map<string, Proc>;
30
28
 
31
- constructor(readonly platform: IPlatform, protected readonly manifest: T, private _parent?: IEntitron, scope?: IEntitron) {
29
+ constructor(readonly platform: IPlatform, protected readonly manifest: T, private _parent?: Arrmatron, scope?: Arrmatron) {
32
30
  this.scope = scope ?? this;
33
31
  this.root = scope?.root ?? this;
34
32
  this.#initialState = this.manifest.resolveInitialProps(this);
35
- this.component = this.createComponent(this.#initialState);
33
+ this.$component = this.createComponent(this.#initialState);
36
34
 
37
35
  if (this.refId) {
36
+ // this.log('addReference', this.refId)
38
37
  this.scope.addReference(this.refId, this);
39
38
  }
40
39
  }
41
40
 
42
- get parent(): IEntitron | undefined {
41
+ get parent(): IArrmatron | undefined {
43
42
  return this._parent;
44
43
  }
45
44
 
@@ -59,64 +58,67 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
59
58
  // --- State
60
59
 
61
60
  // updates component state
62
- up(Δ?: Delta | Promise<any> | null | void | unknown, force = false): void {
63
- if (!Δ || this.isDone) {
61
+ up(delta?: Delta | Promise<any> | null | void | unknown, force = false): void {
62
+ if (!delta || this.isDone) {
64
63
  return;
65
64
  }
66
65
 
67
- if (Δ instanceof Promise) {
68
- this.upAsync(Δ, "*");
66
+ if (delta instanceof Promise) {
67
+ const racer = this.raceCondition(`set`);
68
+ delta.then((val) => racer(() => this.up(val)));
69
69
  return;
70
70
  }
71
71
 
72
72
  const changes = new Map<string, unknown>();
73
- Object.entries(Δ).forEach(([k, v]) => {
73
+ Object.entries(delta).forEach(([k, v]) => {
74
74
  if (v instanceof Promise) {
75
- this.upAsync(v, k);
76
- } else if (k && typeof v !== "undefined" && !areEqual(v, this.#state[k])) {
77
- this.#state[k] = v;
78
- changes.set(k, v);
75
+ const isSpreading = !k || k.startsWith("...");
76
+ const racer = this.raceCondition(`set:${k}`);
77
+ void v.then((val) => racer(() => this.up(isSpreading ? val : { [k]: val })));
78
+
79
+ } else if (k && typeof v !== "undefined") {
80
+ if (k[0] === "$") {
81
+ if (this.$component[k] !== v) {
82
+ changes.set(k.slice(1), v);
83
+ }
84
+ } else {
85
+
86
+ const fprint = objectFingerprint(v);
87
+ if (!(k in this.#fprints) || fprint !== this.#fprints[k]) {
88
+ this.#fprints[k] = fprint;
89
+ changes.set(k, v);
90
+ // } else {
91
+ // if (v && typeof v === "object" && v !== this.get(k)) {
92
+
93
+ // this.log('same skip', k, '\n ', v, '==', this.get(k), '\n ', this.#fprints[k], '==', fprint)
94
+ // }
95
+ }
96
+ }
79
97
  }
80
98
  });
81
99
 
82
100
  if (changes.size || force) {
83
- this.stateChanged(changes);
101
+ applyStateChangedToImpl(changes, this.$component);
102
+ this.touch();
84
103
  }
85
104
  }
86
105
 
87
- private upAsync(promise: Promise<any>, key: string): void {
88
- const racer = this.raceCondition(`set:${key}`);
89
- if (key[0] !== "*") {
90
- const akey = key.replace("Promise", "");
91
- void promise.then((val) => racer(() => this.up({ [akey]: val })));
92
- } else {
93
- void promise.then((r: Delta) => racer(() => this.up(r)));
94
- }
95
- }
96
-
97
- private stateChanged(changes: Map<string, unknown>) {
98
- if (this.component.stateChanged) {
99
- this.component.stateChanged(changes);
100
- } else {
101
- changes.forEach((v, k) => {
102
- const setter = this.component[`set${capitalize(k)}`];
103
- if (typeof setter === "function") {
104
- setter.call(this.component, v);
105
- } else {
106
- this.component[k] = v;
107
- }
108
- });
109
- }
110
- this.touch();
111
- }
112
-
113
106
  // do recontent and notify all
114
107
  touch() {
115
108
  this.recontent();
116
109
  this.notify();
117
110
  }
118
111
 
119
- // gets component property by key
112
+ /**
113
+ * Retrieves the component value associated with the given property ID.
114
+ * Dot-sepated pathes to properties are supported.
115
+ * Supported getters like `get<Name>()`.
116
+ * use '@' prefix to access platform resources.
117
+ * Functional values are bounded and cached.
118
+ *
119
+ * @param {string} propId - The ID of the property.
120
+ * @return {unknown} The value associated with the property ID.
121
+ */
120
122
  get(propId: string): unknown {
121
123
  const map = this.#propFnMap ?? (this.#propFnMap = new Map());
122
124
  if (map.has(propId)) {
@@ -124,15 +126,17 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
124
126
  }
125
127
 
126
128
  let fn: Fn | null = null;
127
- const impl = this.component;
129
+ const impl = this.$component;
128
130
  const instant = impl[propId];
131
+
129
132
  if (instant && typeof instant === "function") {
130
133
  const bound = instant.bind(impl);
131
134
  fn = () => bound;
135
+ } else if (impl.__getProperty) {
136
+ fn = () => (impl.__getProperty as (p: string) => unknown)(propId);
132
137
  } else {
133
138
  const [pk, ...path] = propId.split(".");
134
- if (pk[0] === "@") {
135
- path.unshift(pk.slice(1));
139
+ if (pk === "R") {
136
140
  const val = this.platform.getResource(path) ?? null;
137
141
  fn = () => val;
138
142
  } else {
@@ -159,7 +163,7 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
159
163
  }
160
164
 
161
165
  // add listener to componet state changes
162
- subscribe(handler: (src: Entitron) => any): Proc {
166
+ subscribe(handler: (src: Arrmatron) => any): Proc {
163
167
  const listeners = this.#listeners ?? (this.#listeners = new Set());
164
168
  listeners.add(handler);
165
169
  return () => {
@@ -171,62 +175,60 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
171
175
 
172
176
  // emits action event to this component
173
177
  emit(key: string, data: Delta): void {
178
+ if (this.isDone) return;
179
+
174
180
  try {
175
- // when key.endsWith("...") it will produce empty target, so don't need to hande it peculiarly
176
- let [refId, target] = key.split(".");
177
- if (!target) {
178
- target = refId;
179
- refId = 'this'
180
- }
181
+ if (key.endsWith(")")) {
182
+ const [refId, target] = key.split("(")[0].split(".");
183
+ const ref = this.getByRef(refId);
184
+ if (!ref) {
185
+ throw new Error(`No such reference: ${refId}`);
186
+ }
181
187
 
182
- const ref = this.getByRef(refId);
183
- if (!ref) {
184
- throw new Error(`No such reference: ${refId}`);
185
- }
188
+ const impl = ref.$component;
186
189
 
187
- if (!target) { // spread '...'
188
- ref.up(data);
189
- return;
190
- }
190
+ const method = impl[target];
191
+ if (!(typeof method === "function")) {
192
+ throw new Error(`Not a method: ${refId}.${target}()`);
193
+ }
191
194
 
192
- if (target[0] === "@") {
193
- ref.up({ [target.slice(1)]: data });
194
- return;
195
- }
195
+ const result = method.call(impl, data);
196
196
 
197
- const impl = ref.component;
198
- const method = impl[target] ?? impl[`on${capitalize(target)}`];
197
+ // this.log(`-> ${refId}.${target}(data)`, result, data, impl);
199
198
 
200
- // deprecated. Use @-prefix instead.
201
- if (!method) {
202
- const propKey = target[0] === "@" ? target.slice(1) : target;
203
- ref.up({ [propKey]: data });
204
- return;
205
- }
199
+ ref.up(result);
200
+ } else {
201
+ let [refId, target] = key.split(".");
202
+ if (!target) {
203
+ target = refId;
204
+ refId = "this";
205
+ }
206
206
 
207
- if (!(typeof method === "function")) {
208
- throw new Error(`Not a method: ${refId}.${target}`);
209
- }
207
+ const ref = this.getByRef(refId);
208
+ if (!ref) {
209
+ throw new Error(`No such reference: ${refId}`);
210
+ }
210
211
 
211
- const result = method.call(impl, data, impl, ref);
212
+ ref.up(target === "*" ? data : { [target]: data });
212
213
 
213
- this.log(`-> ${refId}.${target}`, result, data, impl);
214
+ // this.log(`-> ${refId}.${target} = `, data);
215
+ }
214
216
 
215
- ref.up(result);
216
217
  } catch (ex) {
217
218
  this.logError(`emit ${key}:`, ex);
218
219
  }
219
220
  return;
220
221
  }
222
+ // --- life-cycle.
221
223
 
222
- // Done life-cycle hook.
224
+ // Done hook.
223
225
  done() {
224
226
  if (this.isDone) {
225
227
  return;
226
228
  }
227
229
  this.isDone = true;
228
230
 
229
- this.component.done?.(this);
231
+ this.$component.done?.(this);
230
232
 
231
233
  this.children?.forEach((c) => c.done());
232
234
 
@@ -237,22 +239,14 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
237
239
  this._parent = undefined;
238
240
  }
239
241
 
240
- // map of children contexts
241
- get children(): Map<Uid, IEntitron> | undefined {
242
- return this.$children;
243
- }
244
-
245
- // map of chidren of its node
246
- get contentManifests() {
247
- return this.manifest.getSubNodes(this.platform);
248
- }
249
-
250
- // actual scope for re-content
251
- get recontentScope() {
252
- return this.scope;
242
+ // register callback to be called on done
243
+ defer(fn: (c: IArrmatron) => void) {
244
+ if (fn && typeof fn === "function") {
245
+ (this.#defered ?? (this.#defered = [])).push(fn);
246
+ }
253
247
  }
254
248
 
255
- settleAsChild() {
249
+ private settleAsChild() {
256
250
  if (!this.#isInited) {
257
251
  this.#isInited = true;
258
252
 
@@ -260,12 +254,29 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
260
254
 
261
255
  this.up(this.#initialState, true);
262
256
 
263
- this.up(this.component.init?.(this));
257
+ this.up(this.$component.init?.(this));
264
258
  } else {
265
259
  void this.up(this.manifest.resolveProps(this), true);
266
260
  }
267
261
  }
268
262
 
263
+ // --- Content.
264
+
265
+ // map of children contexts
266
+ get children(): Map<Uid, IArrmatron> | undefined {
267
+ return this.$children;
268
+ }
269
+
270
+ // map of chidren of its node
271
+ get contentManifests() {
272
+ return this.manifest.getSubNodes(this.platform);
273
+ }
274
+
275
+ // actual scope for re-content
276
+ get recontentScope() {
277
+ return this.scope;
278
+ }
279
+
269
280
  // perform children instantiation and updates
270
281
  private recontent() {
271
282
  const nodes = this.contentManifests;
@@ -278,10 +289,10 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
278
289
  }
279
290
  });
280
291
 
281
- const children = new Map<Uid, IEntitron>();
292
+ const children = new Map<Uid, Arrmatron>();
282
293
 
283
294
  nodes?.forEach((node: IManifestNode, uid: Uid) => {
284
- const e = this.children?.get(uid) ?? node.createEntitron(this.platform, this, this.recontentScope);
295
+ const e = this.$children?.get(uid) ?? node.createArrmatron(this.platform, this, this.recontentScope);
285
296
  children.set(e.uid, e);
286
297
  });
287
298
 
@@ -299,7 +310,7 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
299
310
  }
300
311
 
301
312
  // gets any context instance in current or upper scope by key
302
- getByRef(refId: string): IEntitron | undefined {
313
+ getByRef(refId: string): IArrmatron | undefined {
303
314
  if (!refId) return undefined;
304
315
 
305
316
  if (refId === "this") {
@@ -309,15 +320,14 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
309
320
  if (local) {
310
321
  return local;
311
322
  }
312
- const ref = this === this.scope ? null : this.scope.getByRef(refId);
313
- return ref as Entitron;
323
+ return this.parent?.getByRef(refId);
314
324
  }
315
325
 
316
- addReference(refId: string, c: IEntitron) {
326
+ private addReference(refId: string, c: IArrmatron) {
317
327
  (this.#refs ?? (this.#refs = {}))[refId] = c;
318
328
  }
319
329
 
320
- // --- Routines -------------------------
330
+ // --- Connectors -------------------------
321
331
 
322
332
  private initConnectors() {
323
333
  if (this.manifest.connectors) {
@@ -325,7 +335,7 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
325
335
  const [srcIds, targetPropName] = key.split("|");
326
336
  const [refId, sourcePropName] = srcIds.split(".");
327
337
 
328
- const ref = this.scope.getByRef(refId) as Entitron;
338
+ const ref = this.parent?.getByRef(refId) as Arrmatron;
329
339
  if (!ref) {
330
340
  this.logError(`Connect: No such ref: ${refId}`);
331
341
  continue;
@@ -336,26 +346,35 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
336
346
  continue;
337
347
  }
338
348
 
339
- const propName = sourcePropName[0] === '@' ? sourcePropName.slice(1) : sourcePropName;
340
349
  const applicator = targetPropName ? (rr: any) => ({ [targetPropName]: rr }) : identity;
341
350
 
342
- this.defer(
343
- ref.subscribe((source: Entitron) => {
351
+ this.defer((() => {
352
+ let popre;
353
+ return ref.subscribe(async (source: Arrmatron) => {
344
354
  try {
345
- const r = applicator(pipes(this, source.get(propName)));
355
+ const val = await source.get(sourcePropName);
356
+ const fingerprint = objectFingerprint(val);
357
+
358
+ if (popre !== undefined && popre === fingerprint) return;
359
+
360
+ popre = fingerprint;
361
+ const r = applicator(pipes(this, val));
346
362
  this.up(r as Delta);
347
363
  } catch (ex) {
348
364
  source.logError("Notify ", ex);
349
365
  }
350
366
  })
367
+ })()
351
368
  );
352
369
 
353
370
  // hot subscription
354
- Object.assign(this.#initialState, applicator(pipes(ref, ref.get(propName))));
371
+ Object.assign(this.#initialState, applicator(pipes(ref, ref.get(sourcePropName))));
355
372
  }
356
373
  }
357
374
  }
358
375
 
376
+ // --- Routines -------------------------
377
+
359
378
  raceCondition(key: string) {
360
379
  const COUNTERS = this.#weak ?? (this.#weak = new Map<string, number>());
361
380
  let counter = 1 + (COUNTERS.get(key) ?? 0);
@@ -368,13 +387,6 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
368
387
  };
369
388
  }
370
389
 
371
- // register callback to be called on done
372
- defer(fn: EProcedure) {
373
- if (fn && typeof fn === "function") {
374
- (this.#defered ?? (this.#defered = [])).push(fn);
375
- }
376
- }
377
-
378
390
  log(val: any, ...args: unknown[]) {
379
391
  this.platform.log({
380
392
  level: "log",
@@ -395,9 +407,8 @@ export class Entitron<T extends IManifestNode = IManifestNode> implements IEntit
395
407
  });
396
408
  }
397
409
 
398
- // access to resource by key
399
- res(key: string | string[]) {
400
- return this.platform.getResource(key);
410
+ toast = (t: LogEntry) => {
411
+ this.platform.toast(t, this);
401
412
  }
402
413
 
403
414
  toString(): string {
@@ -1,5 +1,6 @@
1
- import type { Data, Delta, Hash, LogEntry } from "ultimus/types";
2
- import type { IComponent, IEntitron } from "../../types";
1
+ import type { Delta, Hash, LogEntry } from "ultimus/types";
2
+
3
+ import type { IComponent, IArrmatron } from "../../types";
3
4
 
4
5
  /**
5
6
  * Base Ancestor for custom components.
@@ -7,61 +8,83 @@ import type { IComponent, IEntitron } from "../../types";
7
8
  export abstract class Component implements IComponent {
8
9
  [key: string]: unknown;
9
10
 
10
- constructor(_: Hash, readonly ctx: IEntitron) {
11
+ constructor(_: Hash, private readonly $ctx: IArrmatron) {
11
12
  //no-op
12
13
  }
13
14
 
14
15
  get platform() {
15
- return this.ctx.platform;
16
+ return this.$ctx.platform;
16
17
  }
17
18
 
18
19
  get isDone() {
19
- return this.ctx.isDone;
20
+ return this.$ctx.isDone;
20
21
  }
21
22
 
22
23
  // hook on done
23
- done(_: IEntitron): void {
24
+ done(_: IArrmatron): void {
24
25
  //no-op
25
26
  }
26
27
 
27
28
  // hook on init
28
29
  // returned value will be used to update state
29
- init(_: IEntitron): Delta | null | undefined | unknown {
30
+ init(_: IArrmatron): Delta | null | undefined | unknown {
30
31
  return undefined;
31
32
  }
32
33
 
33
- // update its state
34
+ // get from ctx state
35
+ get(key: string) {
36
+ return this.$ctx.get(key);
37
+ }
38
+
39
+ // update ctx state
34
40
  up(d: Delta) {
35
- return this.ctx.up(d);
41
+ return this.$ctx.up(d);
36
42
  }
43
+
37
44
  touch() {
38
- return this.ctx.touch();
45
+ return this.$ctx.touch();
39
46
  }
47
+
40
48
  // emit action event to another component by key
41
49
  emit(key: string, data: Delta) {
42
- return this.ctx.emit(key, data);
50
+ return this.$ctx.emit(key, data);
43
51
  }
44
52
 
45
- // access to resource entity by key
46
- res(key: string): Data | Data[] {
47
- return this.ctx.res(key);
53
+ // register callback to be called on done
54
+ defer(fn: () => void) {
55
+ this.$ctx.defer(fn);
48
56
  }
49
57
 
50
58
  // register callback to be called on done
51
- defer(fn: () => void) {
52
- this.ctx.defer(fn);
59
+ defineCalculatedProperty(key, fn: (...args: any[]) => any, deps?: string[]) {
60
+ let depKey = "";
61
+ let depValue = undefined;
62
+ Object.defineProperty(this, key, {
63
+ get() {
64
+ const args: any[] = deps?.length ? deps.map((k) => this.$ctx.get(k)) : [];
65
+ const newKey = args.join(':');
66
+ if (depKey !== newKey) {
67
+ depKey = newKey;
68
+ depValue = fn.apply(this, args);
69
+ }
70
+ return depValue;
71
+ },
72
+ });
73
+ }
74
+
75
+ toast(entry: LogEntry) {
76
+ this.$ctx.toast(entry);
53
77
  }
54
78
 
55
79
  log(val: unknown, ...args: unknown[]) {
56
- this.ctx.log(val, ...args);
80
+ this.$ctx.log(val, ...args);
57
81
  }
58
82
 
59
83
  logError(val: unknown, ...args: unknown[]) {
60
- this.ctx.logError(val, ...args);
84
+ this.$ctx.logError(val, ...args);
61
85
  }
62
86
 
63
- toast(message: string | Partial<LogEntry> = "ok") {
64
- // may overriden from props
65
- void this.emit("toasters.send", typeof message === "string" ? { message } : message);
87
+ toString() {
88
+ return this.$ctx.toString();
66
89
  }
67
90
  }