@lvce-editor/server 0.108.11 → 0.108.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ This project incorporates components from the projects listed below, that may ha
2
2
  differing from this project:
3
3
 
4
4
 
5
- 1) License Notice for static/dd9c918/icons (from https://github.com/microsoft/vscode-codicons)
5
+ 1) License Notice for static/241d72e/icons (from https://github.com/microsoft/vscode-codicons)
6
6
  ---------------------------------------
7
7
 
8
8
  Attribution 4.0 International
@@ -402,7 +402,7 @@ public licenses.
402
402
  Creative Commons may be contacted at creativecommons.org.
403
403
 
404
404
 
405
- 2) License Notice for static/dd9c918/fonts/FiraCode-VariableFont.ttf (from https://github.com/tonsky/FiraCode)
405
+ 2) License Notice for static/241d72e/fonts/FiraCode-VariableFont.ttf (from https://github.com/tonsky/FiraCode)
406
406
  ---------------------------------------
407
407
 
408
408
  Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/server",
3
- "version": "0.108.11",
3
+ "version": "0.108.13",
4
4
  "description": "Run LVCE Editor as a server.",
5
5
  "main": "index.js",
6
6
  "bin": "bin/server.js",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@lvce-editor/jsonc-parser": "^1.5.0",
24
- "@lvce-editor/shared-process": "0.108.11",
25
- "@lvce-editor/static-server": "0.108.11"
24
+ "@lvce-editor/shared-process": "0.108.13",
25
+ "@lvce-editor/static-server": "0.108.13"
26
26
  }
27
27
  }
package/src/server.js CHANGED
@@ -63,6 +63,7 @@ const ErrorCodes = {
63
63
  ERR_STREAM_PREMATURE_CLOSE: 'ERR_STREAM_PREMATURE_CLOSE',
64
64
  EISDIR: 'EISDIR',
65
65
  ECONNRESET: 'ECONNRESET',
66
+ EPIPE: 'EPIPE',
66
67
  ENOENT: 'ENOENT',
67
68
  EADDRINUSE: 'EADDRINUSE',
68
69
  }
