@data-fair/lib-node 0.2.0 → 1.0.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/axios-auth.js CHANGED
@@ -1,34 +1,29 @@
1
1
  // build axios instances with sessions on a simple-directory instance
2
2
  // used for integration testing
3
- import { Agent } from 'node:http';
4
- import { axiosBuilder, axiosInstance } from './axios.js';
5
- export async function axiosAuth(opts) {
6
- const body = { email: opts.email, password: opts.password };
7
- if (opts.org)
8
- body.org = opts.org;
9
- if (opts.dep)
10
- body.dep = opts.dep;
11
- if (opts.adminMode)
12
- body.adminMode = opts.adminMode;
13
- const axiosOpts = {
14
- httpAgent: new Agent({ keepAlive: false }),
15
- ...opts.axiosOpts
16
- };
17
- const directoryUrl = opts.directoryUrl ?? 'http://localhost:8080';
18
- let callbackUrl = (await axiosInstance.post(directoryUrl + '/api/auth/password', body, { params: { redirect: directoryUrl }, maxRedirects: 0 })).data;
19
- if (callbackUrl.startsWith(directoryUrl + '/simple-directory')) {
20
- callbackUrl = callbackUrl.replace(directoryUrl + '/simple-directory', directoryUrl);
21
- }
22
- try {
23
- await axiosInstance.get(callbackUrl, { maxRedirects: 0 });
24
- }
25
- catch (err) {
26
- if (err.status !== 302)
27
- throw err;
28
- axiosOpts.headers = axiosOpts.headers ?? {};
29
- axiosOpts.headers.common = axiosOpts.headers.common ?? {};
30
- axiosOpts.headers.common.Cookie = err.headers['set-cookie'].map((s) => s.split(';')[0]).join(';');
31
- }
32
- return axiosBuilder(axiosOpts);
3
+ import { Agent } from 'node:http'
4
+ import { axiosBuilder, axiosInstance } from './axios.js'
5
+ export async function axiosAuth (opts) {
6
+ const body = { email: opts.email, password: opts.password }
7
+ if (opts.org) { body.org = opts.org }
8
+ if (opts.dep) { body.dep = opts.dep }
9
+ if (opts.adminMode) { body.adminMode = opts.adminMode }
10
+ const axiosOpts = {
11
+ httpAgent: new Agent({ keepAlive: false }),
12
+ ...opts.axiosOpts
13
+ }
14
+ const directoryUrl = opts.directoryUrl ?? 'http://localhost:8080'
15
+ let callbackUrl = (await axiosInstance.post(directoryUrl + '/api/auth/password', body, { params: { redirect: directoryUrl }, maxRedirects: 0 })).data
16
+ if (callbackUrl.startsWith(directoryUrl + '/simple-directory')) {
17
+ callbackUrl = callbackUrl.replace(directoryUrl + '/simple-directory', directoryUrl)
18
+ }
19
+ try {
20
+ await axiosInstance.get(callbackUrl, { maxRedirects: 0 })
21
+ } catch (err) {
22
+ if (err.status !== 302) { throw err }
23
+ axiosOpts.headers = axiosOpts.headers ?? {}
24
+ axiosOpts.headers.common = axiosOpts.headers.common ?? {}
25
+ axiosOpts.headers.common.Cookie = err.headers['set-cookie'].map((s) => s.split(';')[0]).join(';')
26
+ }
27
+ return axiosBuilder(axiosOpts)
33
28
  }
34
- export default axiosAuth;
29
+ export default axiosAuth
package/axios.js CHANGED
@@ -1,53 +1,50 @@
1
1
  // prepare an axios instance with improved error management and some performance tuning for nodejs usage
2
- import axios from 'axios';
3
- import { httpAgent, httpsAgent } from './http-agents.js';
4
- const { stackTraceLimit } = Error;
2
+ import axios from 'axios'
3
+ import { httpAgent, httpsAgent } from './http-agents.js'
4
+ const { stackTraceLimit } = Error
5
5
  class AxiosRequestError extends Error {
6
- constructor() {
7
- super();
8
- this.name = 'AxiosRequestError';
9
- }
6
+ constructor () {
7
+ super()
8
+ this.name = 'AxiosRequestError'
9
+ }
10
10
  }
11
11
  const requestInterceptor = (config) => {
12
- // better stack traces
13
- // see https://github.com/axios/axios/issues/2387#issuecomment-652242713
14
- Error.stackTraceLimit = 0;
15
- const errorContext = new AxiosRequestError();
16
- Error.stackTraceLimit = stackTraceLimit;
17
- Error.captureStackTrace(errorContext, requestInterceptor);
18
- // @ts-ignore
19
- config.errorContext = errorContext;
20
- return config;
21
- };
22
- export function axiosBuilder(opts = {}) {
23
- const ax = axios.create({
24
- httpAgent,
25
- httpsAgent,
26
- ...opts
27
- });
28
- ax.interceptors.request.use(requestInterceptor);
29
- // shorter stack traces
30
- ax.interceptors.response.use(response => response, error => {
31
- if (!error.response) {
32
- if (error.config?.errorContext?.stack)
33
- error.stack += '\nRequest context:\n' + error.config?.errorContext?.stack;
34
- return Promise.reject(error);
35
- }
36
- delete error.response.request;
37
- error.response.config = { method: error.response.config.method, url: error.response.config.url, data: error.response.config.data };
38
- if (error.response.config.data && error.response.config.data._writableState)
39
- delete error.response.config.data;
40
- if (error.response.data && error.response.data._readableState)
41
- delete error.response.data;
42
- let messageText = error.response.statusText;
43
- if (error.response.data) {
44
- messageText = typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data);
45
- }
46
- error.response.message = `${error.response.status} - ${messageText}`;
47
- Object.assign(error.config.errorContext, error.response);
48
- return Promise.reject(error.config.errorContext);
49
- });
50
- return ax;
12
+ // better stack traces
13
+ // see https://github.com/axios/axios/issues/2387#issuecomment-652242713
14
+ Error.stackTraceLimit = 0
15
+ const errorContext = new AxiosRequestError()
16
+ Error.stackTraceLimit = stackTraceLimit
17
+ Error.captureStackTrace(errorContext, requestInterceptor)
18
+ // @ts-ignore
19
+ config.errorContext = errorContext
20
+ return config
21
+ }
22
+ export function axiosBuilder (opts = {}) {
23
+ const ax = axios.create({
24
+ httpAgent,
25
+ httpsAgent,
26
+ ...opts
27
+ })
28
+ ax.interceptors.request.use(requestInterceptor)
29
+ // shorter stack traces
30
+ ax.interceptors.response.use(response => response, error => {
31
+ if (!error.response) {
32
+ if (error.config?.errorContext?.stack) { error.stack += '\nRequest context:\n' + error.config?.errorContext?.stack }
33
+ return Promise.reject(error)
34
+ }
35
+ delete error.response.request
36
+ error.response.config = { method: error.response.config.method, url: error.response.config.url, data: error.response.config.data }
37
+ if (error.response.config.data && error.response.config.data._writableState) { delete error.response.config.data }
38
+ if (error.response.data && error.response.data._readableState) { delete error.response.data }
39
+ let messageText = error.response.statusText
40
+ if (error.response.data) {
41
+ messageText = typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data)
42
+ }
43
+ error.response.message = `${error.response.status} - ${messageText}`
44
+ Object.assign(error.config.errorContext, error.response)
45
+ return Promise.reject(error.config.errorContext)
46
+ })
47
+ return ax
51
48
  }
