quickwin 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.en.md +204 -204
  2. package/README.md +203 -203
  3. package/examples/PdfCanvas.tsx +83 -83
  4. package/examples/exec_server.ts +35 -35
  5. package/examples/pdf_preview2.ts +375 -375
  6. package/examples/pdf_viewer.tsx +279 -279
  7. package/examples/test_gallery.tsx +285 -285
  8. package/examples/test_react_lazy.tsx +63 -63
  9. package/examples/test_react_listbox.tsx +71 -71
  10. package/examples/test_root_window.tsx +41 -41
  11. package/examples/test_tab.tsx +75 -75
  12. package/examples/tray_demo.tsx +81 -81
  13. package/lib/assert.ts +6 -6
  14. package/lib/cache-utils.ts +40 -40
  15. package/lib/dumpRects.ts +25 -25
  16. package/lib/fetch.ts +502 -502
  17. package/lib/http-common.ts +399 -399
  18. package/lib/http-server.ts +424 -424
  19. package/lib/polyfill.ts +171 -171
  20. package/lib/react-qw/components/index.ts +17 -17
  21. package/lib/react-qw/index.ts +90 -90
  22. package/lib/react-qw/jsx.d.ts +34 -34
  23. package/lib/react-qw/layout.ts +111 -111
  24. package/lib/react-qw/props.ts +36 -36
  25. package/lib/react-qw/reconciler.ts +373 -373
  26. package/lib/stream.ts +21 -21
  27. package/lib/text-codec.ts +171 -171
  28. package/lib/text-measure.ts +54 -54
  29. package/lib/url.ts +201 -201
  30. package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
  31. package/lib/vendor/web-streams/ponyfill.mjs +8 -8
  32. package/lib/websocket.js +8 -0
  33. package/lib/websocket.ts +563 -555
  34. package/package.json +50 -44
  35. package/qwin-nowasm-x86.exe +0 -0
  36. package/qwin-nowasm.exe +0 -0
  37. package/qwin-x86.exe +0 -0
  38. package/qwin.exe +0 -0
  39. package/test/run.ts +96 -96
  40. package/test/test_basic.ts +9 -9
  41. package/test/test_basic_fetch.ts +33 -33
  42. package/test/test_brotli.ts +52 -52
  43. package/test/test_fetch_cache.ts +187 -187
  44. package/test/test_ffi.ts +174 -174
  45. package/test/test_frame_encoding.ts +132 -132
  46. package/test/test_helper.ts +80 -80
  47. package/test/test_http_import.ts +74 -74
  48. package/test/test_http_server.ts +136 -136
  49. package/test/test_import_dir.ts +5 -5
  50. package/test/test_ipv6.ts +129 -129
  51. package/test/test_meta_url.ts +4 -4
  52. package/test/test_mupdf_render.ts +74 -74
  53. package/test/test_mupdf_twice.ts +81 -81
  54. package/test/test_mupdf_wasm.ts +30 -30
  55. package/test/test_net_event.ts +55 -55
  56. package/test/test_net_fetch.ts +238 -238
  57. package/test/test_net_websocket.ts +164 -164
  58. package/test/test_polyfill.ts +159 -159
  59. package/test/test_url.ts +183 -183
  60. package/test/test_wasm_basic.ts +73 -73
  61. package/test/test_wasm_import_global.ts +45 -45
  62. package/test/test_wasm_sjlj.ts +125 -125
  63. package/test/test_wasm_types.ts +124 -124
  64. package/test/test_worker.ts +24 -24
  65. package/test/test_worker_fetch.ts +27 -27
  66. package/test/test_worker_net.ts +54 -54
  67. package/test/test_worker_wasm_concurrent.ts +45 -45
  68. package/test/worker_fetch.ts +13 -13
  69. package/test/worker_helper.ts +1 -1
  70. package/test/worker_http.ts +20 -20
  71. package/test/worker_import_lib.ts +13 -13
  72. package/test/worker_module.ts +17 -17
  73. package/test/worker_wasm_concurrent.ts +22 -22
  74. package/tsconfig.json +27 -27
  75. package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
  76. package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
  77. package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
  78. package/vendor/mupdf-wasm/mupdf.js +3317 -3317
