epos-unit 1.15.0 → 1.17.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.
@@ -7,7 +7,7 @@ declare const _parent_: unique symbol;
7
7
  declare const _attached_: unique symbol;
8
8
  declare const _disposers_: unique symbol;
9
9
  declare const _ancestors_: unique symbol;
10
- declare const _pendingAttachFns_: unique symbol;
10
+ declare const _pendingAttachHooks_: unique symbol;
11
11
  type Node<T> = Unit<T> | Obj | Arr;
12
12
  declare class Unit<TRoot = unknown> {
13
13
  /**
@@ -19,6 +19,7 @@ declare class Unit<TRoot = unknown> {
19
19
  */
20
20
  [epos.state.DETACH]: () => void;
21
21
  '@': string;
22
+ id: string;
22
23
  log: ReturnType<typeof createLog>;
23
24
  [':version']?: number;
24
25
  [_root_]?: TRoot;
@@ -26,18 +27,13 @@ declare class Unit<TRoot = unknown> {
26
27
  [_attached_]?: boolean;
27
28
  [_disposers_]?: Set<() => void>;
28
29
  [_ancestors_]?: Map<Cls, unknown>;
29
- [_pendingAttachFns_]?: (() => void)[];
30
+ [_pendingAttachHooks_]?: (() => void)[];
30
31
  constructor(parent: Unit<TRoot> | null);
31
32
  /**
32
33
  * Gets the root unit of the current unit's tree.
33
34
  * The result is cached for subsequent calls.
34
35
  */
35
36
  get $(): TRoot | (undefined & TRoot);
36
- /**
37
- * Finds the closest ancestor unit of a given type.
38
- * The result is cached for subsequent calls.
39
- */
40
- closest<T extends Unit>(Ancestor: Cls<T>): T | null;
41
37
  /**
42
38
  * A wrapper around MobX's `autorun` that automatically disposes
43
39
  * the reaction when the unit is detached.
@@ -62,6 +58,11 @@ declare class Unit<TRoot = unknown> {
62
58
  * Creates an error for an unreachable code path.
63
59
  */
64
60
  never(message?: string): Error;
61
+ /**
62
+ * Finds the closest ancestor unit of a given type.
63
+ * The result is cached for subsequent calls.
64
+ */
65
+ closest<T extends Unit>(Ancestor: Cls<T>): T | null;
65
66
  }
66
67
 
67
- export { type Node, Unit, _ancestors_, _attached_, _disposers_, _parent_, _pendingAttachFns_, _root_ };
68
+ export { type Node, Unit, _ancestors_, _attached_, _disposers_, _parent_, _pendingAttachHooks_, _root_ };
package/dist/epos-unit.js CHANGED
@@ -1,26 +1,32 @@
1
1
  // src/epos-unit.ts
2
2
  import { createLog, is } from "dropcap/utils";
3
3
  import "epos";
4
+ import { customAlphabet } from "nanoid";
4
5
  var _root_ = /* @__PURE__ */ Symbol("root");
5
6
  var _parent_ = /* @__PURE__ */ Symbol("parent");
6
7
  var _attached_ = /* @__PURE__ */ Symbol("attached");
7
8
  var _disposers_ = /* @__PURE__ */ Symbol("disposers");
8
9
  var _ancestors_ = /* @__PURE__ */ Symbol("ancestors");
9
- var _pendingAttachFns_ = /* @__PURE__ */ Symbol("pendingAttachFns");
10
+ var _pendingAttachHooks_ = /* @__PURE__ */ Symbol("pendingAttachHooks");
11
+ var nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 8);
10
12
  var Unit = class {
11
13
  constructor(parent) {
14
+ this.id = nanoid();
12
15
  this[_parent_] = parent;
13
- const versions = getVersions(this);
16
+ const versioner = getVersioner(this);
17
+ const versions = getVersions(versioner);
14
18
  if (versions.length > 0) this[":version"] = versions.at(-1);
15
19
  }
20
+ // ---------------------------------------------------------------------------
21
+ // ATTACH / DETACH
22
+ // ---------------------------------------------------------------------------
16
23
  /**
17
24
  * Lifecycle method called when the unit is attached to the state tree.
18
25
  */
19
26
  [epos.state.ATTACH]() {
20
- setProperty(this, "log", createLog(this["@"]));
21
27
  epos.state.transaction(() => {
22
28
  const versioner = getVersioner(this);
23
- const versions = getVersions(this);
29
+ const versions = getVersions(versioner);
24
30
  for (const version of versions) {
25
31
  if (is.number(this[":version"]) && this[":version"] >= version) continue;
26
32
  const versionFn = versioner[version];
@@ -29,40 +35,61 @@ var Unit = class {
29
35
  this[":version"] = version;
30
36
  }
31
37
  });
38
+ let log = createLog(this["@"]);
39
+ Reflect.defineProperty(this, "log", {
40
+ configurable: true,
41
+ get: () => log,
42
+ set: (v) => log = v
43
+ });
44
+ const stateDescriptor = Reflect.getOwnPropertyDescriptor(this.constructor.prototype, "state");
45
+ if (stateDescriptor && stateDescriptor.get) {
46
+ const value = stateDescriptor.get.call(this);
47
+ const state = epos.state.local(value, { deep: false });
48
+ Reflect.defineProperty(this, "state", { get: () => state });
49
+ }
32
50
  for (const prototype of getPrototypes(this)) {
33
51
  const descriptors = Object.getOwnPropertyDescriptors(prototype);
34
52
  for (const [key, descriptor] of Object.entries(descriptors)) {
35
53
  if (key === "constructor") continue;
36
54
  if (this.hasOwnProperty(key)) continue;
37
- if (descriptor.get || descriptor.set) continue;
38
- if (!is.function(descriptor.value)) continue;
39
- const fn = descriptor.value.bind(this);
40
- if (key.endsWith("View")) {
41
- let Component = epos.component(fn);
55
+ if (is.function(descriptor.value) && key.endsWith("View")) {
56
+ let Component = epos.component(descriptor.value.bind(this));
42
57
  Component.displayName = `${this.constructor.name}.${key}`;
43
- setProperty(this, key, Component);
44
- } else {
45
- setProperty(this, key, fn);
58
+ Reflect.defineProperty(this, key, {
59
+ configurable: true,
60
+ get: () => Component,
61
+ set: (v) => Component = v
62
+ });
63
+ } else if (is.function(descriptor.value)) {
64
+ let method = descriptor.value.bind(this);
65
+ Reflect.defineProperty(this, key, {
66
+ configurable: true,
67
+ get: () => method,
68
+ set: (v) => method = v
69
+ });
70
+ } else if (descriptor.get) {
71
+ const getter = descriptor.get;
72
+ const computed = epos.libs.mobx.computed(() => getter.call(this));
73
+ Reflect.defineProperty(this, key, {
74
+ configurable: true,
75
+ get: () => computed.get(),
76
+ set: descriptor.set
77
+ });
46
78
  }
47
79
  }
48
80
  }
49
- const stateDescriptor = Reflect.getOwnPropertyDescriptor(this.constructor.prototype, "state");
50
- if (stateDescriptor && stateDescriptor.get) {
51
- const state = stateDescriptor.get.call(this);
52
- setProperty(this, "state", epos.libs.mobx.observable.object(state, {}, { deep: false }));
53
- }
54
81
  const attach = Reflect.get(this, "attach");
55
82
  if (is.function(attach)) {
56
83
  const unattachedRoot = findUnattachedRoot(this);
57
84
  if (!unattachedRoot) throw this.never();
58
- ensureProperty(unattachedRoot, _pendingAttachFns_, () => []);
59
- unattachedRoot[_pendingAttachFns_].push(() => attach());
60
- if (this[_pendingAttachFns_]) {
61
- this[_pendingAttachFns_].forEach((attach2) => attach2());
62
- delete this[_pendingAttachFns_];
63
- }
85
+ ensure(unattachedRoot, _pendingAttachHooks_, () => []);
86
+ unattachedRoot[_pendingAttachHooks_].push(() => attach());
87
+ }
88
+ if (this[_pendingAttachHooks_]) {
89
+ this[_pendingAttachHooks_].forEach((attach2) => attach2());
90
+ delete this[_pendingAttachHooks_];
64
91
  }
65
- setProperty(this, _attached_, true);
92
+ Reflect.defineProperty(this, _attached_, { configurable: true, get: () => true });
66
93
  }
67
94
  /**
68
95
  * Lifecycle method called when the unit is detached from the state tree.
@@ -72,45 +99,33 @@ var Unit = class {
72
99
  this[_disposers_].forEach((disposer) => disposer());
73
100
  this[_disposers_].clear();
74
101
  }
102
+ if (this[_ancestors_]) {
103
+ this[_ancestors_].clear();
104
+ }
75
105
  const detach = Reflect.get(this, "detach");
76
106
  if (is.function(detach)) detach();
77
- delete this[_root_];
78
- delete this[_attached_];
79
- delete this[_ancestors_];
80
- delete this[_disposers_];
81
107
  }
108
+ // ---------------------------------------------------------------------------
109
+ // ROOT GETTER
110
+ // ---------------------------------------------------------------------------
82
111
  /**
83
112
  * Gets the root unit of the current unit's tree.
84
113
  * The result is cached for subsequent calls.
85
114
  */
86
115
  get $() {
87
- ensureProperty(this, _root_, () => findRoot(this));
116
+ ensure(this, _root_, () => findRoot(this));
88
117
  return this[_root_];
89
118
  }
90
- /**
91
- * Finds the closest ancestor unit of a given type.
92
- * The result is cached for subsequent calls.
93
- */
94
- closest(Ancestor) {
95
- ensureProperty(this, _ancestors_, () => /* @__PURE__ */ new Map());
96
- if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor);
97
- let cursor = this;
98
- while (cursor) {
99
- if (cursor instanceof Ancestor) {
100
- this[_ancestors_].set(Ancestor, cursor);
101
- return cursor;
102
- }
103
- cursor = getParent(cursor);
104
- }
105
- return null;
106
- }
119
+ // ---------------------------------------------------------------------------
120
+ // METHODS
121
+ // ---------------------------------------------------------------------------
107
122
  /**
108
123
  * A wrapper around MobX's `autorun` that automatically disposes
109
124
  * the reaction when the unit is detached.
110
125
  */
111
126
  autorun(...args) {
112
127
  const disposer = epos.libs.mobx.autorun(...args);
113
- ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
128
+ ensure(this, _disposers_, () => /* @__PURE__ */ new Set());
114
129
  this[_disposers_].add(disposer);
115
130
  return disposer;
116
131
  }
@@ -120,7 +135,7 @@ var Unit = class {
120
135
  */
121
136
  reaction(...args) {
122
137
  const disposer = epos.libs.mobx.reaction(...args);
123
- ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
138
+ ensure(this, _disposers_, () => /* @__PURE__ */ new Set());
124
139
  this[_disposers_].add(disposer);
125
140
  return disposer;
126
141
  }
@@ -130,7 +145,7 @@ var Unit = class {
130
145
  */
131
146
  setTimeout(...args) {
132
147
  const id = self.setTimeout(...args);
133
- ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
148
+ ensure(this, _disposers_, () => /* @__PURE__ */ new Set());
134
149
  this[_disposers_].add(() => self.clearTimeout(id));
135
150
  return id;
136
151
  }
@@ -140,7 +155,7 @@ var Unit = class {
140
155
  */
141
156
  setInterval(...args) {
142
157
  const id = self.setInterval(...args);
143
- ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
158
+ ensure(this, _disposers_, () => /* @__PURE__ */ new Set());
144
159
  this[_disposers_].add(() => self.clearInterval(id));
145
160
  return id;
146
161
  }
@@ -153,15 +168,25 @@ var Unit = class {
153
168
  Error.captureStackTrace(error, this.never);
154
169
  return error;
155
170
  }
171
+ /**
172
+ * Finds the closest ancestor unit of a given type.
173
+ * The result is cached for subsequent calls.
174
+ */
175
+ closest(Ancestor) {
176
+ ensure(this, _ancestors_, () => /* @__PURE__ */ new Map());
177
+ if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor);
178
+ let cursor = this;
179
+ while (cursor) {
180
+ if (cursor instanceof Ancestor) {
181
+ this[_ancestors_].set(Ancestor, cursor);
182
+ return cursor;
183
+ }
184
+ cursor = getParent(cursor);
185
+ }
186
+ return null;
187
+ }
156
188
  };
157
- function setProperty(object, key, value) {
158
- Reflect.defineProperty(object, key, {
159
- configurable: true,
160
- get: () => value,
161
- set: (v) => value = v
162
- });
163
- }
164
- function ensureProperty(object, key, getInitialValue) {
189
+ function ensure(object, key, getInitialValue) {
165
190
  if (key in object) return;
166
191
  const value = getInitialValue();
167
192
  Reflect.defineProperty(object, key, { configurable: true, get: () => value });
@@ -198,8 +223,7 @@ function getVersioner(unit) {
198
223
  if (!is.object(versioner)) return {};
199
224
  return versioner;
200
225
  }
201
- function getVersions(unit) {
202
- const versioner = getVersioner(unit);
226
+ function getVersions(versioner) {
203
227
  const numericKeys = Object.keys(versioner).filter((key) => is.numeric(key));
204
228
  return numericKeys.map(Number).sort((v1, v2) => v1 - v2);
205
229
  }
@@ -209,7 +233,7 @@ export {
209
233
  _attached_,
210
234
  _disposers_,
211
235
  _parent_,
212
- _pendingAttachFns_,
236
+ _pendingAttachHooks_,
213
237
  _root_
214
238
  };
215
239
  //# sourceMappingURL=epos-unit.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/epos-unit.ts"],"sourcesContent":["import type { Arr, Cls, Obj } from 'dropcap/types'\nimport { createLog, is } from 'dropcap/utils'\nimport 'epos'\n\nexport const _root_ = Symbol('root')\nexport const _parent_ = Symbol('parent')\nexport const _attached_ = Symbol('attached')\nexport const _disposers_ = Symbol('disposers')\nexport const _ancestors_ = Symbol('ancestors')\nexport const _pendingAttachFns_ = Symbol('pendingAttachFns')\n\nexport type Node<T> = Unit<T> | Obj | Arr\n\nexport class Unit<TRoot = unknown> {\n declare '@': string\n declare log: ReturnType<typeof createLog>;\n declare [':version']?: number;\n declare [_root_]?: TRoot;\n declare [_parent_]?: Unit<TRoot> | null; // Parent reference for a not-yet-attached units\n declare [_attached_]?: boolean;\n declare [_disposers_]?: Set<() => void>;\n declare [_ancestors_]?: Map<Cls, unknown>;\n declare [_pendingAttachFns_]?: (() => void)[]\n\n constructor(parent: Unit<TRoot> | null) {\n this[_parent_] = parent\n const versions = getVersions(this)\n if (versions.length > 0) this[':version'] = versions.at(-1)!\n }\n\n /**\n * Lifecycle method called when the unit is attached to the state tree.\n */\n [epos.state.ATTACH]() {\n // Setup logger\n setProperty(this, 'log', createLog(this['@']))\n\n // Apply versioner\n epos.state.transaction(() => {\n const versioner = getVersioner(this)\n const versions = getVersions(this)\n for (const version of versions) {\n if (is.number(this[':version']) && this[':version'] >= version) continue\n const versionFn = versioner[version]\n if (!is.function(versionFn)) continue\n versionFn.call(this, this)\n this[':version'] = version\n }\n })\n\n // Prepare methods:\n // - Create components for methods ending with `View`\n // - Bind all other methods to the unit instance\n for (const prototype of getPrototypes(this)) {\n const descriptors = Object.getOwnPropertyDescriptors(prototype)\n\n for (const [key, descriptor] of Object.entries(descriptors)) {\n if (key === 'constructor') continue\n if (this.hasOwnProperty(key)) continue\n\n if (descriptor.get || descriptor.set) continue\n if (!is.function(descriptor.value)) continue\n const fn = descriptor.value.bind(this)\n\n if (key.endsWith('View')) {\n let Component = epos.component(fn)\n Component.displayName = `${this.constructor.name}.${key}`\n setProperty(this, key, Component)\n } else {\n setProperty(this, key, fn)\n }\n }\n }\n\n // Setup state\n const stateDescriptor = Reflect.getOwnPropertyDescriptor(this.constructor.prototype, 'state')\n if (stateDescriptor && stateDescriptor.get) {\n const state = stateDescriptor.get.call(this)\n setProperty(this, 'state', epos.libs.mobx.observable.object(state, {}, { deep: false }))\n }\n\n // Process attach queue.\n // We do not execute `attach` methods immediately, but rather queue them\n // on the highest unattached ancestor. This way we ensure that `attach`\n // methods are called after all versioners have been applied in the entire subtree.\n const attach = Reflect.get(this, 'attach')\n if (is.function(attach)) {\n const unattachedRoot = findUnattachedRoot(this)\n if (!unattachedRoot) throw this.never()\n\n ensureProperty(unattachedRoot, _pendingAttachFns_, () => [])\n unattachedRoot[_pendingAttachFns_].push(() => attach())\n\n if (this[_pendingAttachFns_]) {\n this[_pendingAttachFns_].forEach(attach => attach())\n delete this[_pendingAttachFns_]\n }\n }\n\n // Mark as attached\n setProperty(this, _attached_, true)\n }\n\n /**\n * Lifecycle method called when the unit is detached from the state tree.\n */\n [epos.state.DETACH]() {\n // 1. Run disposers\n if (this[_disposers_]) {\n this[_disposers_].forEach(disposer => disposer())\n this[_disposers_].clear()\n }\n\n // 2. Call detach method\n const detach = Reflect.get(this, 'detach')\n if (is.function(detach)) detach()\n\n // 3. Clean up internal properties\n delete this[_root_]\n delete this[_attached_]\n delete this[_ancestors_]\n delete this[_disposers_]\n }\n\n /**\n * Gets the root unit of the current unit's tree.\n * The result is cached for subsequent calls.\n */\n get $() {\n ensureProperty(this, _root_, () => findRoot(this))\n return this[_root_]\n }\n\n /**\n * Finds the closest ancestor unit of a given type.\n * The result is cached for subsequent calls.\n */\n closest<T extends Unit>(Ancestor: Cls<T>) {\n // Has cached value? -> Return it\n ensureProperty(this, _ancestors_, () => new Map())\n if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor) as T\n\n // Find the closest ancestor and cache it\n let cursor: Node<TRoot> | null = this\n while (cursor) {\n if (cursor instanceof Ancestor) {\n this[_ancestors_].set(Ancestor, cursor)\n return cursor\n }\n cursor = getParent(cursor)\n }\n\n return null\n }\n\n /**\n * A wrapper around MobX's `autorun` that automatically disposes\n * the reaction when the unit is detached.\n */\n autorun(...args: Parameters<typeof epos.libs.mobx.autorun>) {\n const disposer = epos.libs.mobx.autorun(...args)\n ensureProperty(this, _disposers_, () => new Set())\n this[_disposers_].add(disposer)\n return disposer\n }\n\n /**\n * A wrapper around MobX's `reaction` that automatically disposes\n * the reaction when the unit is detached.\n */\n reaction(...args: Parameters<typeof epos.libs.mobx.reaction>) {\n const disposer = epos.libs.mobx.reaction(...args)\n ensureProperty(this, _disposers_, () => new Set())\n this[_disposers_].add(disposer)\n return disposer\n }\n\n /**\n * A wrapper around `setTimeout` that automatically clears the timeout\n * when the unit is detached.\n */\n setTimeout(...args: Parameters<typeof self.setTimeout>) {\n const id = self.setTimeout(...args)\n ensureProperty(this, _disposers_, () => new Set())\n this[_disposers_].add(() => self.clearTimeout(id))\n return id\n }\n\n /**\n * A wrapper around `setInterval` that automatically clears the interval\n * when the unit is detached.\n */\n setInterval(...args: Parameters<typeof self.setInterval>) {\n const id = self.setInterval(...args)\n ensureProperty(this, _disposers_, () => new Set())\n this[_disposers_].add(() => self.clearInterval(id))\n return id\n }\n\n /**\n * Creates an error for an unreachable code path.\n */\n never(message = 'This should never happen') {\n const details = message ? `: ${message}` : ''\n const error = new Error(`[${this.constructor.name}] This should never happen${details}`)\n Error.captureStackTrace(error, this.never)\n return error\n }\n}\n\n// ---------------------------------------------------------------------------\n// HELPERS\n// ---------------------------------------------------------------------------\n\n/**\n * Defines a configurable property on an object.\n */\nfunction setProperty(object: object, key: PropertyKey, value: unknown) {\n Reflect.defineProperty(object, key, {\n configurable: true,\n get: () => value,\n set: v => (value = v),\n })\n}\n\n/**\n * Ensures a property exists on an object, initializing it if it doesn't.\n */\nfunction ensureProperty<T extends object, K extends PropertyKey, V>(\n object: T,\n key: K,\n getInitialValue: () => V,\n): asserts object is T & { [key in K]: V } {\n if (key in object) return\n const value = getInitialValue()\n Reflect.defineProperty(object, key, { configurable: true, get: () => value })\n}\n\n/**\n * Gets all prototypes of an object up to `Object.prototype`.\n */\nfunction getPrototypes(object: object): object[] {\n const prototype = Reflect.getPrototypeOf(object)\n if (!prototype || prototype === Object.prototype) return []\n return [prototype, ...getPrototypes(prototype)]\n}\n\n/**\n * Finds the root `Unit` in the hierarchy for a given unit.\n */\nfunction findRoot<T>(unit: Unit<T>) {\n let root: Unit<T> | null = null\n let cursor: Node<T> | null = unit\n\n while (cursor) {\n if (cursor instanceof Unit) root = cursor\n cursor = getParent(cursor)\n }\n\n return root as T\n}\n\n/**\n * Finds the highest unattached `Unit` in the hierarchy for a given unit.\n */\nfunction findUnattachedRoot<T>(unit: Unit<T>) {\n let unattachedRoot: Unit<T> | null = null\n let cursor: Node<T> | null = unit\n\n while (cursor) {\n if (cursor instanceof Unit && !cursor[_attached_]) unattachedRoot = cursor\n cursor = getParent(cursor)\n }\n\n return unattachedRoot\n}\n\n/**\n * Gets the parent of a node, which can be a `Unit`, an object, or an array.\n */\nfunction getParent<T>(node: Node<T>) {\n const parent: Node<T> | null = Reflect.get(node, _parent_) ?? Reflect.get(node, epos.state.PARENT) ?? null\n return parent\n}\n\n/**\n * Gets the versioner object from a unit's constructor.\n */\nfunction getVersioner<T>(unit: Unit<T>) {\n const versioner: unknown = Reflect.get(unit.constructor, 'versioner')\n if (!is.object(versioner)) return {}\n return versioner\n}\n\n/**\n * Gets a sorted list of numeric version keys from a unit's versioner.\n */\nfunction getVersions<T>(unit: Unit<T>) {\n const versioner = getVersioner(unit)\n const numericKeys = Object.keys(versioner).filter(key => is.numeric(key))\n return numericKeys.map(Number).sort((v1, v2) => v1 - v2)\n}\n"],"mappings":"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","names":["attach"]}
1
+ {"version":3,"sources":["../src/epos-unit.ts"],"sourcesContent":["import type { Arr, Cls, Obj } from 'dropcap/types'\nimport { createLog, is } from 'dropcap/utils'\nimport 'epos'\nimport { customAlphabet } from 'nanoid'\n\nexport const _root_ = Symbol('root')\nexport const _parent_ = Symbol('parent')\nexport const _attached_ = Symbol('attached')\nexport const _disposers_ = Symbol('disposers')\nexport const _ancestors_ = Symbol('ancestors')\nexport const _pendingAttachHooks_ = Symbol('pendingAttachHooks')\nconst nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 8)\n\nexport type Node<T> = Unit<T> | Obj | Arr\n\nexport class Unit<TRoot = unknown> {\n declare '@': string\n declare id: string\n declare log: ReturnType<typeof createLog>;\n declare [':version']?: number;\n declare [_root_]?: TRoot;\n declare [_parent_]?: Unit<TRoot> | null; // Parent reference for a not-yet-attached units\n declare [_attached_]?: boolean;\n declare [_disposers_]?: Set<() => void>;\n declare [_ancestors_]?: Map<Cls, unknown>;\n declare [_pendingAttachHooks_]?: (() => void)[]\n\n constructor(parent: Unit<TRoot> | null) {\n this.id = nanoid()\n this[_parent_] = parent\n const versioner = getVersioner(this)\n const versions = getVersions(versioner)\n if (versions.length > 0) this[':version'] = versions.at(-1)!\n }\n\n // ---------------------------------------------------------------------------\n // ATTACH / DETACH\n // ---------------------------------------------------------------------------\n\n /**\n * Lifecycle method called when the unit is attached to the state tree.\n */\n [epos.state.ATTACH]() {\n // Apply versioner\n epos.state.transaction(() => {\n const versioner = getVersioner(this)\n const versions = getVersions(versioner)\n for (const version of versions) {\n if (is.number(this[':version']) && this[':version'] >= version) continue\n const versionFn = versioner[version]\n if (!is.function(versionFn)) continue\n versionFn.call(this, this)\n this[':version'] = version\n }\n })\n\n // Setup logger\n let log = createLog(this['@'])\n Reflect.defineProperty(this, 'log', {\n configurable: true,\n get: () => log,\n set: v => (log = v),\n })\n\n // Setup state\n const stateDescriptor = Reflect.getOwnPropertyDescriptor(this.constructor.prototype, 'state')\n if (stateDescriptor && stateDescriptor.get) {\n const value = stateDescriptor.get.call(this)\n const state = epos.state.local(value, { deep: false })\n Reflect.defineProperty(this, 'state', { get: () => state })\n }\n\n // Prepare properties for the whole prototype chain:\n // - Create components for methods ending with `View`\n // - Bind all other methods to the unit instance\n // - Turn getters into MobX computed properties\n for (const prototype of getPrototypes(this)) {\n const descriptors = Object.getOwnPropertyDescriptors(prototype)\n for (const [key, descriptor] of Object.entries(descriptors)) {\n // Skip constructor and already defined properties\n if (key === 'constructor') continue\n if (this.hasOwnProperty(key)) continue\n\n // Create components for methods ending with `View`\n if (is.function(descriptor.value) && key.endsWith('View')) {\n let Component = epos.component(descriptor.value.bind(this))\n Component.displayName = `${this.constructor.name}.${key}`\n Reflect.defineProperty(this, key, {\n configurable: true,\n get: () => Component,\n set: v => (Component = v),\n })\n }\n\n // Bind all other methods to the unit instance\n else if (is.function(descriptor.value)) {\n let method = descriptor.value.bind(this)\n Reflect.defineProperty(this, key, {\n configurable: true,\n get: () => method,\n set: v => (method = v),\n })\n }\n\n // Turn getters into MobX computed properties\n else if (descriptor.get) {\n const getter = descriptor.get\n const computed = epos.libs.mobx.computed(() => getter.call(this))\n Reflect.defineProperty(this, key, {\n configurable: true,\n get: () => computed.get(),\n set: descriptor.set,\n })\n }\n }\n }\n\n // Queue attach hook.\n // Do not execute `attach` hooks immediately, but rather queue them on the highest unattached ancestor.\n // This way `attach` hooks are called after all versioners have been applied in the entire subtree.\n const attach = Reflect.get(this, 'attach')\n if (is.function(attach)) {\n const unattachedRoot = findUnattachedRoot(this)\n if (!unattachedRoot) throw this.never()\n ensure(unattachedRoot, _pendingAttachHooks_, () => [])\n unattachedRoot[_pendingAttachHooks_].push(() => attach())\n }\n\n // Release attach hooks\n if (this[_pendingAttachHooks_]) {\n this[_pendingAttachHooks_].forEach(attach => attach())\n delete this[_pendingAttachHooks_]\n }\n\n // Mark as attached\n Reflect.defineProperty(this, _attached_, { configurable: true, get: () => true })\n }\n\n /**\n * Lifecycle method called when the unit is detached from the state tree.\n */\n [epos.state.DETACH]() {\n // Run and clear disposers\n if (this[_disposers_]) {\n this[_disposers_].forEach(disposer => disposer())\n this[_disposers_].clear()\n }\n\n // Clear ancestors cache\n if (this[_ancestors_]) {\n this[_ancestors_].clear()\n }\n\n // Call detach method\n const detach = Reflect.get(this, 'detach')\n if (is.function(detach)) detach()\n }\n\n // ---------------------------------------------------------------------------\n // ROOT GETTER\n // ---------------------------------------------------------------------------\n\n /**\n * Gets the root unit of the current unit's tree.\n * The result is cached for subsequent calls.\n */\n get $() {\n ensure(this, _root_, () => findRoot(this))\n return this[_root_]\n }\n\n // ---------------------------------------------------------------------------\n // METHODS\n // ---------------------------------------------------------------------------\n\n /**\n * A wrapper around MobX's `autorun` that automatically disposes\n * the reaction when the unit is detached.\n */\n autorun(...args: Parameters<typeof epos.libs.mobx.autorun>) {\n const disposer = epos.libs.mobx.autorun(...args)\n ensure(this, _disposers_, () => new Set())\n this[_disposers_].add(disposer)\n return disposer\n }\n\n /**\n * A wrapper around MobX's `reaction` that automatically disposes\n * the reaction when the unit is detached.\n */\n reaction(...args: Parameters<typeof epos.libs.mobx.reaction>) {\n const disposer = epos.libs.mobx.reaction(...args)\n ensure(this, _disposers_, () => new Set())\n this[_disposers_].add(disposer)\n return disposer\n }\n\n /**\n * A wrapper around `setTimeout` that automatically clears the timeout\n * when the unit is detached.\n */\n setTimeout(...args: Parameters<typeof self.setTimeout>) {\n const id = self.setTimeout(...args)\n ensure(this, _disposers_, () => new Set())\n this[_disposers_].add(() => self.clearTimeout(id))\n return id\n }\n\n /**\n * A wrapper around `setInterval` that automatically clears the interval\n * when the unit is detached.\n */\n setInterval(...args: Parameters<typeof self.setInterval>) {\n const id = self.setInterval(...args)\n ensure(this, _disposers_, () => new Set())\n this[_disposers_].add(() => self.clearInterval(id))\n return id\n }\n\n /**\n * Creates an error for an unreachable code path.\n */\n never(message = 'This should never happen') {\n const details = message ? `: ${message}` : ''\n const error = new Error(`[${this.constructor.name}] This should never happen${details}`)\n Error.captureStackTrace(error, this.never)\n return error\n }\n\n /**\n * Finds the closest ancestor unit of a given type.\n * The result is cached for subsequent calls.\n */\n closest<T extends Unit>(Ancestor: Cls<T>) {\n // Has cached value? -> Return it\n ensure(this, _ancestors_, () => new Map())\n if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor) as T\n\n // Find the closest ancestor and cache it\n let cursor: Node<TRoot> | null = this\n while (cursor) {\n if (cursor instanceof Ancestor) {\n this[_ancestors_].set(Ancestor, cursor)\n return cursor\n }\n cursor = getParent(cursor)\n }\n\n return null\n }\n}\n\n// ---------------------------------------------------------------------------\n// HELPERS\n// ---------------------------------------------------------------------------\n\n/**\n * Ensures a property exists on an object, initializing it if it doesn't.\n */\nfunction ensure<T extends object, K extends PropertyKey, V>(\n object: T,\n key: K,\n getInitialValue: () => V,\n): asserts object is T & { [key in K]: V } {\n if (key in object) return\n const value = getInitialValue()\n Reflect.defineProperty(object, key, { configurable: true, get: () => value })\n}\n\n/**\n * Gets all prototypes of an object up to `Object.prototype`.\n */\nfunction getPrototypes(object: object): object[] {\n const prototype = Reflect.getPrototypeOf(object)\n if (!prototype || prototype === Object.prototype) return []\n return [prototype, ...getPrototypes(prototype)]\n}\n\n/**\n * Finds the root `Unit` in the hierarchy for a given unit.\n */\nfunction findRoot<T>(unit: Unit<T>) {\n let root: Unit<T> | null = null\n let cursor: Node<T> | null = unit\n\n while (cursor) {\n if (cursor instanceof Unit) root = cursor\n cursor = getParent(cursor)\n }\n\n return root as T\n}\n\n/**\n * Finds the highest unattached `Unit` in the hierarchy for a given unit.\n */\nfunction findUnattachedRoot<T>(unit: Unit<T>) {\n let unattachedRoot: Unit<T> | null = null\n let cursor: Node<T> | null = unit\n\n while (cursor) {\n if (cursor instanceof Unit && !cursor[_attached_]) unattachedRoot = cursor\n cursor = getParent(cursor)\n }\n\n return unattachedRoot\n}\n\n/**\n * Gets the parent of a node, which can be a `Unit`, an object, or an array.\n */\nfunction getParent<T>(node: Node<T>) {\n const parent: Node<T> | null = Reflect.get(node, _parent_) ?? Reflect.get(node, epos.state.PARENT) ?? null\n return parent\n}\n\n/**\n * Gets the versioner object from a unit's constructor.\n */\nfunction getVersioner<T>(unit: Unit<T>) {\n const versioner: unknown = Reflect.get(unit.constructor, 'versioner')\n if (!is.object(versioner)) return {}\n return versioner\n}\n\n/**\n * Gets a sorted list of numeric version keys from a unit's versioner.\n */\nfunction getVersions(versioner: Obj) {\n const numericKeys = Object.keys(versioner).filter(key => is.numeric(key))\n return numericKeys.map(Number).sort((v1, v2) => v1 - v2)\n}\n"],"mappings":"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","names":["attach"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epos-unit",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "imkost",
@@ -21,6 +21,7 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "dropcap": "^1.4.0",
24
- "epos": "^1.32.0"
24
+ "epos": "^1.32.0",
25
+ "nanoid": "^3.3.11"
25
26
  }
26
27
  }
package/src/epos-unit.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  import type { Arr, Cls, Obj } from 'dropcap/types'
2
2
  import { createLog, is } from 'dropcap/utils'
3
3
  import 'epos'
4
+ import { customAlphabet } from 'nanoid'
4
5
 
5
6
  export const _root_ = Symbol('root')
6
7
  export const _parent_ = Symbol('parent')
7
8
  export const _attached_ = Symbol('attached')
8
9
  export const _disposers_ = Symbol('disposers')
9
10
  export const _ancestors_ = Symbol('ancestors')
10
- export const _pendingAttachFns_ = Symbol('pendingAttachFns')
11
+ export const _pendingAttachHooks_ = Symbol('pendingAttachHooks')
12
+ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 8)
11
13
 
12
14
  export type Node<T> = Unit<T> | Obj | Arr
13
15
 
14
16
  export class Unit<TRoot = unknown> {
15
17
  declare '@': string
18
+ declare id: string
16
19
  declare log: ReturnType<typeof createLog>;
17
20
  declare [':version']?: number;
18
21
  declare [_root_]?: TRoot;
@@ -20,25 +23,28 @@ export class Unit<TRoot = unknown> {
20
23
  declare [_attached_]?: boolean;
21
24
  declare [_disposers_]?: Set<() => void>;
22
25
  declare [_ancestors_]?: Map<Cls, unknown>;
23
- declare [_pendingAttachFns_]?: (() => void)[]
26
+ declare [_pendingAttachHooks_]?: (() => void)[]
24
27
 
25
28
  constructor(parent: Unit<TRoot> | null) {
29
+ this.id = nanoid()
26
30
  this[_parent_] = parent
27
- const versions = getVersions(this)
31
+ const versioner = getVersioner(this)
32
+ const versions = getVersions(versioner)
28
33
  if (versions.length > 0) this[':version'] = versions.at(-1)!
29
34
  }
30
35
 
36
+ // ---------------------------------------------------------------------------
37
+ // ATTACH / DETACH
38
+ // ---------------------------------------------------------------------------
39
+
31
40
  /**
32
41
  * Lifecycle method called when the unit is attached to the state tree.
33
42
  */
34
43
  [epos.state.ATTACH]() {
35
- // Setup logger
36
- setProperty(this, 'log', createLog(this['@']))
37
-
38
44
  // Apply versioner
39
45
  epos.state.transaction(() => {
40
46
  const versioner = getVersioner(this)
41
- const versions = getVersions(this)
47
+ const versions = getVersions(versioner)
42
48
  for (const version of versions) {
43
49
  if (is.number(this[':version']) && this[':version'] >= version) continue
44
50
  const versionFn = versioner[version]
@@ -48,110 +54,124 @@ export class Unit<TRoot = unknown> {
48
54
  }
49
55
  })
50
56
 
51
- // Prepare methods:
57
+ // Setup logger
58
+ let log = createLog(this['@'])
59
+ Reflect.defineProperty(this, 'log', {
60
+ configurable: true,
61
+ get: () => log,
62
+ set: v => (log = v),
63
+ })
64
+
65
+ // Setup state
66
+ const stateDescriptor = Reflect.getOwnPropertyDescriptor(this.constructor.prototype, 'state')
67
+ if (stateDescriptor && stateDescriptor.get) {
68
+ const value = stateDescriptor.get.call(this)
69
+ const state = epos.state.local(value, { deep: false })
70
+ Reflect.defineProperty(this, 'state', { get: () => state })
71
+ }
72
+
73
+ // Prepare properties for the whole prototype chain:
52
74
  // - Create components for methods ending with `View`
53
75
  // - Bind all other methods to the unit instance
76
+ // - Turn getters into MobX computed properties
54
77
  for (const prototype of getPrototypes(this)) {
55
78
  const descriptors = Object.getOwnPropertyDescriptors(prototype)
56
-
57
79
  for (const [key, descriptor] of Object.entries(descriptors)) {
80
+ // Skip constructor and already defined properties
58
81
  if (key === 'constructor') continue
59
82
  if (this.hasOwnProperty(key)) continue
60
83
 
61
- if (descriptor.get || descriptor.set) continue
62
- if (!is.function(descriptor.value)) continue
63
- const fn = descriptor.value.bind(this)
64
-
65
- if (key.endsWith('View')) {
66
- let Component = epos.component(fn)
84
+ // Create components for methods ending with `View`
85
+ if (is.function(descriptor.value) && key.endsWith('View')) {
86
+ let Component = epos.component(descriptor.value.bind(this))
67
87
  Component.displayName = `${this.constructor.name}.${key}`
68
- setProperty(this, key, Component)
69
- } else {
70
- setProperty(this, key, fn)
88
+ Reflect.defineProperty(this, key, {
89
+ configurable: true,
90
+ get: () => Component,
91
+ set: v => (Component = v),
92
+ })
71
93
  }
72
- }
73
- }
74
94
 
75
- // Setup state
76
- const stateDescriptor = Reflect.getOwnPropertyDescriptor(this.constructor.prototype, 'state')
77
- if (stateDescriptor && stateDescriptor.get) {
78
- const state = stateDescriptor.get.call(this)
79
- setProperty(this, 'state', epos.libs.mobx.observable.object(state, {}, { deep: false }))
95
+ // Bind all other methods to the unit instance
96
+ else if (is.function(descriptor.value)) {
97
+ let method = descriptor.value.bind(this)
98
+ Reflect.defineProperty(this, key, {
99
+ configurable: true,
100
+ get: () => method,
101
+ set: v => (method = v),
102
+ })
103
+ }
104
+
105
+ // Turn getters into MobX computed properties
106
+ else if (descriptor.get) {
107
+ const getter = descriptor.get
108
+ const computed = epos.libs.mobx.computed(() => getter.call(this))
109
+ Reflect.defineProperty(this, key, {
110
+ configurable: true,
111
+ get: () => computed.get(),
112
+ set: descriptor.set,
113
+ })
114
+ }
115
+ }
80
116
  }
81
117
 
82
- // Process attach queue.
83
- // We do not execute `attach` methods immediately, but rather queue them
84
- // on the highest unattached ancestor. This way we ensure that `attach`
85
- // methods are called after all versioners have been applied in the entire subtree.
118
+ // Queue attach hook.
119
+ // Do not execute `attach` hooks immediately, but rather queue them on the highest unattached ancestor.
120
+ // This way `attach` hooks are called after all versioners have been applied in the entire subtree.
86
121
  const attach = Reflect.get(this, 'attach')
87
122
  if (is.function(attach)) {
88
123
  const unattachedRoot = findUnattachedRoot(this)
89
124
  if (!unattachedRoot) throw this.never()
125
+ ensure(unattachedRoot, _pendingAttachHooks_, () => [])
126
+ unattachedRoot[_pendingAttachHooks_].push(() => attach())
127
+ }
90
128
 
91
- ensureProperty(unattachedRoot, _pendingAttachFns_, () => [])
92
- unattachedRoot[_pendingAttachFns_].push(() => attach())
93
-
94
- if (this[_pendingAttachFns_]) {
95
- this[_pendingAttachFns_].forEach(attach => attach())
96
- delete this[_pendingAttachFns_]
97
- }
129
+ // Release attach hooks
130
+ if (this[_pendingAttachHooks_]) {
131
+ this[_pendingAttachHooks_].forEach(attach => attach())
132
+ delete this[_pendingAttachHooks_]
98
133
  }
99
134
 
100
135
  // Mark as attached
101
- setProperty(this, _attached_, true)
136
+ Reflect.defineProperty(this, _attached_, { configurable: true, get: () => true })
102
137
  }
103
138
 
104
139
  /**
105
140
  * Lifecycle method called when the unit is detached from the state tree.
106
141
  */
107
142
  [epos.state.DETACH]() {
108
- // 1. Run disposers
143
+ // Run and clear disposers
109
144
  if (this[_disposers_]) {
110
145
  this[_disposers_].forEach(disposer => disposer())
111
146
  this[_disposers_].clear()
112
147
  }
113
148
 
114
- // 2. Call detach method
149
+ // Clear ancestors cache
150
+ if (this[_ancestors_]) {
151
+ this[_ancestors_].clear()
152
+ }
153
+
154
+ // Call detach method
115
155
  const detach = Reflect.get(this, 'detach')
116
156
  if (is.function(detach)) detach()
117
-
118
- // 3. Clean up internal properties
119
- delete this[_root_]
120
- delete this[_attached_]
121
- delete this[_ancestors_]
122
- delete this[_disposers_]
123
157
  }
124
158
 
159
+ // ---------------------------------------------------------------------------
160
+ // ROOT GETTER
161
+ // ---------------------------------------------------------------------------
162
+
125
163
  /**
126
164
  * Gets the root unit of the current unit's tree.
127
165
  * The result is cached for subsequent calls.
128
166
  */
129
167
  get $() {
130
- ensureProperty(this, _root_, () => findRoot(this))
168
+ ensure(this, _root_, () => findRoot(this))
131
169
  return this[_root_]
132
170
  }
133
171
 
134
- /**
135
- * Finds the closest ancestor unit of a given type.
136
- * The result is cached for subsequent calls.
137
- */
138
- closest<T extends Unit>(Ancestor: Cls<T>) {
139
- // Has cached value? -> Return it
140
- ensureProperty(this, _ancestors_, () => new Map())
141
- if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor) as T
142
-
143
- // Find the closest ancestor and cache it
144
- let cursor: Node<TRoot> | null = this
145
- while (cursor) {
146
- if (cursor instanceof Ancestor) {
147
- this[_ancestors_].set(Ancestor, cursor)
148
- return cursor
149
- }
150
- cursor = getParent(cursor)
151
- }
152
-
153
- return null
154
- }
172
+ // ---------------------------------------------------------------------------
173
+ // METHODS
174
+ // ---------------------------------------------------------------------------
155
175
 
156
176
  /**
157
177
  * A wrapper around MobX's `autorun` that automatically disposes
@@ -159,7 +179,7 @@ export class Unit<TRoot = unknown> {
159
179
  */
160
180
  autorun(...args: Parameters<typeof epos.libs.mobx.autorun>) {
161
181
  const disposer = epos.libs.mobx.autorun(...args)
162
- ensureProperty(this, _disposers_, () => new Set())
182
+ ensure(this, _disposers_, () => new Set())
163
183
  this[_disposers_].add(disposer)
164
184
  return disposer
165
185
  }
@@ -170,7 +190,7 @@ export class Unit<TRoot = unknown> {
170
190
  */
171
191
  reaction(...args: Parameters<typeof epos.libs.mobx.reaction>) {
172
192
  const disposer = epos.libs.mobx.reaction(...args)
173
- ensureProperty(this, _disposers_, () => new Set())
193
+ ensure(this, _disposers_, () => new Set())
174
194
  this[_disposers_].add(disposer)
175
195
  return disposer
176
196
  }
@@ -181,7 +201,7 @@ export class Unit<TRoot = unknown> {
181
201
  */
182
202
  setTimeout(...args: Parameters<typeof self.setTimeout>) {
183
203
  const id = self.setTimeout(...args)
184
- ensureProperty(this, _disposers_, () => new Set())
204
+ ensure(this, _disposers_, () => new Set())
185
205
  this[_disposers_].add(() => self.clearTimeout(id))
186
206
  return id
187
207
  }
@@ -192,7 +212,7 @@ export class Unit<TRoot = unknown> {
192
212
  */
193
213
  setInterval(...args: Parameters<typeof self.setInterval>) {
194
214
  const id = self.setInterval(...args)
195
- ensureProperty(this, _disposers_, () => new Set())
215
+ ensure(this, _disposers_, () => new Set())
196
216
  this[_disposers_].add(() => self.clearInterval(id))
197
217
  return id
198
218
  }
@@ -206,27 +226,38 @@ export class Unit<TRoot = unknown> {
206
226
  Error.captureStackTrace(error, this.never)
207
227
  return error
208
228
  }
229
+
230
+ /**
231
+ * Finds the closest ancestor unit of a given type.
232
+ * The result is cached for subsequent calls.
233
+ */
234
+ closest<T extends Unit>(Ancestor: Cls<T>) {
235
+ // Has cached value? -> Return it
236
+ ensure(this, _ancestors_, () => new Map())
237
+ if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor) as T
238
+
239
+ // Find the closest ancestor and cache it
240
+ let cursor: Node<TRoot> | null = this
241
+ while (cursor) {
242
+ if (cursor instanceof Ancestor) {
243
+ this[_ancestors_].set(Ancestor, cursor)
244
+ return cursor
245
+ }
246
+ cursor = getParent(cursor)
247
+ }
248
+
249
+ return null
250
+ }
209
251
  }
210
252
 
211
253
  // ---------------------------------------------------------------------------
212
254
  // HELPERS
213
255
  // ---------------------------------------------------------------------------
214
256
 
215
- /**
216
- * Defines a configurable property on an object.
217
- */
218
- function setProperty(object: object, key: PropertyKey, value: unknown) {
219
- Reflect.defineProperty(object, key, {
220
- configurable: true,
221
- get: () => value,
222
- set: v => (value = v),
223
- })
224
- }
225
-
226
257
  /**
227
258
  * Ensures a property exists on an object, initializing it if it doesn't.
228
259
  */
229
- function ensureProperty<T extends object, K extends PropertyKey, V>(
260
+ function ensure<T extends object, K extends PropertyKey, V>(
230
261
  object: T,
231
262
  key: K,
232
263
  getInitialValue: () => V,
@@ -295,8 +326,7 @@ function getVersioner<T>(unit: Unit<T>) {
295
326
  /**
296
327
  * Gets a sorted list of numeric version keys from a unit's versioner.
297
328
  */
298
- function getVersions<T>(unit: Unit<T>) {
299
- const versioner = getVersioner(unit)
329
+ function getVersions(versioner: Obj) {
300
330
  const numericKeys = Object.keys(versioner).filter(key => is.numeric(key))
301
331
  return numericKeys.map(Number).sort((v1, v2) => v1 - v2)
302
332
  }