diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d085c933..ea486dc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,7 @@ jobs: - 20 - 22 - 24 + - 25 os: - ubuntu-latest services: diff --git a/LICENSE b/LICENSE index f87466a6..c5af169d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2025 Metarhia contributors +Copyright (c) 2020-2026 Metarhia contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 65ef4ad7..170e2610 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,11 @@ If you prefer to run without Docker: - Ask questions in Telegram https://t.me/nodeua (node.js related) or https://t.me/metaserverless (metarhia related) +## AI and agent config + +Project rules and skills live in **`.ai/`** (IDE- and platform-independent); templates and hook docs in **`docs/ai/`**. See [AGENTS.md](./AGENTS.md). **Cursor / Claude Code / Windsurf / GitHub Copilot users:** after clone run `npm run link:cursor`, `npm run link:claude`, `npm run link:windsurf`, or `npm run link:github` (or `npm run link:all`) **before** opening the project. If the IDE already created a real folder, remove it then run the same command again. For GitHub, only `.github/rules` and `.github/skills` are linked so `.github/workflows` etc. can stay in git. + ## License -Copyright (c) 2020-2025 Metarhia contributors. +Copyright (c) 2020-2026 Metarhia contributors. This starter kit is [MIT licensed](./LICENSE). diff --git a/application/db/pg/start.js b/application/db/pg/start.js deleted file mode 100644 index 43b60982..00000000 --- a/application/db/pg/start.js +++ /dev/null @@ -1,7 +0,0 @@ -async () => { - if (application.worker.id === 'W1') { - console.debug('Connect to pg'); - } - const options = { ...config.database, console }; - db.pg = new metarhia.metasql.Database(options); -}; diff --git a/application/db/redis/get.js b/application/db/redis/get.js deleted file mode 100644 index 6697b692..00000000 --- a/application/db/redis/get.js +++ /dev/null @@ -1 +0,0 @@ -(key) => db.redis.client.get(key); diff --git a/application/db/redis/set.js b/application/db/redis/set.js deleted file mode 100644 index 076ed5e5..00000000 --- a/application/db/redis/set.js +++ /dev/null @@ -1 +0,0 @@ -(key, value, options) => db.redis.client.set(key, value, options); diff --git a/application/db/redis/start.js b/application/db/redis/start.js deleted file mode 100644 index d86a0391..00000000 --- a/application/db/redis/start.js +++ /dev/null @@ -1,16 +0,0 @@ -async () => { - if (application.worker.id === 'W1') { - console.debug('Connect to redis'); - } - const client = npm.redis.createClient(config.redis); - db.redis.client = client; - client.on('error', async (error) => { - if (application.worker.id === 'W1') { - console.warn('No redis service detected, so quit client'); - const err = new Error('No redis', { cause: error }); - console.error(err); - await client.disconnect(); - } - }); - await client.connect(); -}; diff --git a/application/static/console.js b/application/static/console.js index 183de78e..9809650d 100644 --- a/application/static/console.js +++ b/application/static/console.js @@ -335,7 +335,11 @@ class Application { this.keyboard = new Keyboard(this); this.scroller = new Scroller(this); const protocol = location.protocol === 'http:' ? 'ws' : 'wss'; - this.metacom = Metacom.create(`${protocol}://${location.host}/api`); + return this.init(`${protocol}://${location.host}/api`); + } + + async init(url) { + this.metacom = await Metacom.connect(url); window.api = this.metacom.api; window.application = this; } @@ -467,7 +471,7 @@ class Application { } window.addEventListener('load', async () => { - const application = new Application(); + const application = await new Application(); await application.metacom.load('auth', 'console', 'example', 'files'); const token = localStorage.getItem('metarhia.session.token'); if (token) { diff --git a/application/static/events.js b/application/static/events.js deleted file mode 100644 index 2bb4c42b..00000000 --- a/application/static/events.js +++ /dev/null @@ -1,67 +0,0 @@ -class EventEmitter { - constructor() { - this.events = new Map(); - this.maxListenersCount = 10; - } - - getMaxListeners() { - return this.maxListenersCount; - } - - listenerCount(name) { - const event = this.events.get(name); - if (event) return event.size; - return 0; - } - - on(name, fn) { - const event = this.events.get(name); - if (event) { - event.add(fn); - const tooManyListeners = event.size > this.maxListenersCount; - if (tooManyListeners) { - const name = 'MaxListenersExceededWarning'; - const warn = 'Possible EventEmitter memory leak detected'; - const max = `Current maxListenersCount is ${this.maxListenersCount}`; - const hint = 'Hint: avoid adding listeners in loops'; - console.warn(`${name}: ${warn}. ${max}. ${hint}`); - } - } else { - this.events.set(name, new Set([fn])); - } - } - - once(name, fn) { - const dispose = (...args) => { - this.remove(name, dispose); - return void fn(...args); - }; - this.on(name, dispose); - } - - emit(name, ...args) { - const event = this.events.get(name); - if (!event) return; - for (const fn of event.values()) { - fn(...args); - } - } - - remove(name, fn) { - const event = this.events.get(name); - if (!event) return; - event.delete(fn); - } - - clear(name) { - if (!name) return void this.events.clear(); - this.events.delete(name); - } -} - -const once = (emitter, name) => - new Promise((resolve) => { - emitter.once(name, resolve); - }); - -export { EventEmitter, once }; diff --git a/application/static/metacom.js b/application/static/metacom.js index b82c2968..fea59502 100644 --- a/application/static/metacom.js +++ b/application/static/metacom.js @@ -1,20 +1,194 @@ -import { EventEmitter } from './events.js'; -import { chunkDecode, MetaReadable, MetaWritable } from './streams.js'; +import { Emitter, generateUUID, jsonParse } from './metautil.js'; -const CALL_TIMEOUT = 7 * 1000; -const PING_INTERVAL = 60 * 1000; -const RECONNECT_TIMEOUT = 2 * 1000; +// chunks-browser.js + +const ID_LENGTH_BYTES = 1; + +const chunkEncode = (id, payload) => { + const encoder = new TextEncoder(); + const idBuffer = encoder.encode(id); + const idLength = idBuffer.length; + if (idLength > 255) { + throw new Error(`ID length ${idLength} exceeds maximum of 255 characters`); + } + const chunk = new Uint8Array(ID_LENGTH_BYTES + idLength + payload.length); + chunk[0] = idLength; + chunk.set(idBuffer, ID_LENGTH_BYTES); + chunk.set(payload, ID_LENGTH_BYTES + idLength); + return chunk; +}; + +const chunkDecode = (chunk) => { + const idLength = chunk[0]; + const idBuffer = chunk.subarray(ID_LENGTH_BYTES, ID_LENGTH_BYTES + idLength); + const decoder = new TextDecoder(); + const id = decoder.decode(idBuffer); + const payload = chunk.subarray(ID_LENGTH_BYTES + idLength); + return { id, payload }; +}; + +// streams.js + +const PUSH_EVENT = Symbol(); +const PULL_EVENT = Symbol(); +const DEFAULT_HIGH_WATER_MARK = 32; +const MAX_LISTENERS = 10; +const MAX_HIGH_WATER_MARK = 1000; + +class MetaReadable extends Emitter { + queue = []; + streaming = true; + status = 'active'; + bytesRead = 0; + highWaterMark = DEFAULT_HIGH_WATER_MARK; + + constructor(id, name, size, options = {}) { + super(); + this.id = id; + this.name = name; + this.size = size; + const { highWaterMark } = options; + if (highWaterMark) this.highWaterMark = highWaterMark; + } + + async push(data) { + while (this.queue.length > this.highWaterMark) { + this.checkStreamLimits(); + await this.waitEvent(PULL_EVENT); + } + this.queue.push(data); + if (this.queue.length === 1) this.emit(PUSH_EVENT); + return data; + } + + async finalize(writable) { + const onError = () => this.terminate(); + writable.once('error', onError); + for await (const chunk of this) { + const needDrain = !writable.write(chunk); + if (needDrain) await writable.waitEvent('drain'); + } + this.emit('end'); + writable.end(); + await writable.waitEvent('close'); + await this.close(); + writable.removeListener('error', onError); + } + + pipe(writable) { + this.finalize(writable).catch((error) => this.emit('error', error)); + return writable; + } + + async toBlob(type = '') { + const chunks = []; + for await (const chunk of this) { + chunks.push(chunk); + } + return new Blob(chunks, { type }); + } + + async close() { + await this.stop(); + this.status = 'closed'; + } + + async terminate() { + await this.stop(); + this.status = 'terminated'; + } + + async stop() { + while (this.bytesRead !== this.size) { + await this.waitEvent(PULL_EVENT); + } + this.streaming = false; + this.emit(PUSH_EVENT, null); + } + + async read() { + if (this.queue.length > 0) return this.pull(); + const finisher = await this.waitEvent(PUSH_EVENT); + if (finisher === null) return null; + return this.pull(); + } + + pull() { + const data = this.queue.shift(); + if (!data) return data; + this.bytesRead += data.length; + this.emit(PULL_EVENT); + return data; + } -const connections = new Set(); + checkStreamLimits() { + if (this.listenerCount(PULL_EVENT) >= MAX_LISTENERS) { + ++this.highWaterMark; + } + if (this.highWaterMark > MAX_HIGH_WATER_MARK) { + throw new Error('Stream overflow occurred'); + } + } -if (window) { - window.addEventListener('online', () => { - for (const connection of connections) { - if (!connection.connected) connection.open(); + waitEvent(event) { + return new Promise((resolve) => this.once(event, resolve)); + } + + async *[Symbol.asyncIterator]() { + while (this.streaming) { + const chunk = await this.read(); + if (!chunk) return; + yield chunk; } - }); + } +} + +class MetaWritable extends Emitter { + constructor(id, name, size, transport) { + super(); + this.id = id; + this.name = name; + this.size = size; + this.transport = transport; + this.init(); + } + + init() { + const { id, name, size } = this; + const packet = { type: 'stream', id, name, size }; + this.transport.send(packet); + } + + write(data) { + const chunk = chunkEncode(this.id, data); + this.transport.write(chunk); + return true; + } + + end() { + const packet = { type: 'stream', id: this.id, status: 'end' }; + this.transport.send(packet); + } + + terminate() { + const packet = { type: 'stream', id: this.id, status: 'terminate' }; + this.transport.send(packet); + } } +// metacom.js + +const CALL_TIMEOUT = 7 * 1000; +const RECONNECT_TIMEOUT = 2 * 1000; + +const toByteView = async (input) => { + if (typeof input.arrayBuffer === 'function') { + const buffer = await input.arrayBuffer(); + return new Uint8Array(buffer); + } + return new Uint8Array(input); +}; + class MetacomError extends Error { constructor({ message, code }) { super(message); @@ -22,141 +196,242 @@ class MetacomError extends Error { } } -class MetacomUnit extends EventEmitter { - emit(...args) { - super.emit('*', ...args); - super.emit(...args); +class ClientTransport extends Emitter { + active = false; + + constructor(url) { + super(); + this.url = url; + } + + send(obj) { + this.write(JSON.stringify(obj)); } } -class Metacom extends EventEmitter { - constructor(url, options = {}) { +class Metacom extends Emitter { + static connections = new Set(); + static isOnline = true; + + static online() { + Metacom.isOnline = true; + for (const connection of Metacom.connections) { + connection.#transport.online(); + if (!connection.connected && connection.active) { + connection.open().catch((error) => connection.emit('error', error)); + } + } + } + + static offline() { + Metacom.isOnline = false; + for (const connection of Metacom.connections) { + connection.#transport.offline(); + } + } + + static initialize() { + if (typeof window !== 'undefined') { + window.addEventListener('online', Metacom.online); + window.addEventListener('offline', Metacom.offline); + return; + } + if (typeof self !== 'undefined') { + self.addEventListener('online', Metacom.online); + self.addEventListener('offline', Metacom.offline); + } + } + + api = {}; + #transport = null; + #calls = new Map(); + #streams = new Map(); + #callTimeout = CALL_TIMEOUT; + #reconnectTimeout = RECONNECT_TIMEOUT; + #reconnectTimer = null; + #proxyPacket = null; + #options = {}; + + get active() { + return this.#transport.active; + } + + constructor(url, transport, options = {}) { super(); + const { callTimeout, reconnectTimeout, proxy } = options; + if (callTimeout) this.#callTimeout = callTimeout; + if (reconnectTimeout) this.#reconnectTimeout = reconnectTimeout; + if (proxy) this.#proxyPacket = proxy; this.url = url; - this.socket = null; - this.api = {}; - this.callId = 0; - this.calls = new Map(); - this.streams = new Map(); - this.streamId = 0; - this.active = false; - this.connected = false; - this.opening = null; - this.lastActivity = Date.now(); - this.callTimeout = options.callTimeout || CALL_TIMEOUT; - this.pingInterval = options.pingInterval || PING_INTERVAL; - this.reconnectTimeout = options.reconnectTimeout || RECONNECT_TIMEOUT; - this.ping = null; - this.open(); + this.#transport = transport; + this.#options = options; + this.#bindTransport(); + } + + static async connect(url, options = {}) { + if (options.worker) { + const transport = Metacom.transport.event.getInstance(url); + const metacom = new Metacom(url, transport, options); + await metacom.open(); + return metacom; + } + const isHttp = url.startsWith('http'); + const Transport = isHttp ? Metacom.transport.http : Metacom.transport.ws; + const transport = new Transport(url); + const metacom = new Metacom(url, transport, options); + await metacom.open(); + return metacom; + } + + #bindTransport() { + this.#transport.on('open', () => { + clearTimeout(this.#reconnectTimer); + this.#reconnectTimer = null; + this.emit('open'); + }); + + this.#transport.on('close', () => { + this.emit('close'); + this.#scheduleReconnect(); + }); + + this.#transport.on('error', (error) => { + this.emit('error', error); + }); + + this.#transport.on('message', (data) => { + const escalate = (error) => this.emit('error', error); + if (typeof data === 'string') this.#handlePacket(data).catch(escalate); + else this.#handleBinary(data).catch(escalate); + }); } - static create(url, options) { - const { transport } = Metacom; - const Transport = url.startsWith('ws') ? transport.ws : transport.http; - return new Transport(url, options); + #scheduleReconnect() { + if (this.active) return; + if (!Metacom.connections.has(this)) return; + if (this.#reconnectTimer) return; + this.#reconnectTimer = setTimeout(() => { + this.#reconnectTimer = null; + this.open().catch((error) => this.emit('error', error)); + }, this.#reconnectTimeout); + } + + async open() { + Metacom.connections.add(this); + await this.#transport.open(this.#options); + } + + close() { + clearTimeout(this.#reconnectTimer); + this.#reconnectTimer = null; + Metacom.connections.delete(this); + this.#transport.close(); + } + + write(data) { + this.#transport.write(data); + } + + send(data) { + this.#transport.send(data); } getStream(id) { - const stream = this.streams.get(id); + const stream = this.#streams.get(id); if (stream) return stream; throw new Error(`Stream ${id} is not initialized`); } createStream(name, size) { - const id = ++this.streamId; - const transport = this; - return new MetaWritable(id, name, size, transport); + const id = generateUUID(); + return new MetaWritable(id, name, size, this); } createBlobUploader(blob) { - const name = blob.name || 'blob'; - const size = blob.size; + const { name = 'blob', size } = blob; const consumer = this.createStream(name, size); - return { - id: consumer.id, - upload: async () => { - const reader = blob.stream().getReader(); - let chunk; - while (!(chunk = await reader.read()).done) { - consumer.write(chunk.value); - } - consumer.end(); - }, + const { id } = consumer; + const upload = async () => { + for await (const chunk of blob.stream()) { + consumer.write(chunk); + } + consumer.end(); }; + return { id, upload }; } - async message(data) { - if (data === '{}') return; - this.lastActivity = Date.now(); - let packet; - try { - packet = JSON.parse(data); - } catch { - return; - } + async #handlePacket(data) { + if (this.#proxyPacket) return void this.#proxyPacket(data); + const packet = jsonParse(data); + if (!packet) throw new Error('Invalid JSON packet'); const { type, id, name } = packet; if (type === 'event') { - const [unit, eventName] = name.split('/'); + const parts = name.split('/'); + const unit = parts[0]; + const eventName = parts[1]; const metacomUnit = this.api[unit]; if (metacomUnit) metacomUnit.emit(eventName, packet.data); return; } - if (!id) { - console.error(new Error('Packet structure error')); - return; - } + if (!id) throw new Error('Packet structure error'); if (type === 'callback') { - const promised = this.calls.get(id); - if (!promised) return; - const [resolve, reject, timeout] = promised; - this.calls.delete(id); + const promised = this.#calls.get(id); + if (!promised) throw new Error(`Callback ${id} not found`); + const resolve = promised[0]; + const reject = promised[1]; + const timeout = promised[2]; + this.#calls.delete(id); clearTimeout(timeout); if (packet.error) { return void reject(new MetacomError(packet.error)); } resolve(packet.result); - } else if (type === 'stream') { - const { name, size, status } = packet; - const stream = this.streams.get(id); - if (name && typeof name === 'string' && Number.isSafeInteger(size)) { - if (stream) { - console.error(new Error(`Stream ${name} is already initialized`)); - } else { - const stream = new MetaReadable(id, name, size); - this.streams.set(id, stream); - } - } else if (!stream) { - console.error(new Error(`Stream ${id} is not initialized`)); - } else if (status === 'end') { - await stream.close(); - this.streams.delete(id); - } else if (status === 'terminate') { - await stream.terminate(); - this.streams.delete(id); - } else { - console.error(new Error('Stream packet structure error')); + return; + } + if (type === 'stream') await this.#handleStream(packet); + } + + async #handleStream(packet) { + const { id, name, size, status } = packet; + const stream = this.#streams.get(id); + if (status === undefined) { + if (stream) { + throw new Error(`Stream ${name} is already initialized`); } + const readableStream = new MetaReadable(id, name, size); + this.#streams.set(id, readableStream); + return; + } + if (!stream) throw new Error(`Stream ${id} is not initialized`); + if (status === 'end') { + await stream.close(); + this.#streams.delete(id); + } else if (status === 'terminate') { + await stream.terminate(); + this.#streams.delete(id); } } - async binary(blob) { - const buffer = await blob.arrayBuffer(); - const byteView = new Uint8Array(buffer); + async #handleBinary(input) { + const byteView = await toByteView(input); const { id, payload } = chunkDecode(byteView); - const stream = this.streams.get(id); - if (stream) await stream.push(payload); - else console.warn(`Stream ${id} is not initialized`); + const stream = this.#streams.get(id); + if (!stream) { + throw new Error(`Stream ${id} is not initialized`); + } + await stream.push(payload); } async load(...units) { - const introspect = this.scaffold('system')('introspect'); + if (!this.active) throw new Error('Not connected'); + const introspect = this.#scaffold('system')('introspect'); const introspection = await introspect(units); const available = Object.keys(introspection); for (const unit of units) { if (!available.includes(unit)) continue; - const methods = new MetacomUnit(); + const methods = new Emitter(); const instance = introspection[unit]; - const request = this.scaffold(unit); + const request = this.#scaffold(unit); const methodNames = Object.keys(instance); for (const methodName of methodNames) { methods[methodName] = request(methodName); @@ -165,120 +440,261 @@ class Metacom extends EventEmitter { } } - scaffold(unit, ver) { - return (method) => - async (args = {}) => { - const id = ++this.callId; - const unitName = unit + (ver ? '.' + ver : ''); - const target = unitName + '/' + method; - if (this.opening) await this.opening; - if (!this.connected) await this.open(); + #scaffold(unit, version) { + const createMethod = (methodName) => { + const method = async (args = {}) => { + const id = generateUUID(); + const ver = version ? `.${version}` : ''; + const target = `${unit}${ver}/${methodName}`; + const packet = { type: 'call', id, method: target, args }; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - if (this.calls.has(id)) { - this.calls.delete(id); - reject(new Error('Request timeout')); - } - }, this.callTimeout); - this.calls.set(id, [resolve, reject, timeout]); - const packet = { type: 'call', id, method: target, args }; - this.send(JSON.stringify(packet)); + if (!this.#calls.has(id)) return; + this.#calls.delete(id); + reject(new Error('Request timeout')); + }, this.#callTimeout); + this.#calls.set(id, [resolve, reject, timeout]); + this.send(packet); }); }; + return method; + }; + return createMethod; + } +} + +class ClientWsTransport extends ClientTransport { + #socket = null; + #opening = null; + + async open() { + if (this.active) return Promise.resolve(); + if (this.#opening) return this.#opening; + const opening = new Promise((resolve, reject) => { + const socket = new WebSocket(this.url); + this.#socket = socket; + const onClose = (error) => { + this.#socket = null; + if (this.#opening) { + this.#opening = null; + this.emit('error', error); + return void reject(new Error('Connection closed')); + } else { + this.active = false; + this.emit('close', error); + } + }; + const onOpen = () => { + this.active = true; + this.emit('open'); + this.#opening = null; + resolve(); + }; + socket.addEventListener('open', onOpen, { once: true }); + socket.addEventListener('close', onClose, { once: true }); + socket.addEventListener('error', onClose, { once: true }); + socket.addEventListener('message', ({ data }) => { + this.emit('message', data); + }); + }); + this.#opening = opening; + return opening; + } + + close() { + if (!this.active) return; + this.#socket.close(); + } + + write(data) { + if (!this.active) throw new Error('Not connected'); + this.#socket.send(data); } } -class WebsocketTransport extends Metacom { +class ClientHttpTransport extends ClientTransport { async open() { - if (this.opening) return this.opening; - if (this.connected) return Promise.resolve(); - const socket = new WebSocket(this.url); + if (this.active) return; this.active = true; - this.socket = socket; - connections.add(this); + this.emit('open'); + } - socket.addEventListener('message', ({ data }) => { - if (typeof data === 'string') this.message(data); - else this.binary(data); - }); + close() { + if (!this.active) return; + this.active = false; + this.emit('close'); + } - socket.addEventListener('close', () => { - this.opening = null; - this.connected = false; - this.emit('close'); - setTimeout(() => { - if (this.active) this.open(); - }, this.reconnectTimeout); - }); + write(data) { + const headers = { 'Content-Type': 'application/json' }; + fetch(this.url, { method: 'POST', headers, body: data }) + .then((res) => res.text()) + .then((packet) => this.emit('message', packet)) + .catch((error) => this.emit('error', error)); + } +} - socket.addEventListener('error', (err) => { - this.emit('error', err); - socket.close(); - }); +class ClientEventTransport extends ClientTransport { + static instance = null; - this.ping = setInterval(() => { - if (this.active) { - const interval = Date.now() - this.lastActivity; - if (interval > this.pingInterval) this.send('{}'); - } - }, this.pingInterval); + #port = null; + #worker = null; - this.opening = new Promise((resolve) => { - socket.addEventListener('open', () => { - this.opening = null; - this.connected = true; - this.emit('open'); - resolve(); - }); + static getInstance(url) { + if (ClientEventTransport.instance) { + return ClientEventTransport.instance; + } + const transport = new ClientEventTransport(url); + ClientEventTransport.instance = transport; + return transport; + } + + async open(options = {}) { + if (this.active) return; + const worker = options.worker || this.#worker; + if (!worker) throw new Error('Service Worker not provided'); + this.#worker = worker; + const { port1, port2 } = new MessageChannel(); + this.#port = port1; + port1.addEventListener('message', ({ data }) => { + if (data === undefined) return; + this.emit('message', data); }); - return this.opening; + port1.start(); + this.#worker.postMessage({ type: 'metacom:connect' }, [port2]); + this.active = true; + this.emit('open'); } close() { this.active = false; - connections.delete(this); - clearInterval(this.ping); - if (!this.socket) return; - this.socket.close(); - this.socket = null; + this.#port.close(); + this.#port = null; + this.emit('close'); } - send(data) { - if (!this.connected) return; - this.lastActivity = Date.now(); - this.socket.send(data); + online() { + if (this.#worker) this.#worker.postMessage({ type: 'metacom:online' }); + } + + offline() { + if (this.#worker) this.#worker.postMessage({ type: 'metacom:offline' }); + } + + write(data) { + if (!this.#port) throw new Error('Not connected'); + this.#port.postMessage(data); } } -class HttpTransport extends Metacom { +class MetacomProxy extends Emitter { + #ports = new Set(); + #pending = new Map(); + #connection = null; + #callTimeout = CALL_TIMEOUT; + #reconnectTimeout = RECONNECT_TIMEOUT; + + constructor(options = {}) { + super(); + const { callTimeout, reconnectTimeout } = options; + if (callTimeout) this.#callTimeout = callTimeout; + if (reconnectTimeout) this.#reconnectTimeout = reconnectTimeout; + if (typeof self === 'undefined') { + throw new Error('MetacomProxy must run in ServiceWorker context'); + } + self.addEventListener('message', (event) => { + const { type } = event.data; + if (type?.startsWith('metacom')) this.#handleEvent(event); + }); + } + async open() { - this.active = true; - this.connected = true; - this.emit('open'); + if (this.#connection) { + if (this.#connection.connected) return; + await this.#connection.open(); + return; + } + const protocol = self.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = `${protocol}//${self.location.host}`; + const options = { + callTimeout: this.#callTimeout, + reconnectTimeout: this.#reconnectTimeout, + proxy: (data) => this.#proxyPacket(data), + }; + this.#connection = await Metacom.connect(url, options); } close() { - this.active = false; - this.connected = false; + if (!this.#connection) return; + this.#connection.close(); + this.#connection = null; } - send(data) { - this.lastActivity = Date.now(); - fetch(this.url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: data, - }).then((res) => - res.text().then((packet) => { - this.message(packet); - }), - ); + #handleEvent(event) { + const { type } = event.data; + if (type === 'metacom:connect') { + const port = event.ports[0]; + if (!port) throw new Error('MessagePort not provided'); + this.#ports.add(port); + port.addEventListener('message', (messageEvent) => { + this.#handleMessage(messageEvent, port); + }); + port.start(); + return; + } + if (type === 'metacom:online') Metacom.online(); + else if (type === 'metacom:offline') Metacom.offline(); + else throw new Error(`Unknown event: ${type}`); + } + + async #handleMessage(event, port) { + const { data } = event; + if (data === undefined) throw new Error('Message data is undefined'); + await this.open(); + if (!this.#connection || !this.#connection.active) { + throw new Error('Not connected to server'); + } + const packet = jsonParse(data); + if (!packet || !packet.id) throw new Error('Invalid JSON packet'); + this.#pending.set(packet.id, port); + this.#connection.write(data); + } + + #proxyPacket(data) { + if (typeof data !== 'string') return void this.#broadcast(data); + const packet = jsonParse(data); + if (!packet) return void this.#broadcast(data); + const { type, id, status } = packet; + if (type === 'event') return void this.#broadcast(data); + const port = this.#pending.get(id); + if (!port) return void this.#broadcast(data); + port.postMessage(data); + if (type === 'callback') return void this.#pending.delete(id); + if (type !== 'stream') return; + if (status === 'end' || status === 'terminate') this.#pending.delete(id); + } + + #broadcast(data, excludePort = null) { + for (const port of this.#ports) { + if (port === excludePort) continue; + port.postMessage(data); + } } } Metacom.transport = { - ws: WebsocketTransport, - http: HttpTransport, + ws: ClientWsTransport, + http: ClientHttpTransport, + event: ClientEventTransport, }; -export { Metacom, MetacomUnit }; +Metacom.initialize(); + +export { + chunkEncode, + chunkDecode, + MetaReadable, + MetaWritable, + Metacom, + MetacomProxy, +}; diff --git a/application/static/metautil.js b/application/static/metautil.js new file mode 100644 index 00000000..3f6c0f78 --- /dev/null +++ b/application/static/metautil.js @@ -0,0 +1,1046 @@ +// error.js + +class Error extends globalThis.Error { + constructor(message, options = {}) { + super(message); + const hasOptions = typeof options === 'object'; + const { code, cause } = hasOptions ? options : { code: options }; + this.code = code; + this.cause = cause; + } +} + +class DomainError extends Error { + constructor(code, options = {}) { + const hasCode = typeof code !== 'object'; + const opt = hasCode ? { ...options, code } : code; + super('Domain error', opt); + } + + toError(errors) { + const { code, cause } = this; + const message = errors[this.code] || this.message; + return new Error(message, { code, cause }); + } +} + +const isError = (err) => err?.constructor?.name?.includes('Error') || false; + +// strings.js + +const replace = (str, substr, newstr) => { + if (substr === '') return str; + let src = str; + let res = ''; + do { + const index = src.indexOf(substr); + if (index === -1) return res + src; + const start = src.substring(0, index); + src = src.substring(index + substr.length, src.length); + res += start + newstr; + } while (true); +}; + +const between = (s, prefix, suffix) => { + let i = s.indexOf(prefix); + if (i === -1) return ''; + s = s.substring(i + prefix.length); + if (suffix) { + i = s.indexOf(suffix); + if (i === -1) return ''; + s = s.substring(0, i); + } + return s; +}; + +const split = (s, separator) => { + const i = s.indexOf(separator); + if (i < 0) return [s, '']; + return [s.slice(0, i), s.slice(i + separator.length)]; +}; + +const inRange = (x, min, max) => x >= min && x <= max; + +const isFirstUpper = (s) => !!s && inRange(s[0], 'A', 'Z'); + +const isFirstLower = (s) => !!s && inRange(s[0], 'a', 'z'); + +const isFirstLetter = (s) => isFirstUpper(s) || isFirstLower(s); + +const toLowerCamel = (s) => s.charAt(0).toLowerCase() + s.slice(1); + +const toUpperCamel = (s) => s.charAt(0).toUpperCase() + s.slice(1); + +const toLower = (s) => s.toLowerCase(); + +const toCamel = (separator) => (s) => { + const words = s.split(separator); + const first = words.length > 0 ? words.shift().toLowerCase() : ''; + return first + words.map(toLower).map(toUpperCamel).join(''); +}; + +const spinalToCamel = toCamel('-'); + +const snakeToCamel = toCamel('_'); + +const isConstant = (s) => s === s.toUpperCase(); + +const fileExt = (fileName) => { + const dot = fileName.lastIndexOf('.'); + const slash = fileName.lastIndexOf('/'); + if (slash > dot) return ''; + return fileName.substring(dot + 1, fileName.length).toLowerCase(); +}; + +const trimLines = (s) => { + const chunks = s.split('\n').map((d) => d.trim()); + return chunks.filter((d) => d !== '').join('\n'); +}; + +// array.js + +const sample = (array, random = Math.random) => { + const index = Math.floor(random() * array.length); + return array[index]; +}; + +const shuffle = (array, random = Math.random) => { + // Based on the algorithm described here: + // https://en.wikipedia.org/wiki/Fisher-Yates_shuffle + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; + +const projection = (source, fields) => { + const entries = []; + for (const key of fields) { + if (Object.hasOwn(source, key)) { + const value = source[key]; + entries.push([key, value]); + } + } + return Object.fromEntries(entries); +}; + +// async.js + +const toBool = [() => true, () => false]; + +const timeout = (msec, signal = null) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Timeout of ${msec}ms reached`, 'ETIMEOUT')); + }, msec); + if (!signal) return; + signal.addEventListener('abort', () => { + clearTimeout(timer); + reject(new Error('Timeout aborted')); + }); + }); + +const delay = (msec, signal = null) => + new Promise((resolve, reject) => { + const timer = setTimeout(resolve, msec); + if (!signal) return; + signal.addEventListener('abort', () => { + clearTimeout(timer); + reject(new Error('Delay aborted')); + }); + }); + +const timeoutify = (promise, msec) => + new Promise((resolve, reject) => { + let timer = setTimeout(() => { + timer = null; + reject(new Error(`Timeout of ${msec}ms reached`, 'ETIMEOUT')); + }, msec); + promise.then(resolve, reject).finally(() => { + if (timer) clearTimeout(timer); + }); + }); + +// datetime.js + +const DURATION_UNITS = { + d: 86400, // days + h: 3600, // hours + m: 60, // minutes + s: 1, // seconds +}; + +const duration = (s) => { + if (typeof s === 'number') return s; + if (typeof s !== 'string') return 0; + let result = 0; + const parts = s.split(' '); + for (const part of parts) { + const unit = part.slice(-1); + const value = parseInt(part.slice(0, -1)); + const mult = DURATION_UNITS[unit]; + if (!isNaN(value) && mult) result += value * mult; + } + return result * 1000; +}; + +const twoDigit = (n) => n.toString().padStart(2, '0'); + +const nowDate = (date = new Date()) => { + const yyyy = date.getUTCFullYear().toString(); + const mm = twoDigit(date.getUTCMonth() + 1); + const dd = twoDigit(date.getUTCDate()); + return `${yyyy}-${mm}-${dd}`; +}; + +const nowDateTimeUTC = (date = new Date(), timeSep = ':') => { + const yyyy = date.getUTCFullYear().toString(); + const mm = twoDigit(date.getUTCMonth() + 1); + const dd = twoDigit(date.getUTCDate()); + const hh = twoDigit(date.getUTCHours()); + const min = twoDigit(date.getUTCMinutes()); + const ss = twoDigit(date.getUTCSeconds()); + return `${yyyy}-${mm}-${dd}T${hh}${timeSep}${min}${timeSep}${ss}`; +}; + +const MONTHS = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + +const NAME_LEN = 3; + +const parseMonth = (s) => { + const name = s.substring(0, NAME_LEN); + const i = MONTHS.indexOf(name); + return i >= 0 ? i + 1 : -1; +}; + +const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + +const parseDay = (s) => { + const name = s.substring(0, NAME_LEN); + const i = DAYS.indexOf(name); + return i >= 0 ? i + 1 : -1; +}; + +const ORDINAL = ['st', 'nd', 'rd', 'th']; + +const isOrdinal = (s) => ORDINAL.some((d) => s.endsWith(d)); + +const YEAR_LEN = 4; + +const parseEvery = (s = '') => { + let YY = -1; + let MM = -1; + let DD = -1; + let wd = -1; + let hh = -1; + let mm = -1; + let ms = 0; + const parts = s.split(' '); + for (const part of parts) { + if (part.includes(':')) { + const [h, m] = split(part, ':'); + if (h !== '') hh = parseInt(h); + mm = m === '' ? 0 : parseInt(m); + continue; + } + if (isOrdinal(part)) { + DD = parseInt(part); + continue; + } + if (part.length === YEAR_LEN) { + YY = parseInt(part); + continue; + } + if (MM === -1) { + MM = parseMonth(part); + if (MM > -1) continue; + } + if (wd === -1) { + wd = parseDay(part); + if (wd > -1) continue; + } + const unit = part.slice(-1); + const mult = DURATION_UNITS[unit]; + if (typeof mult === 'number') { + const value = parseInt(part); + if (!isNaN(value)) ms += value * mult; + } + } + return { YY, MM, DD, wd, hh, mm, ms: ms > 0 ? ms * 1000 : -1 }; +}; + +const nextEvent = (ev, d = new Date()) => { + let ms = 0; + const Y = d.getUTCFullYear(); + const M = d.getUTCMonth() + 1; + const D = d.getUTCDate(); + const w = d.getUTCDay() + 1; + const h = d.getUTCHours(); + const m = d.getUTCMinutes(); + + const iY = ev.YY > -1; + const iM = ev.MM > -1; + const iD = ev.DD > -1; + const iw = ev.wd > -1; + const ih = ev.hh > -1; + const im = ev.mm > -1; + const ims = ev.ms > -1; + + if (iY && ev.YY !== Y) return ev.YY < Y ? -1 : 0; + if (iM && ev.MM !== M) return ev.MM < M ? -1 : 0; + if (iD && ev.DD !== D) return ev.DD < D ? -1 : 0; + if (iw && ev.wd !== w) return 0; + if (ih && (ev.hh < h || (ev.hh === h && im && ev.mm < m))) return -1; + + if (ih) ms += (ev.hh - h) * DURATION_UNITS.h; + if (im) ms += (ev.mm - m) * DURATION_UNITS.m; + + ms *= 1000; + if (ims) ms += ev.ms; + return ms; +}; + +// objects.js + +const makePrivate = (instance) => { + const iface = {}; + const fields = Object.keys(instance); + for (const fieldName of fields) { + const field = instance[fieldName]; + if (isConstant(fieldName)) { + iface[fieldName] = field; + } else if (typeof field === 'function') { + const boundMethod = field.bind(instance); + iface[fieldName] = boundMethod; + instance[fieldName] = boundMethod; + } + } + return iface; +}; + +const protect = (allowMixins, ...namespaces) => { + for (const namespace of namespaces) { + const names = Object.keys(namespace); + for (const name of names) { + const target = namespace[name]; + if (!allowMixins.includes(name)) Object.freeze(target); + } + } +}; + +const jsonParse = (data) => { + if (data === null || data === undefined) return null; + if (data.length === 0) return null; + try { + return JSON.parse(data); + } catch { + return null; + } +}; + +const isHashObject = (o) => + typeof o === 'object' && o !== null && !Array.isArray(o); + +const flatObject = (source, fields = []) => { + const target = {}; + for (const [key, value] of Object.entries(source)) { + if (!isHashObject(value)) { + target[key] = value; + continue; + } + if (fields.length > 0 && !fields.includes(key)) { + target[key] = { ...value }; + continue; + } + for (const [childKey, childValue] of Object.entries(value)) { + const combined = `${key}${toUpperCamel(childKey)}`; + if (source[combined] !== undefined) { + const error = `Can not combine keys: key "${combined}" already exists`; + throw new Error(error); + } + target[combined] = childValue; + } + } + return target; +}; + +const unflatObject = (source, fields) => { + const result = {}; + for (const [key, value] of Object.entries(source)) { + const prefix = fields.find((name) => key.startsWith(name)); + if (prefix) { + if (Object.hasOwn(source, prefix)) { + throw new Error(`Can not combine keys: key "${prefix}" already exists`); + } + const newKey = key.substring(prefix.length).toLowerCase(); + const section = result[prefix]; + if (section) section[newKey] = value; + else result[prefix] = { [newKey]: value }; + continue; + } + result[key] = value; + } + return result; +}; + +const getSignature = (method) => { + const src = method.toString(); + const signature = between(src, '({', '})'); + if (signature === '') return []; + return signature.split(',').map((s) => s.trim()); +}; + +const namespaceByPath = (namespace, path) => { + const [key, rest] = split(path, '.'); + const step = namespace[key]; + if (!step) return null; + if (rest === '') return step; + return namespaceByPath(step, rest); +}; + +const serializeArguments = (fields, args) => { + if (!fields) return ''; + const data = {}; + for (const par of fields) { + data[par] = args[par]; + } + return JSON.stringify(data); +}; + +const firstKey = (obj) => Object.keys(obj).find(isFirstLetter); + +const isInstanceOf = (obj, constrName) => obj?.constructor?.name === constrName; + +// collector.js + +class Collector { + done = false; + data = {}; + keys = []; + count = 0; + exact = true; + reassign = false; + timeout = 0; + defaults = {}; + validate = null; + #fulfill = null; + #reject = null; + #cause = null; + #controller = null; + #signal = null; + #timeout = null; + + constructor(keys, options = {}) { + const { exact = true, reassign = false } = options; + const { timeout = 0, defaults = {}, validate } = options; + if (validate) this.validate = validate; + this.keys = keys; + if (exact === false) this.exact = false; + if (reassign === false) this.reassign = reassign; + if (typeof defaults === 'object') this.defaults = defaults; + this.#controller = new AbortController(); + this.#signal = this.#controller.signal; + if (typeof timeout === 'number' && timeout > 0) { + this.#timeout = AbortSignal.timeout(timeout); + this.#signal = AbortSignal.any([this.#signal, this.#timeout]); + this.#signal.addEventListener('abort', () => { + if (Object.keys(this.defaults).length > 0) this.#default(); + if (this.done) return; + this.fail(this.#signal.reason); + }); + } + } + + #default() { + for (const [key, value] of Object.entries(this.defaults)) { + if (this.data[key] === undefined) this.set(key, value); + } + } + + get signal() { + return this.#signal; + } + + set(key, value) { + if (this.done) return; + const expected = this.keys.includes(key); + if (!expected && this.exact) { + this.fail(new Error('Unexpected key: ' + key)); + return; + } + const has = this.data[key] !== undefined; + if (has && !this.reassign) { + const error = new Error('Collector reassign mode is off'); + return void this.fail(error); + } + if (!has && expected) this.count++; + this.data[key] = value; + if (this.count === this.keys.length) { + this.done = true; + this.#timeout = null; + if (this.#fulfill) this.#fulfill(this.data); + } + } + + take(key, fn, ...args) { + fn(...args, (error, data) => { + if (error) this.fail(error); + else this.set(key, data); + }); + } + + wait(key, fn, ...args) { + const promise = fn instanceof Promise ? fn : fn(...args); + promise.then( + (data) => this.set(key, data), + (error) => this.fail(error), + ); + } + + collect(sources) { + for (const [key, collector] of Object.entries(sources)) { + collector.then( + (data) => this.set(key, data), + (error) => this.fail(error), + ); + } + } + + fail(error) { + this.done = true; + this.#timeout = null; + const cause = error || new Error('Collector aborted'); + this.#cause = cause; + this.#controller.abort(); + if (this.#reject) this.#reject(cause); + } + + abort() { + this.fail(); + } + + then(onFulfilled, onRejected = null) { + return new Promise((resolve, reject) => { + this.#fulfill = resolve; + this.#reject = reject; + if (!this.done) return; + if (this.validate) { + try { + this.validate(this.data); + } catch (error) { + this.#cause = error; + } + } + if (this.#cause) reject(this.#cause); + else resolve(this.data); + }).then(onFulfilled, onRejected); + } +} + +const collect = (keys, options) => new Collector(keys, options); + +// events.js + +const DONE = { done: true, value: undefined }; + +class EventIterator { + #resolvers = []; + #emitter = null; + #eventName = ''; + #listener = null; + #onerror = null; + #done = false; + + constructor(emitter, eventName) { + this.#emitter = emitter; + this.#eventName = eventName; + + this.#listener = (value) => { + const resolvers = this.#resolvers; + this.#resolvers = []; + for (const resolver of resolvers) { + resolver.resolve({ done: this.#done, value }); + } + }; + emitter.on(eventName, this.#listener); + + this.#onerror = (error) => { + const resolvers = this.#resolvers; + this.#resolvers = []; + for (const resolver of resolvers) { + resolver.reject(error); + } + this.#finalize(); + }; + emitter.on('error', this.#onerror); + } + + next() { + return new Promise((resolve, reject) => { + if (this.#done) return void resolve(DONE); + this.#resolvers.push({ resolve, reject }); + }); + } + + #finalize() { + if (this.#done) return; + this.#done = true; + this.#emitter.off(this.#eventName, this.#listener); + this.#emitter.off('error', this.#onerror); + for (const resolver of this.#resolvers) { + resolver.resolve(DONE); + } + this.#resolvers.length = 0; + } + + async return() { + this.#finalize(); + return DONE; + } + + async throw() { + this.#finalize(); + return DONE; + } +} + +class EventIterable { + #emitter = null; + #eventName = ''; + + constructor(emitter, eventName) { + this.#emitter = emitter; + this.#eventName = eventName; + } + + [Symbol.asyncIterator]() { + return new EventIterator(this.#emitter, this.#eventName); + } +} + +class Emitter { + #events = new Map(); + #maxListeners = 10; + + constructor(options = {}) { + this.#maxListeners = options.maxListeners ?? 10; + } + + emit(eventName, value) { + const event = this.#events.get(eventName); + if (!event) { + if (eventName !== 'error') return Promise.resolve(); + throw new Error('Unhandled error'); + } + const listeners = event.on.slice(); + const promises = listeners.map(async (fn) => fn(value)); + if (event.once.size > 0) { + const len = event.on.length; + const remaining = new Array(len); + let index = 0; + for (let i = 0; i < len; i++) { + const listener = event.on[i]; + if (!event.once.has(listener)) remaining[index++] = listener; + } + if (index === 0) { + this.#events.delete(eventName); + } else { + remaining.length = index; + this.#events.set(eventName, { on: remaining, once: new Set() }); + } + } + return Promise.all(promises).then(() => undefined); + } + + #addListener(eventName, listener, once) { + let event = this.#events.get(eventName); + if (!event) { + const on = [listener]; + event = { on, once: once ? new Set(on) : new Set() }; + this.#events.set(eventName, event); + } else { + if (event.on.includes(listener)) { + throw new Error('Duplicate listeners detected'); + } + event.on.push(listener); + if (once) event.once.add(listener); + } + if (event.on.length > this.#maxListeners) { + throw new Error( + `MaxListenersExceededWarning: Possible memory leak. ` + + `Current maxListeners is ${this.#maxListeners}.`, + ); + } + } + + on(eventName, listener) { + this.#addListener(eventName, listener, false); + } + + once(eventName, listener) { + this.#addListener(eventName, listener, true); + } + + off(eventName, listener) { + if (!listener) return void this.#events.delete(eventName); + const event = this.#events.get(eventName); + if (!event) return; + const index = event.on.indexOf(listener); + if (index > -1) event.on.splice(index, 1); + event.once.delete(listener); + } + + toPromise(eventName) { + return new Promise((resolve) => { + this.once(eventName, resolve); + }); + } + + toAsyncIterable(eventName) { + return new EventIterable(this, eventName); + } + + clear(eventName) { + if (!eventName) return void this.#events.clear(); + this.#events.delete(eventName); + } + + listeners(eventName) { + if (!eventName) throw new Error('Expected eventName'); + const event = this.#events.get(eventName); + return event ? event.on : []; + } + + listenerCount(eventName) { + if (!eventName) throw new Error('Expected eventName'); + const event = this.#events.get(eventName); + return event ? event.on.length : 0; + } + + eventNames() { + return Array.from(this.#events.keys()); + } +} + +// http.js + +const parseHost = (host) => { + if (!host) return 'no-host-name-in-http-headers'; + const portOffset = host.indexOf(':'); + if (portOffset > -1) return host.substring(0, portOffset); + return host; +}; + +const parseParams = (params) => Object.fromEntries(new URLSearchParams(params)); + +const parseCookies = (cookie) => { + const values = []; + const items = cookie.split(';'); + for (const item of items) { + const [key, val = ''] = item.split('='); + values.push([key.trim(), val.trim()]); + } + return Object.fromEntries(values); +}; + +const parseRange = (range) => { + if (!range || !range.includes('=')) return {}; + const bytes = range.split('=').pop(); + if (!bytes || !range.includes('-')) return {}; + const [start, end] = bytes.split('-').map((n) => parseInt(n)); + if (isNaN(start)) return isNaN(end) ? {} : { tail: end }; + return isNaN(end) ? { start } : { start, end }; +}; + +// pool.js + +class Pool { + constructor(options = {}) { + this.items = []; + this.free = []; + this.queue = []; + this.timeout = options.timeout || 0; + this.current = 0; + this.size = 0; + this.available = 0; + } + + async next(exclusive = false) { + if (this.size === 0) return null; + if (this.available === 0) { + return new Promise((resolve, reject) => { + const waiting = { resolve, timer: null }; + waiting.timer = setTimeout(() => { + waiting.resolve = null; + this.queue.shift(); + reject(new Error('Pool next item timeout')); + }, this.timeout); + this.queue.push(waiting); + }); + } + let item = null; + let free = false; + let attempts = 0; + do { + item = this.items[this.current]; + free = this.free[this.current]; + this.current++; + if (this.current === this.size) this.current = 0; + if (++attempts > this.size) return null; + } while (!item || !free); + if (exclusive) { + const index = this.items.indexOf(item); + this.free[index] = false; + this.available--; + } + return item; + } + + add(item) { + if (this.items.includes(item)) throw new Error('Pool: add duplicates'); + this.size++; + this.available++; + this.items.push(item); + this.free.push(true); + } + + async capture() { + return this.next(true); + } + + release(item) { + const index = this.items.indexOf(item); + if (index < 0) throw new Error('Pool: release unexpected item'); + if (this.free[index]) throw new Error('Pool: release not captured'); + if (this.queue.length > 0) { + const { resolve, timer } = this.queue.shift(); + clearTimeout(timer); + if (resolve) return void setTimeout(resolve, 0, item); + } + this.free[index] = true; + this.available++; + } + + isFree(item) { + const index = this.items.indexOf(item); + if (index < 0) return false; + return this.free[index]; + } +} + +// semaphore.js + +class Semaphore { + constructor({ concurrency, size = 0, timeout = 0 }) { + this.concurrency = concurrency; + this.counter = concurrency; + this.timeout = timeout; + this.size = size; + this.queue = []; + this.empty = true; + } + + async enter() { + return new Promise((resolve, reject) => { + if (this.counter > 0) { + this.counter--; + this.empty = false; + return void resolve(); + } + if (this.queue.length >= this.size) { + return void reject(new Error('Semaphore queue is full')); + } + const waiting = { resolve, timer: null }; + waiting.timer = setTimeout(() => { + waiting.resolve = null; + this.queue.shift(); + const { counter, concurrency } = this; + this.empty = this.queue.length === 0 && counter === concurrency; + reject(new Error('Semaphore timeout')); + }, this.timeout); + this.queue.push(waiting); + this.empty = false; + }); + } + + leave() { + if (this.queue.length === 0) { + this.counter++; + this.empty = this.counter === this.concurrency; + return; + } + const { resolve, timer } = this.queue.shift(); + clearTimeout(timer); + if (resolve) setTimeout(resolve, 0); + const { counter, concurrency } = this; + this.empty = this.queue.length === 0 && counter === concurrency; + } +} + +// units.js + +const SIZE_UNITS = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + +const bytesToSize = (bytes) => { + if (bytes === 0) return '0'; + const exp = Math.floor(Math.log(bytes) / Math.log(1000)); + const size = bytes / 1000 ** exp; + const short = Math.round(size); + const unit = exp === 0 ? '' : ' ' + SIZE_UNITS[exp - 1]; + return short.toString() + unit; +}; + +const UNIT_SIZES = { + yb: 24, // yottabyte + zb: 21, // zettabyte + eb: 18, // exabyte + pb: 15, // petabyte + tb: 12, // terabyte + gb: 9, // gigabyte + mb: 6, // megabyte + kb: 3, // kilobyte +}; + +const sizeToBytes = (size) => { + const length = size.length; + const unit = size.substring(length - 2, length).toLowerCase(); + const value = parseInt(size, 10); + const exp = UNIT_SIZES[unit]; + if (!exp) return value; + return value * 10 ** exp; +}; + +// browser.js + +const UINT32_MAX = 0xffffffff; +const BUF_LEN = 1024; +const BUF_SIZE = BUF_LEN * Uint32Array.BYTES_PER_ELEMENT; + +const randomPrefetcher = { + buf: new Uint8Array(BUF_SIZE), + view: null, + pos: 0, + next() { + const { buf, view, pos } = this; + let start = pos; + if (start === buf.length) { + start = 0; + crypto.getRandomValues(buf); + } + const rnd = view.getUint32(start, true) / (UINT32_MAX + 1); + this.pos = start + Uint32Array.BYTES_PER_ELEMENT; + return rnd; + }, +}; + +crypto.getRandomValues(randomPrefetcher.buf); +randomPrefetcher.view = new DataView( + randomPrefetcher.buf.buffer, + randomPrefetcher.buf.byteOffset, + randomPrefetcher.buf.byteLength, +); + +const cryptoRandom = (min, max) => { + const rnd = randomPrefetcher.next(); + if (min === undefined) return rnd; + const [a, b] = max === undefined ? [0, min] : [min, max]; + return a + Math.floor(rnd * (b - a + 1)); +}; + +const random = (min, max) => { + const rnd = Math.random(); + if (min === undefined) return rnd; + const [a, b] = max === undefined ? [0, min] : [min, max]; + return a + Math.floor(rnd * (b - a + 1)); +}; + +const generateUUID = () => crypto.randomUUID(); + +const latin1Decoder = new TextDecoder('latin1'); + +const generateKey = (possible, length) => { + if (length < 0) return ''; + const base = possible.length; + if (base < 1) return ''; + const key = new Uint8Array(length); + for (let i = 0; i < length; i++) { + const index = cryptoRandom(0, base - 1); + key[i] = possible.charCodeAt(index); + } + return latin1Decoder.decode(key); +}; + +export { + Error, + DomainError, + isError, + replace, + between, + split, + isFirstUpper, + isFirstLower, + isFirstLetter, + toLowerCamel, + toUpperCamel, + toLower, + toCamel, + spinalToCamel, + snakeToCamel, + isConstant, + fileExt, + trimLines, + sample, + shuffle, + projection, + toBool, + timeout, + delay, + timeoutify, + duration, + nowDate, + nowDateTimeUTC, + parseMonth, + parseDay, + parseEvery, + nextEvent, + makePrivate, + protect, + jsonParse, + isHashObject, + flatObject, + unflatObject, + getSignature, + namespaceByPath, + serializeArguments, + firstKey, + isInstanceOf, + Collector, + collect, + Emitter, + parseHost, + parseParams, + parseCookies, + parseRange, + Pool, + Semaphore, + bytesToSize, + sizeToBytes, + cryptoRandom, + random, + generateUUID, + generateKey, +}; diff --git a/application/static/streams.js b/application/static/streams.js deleted file mode 100644 index 1a408920..00000000 --- a/application/static/streams.js +++ /dev/null @@ -1,167 +0,0 @@ -import { EventEmitter } from './events.js'; - -const ID_LENGTH = 4; - -const chunkEncode = (id, payload) => { - const chunk = new Uint8Array(ID_LENGTH + payload.length); - const view = new DataView(chunk.buffer); - view.setInt32(0, id); - chunk.set(payload, ID_LENGTH); - return chunk; -}; - -const chunkDecode = (chunk) => { - const view = new DataView(chunk.buffer); - const id = view.getInt32(0); - const payload = chunk.subarray(ID_LENGTH); - return { id, payload }; -}; - -const PUSH_EVENT = Symbol(); -const PULL_EVENT = Symbol(); -const DEFAULT_HIGH_WATER_MARK = 32; -const MAX_HIGH_WATER_MARK = 1000; - -class MetaReadable extends EventEmitter { - constructor(id, name, size, options = {}) { - super(); - this.id = id; - this.name = name; - this.size = size; - this.highWaterMark = options.highWaterMark || DEFAULT_HIGH_WATER_MARK; - this.queue = []; - this.streaming = true; - this.status = 'active'; - this.bytesRead = 0; - this.maxListenersCount = this.getMaxListeners() - 1; - } - - async push(data) { - if (this.queue.length > this.highWaterMark) { - this.checkStreamLimits(); - await this.waitEvent(PULL_EVENT); - return this.push(data); - } - this.queue.push(data); - if (this.queue.length === 1) this.emit(PUSH_EVENT); - return data; - } - - async finalize(writable) { - const waitWritableEvent = EventEmitter.once.bind(this, writable); - const onError = () => this.terminate(); - writable.once('error', onError); - for await (const chunk of this) { - const needDrain = !writable.write(chunk); - if (needDrain) await waitWritableEvent('drain'); - } - this.emit('end'); - writable.end(); - await waitWritableEvent('close'); - await this.close(); - writable.removeListener('error', onError); - } - - pipe(writable) { - this.finalize(writable); - return writable; - } - - async toBlob(type = '') { - const chunks = []; - for await (const chunk of this) { - chunks.push(chunk); - } - return new Blob(chunks, { type }); - } - - async close() { - await this.stop(); - this.status = 'closed'; - } - - async terminate() { - await this.stop(); - this.status = 'terminated'; - } - - async stop() { - while (this.bytesRead !== this.size) { - await this.waitEvent(PULL_EVENT); - } - this.streaming = false; - this.emit(PUSH_EVENT, null); - } - - async read() { - if (this.queue.length > 0) return this.pull(); - const finisher = await this.waitEvent(PUSH_EVENT); - if (finisher === null) return null; - return this.pull(); - } - - pull() { - const data = this.queue.shift(); - this.bytesRead += data.length; - this.emit(PULL_EVENT); - return data; - } - - // increase queue if source is much faster than reader - // implement remote backpressure to resolve - checkStreamLimits() { - if (this.listenerCount(PULL_EVENT) >= this.maxListenersCount) { - ++this.highWaterMark; - } - if (this.highWaterMark > MAX_HIGH_WATER_MARK) { - throw new Error('Stream overflow occurred'); - } - } - - waitEvent(event) { - return new Promise((resolve) => this.once(event, resolve)); - } - - async *[Symbol.asyncIterator]() { - while (this.streaming) { - const chunk = await this.read(); - if (!chunk) return; - yield chunk; - } - } -} - -class MetaWritable extends EventEmitter { - constructor(id, name, size, transport) { - super(); - this.id = id; - this.name = name; - this.size = size; - this.transport = transport; - this.init(); - } - - init() { - const { id, name, size } = this; - const packet = { type: 'stream', id, name, size }; - this.transport.send(JSON.stringify(packet)); - } - - write(data) { - const chunk = chunkEncode(this.id, data); - this.transport.send(chunk); - return true; - } - - end() { - const packet = { type: 'stream', id: this.id, status: 'end' }; - this.transport.send(JSON.stringify(packet)); - } - - terminate() { - const packet = { type: 'stream', id: this.id, status: 'terminate' }; - this.transport.send(JSON.stringify(packet)); - } -} - -export { chunkEncode, chunkDecode, MetaReadable, MetaWritable }; diff --git a/application/static/worker.js b/application/static/worker.js index ca62b75b..41ae8973 100644 --- a/application/static/worker.js +++ b/application/static/worker.js @@ -1,7 +1,7 @@ const files = [ '/', '/console.css', - '/events.js', + '/metautil.js', '/console.js', '/metacom.js', '/favicon.ico', diff --git a/package-lock.json b/package-lock.json index 915f94d1..90e8f13f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,14 @@ "version": "3.1.0", "license": "MIT", "dependencies": { - "impress": "^3.1.2", - "metasql": "^3.0.0-alpha.4", - "pg": "^8.15.6", - "redis": "^5.9.0" + "impress": "^3.1.2" }, "devDependencies": { - "@types/node": "^24.10.0", - "@types/pg": "^8.15.5", + "@types/node": "^24.12.0", "@types/ws": "^8.18.1", - "eslint": "^9.39.1", - "eslint-config-metarhia": "^9.1.3", - "prettier": "^3.6.2", + "eslint": "^9.39.4", + "eslint-config-metarhia": "^9.1.8", + "prettier": "^3.8.1", "typescript": "^5.9.3" }, "engines": { @@ -28,9 +24,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -70,15 +66,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -111,20 +107,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -135,9 +131,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -172,29 +168,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -236,67 +246,6 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@redis/bloom": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.9.0.tgz", - "integrity": "sha512-W9D8yfKTWl4tP8lkC3MRYkMz4OfbuzE/W8iObe0jFgoRmgMfkBV+Vj38gvIqZPImtY0WB34YZkX3amYuQebvRQ==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.9.0" - } - }, - "node_modules/@redis/client": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.9.0.tgz", - "integrity": "sha512-EI0Ti5pojD2p7TmcS7RRa+AJVahdQvP/urpcSbK/K9Rlk6+dwMJTQ354pCNGCwfke8x4yKr5+iH85wcERSkwLQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@redis/json": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.9.0.tgz", - "integrity": "sha512-Bm2jjLYaXdUWPb9RaEywxnjmzw7dWKDZI4MS79mTWPV16R982jVWBj6lY2ZGelJbwxHtEVg4/FSVgYDkuO/MxA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.9.0" - } - }, - "node_modules/@redis/search": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.9.0.tgz", - "integrity": "sha512-jdk2csmJ29DlpvCIb2ySjix2co14/0iwIT3C0I+7ZaToXgPbgBMB+zfEilSuncI2F9JcVxHki0YtLA0xX3VdpA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.9.0" - } - }, - "node_modules/@redis/time-series": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.9.0.tgz", - "integrity": "sha512-W6ILxcyOqhnI7ELKjJXOktIg3w4+aBHugDbVpgVLPZ+YDjObis1M0v7ZzwlpXhlpwsfePfipeSK+KWNuymk52w==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.9.0" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -312,27 +261,15 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -344,12 +281,11 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -368,9 +304,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -415,9 +351,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -452,15 +388,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -489,9 +416,9 @@ "license": "MIT" }, "node_modules/concolor": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/concolor/-/concolor-1.1.3.tgz", - "integrity": "sha512-jr+xdyBVfxtOS1oyuwmZiAnj99g6KEv2HkO4MKpZkuzlPVDZA6Gr71dJiTcL8dZdHYCwBLzLsp4solYXK6d7Tw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/concolor/-/concolor-1.1.4.tgz", + "integrity": "sha512-mEYtFyDdCiAJh1LSLz6hRAG8qBAClQ/S6MwHS2ZmictjewO5HsDFLphT8ReKoFUsY0RfOmQ6Ff/DpXWSRCmS0Q==", "license": "MIT", "engines": { "node": ">=18" @@ -555,26 +482,25 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -593,7 +519,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -616,32 +542,48 @@ } }, "node_modules/eslint-config-metarhia": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/eslint-config-metarhia/-/eslint-config-metarhia-9.1.3.tgz", - "integrity": "sha512-do2FegACEZDSR4Hai6A5sPL3g1NSoj5dKkafXjKl2HNiuzbawRezsnOXpd2DSu0tnux4AbWl/pLRI+jL/DHp6Q==", + "version": "9.1.9", + "resolved": "https://registry.npmjs.org/eslint-config-metarhia/-/eslint-config-metarhia-9.1.9.tgz", + "integrity": "sha512-M/KVtu9cVeyyLpIseTzmAz3/qraugFaVw6SSnwmvrpKwTgtWz6UowZfK3wSpj1+x0DMIyese65YGfLxETt44Wg==", "dev": true, "license": "MIT", "dependencies": { - "eslint": "^9.34.0", + "@eslint/js": "^9.39.4", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "prettier": "^3.6.2" + "eslint-plugin-prettier": "^5.5.5", + "prettier": "3.8.2" }, "engines": { - "node": "18 || 20 || 21 || 22 || 23 || 24" + "node": ">= 20.19" }, "funding": { "type": "patreon", "url": "https://www.patreon.com/tshemsedinov" } }, + "node_modules/eslint-config-metarhia/node_modules/prettier": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", + "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/eslint-config-prettier": { "version": "10.1.8", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -653,14 +595,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -732,9 +674,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -850,9 +792,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -982,9 +924,9 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1095,22 +1037,6 @@ "url": "https://www.patreon.com/tshemsedinov" } }, - "node_modules/metadomain": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/metadomain/-/metadomain-2.0.0-alpha.3.tgz", - "integrity": "sha512-OUw6ufbo62f2e2DG8DegB5/PHKUOxxbnpXlquLIShqzFbzFYb04oqlLUgYWZKtqjtqj00JX509CnwT+CbSunHg==", - "license": "MIT", - "dependencies": { - "metaschema": "^2.2.2" - }, - "engines": { - "node": "18 || 20 || 21 || 22 || 23 || 24" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/tshemsedinov" - } - }, "node_modules/metalog": { "version": "3.1.18", "resolved": "https://registry.npmjs.org/metalog/-/metalog-3.1.18.tgz", @@ -1145,28 +1071,10 @@ "url": "https://www.patreon.com/tshemsedinov" } }, - "node_modules/metasql": { - "version": "3.0.0-alpha.4", - "resolved": "https://registry.npmjs.org/metasql/-/metasql-3.0.0-alpha.4.tgz", - "integrity": "sha512-vrzUzUBKJdOmV2gVcK0EqeuWBlV3d3AzY6nzJQW18tgaFGqcjZImLxZnmnAcV+w/lAQc7RwLJEAH+Dr6LsNMFw==", - "license": "MIT", - "dependencies": { - "metadomain": "^2.0.0-alpha.3", - "metaschema": "^2.2.2", - "metavm": "^1.4.5", - "pg": "^8.16.3" - }, - "bin": { - "metasql": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/metautil": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/metautil/-/metautil-5.4.0.tgz", - "integrity": "sha512-xs5gtY4yL1bGAnyTdkz7cikYb1BV4RLeSfSJCs8iXkUJybgF2hNJTTW+TirnjW4HXKXhX5duTWdxegB5P64NZA==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/metautil/-/metautil-5.5.2.tgz", + "integrity": "sha512-As5r3AhP0Sn/ilrs7PaSNDgN8fg+t82oKBg5zaCFeQ8YAA3P9ENNblN9ReU7yfqpyAF0FEVDihGcRxCJJXLH2w==", "license": "MIT", "engines": { "node": ">=18" @@ -1190,9 +1098,9 @@ } }, "node_modules/metawatch": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/metawatch/-/metawatch-1.2.4.tgz", - "integrity": "sha512-ciq1UX5UGBlSQt25RLrdRW9UBWe0aG3ouS7kMnCwGAgseI+/RBV0BjnK540gxjtGv2PsH5QrCG17FV0heT5t3g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/metawatch/-/metawatch-1.2.5.tgz", + "integrity": "sha512-gdltyOBfLJMmkm2GDah3v+7geWde2nWRh4HioR+3QhqSYGfJgQiSZKW8VAwhfjWwlBM1B+90fulJ1Tk9LuZlFg==", "license": "MIT", "engines": { "node": ">=18" @@ -1203,9 +1111,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1312,135 +1220,6 @@ "node": ">=8" } }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "peer": true, - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1452,12 +1231,11 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -1469,9 +1247,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -1491,22 +1269,6 @@ "node": ">=6" } }, - "node_modules/redis": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.9.0.tgz", - "integrity": "sha512-E8dQVLSyH6UE/C9darFuwq4usOPrqfZ1864kI4RFbr5Oj9ioB9qPF0oJMwX7s8mf6sPYrz84x/Dx1PGF3/0EaQ==", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.9.0", - "@redis/client": "5.9.0", - "@redis/json": "5.9.0", - "@redis/search": "5.9.0", - "@redis/time-series": "5.9.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1540,15 +1302,6 @@ "node": ">=8" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1576,9 +1329,9 @@ } }, "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1662,9 +1415,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -1682,15 +1435,6 @@ } } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 837eb081..3218ac87 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "scripts": { "test": "npm run lint && npm run types && MODE=test node server.js", "dotest": "npm run lint && npm run types && node server.js", + "prepush": "npm run lint && npm run types && npm run test", "types": "tsc -p tsconfig.json", "lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\"", "fix": "eslint . --fix && prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\"", @@ -54,18 +55,14 @@ "node": ">=18" }, "devDependencies": { - "@types/node": "^24.10.0", - "@types/pg": "^8.15.5", + "@types/node": "^24.12.0", "@types/ws": "^8.18.1", - "eslint": "^9.39.1", - "eslint-config-metarhia": "^9.1.3", - "prettier": "^3.6.2", + "eslint": "^9.39.4", + "eslint-config-metarhia": "^9.1.8", + "prettier": "^3.8.1", "typescript": "^5.9.3" }, "dependencies": { - "impress": "^3.1.2", - "metasql": "^3.0.0-alpha.4", - "pg": "^8.15.6", - "redis": "^5.9.0" + "impress": "^3.1.2" } }