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