UNPKG

10 kBJavaScriptView Raw
1import { id as idUtil } from '@platform/util.value';
2import { Subject } from 'rxjs';
3import { filter, share, take, takeUntil } from 'rxjs/operators';
4import { is } from '../common';
5import { StateObject } from '../StateObject';
6import { TreeIdentity } from '../TreeIdentity';
7import { TreeQuery } from '../TreeQuery';
8import { helpers } from './helpers';
9import * as events from './TreeState.events';
10import * as path from './TreeState.path';
11import * as sync from './TreeState.sync';
12const Identity = TreeIdentity;
13export class TreeState {
14 constructor(args) {
15 this._children = [];
16 this._kind = 'TreeState';
17 this._dispose$ = new Subject();
18 this.dispose$ = this._dispose$.pipe(share());
19 this._event$ = new Subject();
20 this.change = (fn, options) => this._change(fn, options);
21 this.add = (args) => {
22 if (TreeState.isInstance(args)) {
23 args = { parent: this.id, root: args };
24 }
25 const self = this;
26 const child = this.getOrCreateInstance(args);
27 if (this.childExists(child)) {
28 const err = `Cannot add child '${child.id}' as it already exists within the parent '${this.state.id}'.`;
29 throw new Error(err);
30 }
31 this._children.push(child);
32 this.change((draft, ctx) => {
33 const root = args.parent && args.parent !== draft.id ? ctx.findById(args.parent) : draft;
34 if (!root) {
35 const err = `Cannot add child-state because the parent sub-node '${args.parent}' within '${draft.id}' does not exist.`;
36 throw new Error(err);
37 }
38 TreeState.children(root).push(child.state);
39 });
40 this.listen(child);
41 child.dispose$
42 .pipe(take(1))
43 .pipe(filter(() => this.childExists(child)))
44 .subscribe(() => this.remove(child));
45 this.fire({ type: 'TreeState/child/added', payload: { parent: self, child } });
46 return child;
47 };
48 this.remove = (input) => {
49 const child = this.child(input);
50 if (!child) {
51 const err = `Cannot remove child-state as it does not exist in the parent '${this.state.id}'.`;
52 throw new Error(err);
53 }
54 this._children = this._children.filter((item) => item.state.id !== child.state.id);
55 const self = this;
56 this.fire({ type: 'TreeState/child/removed', payload: { parent: self, child } });
57 return child;
58 };
59 this.clear = () => {
60 this.children.forEach((child) => this.remove(child));
61 return this;
62 };
63 this.contains = (match) => {
64 return Boolean(this.find(match));
65 };
66 this.find = (input) => {
67 if (!input) {
68 return undefined;
69 }
70 const match = typeof input === 'function'
71 ? input
72 : (e) => {
73 const id = TreeIdentity.toNodeId(input);
74 return e.id === id ? true : Boolean(e.tree.query.findById(id));
75 };
76 let result;
77 this.walkDown((e) => {
78 if (e.level > 0) {
79 if (match(e) === true) {
80 e.stop();
81 result = e.tree;
82 }
83 }
84 });
85 return result;
86 };
87 this.walkDown = (visit) => {
88 const inner = (level, index, tree, parent, state) => {
89 if (state.stopped) {
90 return;
91 }
92 let skipped = false;
93 const args = {
94 level,
95 id: tree.id,
96 key: Identity.key(tree.id),
97 namespace: tree.namespace,
98 index,
99 tree,
100 parent,
101 stop: () => (state.stopped = true),
102 skip: () => (skipped = true),
103 toString: () => tree.id,
104 };
105 visit(args);
106 if (state.stopped) {
107 return;
108 }
109 if (!skipped && tree.children.length) {
110 tree.children.forEach((child, i) => {
111 inner(level + 1, i, child, tree, state);
112 });
113 }
114 };
115 return inner(0, -1, this, undefined, {});
116 };
117 this.syncFrom = (args) => {
118 const { until$ } = args;
119 const isObservable = is.observable(args.source.event$);
120 const source$ = isObservable
121 ? args.source.event$
122 : args.source.event.$;
123 const parent = isObservable
124 ? args.source.parent
125 : args.source.parent;
126 const initial = isObservable ? undefined : args.source.state;
127 return sync.syncFrom({ target: this, parent, initial, source$, until$ });
128 };
129 this.fire = (e) => this._event$.next(e);
130 const root = (typeof args.root === 'string' ? { id: args.root } : args.root);
131 if (root.id.includes('/')) {
132 const err = `Tree node IDs cannot contain the "/" character`;
133 throw new Error(err);
134 }
135 this.key = Identity.key(root.id);
136 this.namespace = Identity.namespace(root.id) || idUtil.cuid();
137 this.parent = args.parent;
138 const store = (this._store = StateObject.create(root));
139 this.event = events.create({
140 event$: this._event$,
141 until$: this.dispose$,
142 });
143 this._change((draft) => helpers.ensureNamespace(draft, this.namespace), {
144 ensureNamespace: false,
145 });
146 store.event.changed$.pipe(takeUntil(this.dispose$)).subscribe((e) => {
147 this.fire({ type: 'TreeState/changed', payload: e });
148 });
149 store.event.patched$.pipe(takeUntil(this.dispose$)).subscribe((e) => {
150 this.fire({ type: 'TreeState/patched', payload: e });
151 });
152 if (args.dispose$) {
153 args.dispose$.subscribe(() => this.dispose());
154 }
155 }
156 static create(args) {
157 const root = (args === null || args === void 0 ? void 0 : args.root) || 'node';
158 const e = Object.assign(Object.assign({}, args), { root });
159 return new TreeState(e);
160 }
161 dispose() {
162 if (!this.isDisposed) {
163 this.children.forEach((child) => child.dispose());
164 this._store.dispose();
165 this.fire({
166 type: 'TreeState/disposed',
167 payload: { final: this.state },
168 });
169 this._dispose$.next();
170 this._dispose$.complete();
171 }
172 }
173 get isDisposed() {
174 return this._dispose$.isStopped;
175 }
176 get readonly() {
177 return this;
178 }
179 get state() {
180 return this._store.state;
181 }
182 get id() {
183 return this.state.id;
184 }
185 get children() {
186 return this._children;
187 }
188 get query() {
189 const root = this.state;
190 const namespace = this.namespace;
191 return TreeQuery.create({ root, namespace });
192 }
193 get path() {
194 return path.create(this);
195 }
196 _change(fn, options = {}) {
197 const { action } = options;
198 const res = this._store.change((draft) => {
199 const ctx = this.ctx(draft);
200 fn(draft, ctx);
201 if (options.ensureNamespace !== false) {
202 helpers.ensureNamespace(draft, this.namespace);
203 }
204 }, { action });
205 return res;
206 }
207 ctx(root) {
208 const namespace = this.namespace;
209 const query = TreeQuery.create({ root, namespace });
210 const ctx = Object.assign(Object.assign({}, query), { props: TreeState.props, children: TreeState.children, toObject: (draft) => StateObject.toObject(draft), query: (node, namespace) => TreeQuery.create({ root: node || root, namespace }) });
211 return ctx;
212 }
213 child(id) {
214 id = typeof id === 'string' ? id : id.state.id;
215 return this.children.find((item) => item.id === id);
216 }
217 childExists(input) {
218 return Boolean(this.child(input));
219 }
220 getOrCreateInstance(args) {
221 const root = (typeof args.root === 'string' ? { id: args.root } : args.root);
222 if (TreeState.isInstance(root)) {
223 return args.root;
224 }
225 let parent = Identity.toString(args.parent);
226 parent = parent ? parent : Identity.stripNamespace(this.id);
227 if (!this.query.exists((e) => e.key === parent)) {
228 const err = `Cannot add child-state because the parent node '${parent}' does not exist.`;
229 throw new Error(err);
230 }
231 parent = Identity.format(this.namespace, parent);
232 return TreeState.create({ parent, root });
233 }
234 listen(child) {
235 const removed$ = this.event
236 .payload('TreeState/child/removed')
237 .pipe(filter((e) => e.child.id === child.id));
238 removed$.subscribe((e) => {
239 this.change((draft, ctx) => {
240 draft.children = TreeState.children(draft).filter(({ id }) => id !== child.id);
241 });
242 });
243 child.event
244 .payload('TreeState/changed')
245 .pipe(takeUntil(child.dispose$), takeUntil(removed$))
246 .subscribe((e) => {
247 this.change((draft, ctx) => {
248 const children = TreeState.children(draft);
249 const index = children.findIndex(({ id }) => id === child.id);
250 if (index > -1) {
251 children[index] = e.to;
252 }
253 });
254 });
255 }
256}
257TreeState.identity = Identity;
258TreeState.props = helpers.props;
259TreeState.children = helpers.children;
260TreeState.isInstance = helpers.isInstance;