@xaendar/signals 0.0.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,421 +0,0 @@
1
-
2
- import { GLOBAL_STATE, pushComputed, popComputed } from '../globals';
3
- import { PRIVATE, assertPrivateContext } from '../private-symbol';
4
- import { ComputedState } from '../types/computed-state.type';
5
- import { SignalEqual } from '../types/signal-equal.type';
6
- import { SignalOptions } from '../types/signal-options.type';
7
- import { State } from './state';
8
- import { Watcher } from './watcher';
9
-
10
- /**
11
- * A read-only Signal whose value is derived lazily from other Signals.
12
- *
13
- * The value is recomputed only when explicitly read and only if one or more
14
- * of its (recursive) dependencies have changed since the last evaluation.
15
- * The result is cached and reused until the Signal becomes stale again.
16
- *
17
- * @template T The type of the computed value.
18
- *
19
- * @see Signal algorithms — "The Signal.Computed class"
20
- */
21
- export class Computed<T = any> {
22
- /**
23
- * The current value of the signal.
24
- *
25
- * Uninitialised (`!`) until the first evaluation. After that, holds either
26
- * the return value of `#callback` or a boxed error
27
- * `{ isError: true; value: Error }` if the last evaluation threw.
28
- *
29
- * @internalSlot
30
- * @see Signal algorithms — "Signal.Computed internal slots"
31
- */
32
- #value!: T | { isError: true; value: Error };
33
- /**
34
- * The current evaluation state of this Signal.
35
- *
36
- * - `~dirty~` — value is known to be stale or has never been evaluated.
37
- * - `~checked~` — an indirect source changed; may or may not be stale.
38
- * - `~computing~` — `#callback` is currently executing; guards against cycles.
39
- * - `~clean~` — cached value is up-to-date.
40
- *
41
- * @internalSlot
42
- * @see Signal algorithms — "Signal.Computed State machine"
43
- */
44
- #state: ComputedState;
45
- /**
46
- * The ordered set of Signals read during the last evaluation of
47
- * `#callback`. Cleared and rebuilt on every re-evaluation so that
48
- * conditional branches that are no longer taken stop being tracked.
49
- *
50
- * May contain both `State` and `Computed` instances.
51
- *
52
- * @internalSlot
53
- * @see Signal algorithms — "Signal.Computed internal slots"
54
- */
55
- #sources: Set<State<unknown> | Computed<unknown>>;
56
- /**
57
- * Returns a snapshot of the current sources set for introspection.
58
- *
59
- * @param symbol - Private access symbol; rejects calls from outside the library.
60
- * @returns An array of `State` and `Computed` instances that this Signal depends on.
61
- * @internal
62
- */
63
- public getSources(symbol: symbol): (State<unknown> | Computed<unknown>)[] {
64
- assertPrivateContext(symbol);
65
- return [...this.#sources];
66
- }
67
- /**
68
- * The set of Signals and Watchers that directly depend on this Signal.
69
- *
70
- * Populated only when this Signal is reachable from at least one active
71
- * `Watcher`. An un-watched `Computed` has an empty sinks set, which allows
72
- * it to be garbage-collected independently from the rest of the graph.
73
- *
74
- * @internalSlot
75
- * @see Signal algorithms — "Signal.Computed internal slots"
76
- * @see Method — `Signal.Computed.prototype.get` (NOTE on sinks)
77
- */
78
- #sinks: Set<Computed<unknown> | Watcher>;
79
- /**
80
- * Returns a snapshot of the current sinks set for introspection.
81
- *
82
- * @param symbol - Private access symbol; rejects calls from outside the library.
83
- * @returns An array of `Computed` and `Watcher` instances that depend on this Signal.
84
- * @internal
85
- */
86
- public getSinks(symbol: symbol): (Computed<unknown> | Watcher)[] {
87
- assertPrivateContext(symbol);
88
- return [...this.#sinks];
89
- }
90
- /**
91
- * The equality function used to determine whether a newly computed value
92
- * is meaningfully different from the previously cached one.
93
- *
94
- * Called as `equals.call(computed, oldValue, newValue)`. Returns `true` if
95
- * the values are considered equal, in which case no downstream propagation
96
- * occurs. Defaults to `Object.is` when not provided via options.
97
- *
98
- * If this function throws, the exception is cached as the Signal's value
99
- * and the outcome is treated as `~dirty~`.
100
- *
101
- * @internalSlot
102
- * @see Signal algorithms — "Signal.Computed internal slots"
103
- * @see Algorithm — "Set Signal value"
104
- */
105
- #equals: SignalEqual<T>;
106
- /**
107
- * The pure function that produces this Signal's value. Evaluated lazily
108
- * whenever the Signal is read while in a `~dirty~` or `~checked~` state.
109
- *
110
- * Called with `this` bound to the `Computed` instance itself so that
111
- * internal methods (e.g. `addSource`) are accessible if needed.
112
- * Any exception thrown by this function is caught and cached.
113
- *
114
- * @internalSlot
115
- * @see Signal algorithms — "Signal.Computed internal slots"
116
- */
117
- #callback: (this: Computed<T>) => T;
118
-
119
- /**
120
- * Creates a new `Computed` signal.
121
- *
122
- * The Signal starts in the `~dirty~` state with an uninitialised value, so
123
- * `#callback` will be invoked on the first `get()`.
124
- *
125
- * @param cb - Pure function evaluated lazily to produce the value.
126
- * Receives the `Computed` instance as `this`.
127
- * @param options - Optional configuration:
128
- * - `equals` — custom equality function; defaults to `Object.is`.
129
- *
130
- * @see Signal algorithms — "Signal.Computed Constructor"
131
- */
132
- constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>) {
133
- this.#callback = cb;
134
- this.#equals = options?.equals ?? Object.is;
135
- this.#sources = new Set;
136
- this.#sinks = new Set;
137
- this.#state = 'dirty';
138
- }
139
-
140
- /**
141
- * Returns the current value of this Signal, re-evaluating `#callback` if
142
- * the cached value may be stale.
143
- *
144
- * Registers this Signal as a source of any outer `Computed` currently
145
- * being evaluated (automatic dependency tracking).
146
- *
147
- * If the state is `~dirty~` or `~checked~`, walks the source graph
148
- * depth-first to find and recalculate the deepest stale `Computed` first,
149
- * then re-checks upward until this Signal is `~clean~`.
150
- *
151
- * @returns The current computed value, or a boxed error object if the last
152
- * evaluation threw.
153
- * @throws If `frozen` is `true`.
154
- * @throws If the Signal is in the `~computing~` state (cyclic dependency).
155
- *
156
- * @see Signal algorithms — "Method: Signal.Computed.prototype.get"
157
- */
158
- public get(): T | { isError: true; value: Error } {
159
- if (GLOBAL_STATE.frozen) {
160
- throw new Error('Cannot get value of a Computed signal while the global state is frozen');
161
- }
162
-
163
- if (this.#state === 'computing') {
164
- throw new Error('Circular dependency detected while computing a Computed signal');
165
- }
166
-
167
- GLOBAL_STATE.computing?.addSource(this, PRIVATE);
168
-
169
- if (this.#sinks.size === 0) {
170
- this.#computeValue();
171
- } else if (this.#state === 'dirty' || this.#state === 'checked') {
172
- while (this.#state === 'dirty' || this.#state === 'checked') {
173
- const deepest = this.#findDeepestStale();
174
- deepest.#computeValue();
175
- }
176
- }
177
-
178
- return this.#value;
179
- }
180
-
181
- /**
182
- * Registers a Signal as a source of this `Computed`, discovered during the
183
- * execution of `#callback`.
184
- *
185
- * If this `Computed` is currently being watched (has at least one sink),
186
- * the source is also informed of this Signal as a new sink, building the
187
- * live push-notification chain upward.
188
- *
189
- * @param source - The Signal read during evaluation.
190
- * @param symbol - Private access symbol; rejects calls from outside the library.
191
- * @internal
192
- */
193
- public addSource(source: State | Computed, symbol: symbol) {
194
- assertPrivateContext(symbol);
195
- this.#sources.add(source);
196
- source.addSink(this, PRIVATE)
197
- }
198
-
199
- /**
200
- * Returns the current evaluation state of this Signal.
201
- *
202
- * @param symbol - Private access symbol; rejects calls from outside the library.
203
- * @internal
204
- * @see Signal algorithms — "Signal.Computed State machine"
205
- */
206
- public getState(symbol: symbol): ComputedState {
207
- assertPrivateContext(symbol);
208
- return this.#state;
209
- }
210
-
211
- /**
212
- * Transitions this Signal to a new evaluation state.
213
- *
214
- * Only valid transitions (as defined by the state machine) are allowed.
215
- * Invalid transitions throw an error.
216
- *
217
- * @param newState - The target state.
218
- * @param symbol - Private access symbol; rejects calls from outside the library.
219
- * @throws If the transition from the current state to `newState` is not allowed.
220
- * @internal
221
- * @see Signal algorithms — "Signal.Computed State machine"
222
- */
223
- public setState(newState: ComputedState, symbol: symbol): void {
224
- assertPrivateContext(symbol);
225
-
226
- if (this.#state !== newState && this.#isValidTransition(this.#state, newState)) {
227
- this.#state = newState;
228
- }
229
- }
230
-
231
- /**
232
- * Registers a new sink (a `Computed` or `Watcher` that directly depends on
233
- * this Signal) in the internal sinks set.
234
- *
235
- * If this is the first sink, propagates the sink registration recursively
236
- * up through `#sources`, building the live dependency chain that enables
237
- * push-based invalidation.
238
- *
239
- * @param sink - The dependent node to register.
240
- * @param symbol - Private access symbol; rejects calls from outside the library.
241
- * @internal
242
- */
243
- public addSink(sink: Computed<unknown> | Watcher, symbol: symbol) {
244
- assertPrivateContext(symbol);
245
- if (this.#sinks.size === 0) {
246
- this.#sources.forEach(source => source.addSink(this, PRIVATE));
247
- }
248
- this.#sinks.add(sink);
249
- }
250
-
251
- /**
252
- * Removes a sink from the internal sinks set.
253
- *
254
- * If the sinks set becomes empty after removal, propagates the removal
255
- * recursively up through `#sources`, tearing down the live dependency
256
- * chain and allowing garbage collection of un-watched nodes.
257
- *
258
- * @param sink - The dependent node to remove.
259
- * @param symbol - Private access symbol; rejects calls from outside the library.
260
- * @internal
261
- */
262
- public removeSink(sink: Computed | Watcher, symbol: symbol) {
263
- assertPrivateContext(symbol);
264
- this.#sinks.delete(sink);
265
-
266
- if (this.#sinks.size === 0) {
267
- this.#sources.forEach(source => source.removeSink(this, PRIVATE));
268
- }
269
- }
270
-
271
- /**
272
- * Recursively walks the source graph depth-first to find the deepest,
273
- * left-most `Computed` node that is in a `~dirty~` or `~checked~` state.
274
- *
275
- * This ensures that recalculation always starts from the bottom of the
276
- * dependency graph, so every node sees already-updated dependencies —
277
- * the core of glitch-free evaluation.
278
- *
279
- * Cuts off the search when hitting a `~clean~` `Computed` source, since
280
- * its subtree is guaranteed to be up-to-date.
281
- *
282
- * @returns The deepest stale `Computed` found, or `node` itself if none of
283
- * its sources are stale.
284
- */
285
- #findDeepestStale(): Computed {
286
- const unclearNode = [...this.#sources].find((source): source is Computed => source instanceof Computed && (source.#state === 'dirty' || source.#state === 'checked'));
287
- return unclearNode ? unclearNode.#findDeepestStale() : this;
288
- }
289
-
290
- /**
291
- * Executes `#callback` to recompute this Signal's value.
292
- *
293
- * Implements the "recalculate dirty computed Signal" algorithm:
294
- * 1. Clears stale sources and removes this Signal from their sinks.
295
- * 2. Sets `computing` to this Signal for automatic dependency tracking.
296
- * 3. Runs the callback, caching the return value or any thrown exception.
297
- * 4. Restores the previous `computing` value.
298
- * 5. Runs the "set Signal value" algorithm to detect value changes.
299
- * 6. Transitions state to `~clean~`.
300
- * 7. Propagates `~dirty~` to sinks (or attempts `~clean~` if value unchanged).
301
- *
302
- * @see Signal algorithms — "Algorithm: recalculate dirty computed Signal"
303
- */
304
- #computeValue(): void {
305
- this.#sources.forEach(source => source.removeSink(this, PRIVATE));
306
- this.#sources.clear();
307
-
308
- pushComputed(this);
309
- this.#state = 'computing';
310
-
311
- let newValue: T | { isError: true; value: Error };
312
-
313
- try {
314
- newValue = this.#callback.call(this);
315
- } catch (error) {
316
- newValue = { isError: true, value: error as Error };
317
- } finally {
318
- popComputed();
319
- }
320
-
321
- const outcome = this.#setValue(newValue);
322
-
323
- outcome === 'dirty'
324
- ? this.#sinks.forEach(sink => sink instanceof Computed ? sink.setState('dirty', PRIVATE) : sink.notify(PRIVATE))
325
- : this.#propagateClean();
326
-
327
- this.#state = 'clean';
328
- }
329
-
330
- /**
331
- * Implements the "set Signal value" algorithm.
332
- *
333
- * Compares the new value against the cached one using `#equals`. If equal,
334
- * returns `~clean~` and leaves `#value` untouched. Otherwise updates
335
- * `#value` and returns `~dirty~`.
336
- *
337
- * Special cases:
338
- * - If `newValue` is a boxed error, `#equals` is skipped and the error is
339
- * cached directly.
340
- * - If `#equals` itself throws, the exception is cached as a boxed error
341
- * and the outcome is `~dirty~`.
342
- *
343
- * @param newValue - The value (or boxed error) produced by `#callback`.
344
- * @returns `~clean~` if the value is unchanged, `~dirty~` otherwise.
345
- *
346
- * @see Signal algorithms — "Set Signal value algorithm"
347
- */
348
- #setValue(newValue: T | { isError: true; value: Error }): 'clean' | 'dirty' {
349
- const oldValue = this.#value;
350
-
351
- /*
352
- If new value is an error we always update without calling equals
353
- */
354
- if (this.#isErrorValue(newValue)) {
355
- this.#value = newValue;
356
- return 'dirty';
357
- }
358
-
359
- try {
360
- if (!this.#isErrorValue(oldValue) && this.#equals.call(this, oldValue, newValue)) {
361
- return 'clean';
362
- }
363
- } catch (equalsError) {
364
- this.#value = { isError: true, value: equalsError as Error };
365
- return 'dirty';
366
- }
367
-
368
- this.#value = newValue;
369
- return 'dirty';
370
- }
371
-
372
- /**
373
- * Recursively marks `~checked~` sinks as `~clean~` when all of their
374
- * immediate sources are already `~clean~`.
375
- *
376
- * Called after a recalculation that produced an unchanged value (`~clean~`
377
- * outcome from `#setValue`). Propagates the clean signal upward through
378
- * the graph so that Computed nodes that were only transitively dirty — and
379
- * whose dependencies have not actually changed — are not needlessly
380
- * re-evaluated on the next read.
381
- *
382
- * @see Signal algorithms — "Algorithm: recalculate dirty computed Signal"
383
- */
384
- #propagateClean(): void {
385
- [...this.#sinks]
386
- .filter((sink): sink is Computed<unknown> => sink instanceof Computed && sink.#state === 'checked')
387
- .forEach(sink => {
388
- const allSourcesClean = [...sink.#sources].every(source => !(source instanceof Computed) || source.#state === 'clean');
389
- if (allSourcesClean) {
390
- sink.#state = 'clean';
391
- sink.#propagateClean();
392
- }
393
- });
394
- }
395
-
396
- /**
397
- * Type guard that checks whether a value is a boxed error object.
398
- *
399
- * Used to distinguish a legitimately computed value from a cached
400
- * exception produced by `#callback` or `#equals`.
401
- *
402
- * @param value - The value to inspect.
403
- * @returns `true` if `value` is `{ isError: true; value: Error }`.
404
- */
405
- #isErrorValue(value: unknown): value is { isError: true; value: Error } {
406
- return typeof value === 'object' && !!value && 'isError' in value;
407
- }
408
-
409
- #isValidTransition(from: ComputedState, to: ComputedState): boolean {
410
- switch (from) {
411
- case 'checked':
412
- return to === 'clean' || to === 'dirty';
413
- case 'clean':
414
- return to === 'checked' || to === 'dirty';
415
- case 'dirty':
416
- return to === 'computing';
417
- case 'computing':
418
- return to === 'clean';
419
- }
420
- }
421
- }
@@ -1,208 +0,0 @@
1
- import { NoArgsVoidFunction } from '@xaendar/common';
2
- import { GLOBAL_STATE } from '../globals';
3
- import { PRIVATE, assertPrivateContext } from '../private-symbol';
4
- import { SignalEqual } from '../types/signal-equal.type';
5
- import { SignalOptions } from '../types/signal-options.type';
6
- import { Computed } from './computed';
7
- import { Watcher } from './watcher';
8
-
9
- export class State<T = any> {
10
- /**
11
- * The current value of the signal.
12
- *
13
- * Initialised to `initialValue` in the constructor and updated by `set`
14
- * whenever the new value is not equal to the current one according to
15
- * `#equals`.
16
- *
17
- * @internalSlot
18
- * @see Signal algorithms — 'Signal.State internal slots'
19
- */
20
- #value: T;
21
- /**
22
- * The equality function used to determine whether a new value is
23
- * meaningfully different from the current one.
24
- *
25
- * Called as `equals.call(signal, oldValue, newValue)`. If it returns
26
- * `true` the signal is considered unchanged and no propagation occurs.
27
- * Defaults to `Object.is` when not provided via options.
28
- *
29
- * @internalSlot
30
- * @see Signal algorithms — 'Signal.State internal slots'
31
- * @see Algorithm — 'Set Signal value'
32
- */
33
- #equals: SignalEqual<T>
34
- /**
35
- * Optional callback invoked (with `frozen = true`) the first time this
36
- * Signal gains a sink — i.e. when it transitions from un-observed to
37
- * observed by at least one `Watcher` (directly or transitively).
38
- *
39
- * @internalSlot
40
- * @see Signal algorithms — 'Signal.State internal slots'
41
- * @see Method — `Signal.subtle.Watcher.prototype.watch`
42
- */
43
- #watched?: NoArgsVoidFunction;
44
- /**
45
- * Optional callback invoked (with `frozen = true`) when this Signal loses
46
- * its last sink — i.e. when it transitions from observed back to
47
- * un-observed.
48
- *
49
- * @internalSlot
50
- * @see Signal algorithms — 'Signal.State internal slots'
51
- * @see Method — `Signal.subtle.Watcher.prototype.unwatch`
52
- */
53
- #unwatched?: NoArgsVoidFunction;
54
- /**
55
- * The set of watched signals that directly depend on this one.
56
- *
57
- * Populated only when this Signal is reachable from at least one active
58
- * `Watcher` — un-watched Signals have an empty sinks set, which allows
59
- * them to be garbage-collected independently from the rest of the graph.
60
- *
61
- * Should contain both `Computed` and `Watcher` instances, as both can be
62
- * direct dependents of a `State`.
63
- *
64
- * @internalSlot
65
- * @see Signal algorithms — 'Signal.State internal slots'
66
- * @see Method — `Signal.State.prototype.get` (NOTE on sinks)
67
- */
68
- #sinks: Set<Computed<unknown> | Watcher>;
69
- /**
70
- * Returns a snapshot of the current sinks set for introspection.
71
- *
72
- * @param symbol - Private access symbol; rejects calls from outside the library.
73
- * @returns An array of `Computed` and `Watcher` instances that depend on this Signal.
74
- * @internal
75
- */
76
- public getSinks(symbol: symbol): (Computed<unknown> | Watcher)[] {
77
- assertPrivateContext(symbol);
78
- return [...this.#sinks];
79
- }
80
-
81
- /**
82
- * Creates a new `State` signal.
83
- *
84
- * @param initialValue - The initial value of the signal.
85
- * @param options - Optional configuration:
86
- * - `equals` — custom equality function; defaults to `Object.is`.
87
- * - `watched` — called when the signal gains its first sink.
88
- * - `unwatched` — called when the signal loses its last sink.
89
- *
90
- * @see Signal algorithms — 'Constructor: Signal.State(initialValue, options)'
91
- */
92
- constructor(initialValue: T, options?: SignalOptions<T>) {
93
- this.#value = initialValue;
94
- this.#equals = options?.equals ?? Object.is;
95
- this.#watched = options?.watched;
96
- this.#unwatched = options?.unwatched;
97
- this.#sinks = new Set;
98
- }
99
-
100
- /**
101
- * Returns the current value of the signal, registering this Signal as a
102
- * source of the innermost `Computed` currently being evaluated (if any).
103
- *
104
- * @throws If `frozen` is `true` — reads are forbidden while a protected
105
- * callback (`notify`, `watched`, `unwatched`) is executing.
106
- *
107
- * @see Signal algorithms — 'Method: Signal.State.prototype.get()'
108
- */
109
- public get(): T {
110
- if (GLOBAL_STATE.frozen) {
111
- throw new Error('Cannot get value while signals are frozen');
112
- }
113
-
114
- /*
115
- If there is a currently computing `Computed`, register this `State` as a
116
- source of that `Computed`.
117
- This is how the dependency graph is built.
118
-
119
- THis is done every time a `State` is read to guarantee always up-to-date
120
- tracking of dependencies, even if they change between computations
121
- */
122
- GLOBAL_STATE.computing?.addSource(this, PRIVATE)
123
-
124
- return this.#value;
125
- }
126
-
127
- /**
128
- * Updates the signal's value and propagates changes to all dependent
129
- * sinks.
130
- *
131
- * If `equals(currentValue, newValue)` returns `true` the call is a no-op
132
- * and no propagation occurs. Otherwise `#value` is updated, all direct
133
- * `Computed` sinks are marked `~dirty~`, indirect ones `~checked~`, and
134
- * each reachable `Watcher` has its `notify` callback invoked synchronously
135
- * (with `frozen = true`).
136
- *
137
- * @param newValue - The new value to set.
138
- * @throws If `frozen` is `true` — writes are forbidden while a protected
139
- * callback is executing.
140
- *
141
- * @see Signal algorithms — 'Method: Signal.State.prototype.set(newValue)'
142
- * @see Algorithm — 'Set Signal value'
143
- */
144
- public set(newValue: T): void {
145
- if (GLOBAL_STATE.frozen) {
146
- throw new Error('Cannot set value while signals are frozen');
147
- }
148
-
149
- if (!this.#equals.call(this, this.#value, newValue)) {
150
- this.#value = newValue;
151
- this.#sinks.forEach(sink => sink instanceof Computed ? sink.setState('dirty', PRIVATE) : sink.notify(PRIVATE));
152
- }
153
- }
154
-
155
- /**
156
- * Registers a new sink (a `Computed` or `Watcher` that depends on this
157
- * Signal) in the internal sinks set.
158
- *
159
- * Called by `Watcher.prototype.watch` when building the live dependency
160
- * chain, and by `Computed` when propagating sink registration up through
161
- * its sources.
162
- *
163
- * @param sink - The dependent node to register.
164
- * @param symbol - The private symbol for validation.
165
- * @internal
166
- */
167
- public addSink(sink: Computed | Watcher, symbol: symbol): void {
168
- assertPrivateContext(symbol);
169
- const empty = this.#sinks.size === 0;
170
- this.#sinks.add(sink);
171
-
172
- if (empty && this.#watched) {
173
- GLOBAL_STATE.frozen = true;
174
- try {
175
- this.#watched();
176
- } finally {
177
- GLOBAL_STATE.frozen = false;
178
- }
179
- }
180
- }
181
-
182
- /**
183
- * Removes a sink from the internal sinks set.
184
- *
185
- * Called by `Watcher.prototype.unwatch` when tearing down the live
186
- * dependency chain. If the sinks set becomes empty after removal, the
187
- * caller is responsible for propagating the removal up through this
188
- * Signal's sources.
189
- *
190
- * @param sink - The dependent node to remove.
191
- * @param symbol - The private symbol for validation.
192
- * @internal
193
- */
194
- public removeSink(sink: Computed | Watcher, symbol: symbol): void {
195
- assertPrivateContext(symbol);
196
- this.#sinks.delete(sink);
197
- const empty = this.#sinks.size === 0;
198
-
199
- if (empty && this.#unwatched) {
200
- GLOBAL_STATE.frozen = true;
201
- try {
202
- this.#unwatched();
203
- } finally {
204
- GLOBAL_STATE.frozen = false;
205
- }
206
- }
207
- }
208
- }