52
- export const axiosInstance = axiosBuilder();
53
- export default axiosInstance;
49
+ export const axiosInstance = axiosBuilder()
50
+ export default axiosInstance
package/http-agents.js CHANGED
@@ -1,55 +1,55 @@
1
1
  // use better DNS lookup than nodejs default and try to reduce number of socket openings
2
- import CacheableLookup from 'cacheable-lookup';
3
- import HttpAgent, { HttpsAgent } from 'agentkeepalive';
4
- import { Counter, Gauge } from 'prom-client';
2
+ import CacheableLookup from 'cacheable-lookup'
3
+ import HttpAgent, { HttpsAgent } from 'agentkeepalive'
4
+ import { Counter, Gauge } from 'prom-client'
5
5
  // not very high number of sockets but we don't want to saturate our reverse proxies
6
6
  // if a higher number is needed the service should probably be scaled anyway
7
7
  const keepaliveOpts = {
8
- maxSockets: 8,
9
- maxFreeSockets: 8,
10
- timeout: 60000,
11
- freeSocketTimeout: 30000
12
- };
13
- export const httpAgent = new HttpAgent(keepaliveOpts);
14
- export const httpsAgent = new HttpsAgent(keepaliveOpts);
15
- const cacheableLookup = new CacheableLookup();
16
- cacheableLookup.install(httpAgent);
17
- cacheableLookup.install(httpsAgent);
8
+ maxSockets: 8,
9
+ maxFreeSockets: 8,
10
+ timeout: 60000,
11
+ freeSocketTimeout: 30000
12
+ }
13
+ export const httpAgent = new HttpAgent(keepaliveOpts)
14
+ export const httpsAgent = new HttpsAgent(keepaliveOpts)
15
+ const cacheableLookup = new CacheableLookup()
16
+ cacheableLookup.install(httpAgent)
17
+ cacheableLookup.install(httpsAgent)
18
18
  // monitor agent statuses
