mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 01:36:29 -05:00
feat(realtime): use CBOR encoding in production mode
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
8a66031ff3
commit
7f8add6cd4
10 changed files with 225 additions and 19 deletions
|
@ -3,7 +3,11 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { WebsocketTransporter } from '@hedgedoc/commons';
|
import {
|
||||||
|
CborMessageEncoder,
|
||||||
|
JsonMessageEncoder,
|
||||||
|
WebsocketTransporter,
|
||||||
|
} from '@hedgedoc/commons';
|
||||||
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
|
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
|
||||||
import { IncomingMessage } from 'http';
|
import { IncomingMessage } from 'http';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
|
@ -76,7 +80,12 @@ export class WebsocketGateway implements OnGatewayConnection {
|
||||||
const realtimeNote =
|
const realtimeNote =
|
||||||
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
|
await this.realtimeNoteService.getOrCreateRealtimeNote(note);
|
||||||
|
|
||||||
const websocketTransporter = new WebsocketTransporter();
|
const messageEncoder =
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? new JsonMessageEncoder()
|
||||||
|
: new CborMessageEncoder();
|
||||||
|
|
||||||
|
const websocketTransporter = new WebsocketTransporter(messageEncoder);
|
||||||
const connection = new RealtimeConnection(
|
const connection = new RealtimeConnection(
|
||||||
websocketTransporter,
|
websocketTransporter,
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"url": "https://github.com/hedgedoc/hedgedoc.git"
|
"url": "https://github.com/hedgedoc/hedgedoc.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cbor-x": "1.5.1",
|
||||||
"eventemitter2": "6.4.9",
|
"eventemitter2": "6.4.9",
|
||||||
"isomorphic-ws": "5.0.0",
|
"isomorphic-ws": "5.0.0",
|
||||||
"reveal.js": "4.4.0",
|
"reveal.js": "4.4.0",
|
||||||
|
|
|
@ -10,6 +10,10 @@ export * from './message-transporters/message-transporter.js'
|
||||||
export * from './message-transporters/realtime-user.js'
|
export * from './message-transporters/realtime-user.js'
|
||||||
export * from './message-transporters/websocket-transporter.js'
|
export * from './message-transporters/websocket-transporter.js'
|
||||||
|
|
||||||
|
export * from './message-encoders/message-encoder.js'
|
||||||
|
export * from './message-encoders/cbor-message-encoder.js'
|
||||||
|
export * from './message-encoders/json-message-encoder.js'
|
||||||
|
|
||||||
export { parseUrl } from './utils/parse-url.js'
|
export { parseUrl } from './utils/parse-url.js'
|
||||||
export {
|
export {
|
||||||
MissingTrailingSlashError,
|
MissingTrailingSlashError,
|
||||||
|
|
56
commons/src/message-encoders/cbor-message-encoder.ts
Normal file
56
commons/src/message-encoders/cbor-message-encoder.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Message, MessageType } from '../message-transporters/message.js'
|
||||||
|
import { MessageEncoder } from './message-encoder.js'
|
||||||
|
import { Encoder, Decoder } from 'cbor-x'
|
||||||
|
|
||||||
|
interface ReceivedMessage {
|
||||||
|
type: number
|
||||||
|
payload: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyMap = {
|
||||||
|
type: 0,
|
||||||
|
payload: 1,
|
||||||
|
users: 2,
|
||||||
|
ownUser: 3,
|
||||||
|
displayName: 4,
|
||||||
|
styleIndex: 5,
|
||||||
|
active: 6,
|
||||||
|
username: 7,
|
||||||
|
cursor: 8,
|
||||||
|
from: 9,
|
||||||
|
to: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageTypes = Object.values(MessageType)
|
||||||
|
|
||||||
|
export class CborMessageEncoder implements MessageEncoder {
|
||||||
|
private readonly encoder: Encoder = new Encoder({
|
||||||
|
keyMap
|
||||||
|
})
|
||||||
|
private readonly decoder: Decoder = new Decoder({
|
||||||
|
keyMap
|
||||||
|
})
|
||||||
|
|
||||||
|
encode(message: Message<MessageType>): Uint8Array {
|
||||||
|
const type = messageTypes.indexOf(message.type)
|
||||||
|
return this.encoder.encode({
|
||||||
|
...message,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(message: ArrayBuffer): Message<MessageType> {
|
||||||
|
const uint8Array = new Uint8Array(message)
|
||||||
|
const decoded = this.decoder.decode(uint8Array) as ReceivedMessage
|
||||||
|
const type = messageTypes[decoded.type]
|
||||||
|
return {
|
||||||
|
...decoded,
|
||||||
|
type
|
||||||
|
} as Message<MessageType>
|
||||||
|
}
|
||||||
|
}
|
17
commons/src/message-encoders/json-message-encoder.ts
Normal file
17
commons/src/message-encoders/json-message-encoder.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Message, MessageType } from '../message-transporters/message.js'
|
||||||
|
import { MessageEncoder } from './message-encoder.js'
|
||||||
|
|
||||||
|
export class JsonMessageEncoder implements MessageEncoder {
|
||||||
|
public encode(message: Message<MessageType>): string {
|
||||||
|
return JSON.stringify(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
public decode(message: string): Message<MessageType> {
|
||||||
|
return JSON.parse(message) as Message<MessageType>
|
||||||
|
}
|
||||||
|
}
|
13
commons/src/message-encoders/message-encoder.ts
Normal file
13
commons/src/message-encoders/message-encoder.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { Message, MessageType } from '../message-transporters/message.js'
|
||||||
|
import WebSocket from 'isomorphic-ws'
|
||||||
|
|
||||||
|
export abstract class MessageEncoder {
|
||||||
|
public abstract encode(message: Message<MessageType>): WebSocket.Data
|
||||||
|
|
||||||
|
public abstract decode(message: WebSocket.Data): Message<MessageType>
|
||||||
|
}
|
|
@ -6,19 +6,22 @@
|
||||||
import { RealtimeUser, RemoteCursor } from './realtime-user.js'
|
import { RealtimeUser, RemoteCursor } from './realtime-user.js'
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
// This enum is sorted by frequency of usage for efficient binary encoding
|
||||||
|
REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET',
|
||||||
|
REALTIME_USER_SET_ACTIVITY = 'REALTIME_USER_SET_ACTIVITY',
|
||||||
|
REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE',
|
||||||
NOTE_CONTENT_UPDATE = 'NOTE_CONTENT_UPDATE',
|
NOTE_CONTENT_UPDATE = 'NOTE_CONTENT_UPDATE',
|
||||||
|
|
||||||
|
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
||||||
|
REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST',
|
||||||
|
|
||||||
PING = 'PING',
|
PING = 'PING',
|
||||||
PONG = 'PONG',
|
PONG = 'PONG',
|
||||||
|
READY = 'READY',
|
||||||
|
|
||||||
METADATA_UPDATED = 'METADATA_UPDATED',
|
METADATA_UPDATED = 'METADATA_UPDATED',
|
||||||
DOCUMENT_DELETED = 'DOCUMENT_DELETED',
|
DOCUMENT_DELETED = 'DOCUMENT_DELETED',
|
||||||
SERVER_VERSION_UPDATED = 'SERVER_VERSION_UPDATED',
|
SERVER_VERSION_UPDATED = 'SERVER_VERSION_UPDATED'
|
||||||
REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET',
|
|
||||||
REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE',
|
|
||||||
REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST',
|
|
||||||
REALTIME_USER_SET_ACTIVITY = 'REALTIME_USER_SET_ACTIVITY',
|
|
||||||
|
|
||||||
READY = 'READY'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessagePayloads {
|
export interface MessagePayloads {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { MessageEncoder } from '../message-encoders/message-encoder.js'
|
||||||
import { ConnectionState, MessageTransporter } from './message-transporter.js'
|
import { ConnectionState, MessageTransporter } from './message-transporter.js'
|
||||||
import { Message, MessageType } from './message.js'
|
import { Message, MessageType } from './message.js'
|
||||||
import WebSocket, { MessageEvent } from 'isomorphic-ws'
|
import WebSocket, { MessageEvent } from 'isomorphic-ws'
|
||||||
|
@ -13,7 +14,7 @@ export class WebsocketTransporter extends MessageTransporter {
|
||||||
private messageCallback: undefined | ((event: MessageEvent) => void)
|
private messageCallback: undefined | ((event: MessageEvent) => void)
|
||||||
private closeCallback: undefined | (() => void)
|
private closeCallback: undefined | (() => void)
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly encoder: MessageEncoder) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,10 +58,7 @@ export class WebsocketTransporter extends MessageTransporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private processMessageEvent(event: MessageEvent): void {
|
private processMessageEvent(event: MessageEvent): void {
|
||||||
if (typeof event.data !== 'string') {
|
const message = this.encoder.decode(event.data)
|
||||||
return
|
|
||||||
}
|
|
||||||
const message = JSON.parse(event.data) as Message<MessageType>
|
|
||||||
this.receiveMessage(message)
|
this.receiveMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +90,8 @@ export class WebsocketTransporter extends MessageTransporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.websocket.send(JSON.stringify(content))
|
const encoded = this.encoder.encode(content)
|
||||||
|
this.websocket.send(encoded)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -7,10 +7,15 @@ import { useApplicationState } from '../../../../../hooks/common/use-application
|
||||||
import { getGlobalState } from '../../../../../redux'
|
import { getGlobalState } from '../../../../../redux'
|
||||||
import { setRealtimeConnectionState } from '../../../../../redux/realtime/methods'
|
import { setRealtimeConnectionState } from '../../../../../redux/realtime/methods'
|
||||||
import { Logger } from '../../../../../utils/logger'
|
import { Logger } from '../../../../../utils/logger'
|
||||||
import { isMockMode } from '../../../../../utils/test-modes'
|
import { isMockMode, isDevMode } from '../../../../../utils/test-modes'
|
||||||
import { useWebsocketUrl } from './use-websocket-url'
|
import { useWebsocketUrl } from './use-websocket-url'
|
||||||
import type { MessageTransporter } from '@hedgedoc/commons'
|
import type { MessageTransporter } from '@hedgedoc/commons'
|
||||||
import { MockedBackendMessageTransporter, WebsocketTransporter } from '@hedgedoc/commons'
|
import {
|
||||||
|
CborMessageEncoder,
|
||||||
|
JsonMessageEncoder,
|
||||||
|
MockedBackendMessageTransporter,
|
||||||
|
WebsocketTransporter
|
||||||
|
} from '@hedgedoc/commons'
|
||||||
import type { Listener } from 'eventemitter2'
|
import type { Listener } from 'eventemitter2'
|
||||||
import WebSocket from 'isomorphic-ws'
|
import WebSocket from 'isomorphic-ws'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
@ -31,7 +36,8 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
return new MockedBackendMessageTransporter(getGlobalState().noteDetails.markdownContent.plain)
|
return new MockedBackendMessageTransporter(getGlobalState().noteDetails.markdownContent.plain)
|
||||||
} else {
|
} else {
|
||||||
logger.debug('Creating Websocket connection...')
|
logger.debug('Creating Websocket connection...')
|
||||||
return new WebsocketTransporter()
|
const encoder = isDevMode ? new JsonMessageEncoder() : new CborMessageEncoder()
|
||||||
|
return new WebsocketTransporter(encoder)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -39,6 +45,7 @@ export const useRealtimeConnection = (): MessageTransporter => {
|
||||||
if (messageTransporter instanceof WebsocketTransporter && websocketUrl) {
|
if (messageTransporter instanceof WebsocketTransporter && websocketUrl) {
|
||||||
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
logger.debug(`Connecting to ${websocketUrl.toString()}`)
|
||||||
const socket = new WebSocket(websocketUrl)
|
const socket = new WebSocket(websocketUrl)
|
||||||
|
socket.binaryType = 'arraybuffer'
|
||||||
socket.addEventListener('error', () => {
|
socket.addEventListener('error', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
establishWebsocketConnection()
|
establishWebsocketConnection()
|
||||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -1702,6 +1702,48 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-darwin-arm64@npm:2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "@cbor-extract/cbor-extract-darwin-arm64@npm:2.1.1"
|
||||||
|
conditions: os=darwin & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-darwin-x64@npm:2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "@cbor-extract/cbor-extract-darwin-x64@npm:2.1.1"
|
||||||
|
conditions: os=darwin & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm64@npm:2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "@cbor-extract/cbor-extract-linux-arm64@npm:2.1.1"
|
||||||
|
conditions: os=linux & cpu=arm64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm@npm:2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "@cbor-extract/cbor-extract-linux-arm@npm:2.1.1"
|
||||||
|
conditions: os=linux & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-linux-x64@npm:2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "@cbor-extract/cbor-extract-linux-x64@npm:2.1.1"
|
||||||
|
conditions: os=linux & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-win32-x64@npm:2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "@cbor-extract/cbor-extract-win32-x64@npm:2.1.1"
|
||||||
|
conditions: os=win32 & cpu=x64
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@codemirror/autocomplete@npm:6.4.2":
|
"@codemirror/autocomplete@npm:6.4.2":
|
||||||
version: 6.4.2
|
version: 6.4.2
|
||||||
resolution: "@codemirror/autocomplete@npm:6.4.2"
|
resolution: "@codemirror/autocomplete@npm:6.4.2"
|
||||||
|
@ -2320,6 +2362,7 @@ __metadata:
|
||||||
"@types/ws": 8.5.4
|
"@types/ws": 8.5.4
|
||||||
"@typescript-eslint/eslint-plugin": 5.57.0
|
"@typescript-eslint/eslint-plugin": 5.57.0
|
||||||
"@typescript-eslint/parser": 5.57.0
|
"@typescript-eslint/parser": 5.57.0
|
||||||
|
cbor-x: 1.5.1
|
||||||
eslint: 8.37.0
|
eslint: 8.37.0
|
||||||
eslint-config-prettier: 8.8.0
|
eslint-config-prettier: 8.8.0
|
||||||
eslint-plugin-jest: 27.2.1
|
eslint-plugin-jest: 27.2.1
|
||||||
|
@ -6753,6 +6796,49 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cbor-extract@npm:^2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "cbor-extract@npm:2.1.1"
|
||||||
|
dependencies:
|
||||||
|
"@cbor-extract/cbor-extract-darwin-arm64": 2.1.1
|
||||||
|
"@cbor-extract/cbor-extract-darwin-x64": 2.1.1
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm": 2.1.1
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm64": 2.1.1
|
||||||
|
"@cbor-extract/cbor-extract-linux-x64": 2.1.1
|
||||||
|
"@cbor-extract/cbor-extract-win32-x64": 2.1.1
|
||||||
|
node-gyp: latest
|
||||||
|
node-gyp-build-optional-packages: 5.0.3
|
||||||
|
dependenciesMeta:
|
||||||
|
"@cbor-extract/cbor-extract-darwin-arm64":
|
||||||
|
optional: true
|
||||||
|
"@cbor-extract/cbor-extract-darwin-x64":
|
||||||
|
optional: true
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm":
|
||||||
|
optional: true
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm64":
|
||||||
|
optional: true
|
||||||
|
"@cbor-extract/cbor-extract-linux-x64":
|
||||||
|
optional: true
|
||||||
|
"@cbor-extract/cbor-extract-win32-x64":
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
download-cbor-prebuilds: bin/download-prebuilds.js
|
||||||
|
checksum: 283d9cdb3c716b171b5ad8666673f4ac373f975b51d9a38233d280c6f9381d66c6af4c011a561d993c4be6e427e34681bc3c5af194b9da0c9ab3401d424b7988
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"cbor-x@npm:1.5.1":
|
||||||
|
version: 1.5.1
|
||||||
|
resolution: "cbor-x@npm:1.5.1"
|
||||||
|
dependencies:
|
||||||
|
cbor-extract: ^2.1.1
|
||||||
|
dependenciesMeta:
|
||||||
|
cbor-extract:
|
||||||
|
optional: true
|
||||||
|
checksum: e4ff6012194e93739a36027a2d8abbe4e98f62c003c77896bed1c22bf29782fe371480c7db37a0fc288a1a56cb0b711e48072ed6f42d51bdf96395a1ed01bcea
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
|
"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
resolution: "chalk@npm:4.1.2"
|
resolution: "chalk@npm:4.1.2"
|
||||||
|
@ -13816,6 +13902,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"node-gyp-build-optional-packages@npm:5.0.3":
|
||||||
|
version: 5.0.3
|
||||||
|
resolution: "node-gyp-build-optional-packages@npm:5.0.3"
|
||||||
|
bin:
|
||||||
|
node-gyp-build-optional-packages: bin.js
|
||||||
|
node-gyp-build-optional-packages-optional: optional.js
|
||||||
|
node-gyp-build-optional-packages-test: build-test.js
|
||||||
|
checksum: be3f0235925c8361e5bc1a03848f5e24815b0df8aa90bd13f1eac91cd86264bbb8b7689ca6cd083b02c8099c7b54f9fb83066c7bb77c2389dc4eceab921f084f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"node-gyp@npm:8.x":
|
"node-gyp@npm:8.x":
|
||||||
version: 8.4.1
|
version: 8.4.1
|
||||||
resolution: "node-gyp@npm:8.4.1"
|
resolution: "node-gyp@npm:8.4.1"
|
||||||
|
|
Loading…
Reference in a new issue