@@ -1,80 +1,80 @@
1
- import * as std from 'std'
2
-
3
- const GREEN = '\x1b[32m'
4
- const RED = '\x1b[31m'
5
- const BOLD = '\x1b[1m'
6
- const RESET = '\x1b[0m'
7
-
8
- function formatDuration(ms: number): string {
9
- if (ms >= 1000) return (ms / 1000).toFixed(2) + 's'
10
- return ms + 'ms'
11
- }
12
-
13
- export function readWasmFile(path: string): ArrayBuffer | null {
14
- const base = import.meta.url.slice(0, import.meta.url.lastIndexOf('/') + 1)
15
- const parts = (base + path).split('/')
16
- const out: string[] = []
17
- for (const p of parts) {
18
- if (p === '..') out.pop()
19
- else if (p !== '.') out.push(p)
20
- }
21
- let filePath = out.join('/').slice(7)
22
- if (filePath.length >= 3 && filePath[1] === ':') filePath = filePath.slice(1)
23
- const fp = std.open(filePath, 'rb')
24
- if (!fp) return null
25
- fp.seek(0, 2)
26
- const size = fp.tell()
27
- fp.seek(0, 0)
28
- const buffer = new ArrayBuffer(size)
29
- fp.read(buffer, 0, size)
30
- fp.close()
31
- return buffer
32
- }
33
-
34
- export class Tester {
35
- ok = 0
36
- fail = 0
37
- private startTime = Date.now()
38
- private sectionStart = this.startTime
39
- private lastSection = ''
40
-
41
- section(name: string): void {
42
- const now = Date.now()
43
- if (this.lastSection) {
44
- const elapsed = now - this.sectionStart
45
- std.printf(' (%s)\n', formatDuration(elapsed))
46
- }
47
- this.sectionStart = now
48
- this.lastSection = name
49
- std.printf('\n%s=== %s ===%s\n', BOLD, name, RESET)
50
- }
51
-
52
- check(name: string, expected: unknown, actual: unknown): void {
53
- if (typeof expected === 'number' && typeof actual === 'number') {
54
- if (Math.abs(expected - actual) < 0.0001) {
55
- this.ok++; std.printf(' %sPASS:%s %s = %s\n', GREEN, RESET, name, String(actual))
56
- } else {
57
- this.fail++; std.printf(' %sFAIL:%s %s = %s (expected %s)\n', RED, RESET, name, String(actual), String(expected))
58
- }
59
- } else if (expected === actual) {
60
- this.ok++; std.printf(' %sPASS:%s %s = %s\n', GREEN, RESET, name, String(actual))
61
- } else {
62
- this.fail++; std.printf(' %sFAIL:%s %s = %s (expected %s)\n', RED, RESET, name, String(actual), String(expected))
63
- }
64
- }
65
-
66
- checkTrue(name: string, actual: boolean): void {
67
- this.check(name, true, actual)
68
- }
69
-
70
- summary(): void {
71
- const now = Date.now()
72
- if (this.lastSection) {
73
- const elapsed = now - this.sectionStart
74
- std.printf(' (%s)\n', formatDuration(elapsed))
75
- }
76
- const total = now - this.startTime
77
- const color = this.fail > 0 ? RED : GREEN
78
- std.printf('\n%s%d/%d passed (%s)%s\n', color, this.ok, this.ok + this.fail, formatDuration(total), RESET)
79
- }
80
- }
1
+ import * as std from 'std'
2
+
3
+ const GREEN = '\x1b[32m'
4
+ const RED = '\x1b[31m'
5
+ const BOLD = '\x1b[1m'
6
+ const RESET = '\x1b[0m'
7
+
8
+ function formatDuration(ms: number): string {
9
+ if (ms >= 1000) return (ms / 1000).toFixed(2) + 's'
10
+ return ms + 'ms'
11
+ }
12
+
13
+ export function readWasmFile(path: string): ArrayBuffer | null {
14
+ const base = import.meta.url.slice(0, import.meta.url.lastIndexOf('/') + 1)
15
+ const parts = (base + path).split('/')
16
+ const out: string[] = []
17
+ for (const p of parts) {
18
+ if (p === '..') out.pop()
19
+ else if (p !== '.') out.push(p)
20
+ }
21
+ let filePath = out.join('/').slice(7)
22
+ if (filePath.length >= 3 && filePath[1] === ':') filePath = filePath.slice(1)
23
+ const fp = std.open(filePath, 'rb')
24
+ if (!fp) return null
25
+ fp.seek(0, 2)
26
+ const size = fp.tell()
27
+ fp.seek(0, 0)
28
+ const buffer = new ArrayBuffer(size)
29
+ fp.read(buffer, 0, size)
30
+ fp.close()
31
+ return buffer
32
+ }
33
+
34
+ export class Tester {
35
+ ok = 0
36
+ fail = 0
37
+ private startTime = Date.now()
38
+ private sectionStart = this.startTime
39
+ private lastSection = ''
40
+
41
+ section(name: string): void {
42
+ const now = Date.now()
43
+ if (this.lastSection) {
44
+ const elapsed = now - this.sectionStart
45
+ std.printf(' (%s)\n', formatDuration(elapsed))
46
+ }
47
+ this.sectionStart = now
48
+ this.lastSection = name
49
+ std.printf('\n%s=== %s ===%s\n', BOLD, name, RESET)
50
+ }
51
+
52
+ check(name: string, expected: unknown, actual: unknown): void {
53
+ if (typeof expected === 'number' && typeof actual === 'number') {
54
+ if (Math.abs(expected - actual) < 0.0001) {
55
+ this.ok++; std.printf(' %sPASS:%s %s = %s\n', GREEN, RESET, name, String(actual))
56
+ } else {
57
+ this.fail++; std.printf(' %sFAIL:%s %s = %s (expected %s)\n', RED, RESET, name, String(actual), String(expected))
58
+ }
59
+ } else if (expected === actual) {
60
+ this.ok++; std.printf(' %sPASS:%s %s = %s\n', GREEN, RESET, name, String(actual))
61
+ } else {
62
+ this.fail++; std.printf(' %sFAIL:%s %s = %s (expected %s)\n', RED, RESET, name, String(actual), String(expected))
63
+ }
64
+ }
65
+
66
+ checkTrue(name: string, actual: boolean): void {
67
+ this.check(name, true, actual)
68
+ }
69
+
70
+ summary(): void {
71
+ const now = Date.now()
72
+ if (this.lastSection) {
73
+ const elapsed = now - this.sectionStart
74
+ std.printf(' (%s)\n', formatDuration(elapsed))
75
+ }
76
+ const total = now - this.startTime
77
+ const color = this.fail > 0 ? RED : GREEN
78
+ std.printf('\n%s%d/%d passed (%s)%s\n', color, this.ok, this.ok + this.fail, formatDuration(total), RESET)
79
+ }
80
+ }
@@ -1,74 +1,74 @@
1
- import * as std from 'std'
2
- import * as os from 'os'
3
- import { Tester } from './test_helper.js'
4
- import { parseIni } from '../lib/cache-utils.js'
5
-
6
- function getCacheDir(): string {
7
- // import.meta.url: file:///C:/.../_build/test/file.js
8
- const url = import.meta.url
9
- // Strip file:// (7 chars), giving /C:/.../
10
- let path = url.slice(7)
11
- // Remove leading / before drive letter
12
- if (path.length >= 3 && path[1] === ':') path = path.slice(1)
13
- // path now: C:/Users/Docker/quickwin/_build/test/file.js
14
- // Go up to _build/
15
- const idx = path.lastIndexOf('/')
16
- if (idx < 0) return '_cache'
17
- const buildDir = path.slice(0, idx) // .../_build/test
18
- const idx2 = buildDir.lastIndexOf('/')
19
- if (idx2 < 0) return '_cache'
20
- return buildDir.slice(0, idx2 + 1) + '_cache' // .../_build/_cache
21
- }
22
-
23
- export const suite = {
24
- name: 'http-import',
25
- run: async (t: Tester) => {
26
- function assert(name: string, ok: boolean): void {
27
- if (ok) { t.ok++; std.printf(' PASS: %s\n', name) }
28
- else { t.fail++; std.printf(' FAIL: %s\n', name) }
29
- }
30
-
31
- const url = 'https://esm.sh/left-pad@1.3.0'
32
- const cacheDir = getCacheDir()
33
-
34
- t.section('list cache before')
35
- const [before, err] = os.readdir(cacheDir)
36
- if (err !== 0) {
37
- std.printf(' cache dir: %s (error %d, will be created on first request)\n', cacheDir, err)
38
- }
39
-
40
- t.section('import module')
41
- let mod: any
42
- try {
43
- mod = await import(url)
44
- } catch (e) {
45
- assert('import succeeds', false)
46
- std.printf(' ERROR: %s\n', String(e))
47
- return
48
- }
49
- assert('import succeeds', true)
50
- assert('default is function', typeof mod.default === 'function')
51
- assert('left-pad works', mod.default('hello', 8) === ' hello')
52
-
53
- t.section('check cache')
54
- const [after, err2] = os.readdir(cacheDir)
55
- assert('cache dir readable', err2 === 0)
56
- if (err2 !== 0) return
57
- assert('cache has body files', after.some(function(f: string) { return f.endsWith('.body') }))
58
- assert('cache has meta files', after.some(function(f: string) { return f.endsWith('.meta') }))
59
- for (const f of after) {
60
- if (f.endsWith('.meta') && (err !== 0 || before.indexOf(f) < 0)) {
61
- std.printf(' meta: %s\n', f)
62
- const fp = std.open(cacheDir + '/' + f, 'r')
63
- if (fp) {
64
- const metaStr = fp.readAsString()
65
- fp.close()
66
- if (metaStr) {
67
- const ini = parseIni(metaStr)
68
- std.printf(' storedAt: %s, maxAge: %s\n', ini.meta.storedAt || '?', ini.meta.maxAge || '?')
69
- }
70
- }
71
- }
72
- }
73
- }
74
- }
1
+ import * as std from 'std'
2
+ import * as os from 'os'
3
+ import { Tester } from './test_helper.js'
4
+ import { parseIni } from '../lib/cache-utils.js'
5
+
6
+ function getCacheDir(): string {
7
+ // import.meta.url: file:///C:/.../_build/test/file.js
8
+ const url = import.meta.url
9
+ // Strip file:// (7 chars), giving /C:/.../
10
+ let path = url.slice(7)
11
+ // Remove leading / before drive letter
12
+ if (path.length >= 3 && path[1] === ':') path = path.slice(1)
13
+ // path now: C:/Users/Docker/quickwin/_build/test/file.js
14
+ // Go up to _build/
15
+ const idx = path.lastIndexOf('/')
16
+ if (idx < 0) return '_cache'
17
+ const buildDir = path.slice(0, idx) // .../_build/test
18
+ const idx2 = buildDir.lastIndexOf('/')
19
+ if (idx2 < 0) return '_cache'
20
+ return buildDir.slice(0, idx2 + 1) + '_cache' // .../_build/_cache
21
+ }
22
+
23
+ export const suite = {
24
+ name: 'http-import',
25
+ run: async (t: Tester) => {
26
+ function assert(name: string, ok: boolean): void {
27
+ if (ok) { t.ok++; std.printf(' PASS: %s\n', name) }
28
+ else { t.fail++; std.printf(' FAIL: %s\n', name) }
29
+ }
30
+
31
+ const url = 'https://esm.sh/left-pad@1.3.0'
32
+ const cacheDir = getCacheDir()
33
+
34
+ t.section('list cache before')
35
+ const [before, err] = os.readdir(cacheDir)
36
+ if (err !== 0) {
37
+ std.printf(' cache dir: %s (error %d, will be created on first request)\n', cacheDir, err)
38
+ }
39
+
40
+ t.section('import module')
41
+ let mod: any
42
+ try {
43
+ mod = await import(url)
44
+ } catch (e) {
45
+ assert('import succeeds', false)
46
+ std.printf(' ERROR: %s\n', String(e))
47
+ return
48
+ }
49
+ assert('import succeeds', true)
50
+ assert('default is function', typeof mod.default === 'function')
51
+ assert('left-pad works', mod.default('hello', 8) === ' hello')
52
+
53
+ t.section('check cache')
54
+ const [after, err2] = os.readdir(cacheDir)
55
+ assert('cache dir readable', err2 === 0)
56
+ if (err2 !== 0) return
57
+ assert('cache has body files', after.some(function(f: string) { return f.endsWith('.body') }))
58
+ assert('cache has meta files', after.some(function(f: string) { return f.endsWith('.meta') }))
59
+ for (const f of after) {
60
+ if (f.endsWith('.meta') && (err !== 0 || before.indexOf(f) < 0)) {
61
+ std.printf(' meta: %s\n', f)
62
+ const fp = std.open(cacheDir + '/' + f, 'r')
63
+ if (fp) {
64
+ const metaStr = fp.readAsString()
65
+ fp.close()
66
+ if (metaStr) {
67
+ const ini = parseIni(metaStr)
68
+ std.printf(' storedAt: %s, maxAge: %s\n', ini.meta.storedAt || '?', ini.meta.maxAge || '?')
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
@@ -1,136 +1,136 @@
1
- import './test_helper.js'
2
- import * as std from 'std'
3
- import * as os from 'os'
4
- import * as sock from 'sock'
5
- import { Tester } from './test_helper.js'
6
- import { createServer } from '../lib/http-server.js'
7
-
8
- // ── Minimal raw HTTP client for testing ──
9
-
10
- function httpSend(host: string, port: number, raw: string, nResponses = 1): Promise<string> {
11
- return new Promise((resolve, reject) => {
12
- const s = sock.socket()
13
- if (s < 0) { reject(new Error('socket create failed')); return }
14
- let buf = ''
15
- let done = false
16
- const timer = os.setTimeout(() => {
17
- if (!done) { done = true; sock.closesocket(s); reject(new Error('timeout: ' + buf)) }
18
- }, 10000)
19
- sock.set_on_event(s, (event) => {
20
- if (done) return
21
- if (event.lNetworkEvents & sock.FdEvent.FD_CONNECT) {
22
- const err = event.iErrorCode[0]
23
- if (err !== 0) {
24
- done = true
25
- os.clearTimeout(timer)
26
- sock.closesocket(s)
27
- reject(new Error('connect error ' + err))
28
- return
29
- }
30
- const bytes = new TextEncoder().encode(raw)
31
- sock.send(s, bytes.buffer)
32
- }
33
- if (event.lNetworkEvents & sock.FdEvent.FD_READ) {
34
- while (true) {
35
- const d = sock.recv(s, 8192) as unknown as ArrayBuffer | number
36
- if (typeof d === 'number') break
37
- if (!d || d.byteLength === 0) break
38
- buf += new TextDecoder().decode(new Uint8Array(d))
39
- }
40
- const count = (buf.match(/HTTP\/1\.1 /g) || []).length
41
- if (count >= nResponses) {
42
- done = true
43
- os.clearTimeout(timer)
44
- sock.closesocket(s)
45
- resolve(buf)
46
- }
47
- }
48
- if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
49
- if (!done) { done = true; os.clearTimeout(timer); sock.closesocket(s); resolve(buf) }
50
- }
51
- })
52
- sock.connect(s, host, port)
53
- })
54
- }
55
-
56
- // ── Test suite ──
57
-
58
- export const suite = {
59
- name: 'http-server',
60
- run: async (t: Tester) => {
61
- const requests: string[] = []
62
- const server = createServer(async (req) => {
63
- const url = new URL(req.url)
64
- const path = url.pathname
65
- requests.push(req.method + ' ' + path)
66
- if (path === '/') {
67
- return new Response('hello ' + (url.searchParams.get('name') || ''), { headers: { 'Content-Type': 'text/plain' } })
68
- } else if (path === '/echo') {
69
- return new Response(req.method + ':' + await req.text(), { headers: { 'Content-Type': 'text/plain' } })
70
- } else if (path === '/json') {
71
- return new Response(JSON.stringify({ method: req.method, q: Object.fromEntries(url.searchParams) }), { headers: { 'Content-Type': 'application/json' } })
72
- } else {
73
- return new Response('not found', { status: 404 })
74
- }
75
- })
76
-
77
- const rc = server.listen(0, '127.0.0.1')
78
- t.checkTrue('listen ok', rc === 0)
79
- const addr = server.address()
80
- if (!addr) { t.check('address', 'non-null', 'null'); return }
81
- t.checkTrue('bound port > 0', addr.port > 0)
82
- const port = addr.port
83
-
84
- // ── GET with query ──
85
- t.section('GET with query')
86
- let r = await httpSend('127.0.0.1', port, 'GET /?name=world HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
87
- t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
88
- t.checkTrue('body = hello world', r.endsWith('hello world'))
89
- t.checkTrue('content-type text', r.toLowerCase().indexOf('content-type: text/plain') >= 0)
90
-
91
- // ── POST with body ──
92
- t.section('POST with body')
93
- r = await httpSend('127.0.0.1', port, 'POST /echo HTTP/1.1\r\nHost: x\r\nContent-Length: 7\r\nConnection: close\r\n\r\npayload')
94
- t.checkTrue('echo body', r.endsWith('POST:payload'))
95
-
96
- // ── POST with chunked request body ──
97
- t.section('chunked request body')
98
- const chunkedRaw = 'POST /echo HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n' +
99
- '3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n'
100
- r = await httpSend('127.0.0.1', port, chunkedRaw)
101
- t.checkTrue('chunked decoded', r.endsWith('POST:abcdefg'))
102
-
103
- // ── 404 ──
104
- t.section('404')
105
- r = await httpSend('127.0.0.1', port, 'GET /missing HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
106
- t.checkTrue('404 status', r.indexOf('HTTP/1.1 404') === 0)
107
- t.checkTrue('404 body', r.endsWith('not found'))
108
-
109
- // ── HEAD (headers present, empty body) ──
110
- t.section('HEAD')
111
- r = await httpSend('127.0.0.1', port, 'HEAD / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
112
- t.checkTrue('HEAD 200', r.indexOf('HTTP/1.1 200') === 0)
113
- t.checkTrue('HEAD content-length present', r.toLowerCase().indexOf('content-length: 6') >= 0)
114
- const bodyIdx = r.indexOf('\r\n\r\n')
115
- t.checkTrue('HEAD empty body', r.slice(bodyIdx + 4).length === 0)
116
-
117
- // ── keep-alive: two requests on one connection ──
118
- t.section('keep-alive pipelining')
119
- const pipelined = 'GET /?name=a HTTP/1.1\r\nHost: x\r\n\r\n' +
120
- 'GET /?name=b HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n'
121
- r = await httpSend('127.0.0.1', port, pipelined, 2)
122
- t.checkTrue('two responses received', r.indexOf('hello a') >= 0 && r.indexOf('hello b') >= 0)
123
- t.checkTrue('connection close on last', r.toLowerCase().indexOf('connection: close') >= 0)
124
-
125
- // ── server received expected requests ──
126
- t.section('request log')
127
- t.check('total requests', 7, requests.length)
128
- t.checkTrue('saw GET /', requests.indexOf('GET /') >= 0)
129
- t.checkTrue('saw POST /echo', requests.indexOf('POST /echo') >= 0)
130
-
131
- server.close()
132
- t.check('closed ok', true, true)
133
-
134
- std.printf(' (http-server suite done, %d requests)\n', requests.length)
135
- },
136
- }
1
+ import './test_helper.js'
2
+ import * as std from 'std'
3
+ import * as os from 'os'
4
+ import * as sock from 'sock'
5
+ import { Tester } from './test_helper.js'
6
+ import { createServer } from '../lib/http-server.js'
7
+
8
+ // ── Minimal raw HTTP client for testing ──
9
+
10
+ function httpSend(host: string, port: number, raw: string, nResponses = 1): Promise<string> {
11
+ return new Promise((resolve, reject) => {
12
+ const s = sock.socket()
13
+ if (s < 0) { reject(new Error('socket create failed')); return }
14
+ let buf = ''
15
+ let done = false
16
+ const timer = os.setTimeout(() => {
17
+ if (!done) { done = true; sock.closesocket(s); reject(new Error('timeout: ' + buf)) }
18
+ }, 10000)
19
+ sock.set_on_event(s, (event) => {
20
+ if (done) return
21
+ if (event.lNetworkEvents & sock.FdEvent.FD_CONNECT) {
22
+ const err = event.iErrorCode[0]
23
+ if (err !== 0) {
24
+ done = true
25
+ os.clearTimeout(timer)
26
+ sock.closesocket(s)
27
+ reject(new Error('connect error ' + err))
28
+ return
29
+ }
30
+ const bytes = new TextEncoder().encode(raw)
31
+ sock.send(s, bytes.buffer)
32
+ }
33
+ if (event.lNetworkEvents & sock.FdEvent.FD_READ) {
34
+ while (true) {
35
+ const d = sock.recv(s, 8192) as unknown as ArrayBuffer | number
36
+ if (typeof d === 'number') break
37
+ if (!d || d.byteLength === 0) break
38
+ buf += new TextDecoder().decode(new Uint8Array(d))
39
+ }
40
+ const count = (buf.match(/HTTP\/1\.1 /g) || []).length
41
+ if (count >= nResponses) {
42
+ done = true
43
+ os.clearTimeout(timer)
44
+ sock.closesocket(s)
45
+ resolve(buf)
46
+ }
47
+ }
48
+ if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
49
+ if (!done) { done = true; os.clearTimeout(timer); sock.closesocket(s); resolve(buf) }
50
+ }
51
+ })
52
+ sock.connect(s, host, port)
53
+ })
54
+ }
55
+
56
+ // ── Test suite ──
57
+
58
+ export const suite = {
59
+ name: 'http-server',
60
+ run: async (t: Tester) => {
61
+ const requests: string[] = []
62
+ const server = createServer(async (req) => {
63
+ const url = new URL(req.url)
64
+ const path = url.pathname
65
+ requests.push(req.method + ' ' + path)
66
+ if (path === '/') {
67
+ return new Response('hello ' + (url.searchParams.get('name') || ''), { headers: { 'Content-Type': 'text/plain' } })
68
+ } else if (path === '/echo') {
69
+ return new Response(req.method + ':' + await req.text(), { headers: { 'Content-Type': 'text/plain' } })
70
+ } else if (path === '/json') {
71
+ return new Response(JSON.stringify({ method: req.method, q: Object.fromEntries(url.searchParams) }), { headers: { 'Content-Type': 'application/json' } })
72
+ } else {
73
+ return new Response('not found', { status: 404 })
74
+ }
75
+ })
76
+
77
+ const rc = server.listen(0, '127.0.0.1')
78
+ t.checkTrue('listen ok', rc === 0)
79
+ const addr = server.address()
80
+ if (!addr) { t.check('address', 'non-null', 'null'); return }
81
+ t.checkTrue('bound port > 0', addr.port > 0)
82
+ const port = addr.port
83
+
84
+ // ── GET with query ──
85
+ t.section('GET with query')
86
+ let r = await httpSend('127.0.0.1', port, 'GET /?name=world HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
87
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
88
+ t.checkTrue('body = hello world', r.endsWith('hello world'))
89
+ t.checkTrue('content-type text', r.toLowerCase().indexOf('content-type: text/plain') >= 0)
90
+
91
+ // ── POST with body ──
92
+ t.section('POST with body')
93
+ r = await httpSend('127.0.0.1', port, 'POST /echo HTTP/1.1\r\nHost: x\r\nContent-Length: 7\r\nConnection: close\r\n\r\npayload')
94
+ t.checkTrue('echo body', r.endsWith('POST:payload'))
95
+
96
+ // ── POST with chunked request body ──
97
+ t.section('chunked request body')
98
+ const chunkedRaw = 'POST /echo HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n' +
99
+ '3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n'
100
+ r = await httpSend('127.0.0.1', port, chunkedRaw)
101
+ t.checkTrue('chunked decoded', r.endsWith('POST:abcdefg'))
102
+
103
+ // ── 404 ──
104
+ t.section('404')
105
+ r = await httpSend('127.0.0.1', port, 'GET /missing HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
106
+ t.checkTrue('404 status', r.indexOf('HTTP/1.1 404') === 0)
107
+ t.checkTrue('404 body', r.endsWith('not found'))
108
+
109
+ // ── HEAD (headers present, empty body) ──
110
+ t.section('HEAD')
111
+ r = await httpSend('127.0.0.1', port, 'HEAD / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
112
+ t.checkTrue('HEAD 200', r.indexOf('HTTP/1.1 200') === 0)
113
+ t.checkTrue('HEAD content-length present', r.toLowerCase().indexOf('content-length: 6') >= 0)
114
+ const bodyIdx = r.indexOf('\r\n\r\n')
115
+ t.checkTrue('HEAD empty body', r.slice(bodyIdx + 4).length === 0)
116
+
117
+ // ── keep-alive: two requests on one connection ──
118
+ t.section('keep-alive pipelining')
119
+ const pipelined = 'GET /?name=a HTTP/1.1\r\nHost: x\r\n\r\n' +
120
+ 'GET /?name=b HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n'
121
+ r = await httpSend('127.0.0.1', port, pipelined, 2)
122
+ t.checkTrue('two responses received', r.indexOf('hello a') >= 0 && r.indexOf('hello b') >= 0)
123
+ t.checkTrue('connection close on last', r.toLowerCase().indexOf('connection: close') >= 0)
124
+
125
+ // ── server received expected requests ──
126
+ t.section('request log')
127
+ t.check('total requests', 7, requests.length)
128
+ t.checkTrue('saw GET /', requests.indexOf('GET /') >= 0)
129
+ t.checkTrue('saw POST /echo', requests.indexOf('POST /echo') >= 0)
130
+
131
+ server.close()
132
+ t.check('closed ok', true, true)
133
+
134
+ std.printf(' (http-server suite done, %d requests)\n', requests.length)
135
+ },
136
+ }
@@ -1,5 +1,5 @@
1
- import '../lib/fetch.js'
2
- import * as os from 'os'
3
-
4
- const parent = os.Worker.parent
5
- parent.postMessage({ type: 'loaded' })
1
+ import '../lib/fetch.js'
2
+ import * as os from 'os'
3
+
4
+ const parent = os.Worker.parent
5
+ parent.postMessage({ type: 'loaded' })