19
- const agents = { http: httpAgent, https: httpsAgent };
19
+ const agents = { http: httpAgent, https: httpsAgent }
20
20
  const socketsGauge = new Gauge({
21
- name: 'df_http_agent_sockets',
22
- help: 'Number of open sockets in the http agent',
23
- labelNames: ['protocol', 'host']
24
- });
21
+ name: 'df_http_agent_sockets',
22
+ help: 'Number of open sockets in the http agent',
23
+ labelNames: ['protocol', 'host']
24
+ })
25
25
  const freeSocketsGauge = new Gauge({
26
- name: 'df_http_agent_free_sockets',
27
- help: 'Number of free sockets in the http agent',
28
- labelNames: ['protocol', 'host']
29
- });
26
+ name: 'df_http_agent_free_sockets',
27
+ help: 'Number of free sockets in the http agent',
28
+ labelNames: ['protocol', 'host']
29
+ })
30
30
  const createSocketCounter = new Counter({
31
- name: 'df_http_agent_create_socket_total',
32
- help: 'Total number of sockets created by the http agent',
33
- labelNames: ['protocol']
34
- });
31
+ name: 'df_http_agent_create_socket_total',
32
+ help: 'Total number of sockets created by the http agent',
33
+ labelNames: ['protocol']
34
+ })
35
35
  const requestCounter = new Counter({
36
- name: 'df_http_agent_request_total',
37
- help: 'Total number of requests created by the http agent',
38
- labelNames: ['protocol']
39
- });
36
+ name: 'df_http_agent_request_total',
37
+ help: 'Total number of requests created by the http agent',
38
+ labelNames: ['protocol']
39
+ })
40
40
  setInterval(() => {
41
- for (const _protocol of ['http', 'https']) {
42
- const protocol = _protocol;
43
- const agentStatus = agents[protocol].getCurrentStatus();
44
- for (const host in agentStatus.sockets) {
45
- socketsGauge.set({ protocol, host }, agentStatus.sockets[host]);
46
- }
47
- for (const host in agentStatus.freeSockets) {
48
- freeSocketsGauge.set({ protocol, host }, agentStatus.freeSockets[host]);
49
- }
50
- createSocketCounter.remove({ protocol });
51
- createSocketCounter.inc({ protocol }, agentStatus.createSocketCount);
52
- requestCounter.remove({ protocol });
53
- requestCounter.inc({ protocol }, agentStatus.requestCount);
41
+ for (const _protocol of ['http', 'https']) {
42
+ const protocol = _protocol
43
+ const agentStatus = agents[protocol].getCurrentStatus()
44
+ for (const host in agentStatus.sockets) {
45
+ socketsGauge.set({ protocol, host }, agentStatus.sockets[host])
54
46
  }
55
- }, 60000);
47
+ for (const host in agentStatus.freeSockets) {
48
+ freeSocketsGauge.set({ protocol, host }, agentStatus.freeSockets[host])
49
+ }
50
+ createSocketCounter.remove({ protocol })
51
+ createSocketCounter.inc({ protocol }, agentStatus.createSocketCount)
52
+ requestCounter.remove({ protocol })
53
+ requestCounter.inc({ protocol }, agentStatus.requestCount)
54
+ }
55
+ }, 60000)
package/locks.js CHANGED
@@ -1,72 +1,63 @@
1
- import { hostname } from 'node:os';
2
- import { randomBytes } from 'node:crypto';
3
- import Debug from 'debug';
4
- const debug = Debug('locks');
5
- const pid = randomBytes(8).toString('hex');
6
- debug('locks with pid', pid);
7
- let interval;
8
- let _db;
1
+ import { hostname } from 'node:os'
2
+ import { randomBytes } from 'node:crypto'
3
+ import Debug from 'debug'
4
+ const debug = Debug('locks')
5
+ const pid = randomBytes(8).toString('hex')
6
+ debug('locks with pid', pid)
7
+ let interval
8
+ let _db
9
9
  const collection = () => {
10
- if (!_db)
11
- throw new Error('locks utils was not initialized');
12
- return _db?.collection('locks');
13
- };
10
+ if (!_db) { throw new Error('locks utils was not initialized') }
11
+ return _db?.collection('locks')
12
+ }
14
13
  export const init = async (db, ttl = 60) => {
15
- _db = db;
16
- const locks = collection();
17
- await locks.createIndex({ pid: 1 });
18
- try {
19
- await locks.createIndex({ updatedAt: 1 }, { expireAfterSeconds: ttl });
20
- }
21
- catch (err) {
22
- console.log('Failure to create TTL index. Probably because the value changed. Try to update it.');
23
- db.command({ collMod: 'locks', index: { keyPattern: { updatedAt: 1 }, expireAfterSeconds: ttl } });
24
- }
25
- // prolongate lock acquired by this process while it is still active
26
- interval = setInterval(() => {
27
- locks.updateMany({ pid }, { $currentDate: { updatedAt: true } });
28
- }, (ttl / 2) * 1000);
29
- };
14
+ _db = db
15
+ const locks = collection()
16
+ await locks.createIndex({ pid: 1 })
17
+ try {
18
+ await locks.createIndex({ updatedAt: 1 }, { expireAfterSeconds: ttl })
19
+ } catch (err) {
20
+ console.log('Failure to create TTL index. Probably because the value changed. Try to update it.')
21
+ db.command({ collMod: 'locks', index: { keyPattern: { updatedAt: 1 }, expireAfterSeconds: ttl } })
22
+ }
23
+ // prolongate lock acquired by this process while it is still active
24
+ interval = setInterval(() => {
25
+ locks.updateMany({ pid }, { $currentDate: { updatedAt: true } })
26
+ }, (ttl / 2) * 1000)
27
+ }
30
28
  export const stop = async () => {
31
- clearInterval(interval);
32
- if (_db)
33
- await collection().deleteMany({ pid });
34
- };
35
- export async function acquire(_id, origin) {
36
- if (!_db)
37
- throw new Error('locks not initialized');
38
- debug('acquire', _id, origin);
39
- const locks = collection();
29
+ clearInterval(interval)
30
+ if (_db) { await collection().deleteMany({ pid }) }
31
+ }
32
+ export async function acquire (_id, origin) {
33
+ if (!_db) { throw new Error('locks not initialized') }
34
+ debug('acquire', _id, origin)
35
+ const locks = collection()
36
+ try {
37
+ await locks.insertOne({ _id, pid, origin, hostname: hostname(), createdAt: new Date() })
40
38
  try {
41
- await locks.insertOne({ _id, pid, origin, hostname: hostname(), createdAt: new Date() });
42
- try {
43
- await locks.updateOne({ _id }, { $currentDate: { updatedAt: true } });
44
- }
45
- catch (err) {
46
- await locks.deleteOne({ _id, pid });
47
- throw err;
48
- }
49
- debug('acquire ok', _id);
50
- return true;
51
- }
52
- catch (err) {
53
- if (err.code !== 11000)
54
- throw err;
55
- // duplicate means the lock was already acquired
56
- debug('acquire ko', _id);
57
- return false;
39
+ await locks.updateOne({ _id }, { $currentDate: { updatedAt: true } })
40
+ } catch (err) {
41
+ await locks.deleteOne({ _id, pid })
42
+ throw err
58
43
  }
44
+ debug('acquire ok', _id)
45
+ return true
46
+ } catch (err) {
47
+ if (err.code !== 11000) { throw err }
48
+ // duplicate means the lock was already acquired
49
+ debug('acquire ko', _id)
50
+ return false
51
+ }
59
52
  }
