@xyo-network/module-events 2.53.2 → 2.53.4
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/cjs/Events/Events.js +87 -73
- package/dist/cjs/Events/Events.js.map +1 -1
- package/dist/docs.json +1181 -671
- package/dist/esm/Events/Events.js +79 -73
- package/dist/esm/Events/Events.js.map +1 -1
- package/dist/types/Events/Events.d.ts +26 -19
- package/dist/types/Events/Events.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/Events/Events.ts +115 -110
package/src/Events/Events.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertEx } from '@xylabs/assert'
|
|
2
2
|
import { forget } from '@xylabs/forget'
|
|
3
|
+
import { Base, BaseParams } from '@xyo-network/core'
|
|
3
4
|
|
|
4
5
|
import { EventAnyListener, EventArgs, EventData, EventFunctions, EventListener, EventName } from '../model'
|
|
5
6
|
|
|
@@ -10,29 +11,22 @@ To enable this feature set the `DEBUG` environment variable to `emittery` or `*`
|
|
|
10
11
|
|
|
11
12
|
See API for more information on how debugging works.
|
|
12
13
|
*/
|
|
13
|
-
export type DebugLogger
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
export type DebugLogger = (type: string, debugName: string, eventName?: EventName, eventData?: EventArgs) => void
|
|
15
|
+
|
|
16
|
+
type EventListenerInfo<TEventArgs extends EventArgs = EventArgs> = {
|
|
17
|
+
filter?: TEventArgs
|
|
18
|
+
listener: EventListener<TEventArgs>
|
|
19
|
+
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
Configure debug options of an instance.
|
|
22
23
|
*/
|
|
23
|
-
export type DebugOptions
|
|
24
|
+
export type DebugOptions = {
|
|
24
25
|
enabled?: boolean
|
|
25
|
-
logger?: DebugLogger
|
|
26
|
+
logger?: DebugLogger
|
|
26
27
|
readonly name: string
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
Configuration options for Emittery.
|
|
31
|
-
*/
|
|
32
|
-
export type Options<TEventData extends EventData> = {
|
|
33
|
-
readonly debug?: DebugOptions<TEventData>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
30
|
const resolvedPromise = Promise.resolve()
|
|
37
31
|
|
|
38
32
|
export type MetaEventData<TEventData extends EventData> = {
|
|
@@ -46,49 +40,29 @@ export type MetaEventData<TEventData extends EventData> = {
|
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
let canEmitMetaEvents = false
|
|
50
|
-
let isGlobalDebugEnabled = false
|
|
51
|
-
|
|
52
|
-
function assertEventName(eventName: EventName) {
|
|
53
|
-
if (typeof eventName !== 'string' && typeof eventName !== 'symbol' && typeof eventName !== 'number') {
|
|
54
|
-
throw new TypeError('`eventName` must be a string, symbol, or number')
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function assertListener(listener: object) {
|
|
59
|
-
if (typeof listener !== 'function') {
|
|
60
|
-
throw new TypeError('listener must be a function')
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
43
|
const isMetaEvent = (eventName: EventName) => eventName === 'listenerAdded' || eventName === 'listenerRemoved'
|
|
65
44
|
|
|
66
|
-
export
|
|
67
|
-
static anyMap = new WeakMap<object, Set<EventAnyListener>>()
|
|
68
|
-
static eventsMap = new WeakMap<object, Map<EventName, Set<EventListener>>>()
|
|
45
|
+
export type EventsParams = BaseParams<{ readonly debug?: DebugOptions }>
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
47
|
+
export class Events<TEventData extends EventData = EventData> extends Base<EventsParams> implements EventFunctions<TEventData> {
|
|
48
|
+
protected static anyMap = new WeakMap<object, Set<EventAnyListener>>()
|
|
49
|
+
protected static eventsMap = new WeakMap<object, Map<EventName, Set<EventListenerInfo>>>()
|
|
50
|
+
|
|
51
|
+
private static canEmitMetaEvents = false
|
|
52
|
+
private static isGlobalDebugEnabled = false
|
|
72
53
|
|
|
73
54
|
//this is here to be able to query the type, not use
|
|
74
55
|
eventData = {} as TEventData
|
|
75
56
|
|
|
76
|
-
constructor(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (this.debug) {
|
|
83
|
-
this.debug.enabled = !!this.debug.enabled
|
|
84
|
-
|
|
85
|
-
this.debug.logger =
|
|
86
|
-
this.debug.logger ??
|
|
87
|
-
((type: string, debugName: string, eventName?: keyof TEventData, eventData?: TEventData[keyof TEventData]) => {
|
|
57
|
+
constructor(params: EventsParams = {}) {
|
|
58
|
+
const mutatedParams = { ...params }
|
|
59
|
+
if (mutatedParams.debug) {
|
|
60
|
+
mutatedParams.debug.logger =
|
|
61
|
+
mutatedParams.debug.logger ??
|
|
62
|
+
((type: string, debugName: string, eventName?: EventName, eventData?: EventArgs) => {
|
|
88
63
|
let eventDataString: string
|
|
89
64
|
let eventNameString: string | undefined
|
|
90
65
|
try {
|
|
91
|
-
// TODO: Use https://github.com/sindresorhus/safe-stringify when the package is more mature. Just copy-paste the code.
|
|
92
66
|
eventDataString = JSON.stringify(eventData)
|
|
93
67
|
} catch {
|
|
94
68
|
eventDataString = `Object with the following keys failed to stringify: ${Object.keys(eventData ?? {}).join(',')}`
|
|
@@ -102,9 +76,12 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
102
76
|
|
|
103
77
|
const currentTime = new Date()
|
|
104
78
|
const logTime = `${currentTime.getHours()}:${currentTime.getMinutes()}:${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`
|
|
105
|
-
|
|
79
|
+
this.logger.log(`[${logTime}][events:${type}][${debugName}] Event Name: ${eventNameString}\n\tdata: ${eventDataString}`)
|
|
106
80
|
})
|
|
107
81
|
}
|
|
82
|
+
super(mutatedParams)
|
|
83
|
+
Events.anyMap.set(this, new Set<EventAnyListener>())
|
|
84
|
+
Events.eventsMap.set(this, new Map<keyof TEventData, Set<EventListenerInfo>>())
|
|
108
85
|
}
|
|
109
86
|
|
|
110
87
|
static get isDebugEnabled() {
|
|
@@ -112,15 +89,19 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
112
89
|
// so instead of just type checking `globalThis.process`, we need to make sure that `globalThis.process.env` exists.
|
|
113
90
|
|
|
114
91
|
if (typeof globalThis.process?.env !== 'object') {
|
|
115
|
-
return isGlobalDebugEnabled
|
|
92
|
+
return Events.isGlobalDebugEnabled
|
|
116
93
|
}
|
|
117
94
|
|
|
118
95
|
const { env } = globalThis.process ?? { env: {} }
|
|
119
|
-
return env.DEBUG === '
|
|
96
|
+
return env.DEBUG === 'events' || env.DEBUG === '*' || Events.isGlobalDebugEnabled
|
|
120
97
|
}
|
|
121
98
|
|
|
122
99
|
static set isDebugEnabled(newValue) {
|
|
123
|
-
isGlobalDebugEnabled = newValue
|
|
100
|
+
Events.isGlobalDebugEnabled = newValue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get debug() {
|
|
104
|
+
return this.params.debug
|
|
124
105
|
}
|
|
125
106
|
|
|
126
107
|
clearListeners(eventNames: keyof TEventData | (keyof TEventData)[]) {
|
|
@@ -152,53 +133,54 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
152
133
|
async emitMetaEvent<TEventName extends keyof MetaEventData<TEventData>>(eventName: TEventName, eventArgs: MetaEventData<TEventData>[TEventName]) {
|
|
153
134
|
if (isMetaEvent(eventName)) {
|
|
154
135
|
try {
|
|
155
|
-
canEmitMetaEvents = true
|
|
136
|
+
Events.canEmitMetaEvents = true
|
|
156
137
|
await this.emitMetaEventInternal(eventName, eventArgs)
|
|
157
138
|
} finally {
|
|
158
|
-
canEmitMetaEvents = false
|
|
139
|
+
Events.canEmitMetaEvents = false
|
|
159
140
|
}
|
|
160
141
|
}
|
|
161
142
|
}
|
|
162
143
|
|
|
163
144
|
async emitSerial<TEventName extends keyof TEventData>(eventName: TEventName, eventArgs: TEventData[TEventName]) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (isMetaEvent(eventName) && !canEmitMetaEvents) {
|
|
145
|
+
if (isMetaEvent(eventName) && !Events.canEmitMetaEvents) {
|
|
167
146
|
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
|
|
168
147
|
}
|
|
169
148
|
|
|
149
|
+
const filterMatch = (args: TEventData[TEventName], filter: TEventData[TEventName]) => {
|
|
150
|
+
if (filter) {
|
|
151
|
+
switch (typeof filter) {
|
|
152
|
+
case 'object':
|
|
153
|
+
return Object.entries(args).reduce((prev, [key, value]) => ((filter as Record<PropertyKey, unknown>)[key] === value ? true : prev), false)
|
|
154
|
+
default:
|
|
155
|
+
return args === filter
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
170
161
|
this.logIfDebugEnabled('emitSerial', eventName, eventArgs)
|
|
171
162
|
|
|
172
163
|
const listeners = this.getListeners(eventName) ?? new Set()
|
|
164
|
+
const filteredListeners = [...listeners.values()]
|
|
165
|
+
.filter((value) => (value.filter ? filterMatch(eventArgs, value.filter as TEventData[TEventName]) : true))
|
|
166
|
+
.map((info) => info.listener)
|
|
173
167
|
const anyListeners = assertEx(Events.anyMap.get(this))
|
|
174
|
-
const staticListeners = [...
|
|
168
|
+
const staticListeners = [...filteredListeners]
|
|
175
169
|
const staticAnyListeners = [...anyListeners]
|
|
176
170
|
|
|
177
171
|
await resolvedPromise
|
|
178
172
|
|
|
179
173
|
for (const listener of staticListeners) {
|
|
180
|
-
|
|
181
|
-
await listener(eventArgs)
|
|
182
|
-
}
|
|
174
|
+
await this.safeCallListener(eventName, eventArgs, listener)
|
|
183
175
|
}
|
|
184
176
|
|
|
185
177
|
for (const listener of staticAnyListeners) {
|
|
186
|
-
|
|
187
|
-
await listener(eventName, eventArgs)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
getListeners(eventName: keyof TEventData) {
|
|
193
|
-
const events = assertEx(Events.eventsMap.get(this))
|
|
194
|
-
if (!events.has(eventName)) {
|
|
195
|
-
return
|
|
178
|
+
await this.safeCallAnyListener(eventName, eventArgs, listener)
|
|
196
179
|
}
|
|
197
|
-
|
|
198
|
-
return events.get(eventName)
|
|
199
180
|
}
|
|
200
181
|
|
|
201
|
-
|
|
182
|
+
//TODO: Make test for this
|
|
183
|
+
listenerCount(eventNames?: keyof TEventData | (keyof TEventData)[]) {
|
|
202
184
|
const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
|
|
203
185
|
let count = 0
|
|
204
186
|
|
|
@@ -209,10 +191,6 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
209
191
|
continue
|
|
210
192
|
}
|
|
211
193
|
|
|
212
|
-
if (typeof eventName !== 'undefined') {
|
|
213
|
-
assertEventName(eventName)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
194
|
count += assertEx(Events.anyMap.get(this)).size
|
|
217
195
|
|
|
218
196
|
for (const value of assertEx(Events.eventsMap.get(this)).values()) {
|
|
@@ -223,19 +201,19 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
223
201
|
return count
|
|
224
202
|
}
|
|
225
203
|
|
|
226
|
-
logIfDebugEnabled<TEventName extends EventName
|
|
204
|
+
logIfDebugEnabled<TEventName extends EventName>(type: string, eventName?: TEventName, eventArgs?: EventArgs) {
|
|
227
205
|
if (Events.isDebugEnabled || this.debug?.enabled) {
|
|
228
206
|
this.debug?.logger?.(type, this.debug.name, eventName, eventArgs)
|
|
229
207
|
}
|
|
230
208
|
}
|
|
231
209
|
|
|
232
|
-
off<TEventName extends keyof TEventData
|
|
233
|
-
|
|
234
|
-
|
|
210
|
+
off<TEventName extends keyof TEventData, TEventListener = EventListener<TEventData[TEventName]>>(
|
|
211
|
+
eventNames: TEventName | TEventName[],
|
|
212
|
+
listener: TEventListener,
|
|
213
|
+
) {
|
|
235
214
|
const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
|
|
236
215
|
for (const eventName of eventNamesArray) {
|
|
237
|
-
|
|
238
|
-
const set = this.getListeners(eventName) as Set<EventListener<TEventData[TEventName]>>
|
|
216
|
+
const set = this.getListeners(eventName) as Set<TEventListener>
|
|
239
217
|
if (set) {
|
|
240
218
|
set.delete(listener)
|
|
241
219
|
if (set.size === 0) {
|
|
@@ -253,8 +231,6 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
253
231
|
}
|
|
254
232
|
|
|
255
233
|
offAny(listener: EventAnyListener) {
|
|
256
|
-
assertListener(listener)
|
|
257
|
-
|
|
258
234
|
this.logIfDebugEnabled('unsubscribeAny', undefined, undefined)
|
|
259
235
|
|
|
260
236
|
const typedMap = Events.anyMap.get(this) as Set<EventAnyListener<TEventData[keyof TEventData]>>
|
|
@@ -262,12 +238,13 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
262
238
|
forget(this.emitMetaEvent('listenerRemoved', { listener: listener as EventAnyListener }))
|
|
263
239
|
}
|
|
264
240
|
|
|
265
|
-
on<TEventName extends keyof TEventData = keyof TEventData>(
|
|
266
|
-
|
|
267
|
-
|
|
241
|
+
on<TEventName extends keyof TEventData = keyof TEventData>(
|
|
242
|
+
eventNames: TEventName | TEventName[],
|
|
243
|
+
listener: EventListener<TEventData[TEventName]>,
|
|
244
|
+
filter?: TEventData[TEventName],
|
|
245
|
+
) {
|
|
268
246
|
const eventNamesArray = Array.isArray(eventNames) ? eventNames : [eventNames]
|
|
269
247
|
for (const eventName of eventNamesArray) {
|
|
270
|
-
assertEventName(eventName)
|
|
271
248
|
let set = this.getListeners(eventName)
|
|
272
249
|
if (!set) {
|
|
273
250
|
set = new Set()
|
|
@@ -275,7 +252,7 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
275
252
|
events?.set(eventName, set)
|
|
276
253
|
}
|
|
277
254
|
|
|
278
|
-
set.add(listener as EventListener)
|
|
255
|
+
set.add({ filter, listener: listener as EventListener })
|
|
279
256
|
|
|
280
257
|
this.logIfDebugEnabled('subscribe', eventName, undefined)
|
|
281
258
|
|
|
@@ -288,8 +265,6 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
288
265
|
}
|
|
289
266
|
|
|
290
267
|
onAny(listener: EventAnyListener) {
|
|
291
|
-
assertListener(listener)
|
|
292
|
-
|
|
293
268
|
this.logIfDebugEnabled('subscribeAny', undefined, undefined)
|
|
294
269
|
|
|
295
270
|
Events.anyMap.get(this)?.add(listener as EventAnyListener)
|
|
@@ -300,7 +275,7 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
300
275
|
once<TEventName extends keyof TEventData>(eventName: TEventName, listener: EventListener<TEventData[TEventName]>) {
|
|
301
276
|
const subListener = async (args: TEventData[TEventName]) => {
|
|
302
277
|
this.off(eventName, subListener)
|
|
303
|
-
await
|
|
278
|
+
await this.safeCallListener(eventName, args, listener)
|
|
304
279
|
}
|
|
305
280
|
this.on(eventName, subListener)
|
|
306
281
|
return this.off.bind(this, eventName, subListener as EventListener)
|
|
@@ -309,30 +284,28 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
309
284
|
private async emitInternal<TEventName extends keyof TEventData, TEventArgs extends TEventData[TEventName]>(
|
|
310
285
|
eventName: TEventName,
|
|
311
286
|
eventArgs: TEventArgs,
|
|
287
|
+
filter?: TEventArgs,
|
|
312
288
|
) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (isMetaEvent(eventName) && !canEmitMetaEvents) {
|
|
289
|
+
if (isMetaEvent(eventName) && !Events.canEmitMetaEvents) {
|
|
316
290
|
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
|
|
317
291
|
}
|
|
318
292
|
|
|
319
293
|
this.logIfDebugEnabled('emit', eventName, eventArgs)
|
|
320
294
|
|
|
321
295
|
const listeners = this.getListeners(eventName) ?? new Set()
|
|
296
|
+
const filteredListeners = [...listeners.values()].filter((value) => (filter ? value.listener : true)).map((info) => info.listener)
|
|
322
297
|
const anyListeners = assertEx(Events.anyMap.get(this))
|
|
323
|
-
const staticListeners = [...
|
|
298
|
+
const staticListeners = [...filteredListeners]
|
|
324
299
|
const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]
|
|
325
300
|
|
|
326
301
|
await resolvedPromise
|
|
327
302
|
await Promise.all([
|
|
328
303
|
...staticListeners.map(async (listener) => {
|
|
329
|
-
|
|
330
|
-
return await listener(eventArgs)
|
|
331
|
-
}
|
|
304
|
+
await this.safeCallListener(eventName, eventArgs, listener)
|
|
332
305
|
}),
|
|
333
306
|
...staticAnyListeners.map(async (listener) => {
|
|
334
307
|
if (anyListeners.has(listener)) {
|
|
335
|
-
|
|
308
|
+
await this.safeCallAnyListener(eventName, eventArgs, listener)
|
|
336
309
|
}
|
|
337
310
|
}),
|
|
338
311
|
])
|
|
@@ -342,31 +315,63 @@ export class Events<TEventData extends EventData = EventData> implements EventFu
|
|
|
342
315
|
eventName: TEventName,
|
|
343
316
|
eventArgs: MetaEventData<TEventData>[TEventName],
|
|
344
317
|
) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (isMetaEvent(eventName) && !canEmitMetaEvents) {
|
|
318
|
+
if (isMetaEvent(eventName) && !Events.canEmitMetaEvents) {
|
|
348
319
|
throw new TypeError('`eventName` cannot be meta event `listenerAdded` or `listenerRemoved`')
|
|
349
320
|
}
|
|
350
321
|
|
|
351
322
|
this.logIfDebugEnabled('emit', eventName, eventArgs)
|
|
352
323
|
|
|
353
324
|
const listeners = this.getListeners(eventName) ?? new Set()
|
|
325
|
+
const filteredListeners = [...listeners.values()].map((info) => info.listener)
|
|
354
326
|
const anyListeners = assertEx(Events.anyMap.get(this))
|
|
355
|
-
const staticListeners = [...
|
|
327
|
+
const staticListeners = [...filteredListeners]
|
|
356
328
|
const staticAnyListeners = isMetaEvent(eventName) ? [] : [...anyListeners]
|
|
357
329
|
|
|
358
330
|
await resolvedPromise
|
|
359
331
|
await Promise.all([
|
|
360
332
|
...staticListeners.map(async (listener) => {
|
|
361
|
-
|
|
362
|
-
return await listener(eventArgs)
|
|
363
|
-
}
|
|
333
|
+
await this.safeCallListener(eventName, eventArgs, listener)
|
|
364
334
|
}),
|
|
365
335
|
...staticAnyListeners.map(async (listener) => {
|
|
366
336
|
if (anyListeners.has(listener)) {
|
|
367
|
-
|
|
337
|
+
await this.safeCallAnyListener(eventName, eventArgs, listener)
|
|
368
338
|
}
|
|
369
339
|
}),
|
|
370
340
|
])
|
|
371
341
|
}
|
|
342
|
+
|
|
343
|
+
private getListeners<TEventName extends keyof TEventData>(eventName: TEventName) {
|
|
344
|
+
const events = assertEx(Events.eventsMap.get(this))
|
|
345
|
+
if (!events.has(eventName)) {
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return events.get(eventName)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async safeCallAnyListener<TEventData extends EventData, TEventName extends keyof EventData>(
|
|
353
|
+
eventName: TEventName,
|
|
354
|
+
eventArgs: TEventData[TEventName],
|
|
355
|
+
listener: EventAnyListener<TEventData[TEventName]>,
|
|
356
|
+
) {
|
|
357
|
+
try {
|
|
358
|
+
return await listener(eventName, eventArgs)
|
|
359
|
+
} catch (ex) {
|
|
360
|
+
const error = ex as Error
|
|
361
|
+
this.logger?.error(`Listener[${String(eventName)}] Excepted: ${error.message}`)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async safeCallListener<TEventData extends EventData, TEventName extends keyof EventData>(
|
|
366
|
+
eventName: TEventName,
|
|
367
|
+
eventArgs: TEventData[TEventName],
|
|
368
|
+
listener: EventListener<TEventData[TEventName]>,
|
|
369
|
+
) {
|
|
370
|
+
try {
|
|
371
|
+
return await listener(eventArgs)
|
|
372
|
+
} catch (ex) {
|
|
373
|
+
const error = ex as Error
|
|
374
|
+
this.logger?.error(`Listener[${String(eventName)}] Excepted: ${error.message}`)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
372
377
|
}
|