sails-hook-quest 0.0.4 → 0.0.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,9 +1,22 @@
1
+ // @ts-check
2
+
1
3
  /**
2
4
  * core/job-control.js
3
5
  *
4
6
  * Functions for controlling job lifecycle (start, stop, pause, resume)
5
7
  */
6
8
 
9
+ const { getGlobalSails } = require('./global-sails')
10
+
11
+ /** @typedef {import('../types').AnyRecord} AnyRecord */
12
+ /** @typedef {import('../types').QuestExecutionResult} QuestExecutionResult */
13
+ /** @typedef {import('../types').QuestJobNameInput} QuestJobNameInput */
14
+ /** @typedef {import('../types').QuestJobsMap} QuestJobsMap */
15
+ /** @typedef {import('../types').QuestRunContext} QuestRunContext */
16
+ /** @typedef {import('../types').QuestScheduleContext} QuestScheduleContext */
17
+ /** @typedef {import('../types').QuestStartContext} QuestStartContext */
18
+ /** @typedef {import('../types').QuestStopContext} QuestStopContext */
19
+
7
20
  // setTimeout uses a 32-bit signed integer internally.
8
21
  // Delays larger than this overflow and fire immediately (~1ms), causing infinite loops.
9
22
  const MAX_SAFE_TIMEOUT = 2_147_483_647 // 2^31 - 1, ~24.8 days
@@ -11,31 +24,60 @@ const MAX_SAFE_TIMEOUT = 2_147_483_647 // 2^31 - 1, ~24.8 days
11
24
  /**
12
25
  * Schedule a job to run at its next scheduled time
13
26
  * @param {String} name - Job name
14
- * @param {Object} context - Context with jobs, timers maps and helper functions
27
+ * @param {QuestScheduleContext} [context] - Context with jobs, timers maps and helper functions
28
+ * @param {Date} [targetTime] - Original target for a long-delay intermediate timer
15
29
  */