60
53
  export const release = async (_id, delay = 0) => {
61
- if (!_db)
62
- throw new Error('locks not initialized');
63
- debug('release', _id);
64
- const locks = collection();
65
- if (delay) {
66
- const date = new Date((new Date()).getTime() + delay);
67
- await locks.updateOne({ _id, pid }, { $unset: { pid: 1 }, $set: { delayed: true, updatedAt: date } });
68
- }
69
- else {
70
- await locks.deleteOne({ _id, pid });
71
- }
72
- };
54
+ if (!_db) { throw new Error('locks not initialized') }
55
+ debug('release', _id)
56
+ const locks = collection()
57
+ if (delay) {
58
+ const date = new Date((new Date()).getTime() + delay)
59
+ await locks.updateOne({ _id, pid }, { $unset: { pid: 1 }, $set: { delayed: true, updatedAt: date } })
60
+ } else {
61
+ await locks.deleteOne({ _id, pid })
62
+ }
63
+ }
package/mongo-types.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export {}
package/mongo.js CHANGED
@@ -1,63 +1,62 @@
1
- import { MongoClient, MongoError } from 'mongodb';
1
+ import { MongoClient, MongoError } from 'mongodb'
2
2
  export class Mongo {
3
- _client;
4
- get client() {
5
- if (!this._client)
6
- throw new Error('db was not connected');
7
- return this._client;
3
+ _client
4
+ get client () {
5
+ if (!this._client) { throw new Error('db was not connected') }
6
+ return this._client
7
+ }
8
+
9
+ get db () {
10
+ return this.client.db()
11
+ }
12
+
13
+ connect = async (mongoUrl, options = {}) => {
14
+ if (this._client) {
15
+ console.warn('db already connected')
16
+ return
8
17
  }
9
- get db() {
10
- return this.client.db();
18
+ console.log('connecting to mongodb...')
19
+ this._client = await MongoClient.connect(mongoUrl, { maxPoolSize: 5, ignoreUndefined: true, ...options })
20
+ console.log('...ok')
21
+ }
22
+
23
+ // Create an index if it does not exist and manage overwriting existing and conflicting index definitions
24
+ ensureIndex = async (collection, key, options = {}) => {
25
+ try {
26
+ await this.db.collection(collection).createIndex(key, options)
27
+ } catch (err) {
28
+ if (options.name && err instanceof MongoError && (err.code === 85 || err.code === 86)) {
29
+ // if the error is a conflict on keys or params of the index we automatically
30
+ // delete then recreate the index
31
+ console.log(`drop then recreate index ${collection}/${options.name}`)
32
+ await this.db.collection(collection).dropIndex(options.name)
33
+ await this.db.collection(collection).createIndex(key, options)
34
+ } else {
35
+ throw err
36
+ }
11
37
  }
12
- connect = async (mongoUrl, options = {}) => {
13
- if (this._client) {
14
- console.warn('db already connected');
15
- return;
16
- }
17
- console.log('connecting to mongodb...');
18
- this._client = await MongoClient.connect(mongoUrl, { maxPoolSize: 5, ignoreUndefined: true, ...options });
19
- console.log('...ok');
20
- };
21
- // Create an index if it does not exist and manage overwriting existing and conflicting index definitions
22
- ensureIndex = async (collection, key, options = {}) => {
23
- try {
24
- await this.db.collection(collection).createIndex(key, options);
25
- }
26
- catch (err) {
27
- if (options.name && err instanceof MongoError && (err.code === 85 || err.code === 86)) {
28
- // if the error is a conflict on keys or params of the index we automatically
29
- // delete then recreate the index
30
- console.log(`drop then recreate index ${collection}/${options.name}`);
31
- await this.db.collection(collection).dropIndex(options.name);
32
- await this.db.collection(collection).createIndex(key, options);
33
- }
34
- else {
35
- throw err;
36
- }
37
- }
38
- };
39
- /**
38
+ }
39
+
40
+ /**
40
41
  * create a bunch of indexes and overtwrite previous definitions
41
42
  * the structure forces giving names to indexes which is better for managing them properly
42
43
  */
43
- configure = async (indexDefinitions) => {
44
- for (const collectionName in indexDefinitions) {
45
- for (const indexName in indexDefinitions[collectionName]) {
46
- let key, options;
47
- if (Array.isArray(indexDefinitions[collectionName][indexName])) {
48
- [key, options] = indexDefinitions[collectionName][indexName];
49
- if (options.name && options.name !== indexName)
50
- throw new Error(`inconsistent index name ${indexName} or ${options.name}`);
51
- options.name = indexName;
52
- }
53
- else {
54
- key = indexDefinitions[collectionName][indexName];
55
- options = { name: indexName };
56
- }
57
- await this.ensureIndex(collectionName, key, options);
58
- }
44
+ configure = async (indexDefinitions) => {
45
+ for (const collectionName in indexDefinitions) {
46
+ for (const indexName in indexDefinitions[collectionName]) {
47
+ let key, options
48
+ if (Array.isArray(indexDefinitions[collectionName][indexName])) {
49
+ [key, options] = indexDefinitions[collectionName][indexName]
50
+ if (options.name && options.name !== indexName) { throw new Error(`inconsistent index name ${indexName} or ${options.name}`) }
51
+ options.name = indexName
52
+ } else {
53
+ key = indexDefinitions[collectionName][indexName]
54
+ options = { name: indexName }
59
55
  }
60
- };
56
+ await this.ensureIndex(collectionName, key, options)
57
+ }
58
+ }
59
+ }
61
60
  }
62
- const mongo = new Mongo();
63
- export default mongo;
61
+ const mongo = new Mongo()
62
+ export default mongo
package/observer.js CHANGED
@@ -1,83 +1,78 @@
1
1
  // very light web server to expose prometheus metrics, export a cpu profile and maybe other incoming obervability features
2
- import { hostname } from 'node:os';
3
- import { createServer } from 'node:http';
4
- import { Registry, Counter, register, collectDefaultMetrics } from 'prom-client';
5
- import eventPromise from '@data-fair/lib-utils/event-promise.js';
2
+ import { hostname } from 'node:os'
3
+ import { createServer } from 'node:http'
4
+ import { Registry, Counter, register, collectDefaultMetrics } from 'prom-client'
5
+ import eventPromise from '@data-fair/lib-utils/event-promise.js'
6
6
  // a special registry for metrics that are global
7
7
  // not based on this specific process state but instead on the content of db for example
8
- export const servicePromRegistry = new Registry();
9
- collectDefaultMetrics();
8
+ export const servicePromRegistry = new Registry()
9
+ collectDefaultMetrics()
10
10
  // local metrics incremented throughout the code
11
11
  const internalErrorCounter = new Counter({
12
- name: 'df_internal_error',
13
- help: 'A counter of errors from any service, worker, etc. Do not use for client errors, only for anomalies that should trigger alerts. Each increment should be accompanied by an error log with matching code.',
14
- labelNames: ['errorCode']
15
- });
12
+ name: 'df_internal_error',
13
+ help: 'A counter of errors from any service, worker, etc. Do not use for client errors, only for anomalies that should trigger alerts. Each increment should be accompanied by an error log with matching code.',
14
+ labelNames: ['errorCode']
15
+ })
16
16
  export const internalError = (errorCode, error, ...optionalParams) => {
17
- internalErrorCounter.inc({ errorCode });
18
- const message = error.message || error;
19
- console.error(`[${errorCode}] ${message}`, ...optionalParams);
20
- if (error.stack)
21
- console.error(error.stack);
22
- };
17
+ internalErrorCounter.inc({ errorCode })
18
+ const message = error.message || error
19
+ console.error(`[${errorCode}] ${message}`, ...optionalParams)
20
+ if (error.stack) { console.error(error.stack) }
21
+ }
23
22
  /**
24
23
  * @param {import('node:http').ServerResponse} res
25
24
  * @param {any} err
26
25
  */
27
26
  const httpErrorHandler = (res, err) => {
28
- console.error('failed to server prometheus /metrics', err);
29
- res.writeHead(500);
30
- res.end();
31
- };
27
+ console.error('failed to server prometheus /metrics', err)
28
+ res.writeHead(500)
29
+ res.end()
30
+ }
32
31
  const serveRegistry = async (res, registry) => {
33
- const metrics = await registry.metrics();
34
- res.setHeader('Content-Type', registry.contentType);
35
- res.writeHead(200);
36
- res.write(metrics);
37
- res.end();
38
- };
32
+ const metrics = await registry.metrics()
33
+ res.setHeader('Content-Type', registry.contentType)
34
+ res.writeHead(200)
35
+ res.write(metrics)
36
+ res.end()
37
+ }
39
38
  // live CPU performance inspection
