evnty 5.1.1 → 5.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evnty",
3
3
  "description": "Async-first, reactive event handling library for complex event flows in browser and Node.js",
4
- "version": "5.1.1",
4
+ "version": "5.2.0",
5
5
  "type": "module",
6
6
  "types": "build/index.d.ts",
7
7
  "main": "build/index.cjs",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "build",
15
- "src/index.ts",
15
+ "src/*.ts",
16
16
  "src/__tests__/example.js"
17
17
  ],
18
18
  "sideEffects": false,
package/src/async.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { Action, Fn, Emitter, MaybePromise, Promiseable } from './types.js';
2
+
3
+ /**
4
+ * @internal
5
+ */
6
+ export class Disposer {
7
+ #target?: Disposable;
8
+ #abortSignal?: AbortSignal;
9
+
10
+ constructor(target: Disposable, abortSignal?: AbortSignal) {
11
+ if (abortSignal?.aborted) return;
12
+ this.#target = target;
13
+ if (abortSignal) {
14
+ this.#abortSignal = abortSignal;
15
+ abortSignal.addEventListener('abort', this);
16
+ }
17
+ }
18
+
19
+ get disposed(): boolean {
20
+ return !this.#target;
21
+ }
22
+
23
+ [Symbol.dispose](): boolean {
24
+ if (!this.#target) return false;
25
+ this.#target = undefined;
26
+ // Stryker disable all: cleanup is memory optimization, no observable behavior after disposal
27
+ if (this.#abortSignal) {
28
+ this.#abortSignal.removeEventListener('abort', this);
29
+ this.#abortSignal = undefined;
30
+ }
31
+ // Stryker restore all
32
+ return true;
33
+ }
34
+
35
+ handleEvent(): void {
36
+ // Stryker disable next-line OptionalChaining: #target is always set when abort listener fires (synchronous code)
37
+ this.#target?.[Symbol.dispose]();
38
+ }
39
+ }
40
+
41
+ /**
42
+ * @internal
43
+ */
44
+ export interface Async<T, R> extends Emitter<T, R>, Disposable {}
45
+
46
+ /**
47
+ * @internal
48
+ */
49
+ export abstract class Async<T, R> implements Emitter<T, R>, Promiseable<T>, Promise<T>, AsyncIterator<T, void, void>, AsyncIterable<T> {
50
+ abstract [Symbol.toStringTag]: string;
51
+ abstract emit(value: T): R;
52
+ abstract receive(): Promise<T>;
53
+
54
+ dispose?(): void;
55
+
56
+ #sink?: Fn<[T], R>;
57
+ #disposer: Disposer;
58
+
59
+ constructor(abortSignal?: AbortSignal) {
60
+ this.#disposer = new Disposer(this, abortSignal);
61
+ }
62
+
63
+ get disposed(): boolean {
64
+ return this.#disposer.disposed;
65
+ }
66
+
67
+ get sink(): Fn<[T], R> {
68
+ return (this.#sink ??= this.emit.bind(this));
69
+ }
70
+
71
+ handleEvent(event: T) {
72
+ this.emit(event);
73
+ }
74
+
75
+ catch<OK = never>(onrejected?: Fn<[unknown], MaybePromise<OK>> | null): Promise<T | OK> {
76
+ return this.receive().catch(onrejected);
77
+ }
78
+
79
+ finally(onfinally?: Action | null): Promise<T> {
80
+ return this.receive().finally(onfinally);
81
+ }
82
+
83
+ then<OK = T, ERR = never>(onfulfilled?: Fn<[T], MaybePromise<OK>> | null, onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<OK | ERR> {
84
+ return this.receive().then(onfulfilled, onrejected);
85
+ }
86
+
87
+ async next(): Promise<IteratorResult<T, void>> {
88
+ try {
89
+ const value = await this.receive();
90
+ return { value, done: false };
91
+ } catch {
92
+ return { value: undefined, done: true };
93
+ }
94
+ }
95
+
96
+ async return(): Promise<IteratorResult<T, void>> {
97
+ // Stryker disable next-line OptionalChaining: all subclasses define dispose()
98
+ this.dispose?.();
99
+ return { value: undefined, done: true };
100
+ }
101
+
102
+ [Symbol.asyncIterator](): AsyncIterator<T, void, void> {
103
+ return this;
104
+ }
105
+
106
+ [Symbol.dispose](): void {
107
+ if (this.#disposer[Symbol.dispose]()) {
108
+ // Stryker disable next-line OptionalChaining: all subclasses define dispose()
109
+ this.dispose?.();
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,348 @@
1
+ import { RingBuffer } from './ring-buffer.js';
2
+ import { Signal } from './signal.js';
3
+ import { Disposer } from './async.js';
4
+ import { min } from './utils.js';
5
+ import { Action, Fn, Emitter, MaybePromise, Promiseable } from './types.js';
6
+
7
+ /**
8
+ * A handle representing a consumer's position in a Broadcast.
9
+ * Returned by `Broadcast.join()` and used to consume values.
10
+ * Implements Disposable for automatic cleanup via `using` keyword.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const broadcast = new Broadcast<number>();
15
+ * using handle = broadcast.join();
16
+ * broadcast.emit(42);
17
+ * const value = broadcast.consume(handle); // 42
18
+ * ```
19
+ */
20
+ export class ConsumerHandle<T> implements Disposable {
21
+ #broadcast: Broadcast<T>;
22
+
23
+ constructor(broadcast: Broadcast<T>) {
24
+ this.#broadcast = broadcast;
25
+ }
26
+
27
+ /**
28
+ * The current position of this consumer in the buffer.
29
+ */
30
+ get cursor(): number {
31
+ return this.#broadcast.getCursor(this);
32
+ }
33
+
34
+ /**
35
+ * Leaves the broadcast, releasing this consumer's position.
36
+ */
37
+ [Symbol.dispose](): void {
38
+ this.#broadcast.leave(this);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @internal
44
+ */
45
+ export class BroadcastIterator<T> implements AsyncIterator<T, void, void> {
46
+ #broadcast: Broadcast<T>;
47
+ #signal: Signal<T>;
48
+ #handle: ConsumerHandle<T>;
49
+
50
+ constructor(broadcast: Broadcast<T>, signal: Signal<T>, handle: ConsumerHandle<T>) {
51
+ this.#broadcast = broadcast;
52
+ this.#signal = signal;
53
+ this.#handle = handle;
54
+ }
55
+
56
+ async next(): Promise<IteratorResult<T, void>> {
57
+ try {
58
+ while (true) {
59
+ const result = this.#broadcast.tryConsume(this.#handle);
60
+ if (!result.done) {
61
+ return { value: result.value, done: false };
62
+ }
63
+ await this.#signal.receive();
64
+ }
65
+ } catch {
66
+ return { value: undefined, done: true };
67
+ }
68
+ }
69
+
70
+ async return(): Promise<IteratorResult<T, void>> {
71
+ this.#broadcast.leave(this.#handle);
72
+ return { value: undefined, done: true };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * A multi-consumer FIFO queue where each consumer maintains its own read position.
78
+ * Values are buffered and each consumer can read them independently at their own pace.
79
+ * The buffer automatically compacts when all consumers have read past a position.
80
+ *
81
+ * Key characteristics:
82
+ * - Multiple consumers - each gets their own cursor position
83
+ * - Buffered delivery - values are stored until all consumers read them
84
+ * - Late joiners only see values emitted after joining
85
+ * - Automatic cleanup via FinalizationRegistry when handles are garbage collected
86
+ *
87
+ * Differs from:
88
+ * - Event: Broadcast buffers values, Event does not
89
+ * - Sequence: Broadcast supports multiple consumers, Sequence is single-consumer
90
+ * - Signal: Broadcast buffers values, Signal only notifies current waiters
91
+ *
92
+ * @template T - The type of values in the broadcast
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const broadcast = new Broadcast<number>();
97
+ *
98
+ * const handle1 = broadcast.join();
99
+ * const handle2 = broadcast.join();
100
+ *
101
+ * broadcast.emit(1);
102
+ * broadcast.emit(2);
103
+ *
104
+ * broadcast.consume(handle1); // 1
105
+ * broadcast.consume(handle2); // 1
106
+ * broadcast.consume(handle1); // 2
107
+ * ```
108
+ */
109
+ export class Broadcast<T> implements Emitter<T, boolean>, Promiseable<T>, Promise<T>, Disposable, AsyncIterable<T> {
110
+ #buffer = new RingBuffer<T>();
111
+ #signal = new Signal<T>();
112
+ #disposer: Disposer;
113
+ #sink?: Fn<[T], boolean>;
114
+ #nextId = 0;
115
+ #cursors = new Map<number, number>();
116
+ #handles = new WeakMap<ConsumerHandle<T>, number>();
117
+ #minCursor = 0;
118
+
119
+ // Stryker disable all: FinalizationRegistry callback only testable via GC, excluded from mutation testing
120
+ #registry = new FinalizationRegistry<number>((id) => {
121
+ const cursor = this.#cursors.get(id)!;
122
+ this.#cursors.delete(id);
123
+
124
+ if (cursor === this.#minCursor) {
125
+ this.#minCursor = min(this.#cursors.values(), this.#buffer.right);
126
+ const shift = this.#minCursor - this.#buffer.left;
127
+ if (shift > 0) this.#buffer.shiftN(shift);
128
+ }
129
+ });
130
+ // Stryker restore all
131
+
132
+ readonly [Symbol.toStringTag] = 'Broadcast';
133
+
134
+ constructor() {
135
+ this.#disposer = new Disposer(this);
136
+ }
137
+
138
+ /**
139
+ * Returns a bound emit function for use as a callback.
140
+ */
141
+ get sink(): Fn<[T], boolean> {
142
+ return (this.#sink ??= this.emit.bind(this));
143
+ }
144
+
145
+ /**
146
+ * DOM EventListener interface compatibility.
147
+ */
148
+ handleEvent(event: T): void {
149
+ this.emit(event);
150
+ }
151
+
152
+ /**
153
+ * The number of active consumers.
154
+ */
155
+ get size(): number {
156
+ return this.#cursors.size;
157
+ }
158
+
159
+ /**
160
+ * Emits a value to all consumers. The value is buffered for consumption.
161
+ *
162
+ * @param value - The value to emit.
163
+ * @returns `true` if the value was emitted.
164
+ */
165
+ emit(value: T): boolean {
166
+ if (this.#disposer.disposed) {
167
+ return false;
168
+ }
169
+ this.#buffer.push(value);
170
+ this.#signal.emit(value);
171
+ return true;
172
+ }
173
+
174
+ /**
175
+ * Waits for the next emitted value without joining as a consumer.
176
+ * Does not buffer - only receives values emitted after calling.
177
+ *
178
+ * @returns A promise that resolves with the next emitted value.
179
+ */
180
+ receive(): Promise<T> {
181
+ return this.#signal.receive();
182
+ }
183
+
184
+ then<OK = T, ERR = never>(onfulfilled?: Fn<[T], MaybePromise<OK>> | null, onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<OK | ERR> {
185
+ return this.receive().then(onfulfilled, onrejected);
186
+ }
187
+
188
+ catch<ERR = never>(onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<T | ERR> {
189
+ return this.receive().catch(onrejected);
190
+ }
191
+
192
+ finally(onfinally?: Action | null): Promise<T> {
193
+ return this.receive().finally(onfinally);
194
+ }
195
+
196
+ /**
197
+ * Joins the broadcast as a consumer. Returns a handle used to consume values.
198
+ * The consumer starts at the current buffer position and will only see
199
+ * values emitted after joining.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const handle = broadcast.join();
204
+ * // Use handle with consume(), readable(), leave()
205
+ * ```
206
+ */
207
+ join(): ConsumerHandle<T> {
208
+ // Stryker disable next-line UpdateOperator: IDs only need uniqueness, direction is irrelevant
209
+ const id = this.#nextId++;
210
+ const cursor = this.#buffer.right;
211
+ const handle = new ConsumerHandle<T>(this);
212
+
213
+ this.#handles.set(handle, id);
214
+ this.#cursors.set(id, cursor);
215
+
216
+ // Stryker disable all: minCursor is an optimization hint; tryConsume recalculates on use
217
+ if (this.#cursors.size === 1 || cursor < this.#minCursor) {
218
+ this.#minCursor = cursor;
219
+ }
220
+ // Stryker restore all
221
+
222
+ this.#registry.register(handle, id, handle);
223
+ return handle;
224
+ }
225
+
226
+ /**
227
+ * Gets the current cursor position for a consumer handle.
228
+ *
229
+ * @param handle - The consumer handle.
230
+ * @returns The cursor position.
231
+ * @throws If the handle is invalid (already left or never joined).
232
+ */
233
+ getCursor(handle: ConsumerHandle<T>): number {
234
+ const id = this.#handles.get(handle);
235
+ // Stryker disable next-line ConditionalExpression: second cursor check catches invalid handles
236
+ if (id === undefined) throw new Error('Invalid handle');
237
+ const cursor = this.#cursors.get(id);
238
+ if (cursor === undefined) throw new Error('Invalid handle');
239
+ return cursor;
240
+ }
241
+
242
+ /**
243
+ * Removes a consumer from the broadcast. The handle becomes invalid after this call.
244
+ * Idempotent - calling multiple times has no effect.
245
+ *
246
+ * @param handle - The consumer handle to remove.
247
+ */
248
+ leave(handle: ConsumerHandle<T>): void {
249
+ const id = this.#handles.get(handle);
250
+ // Stryker disable next-line ConditionalExpression,EqualityOperator: subsequent ops are safe with undefined id
251
+ if (id === undefined) return;
252
+
253
+ const cursor = this.#cursors.get(id)!;
254
+ this.#handles.delete(handle);
255
+ this.#cursors.delete(id);
256
+ this.#registry.unregister(handle);
257
+
258
+ // Stryker disable all: compaction condition is optimization; buffer reads are correct regardless
259
+ if (cursor === this.#minCursor) {
260
+ this.#minCursor = min(this.#cursors.values(), this.#buffer.right);
261
+ const shift = this.#minCursor - this.#buffer.left;
262
+ if (shift > 0) this.#buffer.shiftN(shift);
263
+ }
264
+ // Stryker restore all
265
+ }
266
+
267
+ /**
268
+ * Consumes and returns the next value for a consumer.
269
+ * Advances the consumer's cursor position.
270
+ *
271
+ * @param handle - The consumer handle.
272
+ * @throws If no value is available or the handle is invalid.
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * if (broadcast.readable(handle)) {
277
+ * const value = broadcast.consume(handle);
278
+ * }
279
+ * ```
280
+ */
281
+ consume(handle: ConsumerHandle<T>): T {
282
+ const result = this.tryConsume(handle);
283
+ if (result.done) {
284
+ throw new Error('No value available');
285
+ }
286
+ return result.value;
287
+ }
288
+
289
+ /**
290
+ * Attempts to consume the next value for a consumer.
291
+ * Returns `{ done: true }` when no value is currently available.
292
+ *
293
+ * @param handle - The consumer handle.
294
+ * @returns The next value, or `{ done: true }` when nothing is available.
295
+ * @throws If the handle is invalid.
296
+ */
297
+ tryConsume(handle: ConsumerHandle<T>): IteratorResult<T, void> {
298
+ const id = this.#handles.get(handle);
299
+ // Stryker disable next-line ConditionalExpression: second cursor check catches invalid handles
300
+ if (id === undefined) throw new Error('Invalid handle');
301
+
302
+ const cursor = this.#cursors.get(id);
303
+ if (cursor === undefined) throw new Error('Invalid handle');
304
+ if (cursor >= this.#buffer.right) {
305
+ return { value: undefined, done: true };
306
+ }
307
+
308
+ const value = this.#buffer.peekAt(cursor)!;
309
+ this.#cursors.set(id, cursor + 1);
310
+
311
+ // Stryker disable all: compaction condition is optimization; buffer reads are correct regardless
312
+ if (cursor === this.#minCursor) {
313
+ this.#minCursor = min(this.#cursors.values(), this.#buffer.right);
314
+ const shift = this.#minCursor - this.#buffer.left;
315
+ if (shift > 0) this.#buffer.shiftN(shift);
316
+ }
317
+ // Stryker restore all
318
+
319
+ return { value, done: false };
320
+ }
321
+
322
+ /**
323
+ * Checks if there are values available for a consumer to read.
324
+ *
325
+ * @param handle - The consumer handle.
326
+ * @returns `true` if there are unread values, `false` otherwise.
327
+ */
328
+ readable(handle: ConsumerHandle<T>): boolean {
329
+ return this.getCursor(handle) < this.#buffer.right;
330
+ }
331
+
332
+ [Symbol.asyncIterator](): AsyncIterator<T, void, void> {
333
+ return new BroadcastIterator(this, this.#signal, this.join());
334
+ }
335
+
336
+ dispose(): void {
337
+ this[Symbol.dispose]();
338
+ }
339
+
340
+ [Symbol.dispose](): void {
341
+ // Stryker disable next-line ConditionalExpression: double-dispose re-clears empty collections, no observable effect
342
+ if (this.#disposer[Symbol.dispose]()) {
343
+ this.#signal[Symbol.dispose]();
344
+ this.#buffer.clear();
345
+ this.#cursors.clear();
346
+ }
347
+ }
348
+ }
@@ -0,0 +1,166 @@
1
+ import { Fn, MaybePromise } from './types.js';
2
+ import { isThenable, noop } from './utils.js';
3
+
4
+ const ERR_BRAND = Symbol.for('evnty.ResultError');
5
+
6
+ /**
7
+ * @internal
8
+ */
9
+ export interface ResultError<E = unknown> {
10
+ readonly error: E;
11
+ readonly [ERR_BRAND]: true;
12
+ }
13
+
14
+ function ResultError<E>(this: ResultError<E>, error: E): void {
15
+ (this as { error: E }).error = error;
16
+ }
17
+ (ResultError.prototype as { [ERR_BRAND]: true })[ERR_BRAND] = true;
18
+
19
+ /**
20
+ * @internal
21
+ */
22
+ export function err<E>(error: E): ResultError<E> {
23
+ return new (ResultError as unknown as new (error: E) => ResultError<E>)(error);
24
+ }
25
+
26
+ /**
27
+ * @internal
28
+ */
29
+ export function isErr(result: unknown): result is ResultError<unknown> {
30
+ return typeof result === 'object' && result !== null && (result as Record<symbol, boolean>)[ERR_BRAND] === true;
31
+ }
32
+
33
+ /**
34
+ * @internal
35
+ */
36
+ export function isOk(result: unknown): boolean {
37
+ return typeof result !== 'object' || result === null || !(result as Record<symbol, boolean>)[ERR_BRAND];
38
+ }
39
+
40
+ /**
41
+ * @internal
42
+ */
43
+ export type DispatchResultItem<T> = MaybePromise<T> | ResultError;
44
+
45
+ /**
46
+ * @internal
47
+ */
48
+ export function unwrap<T>(results: DispatchResultItem<T>[]): MaybePromise<T>[] {
49
+ const len = results.length;
50
+ const unwrapped = new Array<MaybePromise<T>>(len);
51
+ for (let i = 0; i < len; i++) {
52
+ const r = results[i];
53
+ unwrapped[i] = isErr(r) ? Promise.reject(r.error) : r;
54
+ }
55
+ return unwrapped;
56
+ }
57
+
58
+ async function resolveMaybePromises<T>(items: MaybePromise<T>[], asyncIndices: number[]): Promise<T[]> {
59
+ const pending = new Array<PromiseLike<T>>(asyncIndices.length);
60
+ for (let j = 0; j < asyncIndices.length; j++) {
61
+ pending[j] = items[asyncIndices[j]] as PromiseLike<T>;
62
+ }
63
+ const resolved = await Promise.all(pending);
64
+ for (let j = 0; j < asyncIndices.length; j++) {
65
+ items[asyncIndices[j]] = resolved[j];
66
+ }
67
+ return items as T[];
68
+ }
69
+
70
+ function resolveAll<T>(results: DispatchResultItem<T>[]): T[] | Promise<T[]> {
71
+ const len = results.length;
72
+ if (len === 0) return results as T[];
73
+
74
+ let firstError: unknown;
75
+ let hasError = false;
76
+ let asyncIndices: number[] | null = null;
77
+
78
+ for (let i = 0; i < len; i++) {
79
+ const r = results[i];
80
+ if (isErr(r)) {
81
+ if (!hasError) {
82
+ hasError = true;
83
+ firstError = r.error;
84
+ }
85
+ } else if (isThenable(r)) {
86
+ (asyncIndices ??= []).push(i);
87
+ }
88
+ }
89
+
90
+ if (hasError) {
91
+ if (asyncIndices !== null) {
92
+ for (let j = 0; j < asyncIndices.length; j++) {
93
+ (results[asyncIndices[j]] as PromiseLike<T>).then(noop, noop);
94
+ }
95
+ }
96
+ return Promise.reject(firstError);
97
+ }
98
+ if (asyncIndices === null) return results as T[];
99
+
100
+ return resolveMaybePromises(results as MaybePromise<T>[], asyncIndices);
101
+ }
102
+
103
+ function settleAll<T>(results: DispatchResultItem<T>[]): PromiseSettledResult<T>[] | Promise<PromiseSettledResult<T>[]> {
104
+ const len = results.length;
105
+ if (len === 0) return [] as PromiseSettledResult<T>[];
106
+
107
+ let asyncIndices: number[] | null = null;
108
+ const settled = new Array<MaybePromise<PromiseSettledResult<T>>>(len);
109
+
110
+ for (let i = 0; i < len; i++) {
111
+ const r = results[i];
112
+ if (isErr(r)) {
113
+ settled[i] = { status: 'rejected', reason: r.error };
114
+ } else if (isThenable(r)) {
115
+ (asyncIndices ??= []).push(i);
116
+ settled[i] = r.then(
117
+ (value): PromiseFulfilledResult<T> => ({ status: 'fulfilled', value }),
118
+ (reason: unknown): PromiseRejectedResult => ({ status: 'rejected', reason }),
119
+ );
120
+ } else {
121
+ settled[i] = { status: 'fulfilled', value: r };
122
+ }
123
+ }
124
+
125
+ if (asyncIndices === null) return settled as PromiseSettledResult<T>[];
126
+
127
+ return resolveMaybePromises(settled, asyncIndices);
128
+ }
129
+
130
+ /**
131
+ * Wraps an array of values or promises (typically listener results) and provides batch resolution.
132
+ *
133
+ * @template T
134
+ */
135
+ export class DispatchResult<T> implements PromiseLike<T[]> {
136
+ #results: DispatchResultItem<T>[];
137
+
138
+ readonly [Symbol.toStringTag] = 'DispatchResult';
139
+
140
+ constructor(results: DispatchResultItem<T>[]) {
141
+ this.#results = results;
142
+ }
143
+
144
+ then<TResult1 = T, TResult2 = never>(
145
+ onfulfilled?: Fn<[T[]], MaybePromise<TResult1>> | null,
146
+ onrejected?: Fn<[any], MaybePromise<TResult2>> | null,
147
+ ): PromiseLike<TResult1 | TResult2> {
148
+ const resolved = this.all();
149
+ if (resolved instanceof Promise) return resolved.then(onfulfilled, onrejected);
150
+ return Promise.resolve(resolved).then(onfulfilled, onrejected);
151
+ }
152
+
153
+ /**
154
+ * Resolves all listener results, rejecting if any promise rejects or any ResultError exists.
155
+ */
156
+ all(): T[] | Promise<T[]> {
157
+ return resolveAll(this.#results);
158
+ }
159
+
160
+ /**
161
+ * Waits for all listener results to settle, regardless of fulfillment or rejection.
162
+ */
163
+ settled(): PromiseSettledResult<T>[] | Promise<PromiseSettledResult<T>[]> {
164
+ return settleAll(this.#results);
165
+ }
166
+ }