16
- function scheduleJob(name, context = {}) {
17
- const {
18
- jobs = new Map(),
19
- timers = new Map(),
20
- getNextRunTime,
21
- executeJob
22
- } = context
30
+ function scheduleJob(name, context = {}, targetTime) {
31
+ const { jobs = new Map(), timers = new Map() } = context
32
+ const getNextRunTime =
33
+ /** @type {NonNullable<QuestScheduleContext['getNextRunTime']>} */ (
34
+ context.getNextRunTime
35
+ )
36
+ const executeJob =
37
+ /** @type {NonNullable<QuestScheduleContext['executeJob']>} */ (
38
+ context.executeJob
39
+ )
23
40
 
24
41
  const job = jobs.get(name)
25
42
  if (!job) {
26
- if (global.sails) {
43
+ const sails = getGlobalSails()
44
+ if (sails) {
27
45
  sails.log.warn(`Job "${name}" not found in jobs Map`)
28
46
  }
29
47
  return
30
48
  }
31
49
 
32
50
  // Clear any existing timer
33
- stopJob(name, { timers })
51
+ stopJob(name, context)
34
52
 
35
53
  // Get the next run time
36
- const nextRun = getNextRunTime(job)
54
+ const states = context.scheduleStates
55
+ const update = (registration) =>
56
+ states?.set(name, {
57
+ validation: 'not_checked',
58
+ validationErrors: [],
59
+ reason: null,
60
+ ...states.get(name),
61
+ registration
62
+ })
63
+ if (states && !targetTime)
64
+ states.set(name, {
65
+ validation: 'not_checked',
66
+ validationErrors: [],
67
+ reason: null,
68
+ lastAttemptAt: new Date().toISOString(),
69
+ registration: 'not_registered'
70
+ })
71
+ let nextRun
72
+ try {
73
+ nextRun = targetTime || getNextRunTime(job)
74
+ } catch (err) {
75
+ update('failed')
76
+ throw err
77
+ }
37
78
  if (!nextRun) {
38
- if (global.sails) {
79
+ const sails = getGlobalSails()
80
+ if (sails) {
39
81
  sails.log.warn(
40
82
  `Job "${name}" has no valid schedule (interval: ${job.interval}, cron: ${job.cron}, timeout: ${job.timeout})`
41
83
  )
@@ -43,12 +85,32 @@ function scheduleJob(name, context = {}) {
43
85
  return
44
86
  }
45
87
 
88
+ if (!Number.isFinite(nextRun.getTime())) {
89
+ states?.set(name, {
90
+ ...states.get(name),
91
+ registration: 'failed',
92
+ validation: 'invalid',
93
+ reason: null,
94
+ validationErrors: [
95
+ {
96
+ code: 'E_SCHEDULE_TARGET',
97
+ message: 'Schedule target must be a finite date.'
98
+ }
99
+ ]
100
+ })
101
+ return
102
+ }
46
103
  const delay = nextRun.getTime() - Date.now()
104
+ context.dueTimes?.set(name, nextRun)
47
105
 
48
106
  // If delay is negative (past time), run immediately
49
107
  if (delay <= 0) {
50
- executeJob(name).catch((err) => {
51
- if (global.sails) {
108
+ timers.delete(name)
109
+ context.dueTimes?.delete(name)
110
+ update('consumed')
111
+ executeJob(name, undefined, 'scheduled').catch((err) => {
112
+ const sails = getGlobalSails()
113
+ if (sails) {
52
114
  sails.log.error(`Error running job "${name}":`, err)
53
115
  }
54
116
  })
@@ -64,12 +126,14 @@ function scheduleJob(name, context = {}) {
64
126
  // that rechecks when we're closer to the target time
65
127
  if (delay > MAX_SAFE_TIMEOUT) {
66
128
  const timer = setTimeout(() => {
67
- scheduleJob(name, context)
129
+ scheduleJob(name, context, nextRun)
68
130
  }, MAX_SAFE_TIMEOUT)
69
131
 
70
132
  timers.set(name, timer)
133
+ update('registered')
71
134
 
72
- if (global.sails) {
135
+ const sails = getGlobalSails()
136
+ if (sails) {
73
137
  sails.log.verbose(
74
138
  `Job "${name}" scheduled for ${nextRun.toISOString()} (delay exceeds 24.8d, re-checking later)`
75
139
  )
@@ -79,8 +143,12 @@ function scheduleJob(name, context = {}) {
79
143
 
80
144
  // Set timer for the next execution
81
145
  const timer = setTimeout(() => {
82
- executeJob(name).catch((err) => {
83
- if (global.sails) {
146
+ timers.delete(name)
147
+ context.dueTimes?.delete(name)
148
+ update('consumed')
149
+ executeJob(name, undefined, 'scheduled').catch((err) => {
150
+ const sails = getGlobalSails()
151
+ if (sails) {
84
152
  sails.log.error(`Error running job "${name}":`, err)
85
153
  }
86
154
  })
@@ -92,8 +160,10 @@ function scheduleJob(name, context = {}) {
92
160
  }, delay)
93
161
 
94
162
  timers.set(name, timer)
163
+ update('registered')
95
164
 
96
- if (global.sails) {
165
+ const sails = getGlobalSails()
166
+ if (sails) {
97
167
  sails.log.verbose(`Job "${name}" scheduled for ${nextRun.toISOString()}`)
98
168
  }
99
169
  }
@@ -101,17 +171,31 @@ function scheduleJob(name, context = {}) {
101
171
  /**
102
172
  * Stop a single job
103
173
  * @param {String} name - Job name
104
- * @param {Object} context - Context with timers map
174
+ * @param {QuestStopContext} [context] - Context with timers map
105
175
  */
106
176
  function stopJob(name, context = {}) {
107
177
  const { timers = new Map() } = context
108
178
 
179
+ context.dueTimes?.delete(name)
180
+ if (
181
+ context.scheduleStates &&
182
+ (context.scheduleStates.has(name) || context.jobs?.has(name))
183
+ )
184
+ context.scheduleStates.set(name, {
185
+ validation: 'not_checked',
186
+ validationErrors: [],
187
+ reason: null,
188
+ lastAttemptAt: null,
189
+ ...context.scheduleStates.get(name),
190
+ registration: 'stopped'
191
+ })
109
192
  const timer = timers.get(name)
110
193
  if (timer) {
111
194
  clearTimeout(timer)
112
195
  clearInterval(timer)
113
196
  timers.delete(name)
114
- if (global.sails) {
197
+ const sails = getGlobalSails()
198
+ if (sails) {
115
199
  sails.log.verbose(`Job "${name}" stopped`)
116
200
  }
117
201
  }
@@ -119,11 +203,16 @@ function stopJob(name, context = {}) {
119
203
 
120
204
  /**
121
205
  * Start scheduling jobs
122
- * @param {String|Array} jobNames - Job names to start (optional)
123
- * @param {Object} context - Context with jobs map and scheduleJob function
206
+ * @param {QuestJobNameInput} jobNames - Job names to start (optional)
207
+ * @param {QuestStartContext} [context] - Context with jobs map and scheduleJob function
208
+ * @returns {Promise<void>}
124
209
  */
125
210
  async function startJobs(jobNames, context = {}) {
126
- const { jobs = new Map(), scheduleJob } = context
211
+ const { jobs = new Map() } = context
212
+ const scheduleJob =
213
+ /** @type {NonNullable<QuestStartContext['scheduleJob']>} */ (
214
+ context.scheduleJob
215
+ )
127
216
 
128
217
  const names = !jobNames
129
218
  ? Array.from(jobs.keys())
@@ -138,8 +227,9 @@ async function startJobs(jobNames, context = {}) {
138
227
 
139
228
  /**
140
229
  * Stop scheduling jobs
141
- * @param {String|Array} jobNames - Job names to stop (optional)
142
- * @param {Object} context - Context with jobs and timers maps
230
+ * @param {QuestJobNameInput} jobNames - Job names to stop (optional)
231
+ * @param {QuestStopContext} [context] - Context with jobs and timers maps
232
+ * @returns {void}
143
233
  */
144
234
  function stopJobs(jobNames, context = {}) {
145
235
  const { jobs = new Map(), timers = new Map() } = context
@@ -151,18 +241,22 @@ function stopJobs(jobNames, context = {}) {
151
241
  : [jobNames]
152
242
 
153
243
  for (const name of names) {
154
- stopJob(name, { timers })
244
+ stopJob(name, context)
155
245
  }
156
246
  }
157
247
 
158
248
  /**
159
249
  * Run jobs immediately
160
- * @param {String|Array} jobNames - Job names to run
161
- * @param {Object} inputs - Custom inputs
162
- * @param {Object} context - Context with executeJob function
250
+ * @param {QuestJobNameInput} jobNames - Job names to run
251
+ * @param {AnyRecord} [inputs] - Custom inputs
252
+ * @param {QuestRunContext} [context] - Context with executeJob function
253
+ * @returns {Promise<QuestExecutionResult[]>}
163
254
  */
164
255
  async function runJobs(jobNames, inputs, context = {}) {
165
- const { jobs = new Map(), executeJob } = context
256
+ const { jobs = new Map() } = context
257
+ const executeJob = /** @type {NonNullable<QuestRunContext['executeJob']>} */ (
258
+ context.executeJob
259
+ )
166
260
 
167
261
  const names = !jobNames
168
262
  ? Array.from(jobs.keys())
@@ -170,14 +264,15 @@ async function runJobs(jobNames, inputs, context = {}) {
170
264
  ? jobNames
171
265
  : [jobNames]
172
266
 
173
- const promises = names.map((name) => executeJob(name, inputs))
267
+ const promises = names.map((name) => executeJob(name, inputs, 'manual'))
174
268
  return Promise.all(promises)
175
269
  }
176
270
 
177
271
  /**
178
272
  * Pause a job
179
273
  * @param {String} name - Job name
180
- * @param {Map} jobs - Jobs map
274
+ * @param {QuestJobsMap} [jobs] - Jobs map
275
+ * @returns {boolean}
181
276
  */
182
277
  function pauseJob(name, jobs = new Map()) {
183
278
  const job = jobs.get(name)
@@ -191,7 +286,8 @@ function pauseJob(name, jobs = new Map()) {
191
286
  /**
192
287
  * Resume a job
193
288
  * @param {String} name - Job name
194
- * @param {Map} jobs - Jobs map
289
+ * @param {QuestJobsMap} [jobs] - Jobs map
290
+ * @returns {boolean}
195
291
  */
196
292
  function resumeJob(name, jobs = new Map()) {
197
293
  const job = jobs.get(name)
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  /**
2
4
  * core/loader.js
3
5
  *
@@ -6,13 +8,24 @@
6
8
 
7
9
  const path = require('path')
8
10
  const includeAll = require('include-all')
11
+ const { getGlobalSails } = require('./global-sails')
12
+
13
+ /** @typedef {import('../types').AnyRecord} AnyRecord */
14
+ /** @typedef {import('../types').QuestConfig} QuestConfig */
15
+ /** @typedef {import('../types').QuestJob} QuestJob */
16
+ /** @typedef {import('../types').QuestJobDefinition} QuestJobDefinition */
17
+ /** @typedef {import('../types').QuestJobsMap} QuestJobsMap */
18
+ /** @typedef {import('../types').QuestMachineInputs} QuestMachineInputs */
19
+ /** @typedef {import('../types').QuestScriptDefinition} QuestScriptDefinition */
20
+ /** @typedef {import('../types').QuestTimersMap} QuestTimersMap */
9
21
 
10
22
  /**
11
23
  * Extract default values from a script's inputs schema
12
- * @param {Object} inputs - Script's inputs definition (Sails machine format)
13
- * @returns {Object} Object with input names and their defaultsTo values
24
+ * @param {QuestMachineInputs | null | undefined} inputs - Script's inputs definition (Sails machine format)
25
+ * @returns {AnyRecord} Object with input names and their defaultsTo values
14
26
  */
15
27
  function extractScriptInputDefaults(inputs) {
28
+ /** @type {AnyRecord} */
16
29
  const defaults = {}
17
30
  if (!inputs || typeof inputs !== 'object') return defaults
18
31
 
@@ -26,9 +39,9 @@ function extractScriptInputDefaults(inputs) {
26
39
 
27
40
  /**
28
41
  * Load jobs from scripts directory and config
29
- * @param {Object} config - Quest configuration
30
- * @param {Map} jobs - Jobs map to populate
31
- * @returns {Promise<Map>} Populated jobs map
42
+ * @param {QuestConfig} config - Quest configuration
43
+ * @param {QuestJobsMap} [jobs] - Jobs map to populate
44
+ * @returns {Promise<QuestJobsMap>} Populated jobs map
32
45
  */
33
46
  async function loadJobs(config, jobs = new Map()) {
34
47
  // First, load scripts from the scripts directory
@@ -36,17 +49,21 @@ async function loadJobs(config, jobs = new Map()) {
36
49
  const appPath = config.appPath || process.cwd()
37
50
  const fullPath = path.resolve(appPath, scriptsDir)
38
51
 
52
+ /** @type {Record<string, QuestScriptDefinition>} */
39
53
  let scripts = {}
40
54
 
41
55
  try {
42
- scripts = includeAll({
43
- dirname: fullPath,
44
- filter: /(.+)\.js$/,
45
- excludeDirs: /^\.(git|svn)$/,
46
- flatten: true
47
- })
56
+ scripts = /** @type {Record<string, QuestScriptDefinition>} */ (
57
+ includeAll({
58
+ dirname: fullPath,
59
+ filter: /(.+)\.js$/,
60
+ excludeDirs: /^\.(git|svn)$/,
61
+ flatten: true
62
+ })
63
+ )
48
64
  } catch (e) {
49
- if (global.sails) {
65
+ const sails = getGlobalSails()
66
+ if (sails) {
50
67
  sails.log.verbose('No scripts directory found, skipping script jobs')
51
68
  }
52
69
  }
@@ -57,22 +74,28 @@ async function loadJobs(config, jobs = new Map()) {
57
74
 
58
75
  for (const jobDef of config.jobs) {
59
76
  // Handle string shorthand (just job name)
60
- const job = typeof jobDef === 'string' ? { name: jobDef } : jobDef
77
+ /** @type {QuestJobDefinition} */
78
+ const job = typeof jobDef === 'string' ? { name: jobDef } : { ...jobDef }
79
+ const jobName = job.name
61
80
 
62
81
  // Check for duplicate job names in config
63
- if (configJobNames.has(job.name)) {
82
+ if (jobName && configJobNames.has(jobName)) {
64
83
  throw new Error(
65
- `Duplicate job name "${job.name}" in config/quest.js. Each job must have a unique name.`
84
+ `Duplicate job name "${jobName}" in config/quest.js. Each job must have a unique name.`
66
85
  )
67
86
  }
68
- configJobNames.add(job.name)
87
+ if (jobName) {
88
+ configJobNames.add(jobName)
89
+ }
69
90
 
70
91
  // Try to get script inputs if the script exists
71
- const scriptDef = scripts[job.name]
92
+ const scriptDef = jobName ? scripts[job.script || jobName] : undefined
72
93
  if (scriptDef && !job.scriptInputs) {
73
94
  job.scriptInputs = extractScriptInputDefaults(scriptDef.inputs)
74
95
  }
75
96
 
97
+ job.script = job.script || jobName
98
+ job.inputSchema = scriptDef ? scriptDef.inputs || {} : undefined
76
99
  addJobDefinition(job, jobs, config)
77
100
  }
78
101
  }
@@ -92,15 +115,18 @@ async function loadJobs(config, jobs = new Map()) {
92
115
  const existingJob = jobs.get(jobName)
93
116
 
94
117
  // Merge: config as base, script quest config takes priority
118
+ /** @type {QuestJobDefinition} */
95
119
  const jobDef = {
96
120
  name: jobName,
97
121
  friendlyName: scriptDef.friendlyName,
98
122
  description: scriptDef.description,
99
123
  // Preserve config's withoutOverlapping if script doesn't specify
100
124
  withoutOverlapping: existingJob?.withoutOverlapping,
101
- inputs: { ...existingJob?.inputs, ...questConfig.inputs },
125
+ inputs: { ...(existingJob?.inputs || {}), ...(questConfig.inputs || {}) },
102
126
  ...questConfig,
103
- scriptInputs
127
+ scriptInputs,
128
+ script: scriptName,
129
+ inputSchema: scriptDef.inputs || {}
104
130
  }
105
131
 
106
132
  addJobDefinition(jobDef, jobs, config)
@@ -111,23 +137,27 @@ async function loadJobs(config, jobs = new Map()) {
111
137
 
112
138
  /**
113
139
  * Add a job definition to the jobs Map
114
- * @param {Object} jobDef - Job definition
115
- * @param {Map} jobs - Jobs map
116
- * @param {Object} config - Quest configuration
117
- * @returns {Object} Normalized job
140
+ * @param {QuestJobDefinition} jobDef - Job definition
141
+ * @param {QuestJobsMap} [jobs] - Jobs map
142
+ * @param {QuestConfig} [config] - Quest configuration
143
+ * @returns {QuestJob | null} Normalized job
118
144
  */
119
145
  function addJobDefinition(jobDef, jobs = new Map(), config = {}) {
120
146
  const name = jobDef.name
121
147
  if (!name) {
122
- if (global.sails) {
148
+ const sails = getGlobalSails()
149
+ if (sails) {
123
150
  sails.log.warn('Job definition missing name, skipping:', jobDef)
124
151
  }
125
152
  return null
126
153
  }
127
154
 
128
155
  // Parse and normalize the job definition
156
+ /** @type {QuestJob} */
129
157
  const job = {
130
158
  name,
159
+ script: jobDef.script || name,
160
+ inputSchema: jobDef.inputSchema,
131
161
  friendlyName: jobDef.friendlyName || name,
132
162
  description: jobDef.description,
133
163
 
@@ -151,7 +181,8 @@ function addJobDefinition(jobDef, jobs = new Map(), config = {}) {
151
181
 
152
182
  jobs.set(name, job)
153
183
 
154
- if (global.sails) {
184
+ const sails = getGlobalSails()
185
+ if (sails) {
155
186
  const schedule = {}
156
187
  if (job.interval !== undefined) schedule.interval = job.interval
157
188
  if (job.cron !== undefined) schedule.cron = job.cron
@@ -165,8 +196,8 @@ function addJobDefinition(jobDef, jobs = new Map(), config = {}) {
165
196
  /**
166
197
  * Remove a job from the jobs map
167
198
  * @param {String} name - Job name
168
- * @param {Map} jobs - Jobs map
169
- * @param {Map} timers - Timers map
199
+ * @param {QuestJobsMap} [jobs] - Jobs map
200
+ * @param {QuestTimersMap} [timers] - Timers map
170
201
  * @returns {Boolean} Success
171
202
  */
172
203
  function removeJob(name, jobs = new Map(), timers = new Map()) {
@@ -181,7 +212,8 @@ function removeJob(name, jobs = new Map(), timers = new Map()) {
181
212
  // Remove from jobs map
182
213
  const existed = jobs.delete(name)
183
214
 
184
- if (existed && global.sails) {
215
+ const sails = getGlobalSails()
216
+ if (existed && sails) {
185
217
  sails.log.verbose(`Job "${name}" removed`)
186
218
  }
187
219
 
@@ -0,0 +1,91 @@
1
+ // @ts-check
2
+
3
+ const DEFAULT_RESULT_BYTES = 64 * 1024
4
+ const MAX_RESULT_BYTES = 1024 * 1024
5
+
6
+ /** @param {number} [value] */
7
+ function byteLimit(value) {
8
+ return Number.isFinite(value) && value > 0
9
+ ? Math.min(MAX_RESULT_BYTES, Math.floor(value))
10
+ : DEFAULT_RESULT_BYTES
11
+ }
12
+
13
+ /**
14
+ * Encode plain JSON without executing getters/toJSON or allocating an unbounded
15
+ * intermediate JSON string. Depth/node limits bound even tiny nested values.
16
+ * @param {any} value
17
+ * @param {number} [requestedBytes]
18
+ * @returns {{status: string, value?: any, json?: string, exit?: string}}
19
+ */
20
+ function encodeResult(value, requestedBytes) {
21
+ if (value === undefined) return { status: 'undefined' }
22
+ const limit = byteLimit(requestedBytes)
23
+ const parts = []
24
+ const ancestors = new Set()
25
+ let bytes = 0
26
+ let nodes = 0
27
+ function append(text) {
28
+ bytes += Buffer.byteLength(text)
29
+ if (bytes > limit) throw new RangeError('too_large')
30
+ parts.push(text)
31
+ }
32
+ function visit(item, depth) {
33
+ if (++nodes > 10000 || depth > 64) throw new RangeError('too_large')
34
+ if (item === null) return append('null')
35
+ if (typeof item === 'boolean') return append(String(item))
36
+ if (typeof item === 'number' && Number.isFinite(item))
37
+ return append(String(item))
38
+ if (typeof item === 'string') {
39
+ if (
40
+ item.length > limit - bytes ||
41
+ Buffer.byteLength(item) > limit - bytes
42
+ )
43
+ throw new RangeError('too_large')
44
+ return append(JSON.stringify(item))
45
+ }
46
+ if (typeof item !== 'object') throw new TypeError('non_json')
47
+ if (ancestors.has(item)) throw new TypeError('circular')
48
+ const array = Array.isArray(item)
49
+ if (
50
+ !array &&
51
+ Object.getPrototypeOf(item) !== Object.prototype &&
52
+ Object.getPrototypeOf(item) !== null
53
+ ) {
54
+ throw new TypeError('non_plain_object')
55
+ }
56
+ ancestors.add(item)
57
+ append(array ? '[' : '{')
58
+ const keys = array ? null : Object.keys(item)
59
+ const count = array ? item.length : keys.length
60
+ if (count > 10000) throw new RangeError('too_large')
61
+ for (let i = 0; i < count; i++) {
62
+ if (i) append(',')
63
+ const key = array ? String(i) : keys[i]
64
+ if (!array) {
65
+ if (
66
+ key.length > limit - bytes ||
67
+ Buffer.byteLength(key) > limit - bytes
68
+ )
69
+ throw new RangeError('too_large')
70
+ append(JSON.stringify(key) + ':')
71
+ }
72
+ const descriptor = Object.getOwnPropertyDescriptor(item, key)
73
+ if (!descriptor || !('value' in descriptor))
74
+ throw new TypeError('non_json_property')
75
+ visit(descriptor.value, depth + 1)
76
+ }
77
+ append(array ? ']' : '}')
78
+ ancestors.delete(item)
79
+ }
80
+ try {
81
+ visit(value, 0)
82
+ const json = parts.join('')
83
+ return { status: 'available', value: JSON.parse(json), json }
84
+ } catch (error) {
85
+ return {
86
+ status: error instanceof RangeError ? 'too_large' : 'serialization_error'
87
+ }
88
+ }
89
+ }
90
+
91
+ module.exports = { encodeResult, byteLimit }