40
39
  const serveCPUProfile = async (res, duration = 2000) => {
41
- // @ts-ignore
42
- const { Session } = await import('node:inspector/promises');
43
- const session = new Session();
44
- session.connect();
45
- await session.post('Profiler.enable');
46
- await session.post('Profiler.start');
47
- await new Promise(resolve => setTimeout(resolve, duration));
48
- const { profile } = await session.post('Profiler.stop');
49
- session.disconnect();
50
- res.setHeader('Content-Disposition', `attachment; filename="data-fair-${hostname()}-${new Date().toISOString()}.cpuprofile"`);
51
- res.write(JSON.stringify(profile));
52
- res.end();
53
- };
54
- let server;
40
+ // @ts-ignore
41
+ const { Session } = await import('node:inspector/promises')
42
+ const session = new Session()
43
+ session.connect()
44
+ await session.post('Profiler.enable')
45
+ await session.post('Profiler.start')
46
+ await new Promise(resolve => setTimeout(resolve, duration))
47
+ const { profile } = await session.post('Profiler.stop')
48
+ session.disconnect()
49
+ res.setHeader('Content-Disposition', `attachment; filename="data-fair-${hostname()}-${new Date().toISOString()}.cpuprofile"`)
50
+ res.write(JSON.stringify(profile))
51
+ res.end()
52
+ }
53
+ let server
55
54
  export const startObserver = async (port = 9090) => {
56
- server = createServer((req, res) => {
57
- if (req.method === 'GET' && req.url === '/metrics') {
58
- serveRegistry(res, register).catch(err => httpErrorHandler(res, err));
59
- }
60
- else if (req.method === 'GET' && (req.url === '/global-metrics' || req.url === '/service-metrics')) {
61
- serveRegistry(res, servicePromRegistry).catch(err => httpErrorHandler(res, err));
62
- }
63
- else if (req.method === 'GET' && req.url === '/cpu-profile') {
64
- // TODO: duration
65
- serveCPUProfile(res).catch(err => httpErrorHandler(res, err));
66
- }
67
- else {
68
- res.writeHead(404);
69
- res.end();
70
- }
71
- });
72
- server.listen(port);
73
- await eventPromise(server, 'listening');
74
- console.log(`Observer server available on http://localhost:${port}
55
+ server = createServer((req, res) => {
56
+ if (req.method === 'GET' && req.url === '/metrics') {
57
+ serveRegistry(res, register).catch(err => httpErrorHandler(res, err))
58
+ } else if (req.method === 'GET' && (req.url === '/global-metrics' || req.url === '/service-metrics')) {
59
+ serveRegistry(res, servicePromRegistry).catch(err => httpErrorHandler(res, err))
60
+ } else if (req.method === 'GET' && req.url === '/cpu-profile') {
61
+ // TODO: duration
62
+ serveCPUProfile(res).catch(err => httpErrorHandler(res, err))
63
+ } else {
64
+ res.writeHead(404)
65
+ res.end()
66
+ }
67
+ })
68
+ server.listen(port)
69
+ await eventPromise(server, 'listening')
70
+ console.log(`Observer server available on http://localhost:${port}
75
71
  GET /metrics -> get prometheus metrics for this instance
76
72
  GET /service-metrics -> get prometheus metrics shared accross all instances
77
73
  GET /cpu-profile -> generate and fetch a CPU profile
78
- `);
79
- };
74
+ `)
75
+ }
80
76
  export const stopObserver = async () => {
81
- if (server)
82
- server.close();
83
- };
77
+ if (server) { server.close() }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/lib-node",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Small nodejs related utility functions for the data-fair stack.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -9,9 +9,12 @@
9
9
  ],
