epos-unit 1.14.0 → 1.16.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 -15
- package/dist/epos-unit.js +155 -65
- package/dist/epos-unit.js.map +1 -0
- package/package.json +3 -4
- package/src/epos-unit.ts +225 -92
package/dist/epos-unit.d.ts
CHANGED
|
@@ -1,31 +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.
|
|
14
|
-
|
|
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;
|
|
15
21
|
'@': string;
|
|
16
|
-
log:
|
|
22
|
+
log: ReturnType<typeof createLog>;
|
|
17
23
|
[':version']?: number;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
[_root_]?: TRoot;
|
|
25
|
+
[_parent_]?: Unit<TRoot> | null;
|
|
26
|
+
[_attached_]?: boolean;
|
|
27
|
+
[_disposers_]?: Set<() => void>;
|
|
28
|
+
[_ancestors_]?: Map<Cls, unknown>;
|
|
29
|
+
[_pendingAttachFns_]?: (() => void)[];
|
|
21
30
|
constructor(parent: Unit<TRoot> | null);
|
|
22
|
-
|
|
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
|
+
*/
|
|
23
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
|
+
*/
|
|
24
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
|
+
*/
|
|
25
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
|
+
*/
|
|
26
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
|
+
*/
|
|
27
60
|
setInterval(...args: Parameters<typeof self.setInterval>): number;
|
|
28
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Creates an error for an unreachable code path.
|
|
63
|
+
*/
|
|
64
|
+
never(message?: string): Error;
|
|
29
65
|
}
|
|
30
66
|
|
|
31
|
-
export { type
|
|
67
|
+
export { type Node, Unit, _ancestors_, _attached_, _disposers_, _parent_, _pendingAttachFns_, _root_ };
|
package/dist/epos-unit.js
CHANGED
|
@@ -1,115 +1,178 @@
|
|
|
1
1
|
// src/epos-unit.ts
|
|
2
|
-
import { createLog } from "dropcap/utils";
|
|
2
|
+
import { createLog, is } from "dropcap/utils";
|
|
3
3
|
import "epos";
|
|
4
|
-
import { nanoid } from "nanoid";
|
|
5
4
|
var _root_ = /* @__PURE__ */ Symbol("root");
|
|
6
5
|
var _parent_ = /* @__PURE__ */ Symbol("parent");
|
|
6
|
+
var _attached_ = /* @__PURE__ */ Symbol("attached");
|
|
7
7
|
var _disposers_ = /* @__PURE__ */ Symbol("disposers");
|
|
8
|
+
var _ancestors_ = /* @__PURE__ */ Symbol("ancestors");
|
|
9
|
+
var _pendingAttachFns_ = /* @__PURE__ */ Symbol("pendingAttachFns");
|
|
8
10
|
var Unit = class {
|
|
9
|
-
id = nanoid();
|
|
10
|
-
static get [epos.symbols.stateModelStrict]() {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
11
|
constructor(parent) {
|
|
14
|
-
|
|
12
|
+
this[_parent_] = parent;
|
|
13
|
+
const versions = getVersions(this);
|
|
14
|
+
if (versions.length > 0) this[":version"] = versions.at(-1);
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[epos.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
48
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!descriptor) continue;
|
|
39
|
-
if (descriptor.get || descriptor.set) continue;
|
|
40
|
-
if (typeof _this[key] !== "function") continue;
|
|
41
|
-
_this[key] = epos.component(_this[key]);
|
|
42
|
-
_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 }));
|
|
43
53
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
61
|
+
if (this[_pendingAttachFns_]) {
|
|
62
|
+
this[_pendingAttachFns_].forEach((attach2) => attach2());
|
|
63
|
+
delete this[_pendingAttachFns_];
|
|
64
|
+
}
|
|
65
|
+
setProperty(this, _attached_, true);
|
|
56
66
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
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_];
|
|
63
81
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Gets the root unit of the current unit's tree.
|
|
84
|
+
* The result is cached for subsequent calls.
|
|
85
|
+
*/
|
|
67
86
|
get $() {
|
|
68
|
-
this
|
|
87
|
+
ensureProperty(this, _root_, () => findRoot(this));
|
|
69
88
|
return this[_root_];
|
|
70
89
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Finds the closest ancestor unit of a given type.
|
|
92
|
+
* The result is cached for subsequent calls.
|
|
93
|
+
*/
|
|
74
94
|
closest(Ancestor) {
|
|
75
|
-
|
|
95
|
+
ensureProperty(this, _ancestors_, () => /* @__PURE__ */ new Map());
|
|
96
|
+
if (this[_ancestors_].has(Ancestor)) return this[_ancestors_].get(Ancestor);
|
|
97
|
+
let cursor = this;
|
|
76
98
|
while (cursor) {
|
|
77
|
-
if (cursor instanceof Ancestor)
|
|
99
|
+
if (cursor instanceof Ancestor) {
|
|
100
|
+
this[_ancestors_].set(Ancestor, cursor);
|
|
101
|
+
return cursor;
|
|
102
|
+
}
|
|
78
103
|
cursor = getParent(cursor);
|
|
79
104
|
}
|
|
80
105
|
return null;
|
|
81
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* A wrapper around MobX's `autorun` that automatically disposes
|
|
109
|
+
* the reaction when the unit is detached.
|
|
110
|
+
*/
|
|
82
111
|
autorun(...args) {
|
|
83
112
|
const disposer = epos.libs.mobx.autorun(...args);
|
|
113
|
+
ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
|
|
84
114
|
this[_disposers_].add(disposer);
|
|
85
115
|
return disposer;
|
|
86
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A wrapper around MobX's `reaction` that automatically disposes
|
|
119
|
+
* the reaction when the unit is detached.
|
|
120
|
+
*/
|
|
87
121
|
reaction(...args) {
|
|
88
122
|
const disposer = epos.libs.mobx.reaction(...args);
|
|
123
|
+
ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
|
|
89
124
|
this[_disposers_].add(disposer);
|
|
90
125
|
return disposer;
|
|
91
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* A wrapper around `setTimeout` that automatically clears the timeout
|
|
129
|
+
* when the unit is detached.
|
|
130
|
+
*/
|
|
92
131
|
setTimeout(...args) {
|
|
93
132
|
const id = self.setTimeout(...args);
|
|
133
|
+
ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
|
|
94
134
|
this[_disposers_].add(() => self.clearTimeout(id));
|
|
95
135
|
return id;
|
|
96
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* A wrapper around `setInterval` that automatically clears the interval
|
|
139
|
+
* when the unit is detached.
|
|
140
|
+
*/
|
|
97
141
|
setInterval(...args) {
|
|
98
142
|
const id = self.setInterval(...args);
|
|
143
|
+
ensureProperty(this, _disposers_, () => /* @__PURE__ */ new Set());
|
|
99
144
|
this[_disposers_].add(() => self.clearInterval(id));
|
|
100
145
|
return id;
|
|
101
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Creates an error for an unreachable code path.
|
|
149
|
+
*/
|
|
102
150
|
never(message = "This should never happen") {
|
|
103
|
-
const
|
|
151
|
+
const details = message ? `: ${message}` : "";
|
|
152
|
+
const error = new Error(`[${this.constructor.name}] This should never happen${details}`);
|
|
104
153
|
Error.captureStackTrace(error, this.never);
|
|
105
|
-
|
|
154
|
+
return error;
|
|
106
155
|
}
|
|
107
156
|
};
|
|
108
|
-
function
|
|
109
|
-
|
|
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)];
|
|
110
173
|
}
|
|
111
174
|
function findRoot(unit) {
|
|
112
|
-
let root =
|
|
175
|
+
let root = null;
|
|
113
176
|
let cursor = unit;
|
|
114
177
|
while (cursor) {
|
|
115
178
|
if (cursor instanceof Unit) root = cursor;
|
|
@@ -117,9 +180,36 @@ function findRoot(unit) {
|
|
|
117
180
|
}
|
|
118
181
|
return root;
|
|
119
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
|
+
}
|
|
120
206
|
export {
|
|
121
207
|
Unit,
|
|
208
|
+
_ancestors_,
|
|
209
|
+
_attached_,
|
|
122
210
|
_disposers_,
|
|
123
211
|
_parent_,
|
|
212
|
+
_pendingAttachFns_,
|
|
124
213
|
_root_
|
|
125
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\n if (this[_pendingAttachFns_]) {\n this[_pendingAttachFns_].forEach(attach => attach())\n delete this[_pendingAttachFns_]\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.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "imkost",
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"dropcap": "^1.
|
|
24
|
-
"epos": "^1.32.0"
|
|
25
|
-
"nanoid": "^3.3.11"
|
|
23
|
+
"dropcap": "^1.4.0",
|
|
24
|
+
"epos": "^1.32.0"
|
|
26
25
|
}
|
|
27
26
|
}
|
package/src/epos-unit.ts
CHANGED
|
@@ -1,150 +1,210 @@
|
|
|
1
|
-
import type { Cls } from 'dropcap/types'
|
|
2
|
-
import { createLog,
|
|
1
|
+
import type { Arr, Cls, Obj } from 'dropcap/types'
|
|
2
|
+
import { createLog, is } from 'dropcap/utils'
|
|
3
3
|
import 'epos'
|
|
4
|
-
import { nanoid } from 'nanoid'
|
|
5
|
-
import type { FC } from 'react'
|
|
6
4
|
|
|
7
5
|
export const _root_ = Symbol('root')
|
|
8
6
|
export const _parent_ = Symbol('parent')
|
|
7
|
+
export const _attached_ = Symbol('attached')
|
|
9
8
|
export const _disposers_ = Symbol('disposers')
|
|
10
|
-
export
|
|
9
|
+
export const _ancestors_ = Symbol('ancestors')
|
|
10
|
+
export const _pendingAttachFns_ = Symbol('pendingAttachFns')
|
|
11
|
+
|
|
12
|
+
export type Node<T> = Unit<T> | Obj | Arr
|
|
11
13
|
|
|
12
14
|
export class Unit<TRoot = unknown> {
|
|
13
|
-
id = nanoid()
|
|
14
15
|
declare '@': string
|
|
15
|
-
declare log:
|
|
16
|
-
declare [':version']?: number
|
|
17
|
-
declare
|
|
18
|
-
declare
|
|
19
|
-
declare
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
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)[]
|
|
24
24
|
|
|
25
25
|
constructor(parent: Unit<TRoot> | null) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
this[_parent_] = parent
|
|
27
|
+
const versions = getVersions(this)
|
|
28
|
+
if (versions.length > 0) this[':version'] = versions.at(-1)!
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const Unit = this.constructor
|
|
37
|
-
const descriptors: Descriptors = Object.getOwnPropertyDescriptors(Unit.prototype)
|
|
38
|
-
const keys = Reflect.ownKeys(descriptors)
|
|
39
|
-
|
|
40
|
-
// Setup disposers container
|
|
41
|
-
const disposers = new Set<() => void>()
|
|
42
|
-
Reflect.defineProperty(this, _disposers_, { get: () => disposers })
|
|
43
|
-
|
|
44
|
-
// Bind all methods
|
|
45
|
-
for (const key of keys) {
|
|
46
|
-
if (key === 'constructor') continue
|
|
47
|
-
const descriptor = descriptors[key]
|
|
48
|
-
if (!descriptor) continue
|
|
49
|
-
if (descriptor.get || descriptor.set) continue
|
|
50
|
-
if (typeof descriptor.value !== 'function') continue
|
|
51
|
-
_this[key] = descriptor.value.bind(this)
|
|
52
|
-
}
|
|
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['@']))
|
|
53
37
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
})
|
|
65
50
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
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)
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
58
|
+
if (key === 'constructor') continue
|
|
59
|
+
if (this.hasOwnProperty(key)) continue
|
|
73
60
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
if (descriptor.get || descriptor.set) continue
|
|
62
|
+
if (!is.function(descriptor.value)) continue
|
|
63
|
+
const fn = descriptor.value.bind(this)
|
|
77
64
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
74
|
|
|
81
|
-
//
|
|
82
|
-
this
|
|
83
|
-
|
|
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
|
+
}
|
|
84
81
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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()
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
ensureProperty(unattachedRoot, _pendingAttachFns_, () => [])
|
|
92
|
+
unattachedRoot[_pendingAttachFns_].push(() => attach())
|
|
93
|
+
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
if (this[_pendingAttachFns_]) {
|
|
96
|
+
this[_pendingAttachFns_].forEach(attach => attach())
|
|
97
|
+
delete this[_pendingAttachFns_]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Mark as attached
|
|
101
|
+
setProperty(this, _attached_, true)
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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()
|
|
101
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
|
+
*/
|
|
102
129
|
get $() {
|
|
103
|
-
this
|
|
130
|
+
ensureProperty(this, _root_, () => findRoot(this))
|
|
104
131
|
return this[_root_]
|
|
105
132
|
}
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
142
|
|
|
111
|
-
|
|
112
|
-
let cursor:
|
|
143
|
+
// Find the closest ancestor and cache it
|
|
144
|
+
let cursor: Node<TRoot> | null = this
|
|
113
145
|
while (cursor) {
|
|
114
|
-
if (cursor instanceof Ancestor)
|
|
146
|
+
if (cursor instanceof Ancestor) {
|
|
147
|
+
this[_ancestors_].set(Ancestor, cursor)
|
|
148
|
+
return cursor
|
|
149
|
+
}
|
|
115
150
|
cursor = getParent(cursor)
|
|
116
151
|
}
|
|
152
|
+
|
|
117
153
|
return null
|
|
118
154
|
}
|
|
119
155
|
|
|
156
|
+
/**
|
|
157
|
+
* A wrapper around MobX's `autorun` that automatically disposes
|
|
158
|
+
* the reaction when the unit is detached.
|
|
159
|
+
*/
|
|
120
160
|
autorun(...args: Parameters<typeof epos.libs.mobx.autorun>) {
|
|
121
161
|
const disposer = epos.libs.mobx.autorun(...args)
|
|
162
|
+
ensureProperty(this, _disposers_, () => new Set())
|
|
122
163
|
this[_disposers_].add(disposer)
|
|
123
164
|
return disposer
|
|
124
165
|
}
|
|
125
166
|
|
|
167
|
+
/**
|
|
168
|
+
* A wrapper around MobX's `reaction` that automatically disposes
|
|
169
|
+
* the reaction when the unit is detached.
|
|
170
|
+
*/
|
|
126
171
|
reaction(...args: Parameters<typeof epos.libs.mobx.reaction>) {
|
|
127
172
|
const disposer = epos.libs.mobx.reaction(...args)
|
|
173
|
+
ensureProperty(this, _disposers_, () => new Set())
|
|
128
174
|
this[_disposers_].add(disposer)
|
|
129
175
|
return disposer
|
|
130
176
|
}
|
|
131
177
|
|
|
178
|
+
/**
|
|
179
|
+
* A wrapper around `setTimeout` that automatically clears the timeout
|
|
180
|
+
* when the unit is detached.
|
|
181
|
+
*/
|
|
132
182
|
setTimeout(...args: Parameters<typeof self.setTimeout>) {
|
|
133
183
|
const id = self.setTimeout(...args)
|
|
184
|
+
ensureProperty(this, _disposers_, () => new Set())
|
|
134
185
|
this[_disposers_].add(() => self.clearTimeout(id))
|
|
135
186
|
return id
|
|
136
187
|
}
|
|
137
188
|
|
|
189
|
+
/**
|
|
190
|
+
* A wrapper around `setInterval` that automatically clears the interval
|
|
191
|
+
* when the unit is detached.
|
|
192
|
+
*/
|
|
138
193
|
setInterval(...args: Parameters<typeof self.setInterval>) {
|
|
139
194
|
const id = self.setInterval(...args)
|
|
195
|
+
ensureProperty(this, _disposers_, () => new Set())
|
|
140
196
|
this[_disposers_].add(() => self.clearInterval(id))
|
|
141
197
|
return id
|
|
142
198
|
}
|
|
143
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Creates an error for an unreachable code path.
|
|
202
|
+
*/
|
|
144
203
|
never(message = 'This should never happen') {
|
|
145
|
-
const
|
|
204
|
+
const details = message ? `: ${message}` : ''
|
|
205
|
+
const error = new Error(`[${this.constructor.name}] This should never happen${details}`)
|
|
146
206
|
Error.captureStackTrace(error, this.never)
|
|
147
|
-
|
|
207
|
+
return error
|
|
148
208
|
}
|
|
149
209
|
}
|
|
150
210
|
|
|
@@ -152,18 +212,91 @@ export class Unit<TRoot = unknown> {
|
|
|
152
212
|
// HELPERS
|
|
153
213
|
// ---------------------------------------------------------------------------
|
|
154
214
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
})
|
|
157
224
|
}
|
|
158
225
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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)]
|
|
246
|
+
}
|
|
247
|
+
|
|
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
|
|
162
254
|
|
|
163
255
|
while (cursor) {
|
|
164
256
|
if (cursor instanceof Unit) root = cursor
|
|
165
257
|
cursor = getParent(cursor)
|
|
166
258
|
}
|
|
167
259
|
|
|
168
|
-
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)
|
|
169
302
|
}
|