@@ -76,7 +77,7 @@ const isStatic = (url) => {
76
77
  if (url === '/index.html' || url.startsWith('/index.html?')) {
77
78
  return !hasLinkedExtensions
78
79
  }
79
- if (url.startsWith('/dd9c918')) {
80
+ if (url.startsWith('/241d72e')) {
80
81
  return true
81
82
  }
82
83
  if (url.startsWith('/favicon.ico')) {
@@ -98,9 +99,9 @@ const handleRequest = (req, res) => {
98
99
  return
99
100
  }
100
101
  if (isStatic(req.url)) {
101
- return handleResponseViaStaticServer(req, res, 'StaticServer.getResponse')
102
+ return handleResponseViaProcess(req, res, getOrCreateStaticServerPathProcess, 'StaticServer.getResponse')
102
103
  }
103
- return sendHandleSharedProcess(req, res.socket, 'HandleRequest.handleRequest')
104
+ return handleResponseViaProcess(req, res, getOrCreateSharedProcess, 'HandleRequest.handleRequest')
104
105
  }
105
106
 
106
107
  const state = {
@@ -189,6 +190,7 @@ const launchProcess = async (processPath, execArgv) => {
189
190
  ...process.env,
190
191
  },
191
192
  execArgv: [],
193
+ serialization: 'advanced',
192
194
  })
193
195
  childProcess.on('exit', handleExit)
194
196
  childProcess.on('disconnect', handleSharedProcessDisconnect)
@@ -204,7 +206,9 @@ const launchProcess = async (processPath, execArgv) => {
204
206
  const launchSharedProcess = async () => {
205
207
  const sharedProcessUrl = new URL('src/sharedProcessMain.js', import.meta.resolve('@lvce-editor/shared-process')).toString()
206
208
  const sharedProcessPath = fileURLToPath(sharedProcessUrl)
207
- return launchProcess(sharedProcessPath, ['--enable-source-maps', '--ipc-type=node-forked-process', ...argvSliced])
209
+ const ipc = await launchProcess(sharedProcessPath, ['--enable-source-maps', '--ipc-type=node-forked-process', ...argvSliced])
210
+ ipc.on('message', handleMessage)
211
+ return ipc
208
212
  }
209
213
 
210
214
  /**
@@ -256,25 +260,21 @@ const getHandleMessage = (request) => {
256
260
  }
257
261
  }
258
262
 
259
- const handleRequestError = (error) => {
260
- if (error && error.code === 'ECONNRESET') {
261
- // ignore
262
- return
263
- }
264
- console.info('[info]: request upgrade error', error)
263
+ const isExpectedConnectionError = (error) => {
264
+ return error && (error.code === ErrorCodes.ECONNRESET || error.code === ErrorCodes.EPIPE)
265
265
  }
266
266
 
267
- const handleSocketUpgradeError = (error) => {
268
- // @ts-ignore
269
- console.info('[info] request socket upgrade error', error)
267
+ const handleRequestError = (error) => {
268
+ if (!isExpectedConnectionError(error)) {
269
+ console.info('[info]: request error', error)
270
+ }
270
271
  }
271
272
 
272
273
  const handleSocketError = (error) => {
273
- if (error && error.code === 'ECONNRESET') {
274
- return
274
+ if (!isExpectedConnectionError(error)) {
275
+ // @ts-ignore
276
+ console.info('[info] request socket error', error)
275
277
  }
276
- // @ts-ignore
277
- console.info('[info] request socket error', error)
278
278
  }
279
279
 
280
280
  const sendHandleSharedProcess = async (request, socket, method, ...params) => {
@@ -282,7 +282,7 @@ const sendHandleSharedProcess = async (request, socket, method, ...params) => {
282
282
  if (!socket) {
283
283
  return
284
284
  }
285
- socket.on('error', handleSocketUpgradeError)
285
+ socket.on('error', handleSocketError)
286
286
  const sharedProcess = await getOrCreateSharedProcess()
287
287
  sharedProcess.send(
288
288
  {
@@ -331,27 +331,29 @@ const handleMessage = (message) => {
331
331
 
332
332
  const hasErrorListener = new WeakSet()
333
333
 
334
- const handleResponseViaStaticServer = async (request, res, method, ...params) => {
335
- request.on('error', handleRequestError)
336
- if (res.socket && !hasErrorListener.has(res.socket)) {
337
- res.socket.on('error', handleSocketError)
338
- hasErrorListener.add(res.socket)
339
- }
340
- const staticServerProcess = await getOrCreateStaticServerPathProcess()
334
+ const invoke = async (ipc, method, ...params) => {
341
335
  const { id, promise } = registerCallback()
342
- // TODO use rpc and invoke
343
- staticServerProcess.send({
336
+ ipc.send({
344
337
  jsonrpc: '2.0',
345
338
  id,
346
339
  method,
347
- params: [getHandleMessage(request), ...params],
340
+ params,
348
341
  })
349
342
  const response = await promise
350
- const { result } = response
343
+ if (response.error) {
344
+ throw new Error(response.error.message)
345
+ }
346
+ return response.result
347
+ }
348
+
349
+ const sendResponse = (res, result) => {
351
350
  const { status, headers, body, hasBody } = result
352
351
  if (!status) {
353
352
  throw new Error('invalid status')
354
353
  }
354
+ if (res.destroyed) {
355
+ return
356
+ }
355
357
  res.statusCode = status
356
358
  setHeaders(res, headers)
357
359
  if (hasBody) {
@@ -361,6 +363,18 @@ const handleResponseViaStaticServer = async (request, res, method, ...params) =>
361
363
  }
362
364
  }
363
365
 
366
+ const handleResponseViaProcess = async (request, res, getProcess, method, ...params) => {
367
+ request.on('error', handleRequestError)
368
+ res.on('error', handleSocketError)
369
+ if (res.socket && !hasErrorListener.has(res.socket)) {
370
+ res.socket.on('error', handleSocketError)
371
+ hasErrorListener.add(res.socket)
372
+ }
373
+ const ipc = await getProcess()
374
+ const result = await invoke(ipc, method, getHandleMessage(request), ...params)
375
+ sendResponse(res, result)
376
+ }
377
+
364
378
  /**
365
379
  *
366
380
  * @param {import('http').IncomingMessage} request
@@ -0,0 +1,100 @@
1
+ import assert from 'node:assert/strict'
2
+ import { spawn } from 'node:child_process'
3
+ import { once } from 'node:events'
4
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
5
+ import { tmpdir } from 'node:os'
6
+ import path from 'node:path'
7
+ import test from 'node:test'
8
+
9
+ const serverPath = path.join(import.meta.dirname, '..', 'src', 'server.js')
10
+
11
+ const waitForListeningPort = (child) => {
12
+ return new Promise((resolve, reject) => {
13
+ let output = ''
14
+ const timeout = setTimeout(() => reject(new Error(`Timed out waiting for server\n${output}`)), 30_000)
15
+ const onData = (chunk) => {
16
+ output += chunk.toString('utf8')
17
+ const match = output.match(/listening on http:\/\/localhost:(\d+)/)
18
+ if (match) {
19
+ clearTimeout(timeout)
20
+ child.stdout.off('data', onData)
21
+ resolve(Number.parseInt(match[1], 10))
22
+ }
23
+ }
24
+ child.stdout.on('data', onData)
25
+ child.stderr.on('data', (chunk) => {
26
+ output += chunk.toString('utf8')
27
+ })
28
+ child.once('error', reject)
29
+ child.once('exit', (code) => reject(new Error(`Server exited with code ${code}\n${output}`)))
30
+ })
31
+ }
32
+
33
+ const killProcessGroup = (child, signal) => {
34
+ if (child.exitCode !== null || child.signalCode !== null) {
35
+ return
36
+ }
37
+ try {
38
+ process.kill(process.platform === 'win32' ? child.pid : -child.pid, signal)
39
+ } catch {
40
+ child.kill(signal)
41
+ }
42
+ }
43
+
44
+ const stopProcessGroup = async (child) => {
45
+ if (child.exitCode !== null || child.signalCode !== null) {
46
+ return
47
+ }
48
+ const exited = once(child, 'exit')
49
+ killProcessGroup(child, 'SIGTERM')
50
+ let timeout
51
+ const timedOut = new Promise((resolve) => {
52
+ timeout = setTimeout(resolve, 5_000, 'timeout')
53
+ })
54
+ const result = await Promise.race([exited.then(() => 'exited'), timedOut])
55
+ clearTimeout(timeout)
56
+ if (result === 'timeout') {
57
+ killProcessGroup(child, 'SIGKILL')
58
+ await exited
59
+ }
60
+ }
61
+
62
+ test('serves binary and partial responses returned by the shared process', async (context) => {
63
+ const directory = await mkdtemp(path.join(tmpdir(), 'lvce-http-response-'))
64
+ const file = path.join(directory, 'video.bin')
65
+ const content = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
66
+ await writeFile(file, content)
67
+
68
+ const child = spawn(process.execPath, [serverPath], {
69
+ detached: true,
70
+ env: {
71
+ ...process.env,
72
+ PORT: '0',
73
+ },
74
+ stdio: ['ignore', 'pipe', 'pipe'],
75
+ })
76
+ context.after(async () => {
77
+ await stopProcessGroup(child)
78
+ await rm(directory, { force: true, recursive: true })
79
+ })
80
+ const port = await waitForListeningPort(child)
81
+ const url = `http://localhost:${port}/remote${encodeURI(file)}`
82
+
83
+ const response = await fetch(url)
84
+ assert.equal(response.status, 200)
85
+ assert.deepEqual(Buffer.from(await response.arrayBuffer()), content)
86
+
87
+ const partialResponse = await fetch(url, {
88
+ headers: {
89
+ Range: 'bytes=2-5',
90
+ },
91
+ })
92
+ assert.equal(partialResponse.status, 206)
93
+ assert.equal(partialResponse.headers.get('accept-ranges'), 'bytes')
94
+ assert.equal(partialResponse.headers.get('content-range'), 'bytes 2-5/10')
95
+ assert.deepEqual(Buffer.from(await partialResponse.arrayBuffer()), Buffer.from([2, 3, 4, 5]))
96
+
97
+ const headResponse = await fetch(url, { method: 'HEAD' })
98
+ assert.equal(headResponse.status, 200)
99
+ assert.equal((await headResponse.arrayBuffer()).byteLength, 0)
100
+ })