10
10
  "author": "",
11
11
  "license": "MIT",
12
+ "scripts": {
13
+ "prepublishOnly": "cd .. && npm run prepublishOnly"
14
+ },
12
15
  "dependencies": {
13
- "@data-fair/lib-common-types": "^0.2.0",
14
- "@data-fair/lib-utils": "0.2.0",
16
+ "@data-fair/lib-common-types": "^1.1.0",
17
+ "@data-fair/lib-utils": "1.0.0",
15
18
  "agentkeepalive": "^4.5.0",
16
19
  "ajv-i18n": "^4.2.0",
17
20
  "axios": "^1.7.7",
@@ -19,9 +22,9 @@
19
22
  "semver": "^7.6.3"
20
23
  },
21
24
  "peerDependencies": {
22
- "mongodb": "^6.9.0",
23
- "prom-client": "^15.1.3",
24
- "ws": "^8.18.0"
25
+ "mongodb": "6",
26
+ "prom-client": "15",
27
+ "ws": "8"
25
28
  },
26
29
  "peerDependenciesMeta": {
27
30
  "mongodb": {
@@ -34,5 +37,10 @@
34
37
  "optional": true
35
38
  }
36
39
  },
37
- "type": "module"
40
+ "type": "module",
41
+ "devDependencies": {
42
+ "mongodb": "^6.9.0",
43
+ "prom-client": "^15.1.3",
44
+ "ws": "^8.18.0"
45
+ }
38
46
  }
@@ -1,16 +1,16 @@
1
1
  // @ts-nocheck
2
2
  // cf https://nodejs.org/api/module.html#customization-hooks
