pg-boss 10.3.3 → 11.0.1

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/src/timekeeper.js CHANGED
@@ -9,7 +9,14 @@ const QUEUES = {
9
9
 
10
10
  const EVENTS = {
11
11
  error: 'error',
12
- schedule: 'schedule'
12
+ schedule: 'schedule',
13
+ warning: 'warning'
14
+ }
15
+
16
+ const WARNINGS = {
17
+ CLOCK_SKEW: {
18
+ message: 'Warning: Clock skew between this instance and the database server. This will not break scheduling, but is emitted any time the skew exceeds 60 seconds.'
19
+ }
13
20
  }
14
21
 
15
22
  class Timekeeper extends EventEmitter {
@@ -19,19 +26,9 @@ class Timekeeper extends EventEmitter {
19
26
  this.db = db
20
27
  this.config = config
21
28
  this.manager = config.manager
22
- this.skewMonitorIntervalMs = config.clockMonitorIntervalSeconds * 1000
23
- this.cronMonitorIntervalMs = config.cronMonitorIntervalSeconds * 1000
24
29
  this.clockSkew = 0
25
-
26
30
  this.events = EVENTS
27
31
 
28
- this.getTimeCommand = plans.getTime(config.schema)
29
- this.getQueueCommand = plans.getQueueByName(config.schema)
30
- this.getSchedulesCommand = plans.getSchedules(config.schema)
31
- this.scheduleCommand = plans.schedule(config.schema)
32
- this.unscheduleCommand = plans.unschedule(config.schema)
33
- this.trySetCronTimeCommand = plans.trySetCronTime(config.schema)
34
-
35
32
  this.functions = [
36
33
  this.schedule,
37
34
  this.unschedule,
@@ -42,30 +39,22 @@ class Timekeeper extends EventEmitter {
42
39
  }
43
40
 
44
41
  async start () {
45
- // setting the archive config too low breaks the cron 60s debounce interval so don't even try
46
- if (this.config.archiveSeconds < 60 || this.config.archiveFailedSeconds < 60) {
47
- return
48
- }
49
-
50
42
  this.stopped = false
51
43
 
52
44
  await this.cacheClockSkew()
53
-
54
- try {
55
- await this.manager.createQueue(QUEUES.SEND_IT)
56
- } catch {}
45
+ await this.manager.createQueue(QUEUES.SEND_IT)
57
46
 
58
47
  const options = {
59
48
  pollingIntervalSeconds: this.config.cronWorkerIntervalSeconds,
60
49
  batchSize: 50
61
50
  }
62
51
 
63
- await this.manager.work(QUEUES.SEND_IT, options, async (jobs) => { await this.manager.insert(jobs.map(i => i.data)) })
52
+ await this.manager.work(QUEUES.SEND_IT, options, (jobs) => this.onSendIt(jobs))
64
53
 
65
54
  setImmediate(() => this.onCron())
66
55
 
67
- this.cronMonitorInterval = setInterval(async () => await this.onCron(), this.cronMonitorIntervalMs)
68
- this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
56
+ this.cronMonitorInterval = setInterval(async () => await this.onCron(), this.config.cronMonitorIntervalSeconds * 1000)
57
+ this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.config.clockMonitorIntervalSeconds * 1000)
69
58
  }
70
59
 
71
60
  async stop () {
@@ -96,7 +85,7 @@ class Timekeeper extends EventEmitter {
96
85
  throw new Error(this.config.__test__force_clock_monitoring_error)
97
86
  }
98
87
 
99
- const { rows } = await this.db.executeSql(this.getTimeCommand)
88
+ const { rows } = await this.db.executeSql(plans.getTime())
100
89
 
101
90
  const local = Date.now()
102
91
 
@@ -107,7 +96,7 @@ class Timekeeper extends EventEmitter {
107
96
  const skewSeconds = Math.abs(skew) / 1000
108
97
 
109
98
  if (skewSeconds >= 60 || this.config.__test__force_clock_skew_warning) {
110
- Attorney.warnClockSkew(`Instance clock is ${skewSeconds}s ${skew > 0 ? 'slower' : 'faster'} than database.`)
99
+ this.emit(this.events.warning, { message: WARNINGS.CLOCK_SKEW.message, data: { seconds: skewSeconds, direction: skew > 0 ? 'slower' : 'faster' } })
111
100
  }
112
101
  } catch (err) {
113
102
  this.emit(this.events.error, err)
@@ -126,10 +115,14 @@ class Timekeeper extends EventEmitter {
126
115
 
127
116
  this.timekeeping = true
128
117
 
129
- const { rows } = await this.db.executeSql(this.trySetCronTimeCommand, [this.config.cronMonitorIntervalSeconds])
118
+ const sql = plans.trySetCronTime(this.config.schema, this.config.cronMonitorIntervalSeconds)
119
+
120
+ if (!this.stopped) {
121
+ const { rows } = await this.db.executeSql(sql)
130
122
 
131
- if (rows.length === 1 && !this.stopped) {
132
- await this.cron()
123
+ if (!this.stopped && rows.length === 1) {
124
+ await this.cron()
125
+ }
133
126
  }
134
127
  } catch (err) {
135
128
  this.emit(this.events.error, err)
@@ -143,11 +136,10 @@ class Timekeeper extends EventEmitter {
143
136
 
144
137
  const scheduled = schedules
145
138
  .filter(i => this.shouldSendIt(i.cron, i.timezone))
146
- .map(({ name, data, options }) =>
147
- ({ name: QUEUES.SEND_IT, data: { name, data, ...options }, singletonKey: name, singletonSeconds: 60 }))
139
+ .map(({ name, data, options }) => ({ data: { name, data, options }, singletonKey: name, singletonSeconds: 60 }))
148
140
 
149
141
  if (scheduled.length > 0 && !this.stopped) {
150
- await this.manager.insert(scheduled)
142
+ await this.manager.insert(QUEUES.SEND_IT, scheduled)
151
143
  }
152
144
  }
153
145
 
@@ -163,22 +155,35 @@ class Timekeeper extends EventEmitter {
163
155
  return prevDiff < 60
164
156
  }
165
157
 
166
- async getSchedules () {
167
- const { rows } = await this.db.executeSql(this.getSchedulesCommand)
158
+ async onSendIt (jobs) {
159
+ await Promise.allSettled(jobs.map(({ data }) => this.manager.send(data)))
160
+ }
161
+
162
+ async getSchedules (name, key = '') {
163
+ let sql = plans.getSchedules(this.config.schema)
164
+ let params = []
165
+
166
+ if (name) {
167
+ sql = plans.getSchedulesByQueue(this.config.schema)
168
+ params = [name, key]
169
+ }
170
+
171
+ const { rows } = await this.db.executeSql(sql, params)
172
+
168
173
  return rows
169
174
  }
170
175
 
171
176
  async schedule (name, cron, data, options = {}) {
172
- const { tz = 'UTC' } = options
177
+ const { tz = 'UTC', key = '', ...rest } = options
173
178
 
174
179
  cronParser.parseExpression(cron, { tz })
175
180
 
176
- Attorney.checkSendArgs([name, data, options], this.config)
177
-
178
- const values = [name, cron, tz, data, options]
181
+ Attorney.checkSendArgs([name, data, { ...rest }])
182
+ Attorney.assertKey(key)
179
183
 
180
184
  try {
181
- await this.db.executeSql(this.scheduleCommand, values)
185
+ const sql = plans.schedule(this.config.schema)
186
+ await this.db.executeSql(sql, [name, key, cron, tz, data, options])
182
187
  } catch (err) {
183
188
  if (err.message.includes('foreign key')) {
184
189
  err.message = `Queue ${name} not found`
@@ -188,8 +193,9 @@ class Timekeeper extends EventEmitter {
188
193
  }
189
194
  }
190
195
 
191
- async unschedule (name) {
192
- await this.db.executeSql(this.unscheduleCommand, [name])
196
+ async unschedule (name, key = '') {
197
+ const sql = plans.unschedule(this.config.schema)
198
+ await this.db.executeSql(sql, [name, key])
193
199
  }
194
200
  }
195
201
 
package/src/tools.js CHANGED
@@ -1,9 +1,11 @@
1
+ const { setTimeout } = require('node:timers/promises')
2
+
1
3
  module.exports = {
2
- delay
4
+ delay,
5
+ resolveWithinSeconds
3
6
  }
4
7
 
5
8
  function delay (ms, error) {
6
- const { setTimeout } = require('node:timers/promises')
7
9
  const ac = new AbortController()
8
10
 
9
11
  const promise = new Promise((resolve, reject) => {
@@ -26,3 +28,18 @@ function delay (ms, error) {
26
28
 
27
29
  return promise
28
30
  }
31
+
32
+ async function resolveWithinSeconds (promise, seconds, message) {
33
+ const timeout = Math.max(1, seconds) * 1000
34
+ const reject = delay(timeout, message)
35
+
36
+ let result
37
+
38
+ try {
39
+ result = await Promise.race([promise, reject])
40
+ } finally {
41
+ reject.abort()
42
+ }
43
+
44
+ return result
45
+ }
package/types.d.ts CHANGED
@@ -17,6 +17,7 @@ declare namespace PgBoss {
17
17
  singleton: 'singleton',
18
18
  stately: 'stately'
19
19
  }
20
+
20
21
  interface Db {
21
22
  executeSql(text: string, values: any[]): Promise<{ rows: any[] }>;
22
23
  }
@@ -35,76 +36,43 @@ declare namespace PgBoss {
35
36
  db?: Db;
36
37
  }
37
38
 
38
- interface QueueOptions {
39
- monitorStateIntervalSeconds?: number;
40
- monitorStateIntervalMinutes?: number;
41
- }
42
-
43
39
  interface SchedulingOptions {
44
40
  schedule?: boolean;
45
-
46
41
  clockMonitorIntervalSeconds?: number;
47
- clockMonitorIntervalMinutes?: number;
48
-
49
- cronMonitorIntervalSeconds?: number;
50
42
  cronWorkerIntervalSeconds?: number;
43
+ cronMonitorIntervalSeconds?: number;
51
44
  }
52
45
 
53
46
  interface MaintenanceOptions {
54
47
  supervise?: boolean;
55
48
  migrate?: boolean;
56
-
57
- deleteAfterSeconds?: number;
58
- deleteAfterMinutes?: number;
59
- deleteAfterHours?: number;
60
- deleteAfterDays?: number;
61
-
49
+ warningSlowQuerySeconds?: number;
50
+ warningQueueSize?: number;
51
+ superviseIntervalSeconds?: number;
62
52
  maintenanceIntervalSeconds?: number;
63
- maintenanceIntervalMinutes?: number;
64
-
65
- archiveCompletedAfterSeconds?: number;
66
- archiveFailedAfterSeconds?: number;
53
+ queueCacheIntervalSeconds?: number;
54
+ monitorIntervalSeconds?: number;
67
55
  }
68
56
 
69
- type ConstructorOptions =
70
- DatabaseOptions
71
- & QueueOptions
72
- & SchedulingOptions
73
- & MaintenanceOptions
74
- & ExpirationOptions
75
- & RetentionOptions
76
- & RetryOptions
77
- & JobPollingOptions
78
-
79
- interface ExpirationOptions {
80
- expireInSeconds?: number;
81
- expireInMinutes?: number;
82
- expireInHours?: number;
83
- }
57
+ type ConstructorOptions = DatabaseOptions & SchedulingOptions & MaintenanceOptions
84
58
 
85
- interface RetentionOptions {
59
+ interface QueueOptions {
60
+ expireInSeconds?: number;
86
61
  retentionSeconds?: number;
87
- retentionMinutes?: number;
88
- retentionHours?: number;
89
- retentionDays?: number;
90
- }
91
-
92
- interface RetryOptions {
62
+ deleteAfterSeconds?: number;
93
63
  retryLimit?: number;
94
64
  retryDelay?: number;
95
65
  retryBackoff?: boolean;
66
+ retryDelayMax?: number;
96
67
  }
97
68
 
98
69
  interface JobOptions {
99
- id?: string,
70
+ id?: string;
100
71
  priority?: number;
101
72
  startAfter?: number | string | Date;
102
73
  singletonKey?: string;
103
74
  singletonSeconds?: number;
104
- singletonMinutes?: number;
105
- singletonHours?: number;
106
75
  singletonNextSlot?: boolean;
107
- deadLetter?: string;
108
76
  }
109
77
 
110
78
  interface ConnectionOptions {
@@ -113,13 +81,29 @@ declare namespace PgBoss {
113
81
 
114
82
  type InsertOptions = ConnectionOptions;
115
83
 
116
- type SendOptions = JobOptions & ExpirationOptions & RetentionOptions & RetryOptions & ConnectionOptions;
84
+ type SendOptions = JobOptions & QueueOptions & ConnectionOptions;
117
85
 
118
86
  type QueuePolicy = 'standard' | 'short' | 'singleton' | 'stately'
119
87
 
120
- type Queue = RetryOptions & ExpirationOptions & RetentionOptions & { name: string, policy?: QueuePolicy, deadLetter?: string }
121
- type QueueResult = Queue & { createdOn: Date, updatedOn: Date }
122
- type ScheduleOptions = SendOptions & { tz?: string }
88
+ type Queue = {
89
+ name: string;
90
+ policy?: QueuePolicy;
91
+ partition?: boolean;
92
+ deadLetter?: string;
93
+ warningQueueSize?: number;
94
+ } & QueueOptions
95
+
96
+ type QueueResult = Queue & {
97
+ deferredCount: number;
98
+ queuedCount: number;
99
+ activeCount: number;
100
+ completedCount: number;
101
+ table: number;
102
+ createdOn: Date;
103
+ updatedOn: Date;
104
+ }
105
+
106
+ type ScheduleOptions = SendOptions & { tz?: string, key?: string }
123
107
 
124
108
  interface JobPollingOptions {
125
109
  pollingIntervalSeconds?: number;
@@ -151,25 +135,11 @@ declare namespace PgBoss {
151
135
 
152
136
  interface Schedule {
153
137
  name: string;
138
+ key: string;
154
139
  cron: string;
140
+ timezone: string;
155
141
  data?: object;
156
- options?: ScheduleOptions;
157
- }
158
-
159
- // source (for now): https://github.com/bendrucker/postgres-interval/blob/master/index.d.ts
160
- interface PostgresInterval {
161
- years?: number;
162
- months?: number;
163
- days?: number;
164
- hours?: number;
165
- minutes?: number;
166
- seconds?: number;
167
- milliseconds?: number;
168
-
169
- toPostgres(): string;
170
-
171
- toISO(): string;
172
- toISOString(): string;
142
+ options?: SendOptions;
173
143
  }
174
144
 
175
145
  interface Job<T = object> {
@@ -186,69 +156,58 @@ declare namespace PgBoss {
186
156
  retryCount: number;
187
157
  retryDelay: number;
188
158
  retryBackoff: boolean;
159
+ retryDelayMax?: number;
189
160
  startAfter: Date;
190
161
  startedOn: Date;
191
162
  singletonKey: string | null;
192
163
  singletonOn: Date | null;
193
- expireIn: PostgresInterval;
164
+ expireInSeconds: number;
165
+ deleteAfterSeconds: number;
194
166
  createdOn: Date;
195
167
  completedOn: Date | null;
196
168
  keepUntil: Date;
197
- deadLetter: string,
198
- policy: QueuePolicy,
199
- output: object
169
+ policy: QueuePolicy;
170
+ deadLetter: string;
171
+ output: object;
200
172
  }
201
173
 
202
174
  interface JobInsert<T = object> {
203
- id?: string,
175
+ id?: string;
204
176
  name: string;
205
177
  data?: T;
206
178
  priority?: number;
207
179
  retryLimit?: number;
208
180
  retryDelay?: number;
209
181
  retryBackoff?: boolean;
182
+ retryDelayMax?: number;
210
183
  startAfter?: Date | string;
211
184
  singletonKey?: string;
212
185
  singletonSeconds?: number;
213
186
  expireInSeconds?: number;
187
+ deleteAfterSeconds: number;
214
188
  keepUntil?: Date | string;
215
- deadLetter?: string;
216
- }
217
-
218
- interface MonitorState {
219
- all: number;
220
- created: number;
221
- retry: number;
222
- active: number;
223
- completed: number;
224
- cancelled: number;
225
- failed: number;
226
- }
227
-
228
- interface MonitorStates extends MonitorState {
229
- queues: { [queueName: string]: MonitorState };
230
189
  }
231
190
 
232
191
  interface Worker {
233
- id: string,
234
- name: string,
235
- options: WorkOptions,
236
- state: 'created' | 'active' | 'stopping' | 'stopped'
237
- count: number,
238
- createdOn: Date,
239
- lastFetchedOn: Date,
240
- lastJobStartedOn: Date,
241
- lastJobEndedOn: Date,
242
- lastJobDuration: number,
243
- lastError: object,
244
- lastErrorOn: Date
192
+ id: string;
193
+ name: string;
194
+ options: WorkOptions;
195
+ state: 'created' | 'active' | 'stopping' | 'stopped';
196
+ count: number;
197
+ createdOn: Date;
198
+ lastFetchedOn: Date;
199
+ lastJobStartedOn: Date;
200
+ lastJobEndedOn: Date;
201
+ lastJobDuration: number;
202
+ lastError: object;
203
+ lastErrorOn: Date;
245
204
  }
246
205
 
247
206
  interface StopOptions {
248
- close?: boolean,
249
- graceful?: boolean,
250
- timeout?: number,
251
- wait?: boolean
207
+ close?: boolean;
208
+ graceful?: boolean;
209
+ timeout?: number;
210
+ wait?: boolean;
252
211
  }
253
212
 
254
213
  interface OffWorkOptions {
@@ -261,16 +220,9 @@ declare class PgBoss extends EventEmitter {
261
220
  constructor(connectionString: string);
262
221
  constructor(options: PgBoss.ConstructorOptions);
263
222
 
264
- static getConstructionPlans(schema: string): string;
265
- static getConstructionPlans(): string;
266
-
267
- static getMigrationPlans(schema: string, version: string): string;
268
- static getMigrationPlans(schema: string): string;
269
- static getMigrationPlans(): string;
270
-
271
- static getRollbackPlans(schema: string, version: string): string;
272
- static getRollbackPlans(schema: string): string;
273
- static getRollbackPlans(): string;
223
+ static getConstructionPlans(schema?: string): string;
224
+ static getMigrationPlans(schema?: string, version?: string): string;
225
+ static getRollbackPlans(schema?: string, version?: string): string;
274
226
 
275
227
  static states: PgBoss.JobStates
276
228
  static policies: PgBoss.QueuePolicies
@@ -278,18 +230,12 @@ declare class PgBoss extends EventEmitter {
278
230
  on(event: "error", handler: (error: Error) => void): this;
279
231
  off(event: "error", handler: (error: Error) => void): this;
280
232
 
281
- on(event: "maintenance", handler: () => void): this;
282
- off(event: "maintenance", handler: () => void): this;
283
-
284
- on(event: "monitor-states", handler: (monitorStates: PgBoss.MonitorStates) => void): this;
285
- off(event: "monitor-states", handler: (monitorStates: PgBoss.MonitorStates) => void): this;
233
+ on(event: "warning", handler: (warning: { message: string, data: object }) => void): this;
234
+ off(event: "warning", handler: (warning: { message: string, data: object }) => void): this;
286
235
 
287
236
  on(event: "wip", handler: (data: PgBoss.Worker[]) => void): this;
288
237
  off(event: "wip", handler: (data: PgBoss.Worker[]) => void): this;
289
238
 
290
- on(event: "stopped", handler: () => void): this;
291
- off(event: "stopped", handler: () => void): this;
292
-
293
239
  start(): Promise<PgBoss>;
294
240
  stop(options?: PgBoss.StopOptions): Promise<void>;
295
241
 
@@ -301,14 +247,11 @@ declare class PgBoss extends EventEmitter {
301
247
  sendAfter(name: string, data: object, options: PgBoss.SendOptions, dateString: string): Promise<string | null>;
302
248
  sendAfter(name: string, data: object, options: PgBoss.SendOptions, seconds: number): Promise<string | null>;
303
249
 
304
- sendThrottled(name: string, data: object, options: PgBoss.SendOptions, seconds: number): Promise<string | null>;
305
- sendThrottled(name: string, data: object, options: PgBoss.SendOptions, seconds: number, key: string): Promise<string | null>;
306
-
307
- sendDebounced(name: string, data: object, options: PgBoss.SendOptions, seconds: number): Promise<string | null>;
308
- sendDebounced(name: string, data: object, options: PgBoss.SendOptions, seconds: number, key: string): Promise<string | null>;
250
+ sendThrottled(name: string, data: object, options: PgBoss.SendOptions, seconds: number, key?: string): Promise<string | null>;
251
+ sendDebounced(name: string, data: object, options: PgBoss.SendOptions, seconds: number, key?: string): Promise<string | null>;
309
252
 
310
- insert(jobs: PgBoss.JobInsert[]): Promise<void>;
311
- insert(jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
253
+ insert(name: string, jobs: PgBoss.JobInsert[]): Promise<void>;
254
+ insert(name: string, jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
312
255
 
313
256
  fetch<T>(name: string): Promise<PgBoss.Job<T>[]>;
314
257
  fetch<T>(name: string, options: PgBoss.FetchOptions & { includeMetadata: true }): Promise<PgBoss.JobWithMetadata<T>[]>;
@@ -340,6 +283,9 @@ declare class PgBoss extends EventEmitter {
340
283
 
341
284
  deleteJob(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
342
285
  deleteJob(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
286
+ deleteQueuedJobs(name: string): Promise<void>;
287
+ deleteStoredJobs(name: string): Promise<void>;
288
+ deleteAllJobs(name: string): Promise<void>;
343
289
 
344
290
  complete(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
345
291
  complete(name: string, id: string, data: object, options?: PgBoss.ConnectionOptions): Promise<void>;
@@ -349,27 +295,22 @@ declare class PgBoss extends EventEmitter {
349
295
  fail(name: string, id: string, data: object, options?: PgBoss.ConnectionOptions): Promise<void>;
350
296
  fail(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
351
297
 
352
- getJobById<T>(name: string, id: string, options?: PgBoss.ConnectionOptions & { includeArchive: boolean }): Promise<PgBoss.JobWithMetadata<T> | null>;
298
+ getJobById<T>(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<PgBoss.JobWithMetadata<T> | null>;
353
299
 
354
300
  createQueue(name: string, options?: PgBoss.Queue): Promise<void>;
355
301
  updateQueue(name: string, options?: PgBoss.Queue): Promise<void>;
356
302
  deleteQueue(name: string): Promise<void>;
357
- purgeQueue(name: string): Promise<void>;
358
303
  getQueues(): Promise<PgBoss.QueueResult[]>;
359
304
  getQueue(name: string): Promise<PgBoss.QueueResult | null>;
360
- getQueueSize(name: string, options?: { before: 'retry' | 'active' | 'completed' | 'cancelled' | 'failed' }): Promise<number>;
305
+ getQueueStats(name: string): Promise<number>;
361
306
 
362
- clearStorage(): Promise<void>;
363
- archive(): Promise<void>;
364
- purge(): Promise<void>;
365
- expire(): Promise<void>;
366
- maintain(): Promise<void>;
307
+ supervise(name?: string): Promise<void>;
367
308
  isInstalled(): Promise<Boolean>;
368
309
  schemaVersion(): Promise<Number>;
369
310
 
370
311
  schedule(name: string, cron: string, data?: object, options?: PgBoss.ScheduleOptions): Promise<void>;
371
- unschedule(name: string): Promise<void>;
372
- getSchedules(): Promise<PgBoss.Schedule[]>;
312
+ unschedule(name: string, key?: string): Promise<void>;
313
+ getSchedules(name?: string, key?: string): Promise<PgBoss.Schedule[]>;
373
314
 
374
315
  getDb(): PgBoss.Db;
375
316
  }
package/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "schema": 24
2
+ "schema": 25
3
3
  }