@data-fair/lib-node 2.8.1 → 2.9.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/test-spies.js CHANGED
@@ -1,41 +1,42 @@
1
- import { register } from 'node:module'
2
- import debugModule from 'debug'
3
- const debug = debugModule('test-spies')
4
- export function registerModuleHooks () {
5
- register('@data-fair/lib-node/test-module-hooks.js', { parentURL: import.meta.url })
1
+ import { register } from 'node:module';
2
+ import debugModule from 'debug';
3
+ const debug = debugModule('test-spies');
4
+ export function registerModuleHooks() {
5
+ register('@data-fair/lib-node/test-module-hooks.js', { parentURL: import.meta.url });
6
6
  }
7
- const { stackTraceLimit } = Error
8
- const pendingPromises = {}
9
- let i = 0
10
- export function waitFor (eventName, timeout = 2000) {
11
- i++
12
- const id = i
13
- debug(`Waiting for test spy ${eventName} (id=${id}, timeout: ${timeout}ms)`)
14
- // better stack traces
15
- // see https://github.com/axios/axios/issues/2387#issuecomment-652242713
16
- Error.stackTraceLimit = 0
17
- const timeoutError = new Error(`Timeout waiting for test spy ${eventName}`)
18
- Error.stackTraceLimit = stackTraceLimit
19
- Error.captureStackTrace(timeoutError, waitFor)
20
- const p = new Promise((resolve, reject) => {
21
- pendingPromises[eventName] = pendingPromises[eventName] ?? []
22
- pendingPromises[eventName].push({ resolve, id })
23
- setTimeout(() => reject(timeoutError), timeout)
24
- })
25
- p.finally(() => {
26
- debug(`Cleaning up test spy ${eventName} (id=${id})`)
27
- pendingPromises[eventName] = pendingPromises[eventName].filter(p => p.id !== id)
28
- if (pendingPromises[eventName].length === 0) { delete pendingPromises[eventName] }
29
- })
30
- return p
7
+ const { stackTraceLimit } = Error;
8
+ const pendingPromises = {};
9
+ let i = 0;
10
+ export function waitFor(eventName, timeout = 2000) {
11
+ i++;
12
+ const id = i;
13
+ debug(`Waiting for test spy ${eventName} (id=${id}, timeout: ${timeout}ms)`);
14
+ // better stack traces
15
+ // see https://github.com/axios/axios/issues/2387#issuecomment-652242713
16
+ Error.stackTraceLimit = 0;
17
+ const timeoutError = new Error(`Timeout waiting for test spy ${eventName}`);
18
+ Error.stackTraceLimit = stackTraceLimit;
19
+ Error.captureStackTrace(timeoutError, waitFor);
20
+ const p = new Promise((resolve, reject) => {
21
+ pendingPromises[eventName] = pendingPromises[eventName] ?? [];
22
+ pendingPromises[eventName].push({ resolve, id });
23
+ setTimeout(() => reject(timeoutError), timeout);
24
+ });
25
+ p.finally(() => {
26
+ debug(`Cleaning up test spy ${eventName} (id=${id})`);
27
+ pendingPromises[eventName] = pendingPromises[eventName].filter(p => p.id !== id);
28
+ if (pendingPromises[eventName].length === 0)
29
+ delete pendingPromises[eventName];
30
+ });
31
+ return p;
31
32
  }
32
- export function emit (eventName, data) {
33
- debug(`Emitting test spy ${eventName} (${pendingPromises[eventName]?.length} listeners)`)
34
- if (!pendingPromises[eventName]?.length) {
35
- console.debug(`No listener for test spy ${eventName}`)
36
- return
37
- }
38
- for (const p of pendingPromises[eventName]) {
39
- p.resolve(data)
40
- }
33
+ export function emit(eventName, data) {
34
+ debug(`Emitting test spy ${eventName} (${pendingPromises[eventName]?.length} listeners)`);
35
+ if (!pendingPromises[eventName]?.length) {
36
+ console.debug(`No listener for test spy ${eventName}`);
37
+ return;
38
+ }
39
+ for (const p of pendingPromises[eventName]) {
40
+ p.resolve(data);
41
+ }
41
42
  }
@@ -1,80 +1,86 @@
1
- import semver from 'semver'
2
- import path from 'path'
3
- import { readdirSync, readFileSync, statSync } from 'fs'
4
- import Debug from 'debug'
5
- const debug = Debug('upgrade')
1
+ import semver from 'semver';
2
+ import path from 'path';
3
+ import { readdirSync, readFileSync, statSync } from 'fs';
4
+ import Debug from 'debug';
5
+ const debug = Debug('upgrade');
6
6
  // chose the proper scripts to execute, then run them
7
7
  export default async function (db, locks, basePath = './', isFresh) {
8
- const ack = await locks.acquire('upgrade')
9
- if (!ack) {
10
- console.warn('upgrade scripts lock is already acquired, skip them')
11
- // IMPORTANT: this behaviour of running the process when the upgrade scripts are still running on another one
12
- // implies that they cannot be considered a pre-requisite..
13
- // if we want to consider the upgrade scripts as a pre-requisite we should implement a wait on all
14
- // containers for the scripts that are running in only 1 (while loop on "acquire" ?) and a healthcheck
15
- // are not considered "up" and the previous versions keep running in the mean time
16
- } else {
17
- try {
18
- await runScripts(db, basePath, isFresh)
19
- } finally {
20
- await locks.release('upgrade')
8
+ const ack = await locks.acquire('upgrade');
9
+ if (!ack) {
10
+ console.warn('upgrade scripts lock is already acquired, skip them');
11
+ // IMPORTANT: this behaviour of running the process when the upgrade scripts are still running on another one
12
+ // implies that they cannot be considered a pre-requisite..
13
+ // if we want to consider the upgrade scripts as a pre-requisite we should implement a wait on all
14
+ // containers for the scripts that are running in only 1 (while loop on "acquire" ?) and a healthcheck
15
+ // are not considered "up" and the previous versions keep running in the mean time
16
+ }
17
+ else {
18
+ try {
19
+ await runScripts(db, basePath, isFresh);
20
+ }
21
+ finally {
22
+ await locks.release('upgrade');
23
+ }
21
24
  }
22
- }
23
25
  }
24
- async function runScripts (db, basePath, isFresh) {
25
- const dir = path.resolve(basePath)
26
- const pjsonPath = path.join(dir, 'package.json')
27
- debug('read service info from ' + pjsonPath)
28
- const pjson = JSON.parse(readFileSync(pjsonPath, 'utf8'))
29
- const scriptsRoot = path.join(dir, 'upgrade')
30
- debug(`service=${pjson.name} version=${pjson.version}`)
31
- const services = db.collection('services')
32
- const service = await services.findOne({ id: pjson.name })
33
- let previousPersion = service?.version
34
- debug('list scripts from ' + scriptsRoot)
35
- let scripts = await listScripts(scriptsRoot)
36
- if (previousPersion) {
37
- debug(`current service version from database : ${previousPersion}`)
38
- } else {
39
- debug('no service version found in database, this is the first time the upgrade system is run')
40
- scripts = scripts.filter(scriptDef => scriptDef.version === 'init')
41
- if (isFresh) {
42
- if (await isFresh()) {
43
- debug('isFresh function returned true, this is a fresh install, do not run any script')
44
- } else {
45
- debug('isFresh function returned false, this is not a fresh install, run all init scripts')
46
- previousPersion = '0.0.0'
47
- }
48
- } else {
49
- debug('no isFresh function, it could be a fresh install, do not run any script')
26
+ async function runScripts(db, basePath, isFresh) {
27
+ const dir = path.resolve(basePath);
28
+ const pjsonPath = path.join(dir, 'package.json');
29
+ debug('read service info from ' + pjsonPath);
30
+ const pjson = JSON.parse(readFileSync(pjsonPath, 'utf8'));
31
+ const scriptsRoot = path.join(dir, 'upgrade');
32
+ debug(`service=${pjson.name} version=${pjson.version}`);
33
+ const services = db.collection('services');
34
+ const service = await services.findOne({ id: pjson.name });
35
+ let previousPersion = service?.version;
36
+ debug('list scripts from ' + scriptsRoot);
37
+ let scripts = await listScripts(scriptsRoot);
38
+ if (previousPersion) {
39
+ debug(`current service version from database : ${previousPersion}`);
50
40
  }
51
- }
52
- if (previousPersion) {
53
- for (const scriptDef of scripts) {
54
- if (semver.gte(scriptDef.version, previousPersion)) {
55
- for (const scriptName of scriptDef.names) {
56
- const script = (await import(path.join(scriptsRoot, scriptDef.version, scriptName))).default
57
- debug('apply script %s/%s : %s', scriptDef.version, scriptName, script.description)
58
- await script.exec(db, Debug(`upgrade:${scriptDef.version}:${scriptName}`))
41
+ else {
42
+ debug('no service version found in database, this is the first time the upgrade system is run');
43
+ scripts = scripts.filter(scriptDef => scriptDef.version === 'init');
44
+ if (isFresh) {
45
+ if (await isFresh()) {
46
+ debug('isFresh function returned true, this is a fresh install, do not run any script');
47
+ }
48
+ else {
49
+ debug('isFresh function returned false, this is not a fresh install, run all init scripts');
50
+ previousPersion = '0.0.0';
51
+ }
52
+ }
53
+ else {
54
+ debug('no isFresh function, it could be a fresh install, do not run any script');
59
55
  }
60
- }
61
56
  }
62
- }
63
- const coercedVersion = semver.coerce(pjson.version)
64
- if (!coercedVersion) { throw new Error(`Invalid version number in package.json ${pjson.version}`) }
65
- const newService = { id: pjson.name, version: coercedVersion.version }
66
- debug(`upgrade scripts are over, save current version number ${newService.version}`)
67
- await services.updateOne({ id: pjson.name }, { $set: newService }, { upsert: true })
57
+ if (previousPersion) {
58
+ for (const scriptDef of scripts) {
59
+ if (semver.gte(scriptDef.version, previousPersion)) {
60
+ for (const scriptName of scriptDef.names) {
61
+ const script = (await import(path.join(scriptsRoot, scriptDef.version, scriptName))).default;
62
+ debug('apply script %s/%s : %s', scriptDef.version, scriptName, script.description);
63
+ await script.exec(db, Debug(`upgrade:${scriptDef.version}:${scriptName}`));
64
+ }
65
+ }
66
+ }
67
+ }
68
+ const coercedVersion = semver.coerce(pjson.version);
69
+ if (!coercedVersion)
70
+ throw new Error(`Invalid version number in package.json ${pjson.version}`);
71
+ const newService = { id: pjson.name, version: coercedVersion.version };
72
+ debug(`upgrade scripts are over, save current version number ${newService.version}`);
73
+ await services.updateOne({ id: pjson.name }, { $set: newService }, { upsert: true });
68
74
  }
69
75
  // Walk the scripts directories
70
- async function listScripts (scriptsRoot) {
71
- const dirs = readdirSync(scriptsRoot)
72
- .filter(dir => statSync(path.join(scriptsRoot, dir)).isDirectory())
73
- .sort()
74
- return dirs.map(dir => {
75
- return {
76
- version: dir,
77
- names: readdirSync(path.join(scriptsRoot, dir)).sort()
78
- }
79
- })
76
+ async function listScripts(scriptsRoot) {
77
+ const dirs = readdirSync(scriptsRoot)
78
+ .filter(dir => statSync(path.join(scriptsRoot, dir)).isDirectory())
79
+ .sort();
80
+ return dirs.map(dir => {
81
+ return {
82
+ version: dir,
83
+ names: readdirSync(path.join(scriptsRoot, dir)).sort()
84
+ };
85
+ });
80
86
  }
package/validation.js CHANGED
@@ -1 +1 @@
1
- export * from '@data-fair/lib-validation'
1
+ export * from '@data-fair/lib-validation';
package/ws-client.js CHANGED
@@ -1,93 +1,97 @@
1
1
  // a wrapper around a websocket connection with auto reconnect
2
2
  // JSON parsing and channel subscriptions
3
- import EventEmitter from 'node:events'
4
- import WebSocket from 'ws'
5
- import Debug from 'debug'
6
- const debug = Debug('ws')
3
+ import EventEmitter from 'node:events';
4
+ import WebSocket from 'ws';
5
+ import Debug from 'debug';
6
+ const debug = Debug('ws');
7
7
  export class WsClient extends EventEmitter {
8
- _channels
9
- _ws
10
- opts
11
- constructor (opts) {
12
- super()
13
- this._channels = []
14
- this.opts = { log: console, ...opts }
15
- }
16
-
17
- async _connect () {
18
- return await new Promise((resolve, reject) => {
19
- const wsUrl = this.opts.url.replace('http://', 'ws://').replace('https://', 'wss://') + '/'
20
- debug(`connect Web Socket to ${wsUrl}`)
21
- const ws = new WebSocket(wsUrl, { headers: this.opts.headers ?? {} })
22
- this._ws = ws
23
- ws.on('error', (err) => {
24
- debug('WS encountered an error', err.message)
25
- this._reconnect()
26
- reject(err)
27
- })
28
- ws.once('open', () => {
29
- debug('WS is opened')
30
- resolve(ws)
31
- })
32
- ws.on('message', (msg) => {
33
- const message = JSON.parse(msg)
34
- debug('received message', message)
35
- this.emit('message', message)
36
- })
37
- })
38
- }
39
-
40
- async _reconnect () {
41
- if (!this._ws) { return }
42
- debug('reconnect')
43
- this._ws.terminate()
44
- await this._connect()
45
- for (const channel of this._channels) {
46
- await this.subscribe(channel, true)
8
+ _channels;
9
+ _ws;
10
+ opts;
11
+ constructor(opts) {
12
+ super();
13
+ this._channels = [];
14
+ this.opts = { log: console, ...opts };
47
15
  }
48
- }
49
-
50
- async subscribe (channel, force = false, timeout = 2000) {
51
- if (this._channels.includes(channel) && !force) { return }
52
- const ws = this._ws ?? await this._connect()
53
- debug('subscribe to channel', channel)
54
- const subscribeMessage = { type: 'subscribe', channel }
55
- if (this.opts.apiKey) { subscribeMessage.apiKey = this.opts.apiKey }
56
- if (this.opts.adminMode && this.opts.account) { subscribeMessage.account = JSON.stringify(this.opts.account) }
57
- ws.send(JSON.stringify(subscribeMessage))
58
- const event = await this.waitFor(channel, (e) => {
59
- return e.type === 'subscribe-confirm' || e.type === 'error'
60
- }, timeout, true, true)
61
- if (event.type === 'error') { throw new Error(event.data) }
62
- if (this._channels.includes(channel)) { this._channels.push(channel) }
63
- }
64
-
65
- async waitFor (channel, filter, timeout = 300000, skipSubscribe = false, fullMessage = false) {
66
- if (!skipSubscribe) { await this.subscribe(channel) }
67
- return await new Promise((resolve, reject) => {
68
- const _timeout = setTimeout(() => { reject(new Error('timeout')) }, timeout)
69
- const messageCb = (message) => {
70
- if (message.channel === channel && (!filter || filter(fullMessage ? message : message.data))) {
71
- clearTimeout(_timeout)
72
- this.off('message', messageCb)
73
- resolve(fullMessage ? message : message.data)
16
+ async _connect() {
17
+ return await new Promise((resolve, reject) => {
18
+ const wsUrl = this.opts.url.replace('http://', 'ws://').replace('https://', 'wss://') + '/';
19
+ debug(`connect Web Socket to ${wsUrl}`);
20
+ const ws = new WebSocket(wsUrl, { headers: this.opts.headers ?? {} });
21
+ this._ws = ws;
22
+ ws.on('error', (err) => {
23
+ debug('WS encountered an error', err.message);
24
+ this._reconnect();
25
+ reject(err);
26
+ });
27
+ ws.once('open', () => {
28
+ debug('WS is opened');
29
+ resolve(ws);
30
+ });
31
+ ws.on('message', (msg) => {
32
+ const message = JSON.parse(msg);
33
+ debug('received message', message);
34
+ this.emit('message', message);
35
+ });
36
+ });
37
+ }
38
+ async _reconnect() {
39
+ if (!this._ws)
40
+ return;
41
+ debug('reconnect');
42
+ this._ws.terminate();
43
+ await this._connect();
44
+ for (const channel of this._channels) {
45
+ await this.subscribe(channel, true);
74
46
  }
75
- }
76
- this.on('message', messageCb)
77
- })
78
- }
79
-
80
- close () {
81
- if (this._ws) { this._ws.terminate() }
82
- }
47
+ }
48
+ async subscribe(channel, force = false, timeout = 2000) {
49
+ if (this._channels.includes(channel) && !force)
50
+ return;
51
+ const ws = this._ws ?? await this._connect();
52
+ debug('subscribe to channel', channel);
53
+ const subscribeMessage = { type: 'subscribe', channel };
54
+ if (this.opts.apiKey)
55
+ subscribeMessage.apiKey = this.opts.apiKey;
56
+ if (this.opts.adminMode && this.opts.account)
57
+ subscribeMessage.account = JSON.stringify(this.opts.account);
58
+ ws.send(JSON.stringify(subscribeMessage));
59
+ const event = await this.waitFor(channel, (e) => {
60
+ return e.type === 'subscribe-confirm' || e.type === 'error';
61
+ }, timeout, true, true);
62
+ if (event.type === 'error')
63
+ throw new Error(event.data);
64
+ if (this._channels.includes(channel))
65
+ this._channels.push(channel);
66
+ }
67
+ async waitFor(channel, filter, timeout = 300000, skipSubscribe = false, fullMessage = false) {
68
+ if (!skipSubscribe)
69
+ await this.subscribe(channel);
70
+ return await new Promise((resolve, reject) => {
71
+ const _timeout = setTimeout(() => { reject(new Error('timeout')); }, timeout);
72
+ const messageCb = (message) => {
73
+ if (message.channel === channel && (!filter || filter(fullMessage ? message : message.data))) {
74
+ clearTimeout(_timeout);
75
+ this.off('message', messageCb);
76
+ resolve(fullMessage ? message : message.data);
77
+ }
78
+ };
79
+ this.on('message', messageCb);
80
+ });
81
+ }
82
+ close() {
83
+ if (this._ws)
84
+ this._ws.terminate();
85
+ }
83
86
  }
84
87
  export class DataFairWsClient extends WsClient {
85
- async waitForJournal (datasetId, eventType, timeout = 300000) {
86
- await this.opts.log.info(`wait for event "${eventType}" on dataset "${datasetId}"`)
87
- const event = await this.waitFor(`datasets/${datasetId}/journal`, (e) => {
88
- return e.type === eventType || e.type === 'error'
89
- }, timeout)
90
- if (event.type === 'error') { throw new Error(event.data) }
91
- return event
92
- }
88
+ async waitForJournal(datasetId, eventType, timeout = 300000) {
89
+ await this.opts.log.info(`wait for event "${eventType}" on dataset "${datasetId}"`);
90
+ const event = await this.waitFor(`datasets/${datasetId}/journal`, (e) => {
91
+ return e.type === eventType || e.type === 'error';
92
+ }, timeout);
93
+ if (event.type === 'error')
94
+ throw new Error(event.data);
95
+ return event;
96
+ }
93
97
  }
package/ws-emitter.js CHANGED
@@ -1,16 +1,18 @@
1
1
  // write messages to a mongodb collection/queue for publication by ws-server.js and consumption by ws.js
2
2
  // return a capped mongodb collection that acts as a simple queue
3
3
  export const initMessagesCollection = async (db) => {
4
- const collection = (await db.listCollections({ name: 'ws-messages' }).toArray())[0]
5
- if (!collection) { await db.createCollection('ws-messages', { capped: true, size: 100000, max: 1000 }) }
6
- return db.collection('ws-messages')
7
- }
8
- let messagesCollection
4
+ const collection = (await db.listCollections({ name: 'ws-messages' }).toArray())[0];
5
+ if (!collection)
6
+ await db.createCollection('ws-messages', { capped: true, size: 100000, max: 1000 });
7
+ return db.collection('ws-messages');
8
+ };
9
+ let messagesCollection;
9
10
  export const init = async (db) => {
10
- messagesCollection = await initMessagesCollection(db)
11
- await messagesCollection.insertOne({ type: 'init' })
12
- }
11
+ messagesCollection = await initMessagesCollection(db);
12
+ await messagesCollection.insertOne({ type: 'init' });
13
+ };
13
14
  export const emit = async (channel, data) => {
14
- if (!messagesCollection) { throw new Error('wsEmitter.init was not called before sending data') }
15
- await messagesCollection.insertOne({ type: 'message', channel, data, date: new Date().toISOString() })
16
- }
15
+ if (!messagesCollection)
16
+ throw new Error('wsEmitter.init was not called before sending data');
17
+ await messagesCollection.insertOne({ type: 'message', channel, data, date: new Date().toISOString() });
18
+ };