3
- const regexp = /\/\/ @test:spy\((.*)\)\n/g;
4
- export async function load(url, context, nextLoad) {
5
- const result = await nextLoad(url, context);
6
- if (result.format === 'module' || result.format === 'module-typescript') {
7
- let source = result.source.toString();
8
- const matches = source.match(regexp);
9
- if (matches) {
10
- source = 'import { emit as __emitTestSpy } from "@data-fair/lib/node/test-spies.js"\n' + source;
11
- source = source.replace(regexp, '__emitTestSpy($1)\n');
12
- result.source = Buffer.from(source, 'utf-8');
13
- }
3
+ const regexp = /\/\/ @test:spy\((.*)\)\n/g
4
+ export async function load (url, context, nextLoad) {
5
+ const result = await nextLoad(url, context)
6
+ if (result.format === 'module' || result.format === 'module-typescript') {
7
+ let source = result.source.toString()
8
+ const matches = source.match(regexp)
9
+ if (matches) {
10
+ source = 'import { emit as __emitTestSpy } from "@data-fair/lib/node/test-spies.js"\n' + source
11
+ source = source.replace(regexp, '__emitTestSpy($1)\n')
12
+ result.source = Buffer.from(source, 'utf-8')
14
13
  }
15
- return result;
14
+ }
15
+ return result
16
16
  }
package/test-spies.js CHANGED
@@ -1,28 +1,26 @@
1
- import { register } from 'node:module';
2
- export function registerModuleHooks() {
3
- register('@data-fair/lib/node/test-module-hooks.js', { parentURL: import.meta.url });
1
+ import { register } from 'node:module'
2
+ export function registerModuleHooks () {
3
+ register('@data-fair/lib/node/test-module-hooks.js', { parentURL: import.meta.url })
4
4
  }
5
- const pendingPromises = {};
6
- let i = 0;
7
- export function waitFor(eventName, timeout = 2000) {
8
- i++;
9
- const id = i;
10
- const p = new Promise((resolve, reject) => {
11
- pendingPromises[eventName] = pendingPromises[eventName] ?? [];
12
- pendingPromises[eventName].push({ resolve, id });
13
- setTimeout(() => reject(new Error(`Timeout waiting for test spy ${eventName}`)), timeout);
14
- });
15
- p.finally(() => {
16
- pendingPromises[eventName] = pendingPromises[eventName].filter(p => p.id !== id);
17
- if (pendingPromises[eventName].length === 0)
18
- delete pendingPromises[eventName];
19
- });
20
- return p;
5
+ const pendingPromises = {}
6
+ let i = 0
7
+ export function waitFor (eventName, timeout = 2000) {
8
+ i++
9
+ const id = i
10
+ const p = new Promise((resolve, reject) => {
11
+ pendingPromises[eventName] = pendingPromises[eventName] ?? []
12
+ pendingPromises[eventName].push({ resolve, id })
13
+ setTimeout(() => reject(new Error(`Timeout waiting for test spy ${eventName}`)), timeout)
14
+ })
15
+ p.finally(() => {
16
+ pendingPromises[eventName] = pendingPromises[eventName].filter(p => p.id !== id)
17
+ if (pendingPromises[eventName].length === 0) { delete pendingPromises[eventName] }
18
+ })
19
+ return p
21
20
  }
22
- export function emit(eventName, data) {
23
- if (!pendingPromises[eventName])
24
- return;
25
- for (const p of pendingPromises[eventName]) {
26
- p.resolve(data);
27
- }
21
+ export function emit (eventName, data) {
22
+ if (!pendingPromises[eventName]) { return }
23
+ for (const p of pendingPromises[eventName]) {
24
+ p.resolve(data)
25
+ }
28
26
  }
@@ -1,68 +1,65 @@
1
- import semver from 'semver';
2
- import path from 'path';
3
- import { readdirSync, readFileSync } from 'fs';
4
- import Debug from 'debug';
5
- import { acquire, release } from './locks.js';
6
- const debug = Debug('upgrade');
1
+ import semver from 'semver'
2
+ import path from 'path'
3
+ import { readdirSync, readFileSync } from 'fs'
4
+ import Debug from 'debug'
5
+ import { acquire, release } from './locks.js'
6
+ const debug = Debug('upgrade')
7
7
  // chose the proper scripts to execute, then run them
8
8
  export default async function (db, basePath = './') {
9
- const ack = await acquire('upgrade');
10
- if (!ack) {
11
- console.warn('upgrade scripts lock is already acquired, skip them');
12
- // IMPORTANT: this behaviour of running the process when the upgrade scripts are still running on another one
13
- // implies that they cannot be considered a pre-requisite..
14
- // if we want to consider the upgrade scripts as a pre-requisite we should implement a wait on all
15
- // containers for the scripts that are running in only 1 (while loop on "acquire" ?) and a healthcheck
16
- // are not considered "up" and the previous versions keep running in the mean time
17
- }
18
- else {
19
- await runScripts(db, basePath);
20
- await release('upgrade');
21
- }
9
+ const ack = await acquire('upgrade')
10
+ if (!ack) {
11
+ console.warn('upgrade scripts lock is already acquired, skip them')
12
+ // IMPORTANT: this behaviour of running the process when the upgrade scripts are still running on another one
13
+ // implies that they cannot be considered a pre-requisite..
14
+ // if we want to consider the upgrade scripts as a pre-requisite we should implement a wait on all
15
+ // containers for the scripts that are running in only 1 (while loop on "acquire" ?) and a healthcheck
16
+ // are not considered "up" and the previous versions keep running in the mean time
17
+ } else {
18
+ await runScripts(db, basePath)
19
+ await release('upgrade')
20
+ }
22
21
  }
23
- async function runScripts(db, basePath) {
24
- const dir = path.resolve(basePath);
25
- const pjsonPath = path.join(dir, 'package.json');
26
- debug('read service info from ' + pjsonPath);
27
- const pjson = JSON.parse(readFileSync(pjsonPath, 'utf8'));
28
- const scriptsRoot = path.join(dir, 'upgrade');
29
- debug(`service=${pjson.name} version=${pjson.version}`);
30
- const services = db.collection('services');
31
- const service = await services.findOne({ id: pjson.name });
32
- const version = service?.version;
33
- debug('list scripts from ' + scriptsRoot);
34
- let scripts = await listScripts(scriptsRoot);
35
- if (!version) {
36
- debug('No service version found in database, this is probably a fresh install');
37
- scripts = scripts.filter(scriptDef => scriptDef.version === 'init');
38
- }
39
- else {
40
- debug(`Current service version from database : ${version}`);
41
- scripts = scripts.filter(scriptDef => scriptDef.version !== 'init');
22
+ async function runScripts (db, basePath) {
23
+ const dir = path.resolve(basePath)
24
+ const pjsonPath = path.join(dir, 'package.json')
25
+ debug('read service info from ' + pjsonPath)
26
+ const pjson = JSON.parse(readFileSync(pjsonPath, 'utf8'))
27
+ const scriptsRoot = path.join(dir, 'upgrade')
28
+ debug(`service=${pjson.name} version=${pjson.version}`)
29
+ const services = db.collection('services')
30
+ const service = await services.findOne({ id: pjson.name })
31
+ const version = service?.version
32
+ debug('list scripts from ' + scriptsRoot)
33
+ let scripts = await listScripts(scriptsRoot)
34
+ if (!version) {
35
+ debug('No service version found in database, this is probably a fresh install')
36
+ scripts = scripts.filter(scriptDef => scriptDef.version === 'init')
37
+ } else {
38
+ debug(`Current service version from database : ${version}`)
39
+ scripts = scripts.filter(scriptDef => scriptDef.version !== 'init')
40
+ }
41
+ for (const scriptDef of scripts) {
42
+ if (semver.gte(scriptDef.version, version)) {
43
+ for (const scriptName of scriptDef.names) {
44
+ const script = (await import(path.join(scriptsRoot, scriptDef.version, scriptName))).default
45
+ debug('Apply script %s/%s : %s', scriptDef.version, scriptName, script.description)
46
+ await script.exec(db, Debug(`upgrade:${scriptDef.version}:${scriptName}`))
47
+ }
42
48
  }
43
- for (const scriptDef of scripts) {
44
- if (semver.gte(scriptDef.version, version)) {
45
- for (const scriptName of scriptDef.names) {
46
- const script = (await import(path.join(scriptsRoot, scriptDef.version, scriptName))).default;
47
- debug('Apply script %s/%s : %s', scriptDef.version, scriptName, script.description);
48
- await script.exec(db, Debug(`upgrade:${scriptDef.version}:${scriptName}`));
49
- }
50
- }
51
- }
52
- const coercedVersion = semver.coerce(pjson.version);
53
- if (!coercedVersion)
54
- throw new Error(`Invalid version number in package.json ${pjson.version}`);
55
- const newService = { id: pjson.name, version: coercedVersion.version };
56
- debug(`Upgrade scripts are over, save current version number ${newService.version}`);
57
- await services.updateOne({ id: pjson.name }, { $set: newService }, { upsert: true });
49
+ }
50
+ const coercedVersion = semver.coerce(pjson.version)
51
+ if (!coercedVersion) { throw new Error(`Invalid version number in package.json ${pjson.version}`) }
52
+ const newService = { id: pjson.name, version: coercedVersion.version }
53
+ debug(`Upgrade scripts are over, save current version number ${newService.version}`)
54
+ await services.updateOne({ id: pjson.name }, { $set: newService }, { upsert: true })
58
55
  }
59
56
  // Walk the scripts directories
60
- async function listScripts(scriptsRoot) {
61
- const dirs = readdirSync(scriptsRoot).sort();
62
- return dirs.map(dir => {
63
- return {
64
- version: dir,
65
- names: readdirSync(path.join(scriptsRoot, dir)).sort()
66
- };
67
- });
57
+ async function listScripts (scriptsRoot) {
58
+ const dirs = readdirSync(scriptsRoot).sort()
59
+ return dirs.map(dir => {
60
+ return {
61
+ version: dir,
62
+ names: readdirSync(path.join(scriptsRoot, dir)).sort()
63
+ }
64
+ })
68
65
  }
package/validation.js CHANGED
@@ -1,44 +1,37 @@
1
- import ajvFrModule from 'ajv-i18n/localize/fr/index.js';
2
- import ajvEnModule from 'ajv-i18n/localize/en/index.js';
3
- import { httpError } from '@data-fair/lib-utils/http-errors.js';
4
- const ajvFr = ajvFrModule;
5
- const ajvEn = ajvEnModule;
1
+ import ajvFrModule from 'ajv-i18n/localize/fr/index.js'
2
+ import ajvEnModule from 'ajv-i18n/localize/en/index.js'
3
+ import { httpError } from '@data-fair/lib-utils/http-errors.js'
4
+ const ajvFr = ajvFrModule
5
+ const ajvEn = ajvEnModule
6
6
  const localize = {
7
- fr: ajvFr,
8
- en: ajvEn
9
- };
7
+ fr: ajvFr,
8
+ en: ajvEn
9
+ }
10
10
  // cf https://github.com/ajv-validator/ajv/blob/b3e0cb17d0e095b5c883042b2306571be5ec86b7/lib/core.ts#L650
11
11
  export const errorsText = (errors, varName = 'data') => {
12
- if (!errors || errors.length === 0)
13
- return 'No errors';
14
- return errors
15
- .map((e) => {
16
- let msg = `${varName}${e.instancePath} ${e.message}`.trim();
17
- const paramKeys = Object.keys(e.params || {}).filter(key => {
18
- if (key === 'error')
19
- return false;
20
- if (e.keyword === 'type' && key === 'type')
21
- return false;
22
- return true;
23
- });
24
- const params = paramKeys
25
- .map(key => paramKeys.length > 1 ? `${key}=${e.params[key]}` : e.params[key])
26
- .join(', ');
27
- if (params)
28
- msg += ` (${params})`;
29
- return msg;
12
+ if (!errors || errors.length === 0) { return 'No errors' }
13
+ return errors
14
+ .map((e) => {
15
+ let msg = `${varName}${e.instancePath} ${e.message}`.trim()
16
+ const paramKeys = Object.keys(e.params || {}).filter(key => {
17
+ if (key === 'error') { return false }
18
+ if (e.keyword === 'type' && key === 'type') { return false }
19
+ return true
20
+ })
21
+ const params = paramKeys
22
+ .map(key => paramKeys.length > 1 ? `${key}=${e.params[key]}` : e.params[key])
23
+ .join(', ')
24
+ if (params) { msg += ` (${params})` }
25
+ return msg
30
26
  })
31
- .reduce((text, msg) => text + ', ' + msg);
32
- };
33
- export function assertValid(validate, data, options = {}) {
34
- const lang = options.lang ?? 'fr';
35
- const name = options.name ?? 'data';
36
- if (!validate(data)) {
37
- (localize[lang] || localize.fr)(validate.errors);
38
- const message = errorsText(validate.errors, name);
39
- if (options.internal)
40
- throw new Error(message);
41
- else
42
- throw httpError(400, message);
43
- }
27
+ .reduce((text, msg) => text + ', ' + msg)
28
+ }
29
+ export function assertValid (validate, data, options = {}) {
30
+ const lang = options.lang ?? 'fr'
31
+ const name = options.name ?? 'data'
32
+ if (!validate(data)) {
33
+ (localize[lang] || localize.fr)(validate.errors)
34
+ const message = errorsText(validate.errors, name)
35
+ if (options.internal) { throw new Error(message) } else { throw httpError(400, message) }
36
+ }
44
37
  }
package/ws-client.js CHANGED
@@ -1,97 +1,93 @@
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 };
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)
15
47
  }
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);
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)
46
74
  }
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
- }
75
+ }
76
+ this.on('message', messageCb)
77
+ })
78
+ }
79
+
80
+ close () {
81
+ if (this._ws) { this._ws.terminate() }
82
+ }
86
83
  }
87
84
  export class DataFairWsClient extends WsClient {
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
- }
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
+ }
97
93
  }
package/ws-emitter.js CHANGED
@@ -1,18 +1,16 @@
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)
6
- await db.createCollection('ws-messages', { capped: true, size: 100000, max: 1000 });
7
- return db.collection('ws-messages');
8
- };
9
- let messagesCollection;
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
10
9
  export const init = async (db) => {
11
- messagesCollection = await initMessagesCollection(db);
12
- await messagesCollection.insertOne({ type: 'init' });
13
- };
10
+ messagesCollection = await initMessagesCollection(db)
11
+ await messagesCollection.insertOne({ type: 'init' })
12
+ }
14
13
  export const emit = async (channel, data) => {
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
- };
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
+ }