goscript 0.2.2 → 0.2.3

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 (136) hide show
  1. package/compiler/gotest/testdata/browserapi/browserapi_test.go +36 -0
  2. package/compiler/lowering.go +223 -9
  3. package/compiler/override-registry_test.go +50 -0
  4. package/compiler/protobuf-ts-binding.go +154 -6
  5. package/compiler/protobuf-ts-binding_test.go +7 -2
  6. package/compiler/runtime-contract.go +2 -0
  7. package/compiler/runtime-contract_test.go +1 -0
  8. package/compiler/semantic-model.go +16 -0
  9. package/compiler/semantic-model_test.go +38 -0
  10. package/compiler/skeleton_test.go +473 -15
  11. package/compiler/typescript-emitter.go +4 -0
  12. package/dist/gs/builtin/builtin.js +7 -9
  13. package/dist/gs/builtin/builtin.js.map +1 -1
  14. package/dist/gs/builtin/defer.js +2 -2
  15. package/dist/gs/builtin/hostio.js +5 -5
  16. package/dist/gs/builtin/hostio.js.map +1 -1
  17. package/dist/gs/builtin/map.js +2 -1
  18. package/dist/gs/builtin/map.js.map +1 -1
  19. package/dist/gs/builtin/slice.d.ts +3 -0
  20. package/dist/gs/builtin/slice.js +39 -0
  21. package/dist/gs/builtin/slice.js.map +1 -1
  22. package/dist/gs/builtin/type.js +49 -0
  23. package/dist/gs/builtin/type.js.map +1 -1
  24. package/dist/gs/compress/zlib/index.js +5 -2
  25. package/dist/gs/compress/zlib/index.js.map +1 -1
  26. package/dist/gs/crypto/ecdh/index.js +27 -8
  27. package/dist/gs/crypto/ecdh/index.js.map +1 -1
  28. package/dist/gs/crypto/ed25519/index.js +3 -3
  29. package/dist/gs/crypto/ed25519/index.js.map +1 -1
  30. package/dist/gs/crypto/rand/index.js +6 -3
  31. package/dist/gs/crypto/rand/index.js.map +1 -1
  32. package/dist/gs/embed/index.js +9 -3
  33. package/dist/gs/embed/index.js.map +1 -1
  34. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +1 -0
  35. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +33 -0
  36. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
  37. package/dist/gs/github.com/mr-tron/base58/base58/index.js +4 -1
  38. package/dist/gs/github.com/mr-tron/base58/base58/index.js.map +1 -1
  39. package/dist/gs/golang.org/x/crypto/scrypt/index.d.ts +2 -0
  40. package/dist/gs/golang.org/x/crypto/scrypt/index.js +39 -0
  41. package/dist/gs/golang.org/x/crypto/scrypt/index.js.map +1 -0
  42. package/dist/gs/hash/fnv/index.js +13 -5
  43. package/dist/gs/hash/fnv/index.js.map +1 -1
  44. package/dist/gs/io/fs/glob.d.ts +3 -3
  45. package/dist/gs/io/fs/glob.js +8 -8
  46. package/dist/gs/io/fs/glob.js.map +1 -1
  47. package/dist/gs/io/fs/readdir.d.ts +2 -2
  48. package/dist/gs/io/fs/readdir.js +13 -74
  49. package/dist/gs/io/fs/readdir.js.map +1 -1
  50. package/dist/gs/io/fs/sub.js +4 -4
  51. package/dist/gs/io/fs/sub.js.map +1 -1
  52. package/dist/gs/io/fs/walk.js +1 -1
  53. package/dist/gs/io/fs/walk.js.map +1 -1
  54. package/dist/gs/maps/iter.js.map +1 -1
  55. package/dist/gs/maps/maps.js.map +1 -1
  56. package/dist/gs/mime/index.js +5 -2
  57. package/dist/gs/mime/index.js.map +1 -1
  58. package/dist/gs/net/http/httptest/index.js +6 -3
  59. package/dist/gs/net/http/httptest/index.js.map +1 -1
  60. package/dist/gs/net/http/index.d.ts +16 -4
  61. package/dist/gs/net/http/index.js +236 -40
  62. package/dist/gs/net/http/index.js.map +1 -1
  63. package/dist/gs/net/http/pprof/index.js.map +1 -1
  64. package/dist/gs/reflect/iter.js +1 -1
  65. package/dist/gs/reflect/iter.js.map +1 -1
  66. package/dist/gs/reflect/type.d.ts +2 -0
  67. package/dist/gs/reflect/type.js +53 -21
  68. package/dist/gs/reflect/type.js.map +1 -1
  69. package/dist/gs/runtime/pprof/index.js.map +1 -1
  70. package/dist/gs/runtime/runtime.js +2 -2
  71. package/dist/gs/runtime/runtime.js.map +1 -1
  72. package/dist/gs/runtime/trace/index.js.map +1 -1
  73. package/dist/gs/slices/slices.d.ts +1 -1
  74. package/dist/gs/slices/slices.js +37 -4
  75. package/dist/gs/slices/slices.js.map +1 -1
  76. package/gs/builtin/builtin.ts +11 -14
  77. package/gs/builtin/defer.ts +2 -2
  78. package/gs/builtin/hostio.ts +5 -5
  79. package/gs/builtin/map.ts +4 -1
  80. package/gs/builtin/slice.test.ts +14 -0
  81. package/gs/builtin/slice.ts +64 -0
  82. package/gs/builtin/type.ts +72 -0
  83. package/gs/bytes/bytes.test.ts +14 -13
  84. package/gs/compress/zlib/index.test.ts +19 -5
  85. package/gs/compress/zlib/index.ts +16 -7
  86. package/gs/context/context.test.ts +3 -1
  87. package/gs/crypto/ecdh/index.test.ts +6 -2
  88. package/gs/crypto/ecdh/index.ts +49 -12
  89. package/gs/crypto/ed25519/index.ts +20 -7
  90. package/gs/crypto/rand/index.ts +6 -3
  91. package/gs/embed/index.test.ts +3 -3
  92. package/gs/embed/index.ts +9 -3
  93. package/gs/fmt/fmt.test.ts +29 -4
  94. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +126 -0
  95. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +46 -0
  96. package/gs/github.com/mr-tron/base58/base58/index.ts +9 -3
  97. package/gs/github.com/zeebo/blake3/internal/consts/index.test.ts +2 -8
  98. package/gs/golang.org/x/crypto/scrypt/index.test.ts +81 -0
  99. package/gs/golang.org/x/crypto/scrypt/index.ts +54 -0
  100. package/gs/golang.org/x/crypto/scrypt/meta.json +5 -0
  101. package/gs/hash/fnv/index.test.ts +1 -8
  102. package/gs/hash/fnv/index.ts +27 -10
  103. package/gs/io/fs/glob.ts +13 -10
  104. package/gs/io/fs/meta.json +2 -0
  105. package/gs/io/fs/readdir.test.ts +63 -2
  106. package/gs/io/fs/readdir.ts +33 -30
  107. package/gs/io/fs/sub.ts +4 -4
  108. package/gs/io/fs/walk.ts +1 -1
  109. package/gs/maps/iter.ts +9 -9
  110. package/gs/maps/maps.ts +4 -4
  111. package/gs/math/bits/index.test.ts +10 -1
  112. package/gs/mime/index.test.ts +33 -15
  113. package/gs/mime/index.ts +9 -2
  114. package/gs/net/http/httptest/index.test.ts +17 -3
  115. package/gs/net/http/httptest/index.ts +8 -3
  116. package/gs/net/http/index.test.ts +645 -123
  117. package/gs/net/http/index.ts +548 -113
  118. package/gs/net/http/pprof/index.ts +24 -6
  119. package/gs/os/file_unix_js.test.ts +22 -0
  120. package/gs/reflect/iter.ts +4 -2
  121. package/gs/reflect/map.test.ts +56 -1
  122. package/gs/reflect/type.ts +76 -37
  123. package/gs/runtime/pprof/index.test.ts +7 -1
  124. package/gs/runtime/pprof/index.ts +5 -1
  125. package/gs/runtime/runtime.test.ts +7 -0
  126. package/gs/runtime/runtime.ts +2 -4
  127. package/gs/runtime/trace/index.test.ts +9 -1
  128. package/gs/runtime/trace/index.ts +5 -1
  129. package/gs/slices/meta.json +3 -0
  130. package/gs/slices/slices.test.ts +59 -21
  131. package/gs/slices/slices.ts +61 -20
  132. package/gs/strconv/complex.test.ts +17 -3
  133. package/gs/sync/atomic/doc_64.test.ts +2 -9
  134. package/gs/sync/sync.test.ts +18 -8
  135. package/gs/syscall/js/index.test.ts +9 -4
  136. package/package.json +5 -4
@@ -124,12 +124,16 @@ describe('net/http override', () => {
124
124
  expect(StatusText(StatusUnauthorized)).toBe('Unauthorized')
125
125
  expect(StatusText(StatusForbidden)).toBe('Forbidden')
126
126
  expect(StatusText(StatusMethodNotAllowed)).toBe('Method Not Allowed')
127
- expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
127
+ expect(StatusText(StatusUnsupportedMediaType)).toBe(
128
+ 'Unsupported Media Type',
129
+ )
128
130
  expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
129
131
  expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
130
132
  expect(StatusText(StatusBadGateway)).toBe('Bad Gateway')
131
133
  expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
132
- expect(StatusText(StatusNetworkAuthenticationRequired)).toBe('Network Authentication Required')
134
+ expect(StatusText(StatusNetworkAuthenticationRequired)).toBe(
135
+ 'Network Authentication Required',
136
+ )
133
137
  expect(StatusText(599)).toBe('')
134
138
  Header_Set(resp.Header, 'X-Test', 'ok')
135
139
  resp.Body = io.NopCloser(bytes.NewReader($.stringToBytes('body')))
@@ -164,12 +168,18 @@ describe('net/http override', () => {
164
168
  'text/plain',
165
169
  'charset=utf-8',
166
170
  ])
167
- expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain('copy')
168
- expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).not.toContain('copy')
171
+ expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain(
172
+ 'copy',
173
+ )
174
+ expect(
175
+ Array.from(Header_Values(header, 'Content-Type') ?? []),
176
+ ).not.toContain('copy')
169
177
 
170
178
  const written = new bytes.Buffer()
171
179
  expect(Header_Write(header, written)).toBeNull()
172
- expect(Buffer.from(written.Bytes()).toString('utf8')).toContain('Content-Type: text/plain\r\n')
180
+ expect(Buffer.from(written.Bytes()).toString('utf8')).toContain(
181
+ 'Content-Type: text/plain\r\n',
182
+ )
173
183
 
174
184
  expect(ParseHTTPVersion('HTTP/2.0')).toEqual([2, 0, true])
175
185
  expect(ParseHTTPVersion('h2')).toEqual([0, 0, false])
@@ -195,22 +205,58 @@ describe('net/http override', () => {
195
205
  expect(req?.URL?.Path).toBe('/path')
196
206
  expect(req?.URL?.RawQuery).toBe('q=1')
197
207
 
198
- expect(NewRequestWithContext(context.Background(), 'bad method', 'https://example.invalid/', null)[1]?.Error()).toBe(
199
- 'net/http: invalid method "bad method"',
200
- )
201
- expect(NewRequestWithContext(null, MethodGet, 'https://example.invalid/', null)[1]?.Error()).toBe(
202
- 'net/http: nil Context',
208
+ expect(
209
+ NewRequestWithContext(
210
+ context.Background(),
211
+ 'bad method',
212
+ 'https://example.invalid/',
213
+ null,
214
+ )[1]?.Error(),
215
+ ).toBe('net/http: invalid method "bad method"')
216
+ expect(
217
+ NewRequestWithContext(
218
+ null,
219
+ MethodGet,
220
+ 'https://example.invalid/',
221
+ null,
222
+ )[1]?.Error(),
223
+ ).toBe('net/http: nil Context')
224
+ expect(
225
+ NewRequestWithContext(
226
+ context.Background(),
227
+ MethodGet,
228
+ 'http://[::1',
229
+ null,
230
+ )[1],
231
+ ).not.toBeNull()
232
+ expect(
233
+ NewRequestWithContext(
234
+ context.Background(),
235
+ MethodGet,
236
+ 'http://x/%zz',
237
+ null,
238
+ )[1],
239
+ ).not.toBeNull()
240
+
241
+ const [bodyReq, bodyReqErr] = NewRequest(
242
+ MethodPost,
243
+ 'https://example.invalid/upload',
244
+ bytes.NewReader($.stringToBytes('abc')),
203
245
  )
204
- expect(NewRequestWithContext(context.Background(), MethodGet, 'http://[::1', null)[1]).not.toBeNull()
205
- expect(NewRequestWithContext(context.Background(), MethodGet, 'http://x/%zz', null)[1]).not.toBeNull()
206
-
207
- const [bodyReq, bodyReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', bytes.NewReader($.stringToBytes('abc')))
208
246
  expect(bodyReqErr).toBeNull()
209
247
  expect(bodyReq?.ContentLength).toBe(3)
210
- const [stringReq, stringReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', strings.NewReader('abcd'))
248
+ const [stringReq, stringReqErr] = NewRequest(
249
+ MethodPost,
250
+ 'https://example.invalid/upload',
251
+ strings.NewReader('abcd'),
252
+ )
211
253
  expect(stringReqErr).toBeNull()
212
254
  expect(stringReq?.ContentLength).toBe(4)
213
- const [noBodyReq, noBodyErr] = NewRequest(MethodPost, 'https://example.invalid/upload', NoBody)
255
+ const [noBodyReq, noBodyErr] = NewRequest(
256
+ MethodPost,
257
+ 'https://example.invalid/upload',
258
+ NoBody,
259
+ )
214
260
  expect(noBodyErr).toBeNull()
215
261
  expect(noBodyReq?.ContentLength).toBe(0)
216
262
  expect(noBodyReq?.Body).toBe(NoBody)
@@ -218,44 +264,84 @@ describe('net/http override', () => {
218
264
 
219
265
  it('applies cross-origin protection checks', () => {
220
266
  const protection = NewCrossOriginProtection()
221
- const [sameOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
267
+ const [sameOrigin] = NewRequest(
268
+ MethodPost,
269
+ 'https://example.invalid/update',
270
+ null,
271
+ )
222
272
  Header_Set(sameOrigin!.Header, 'Sec-Fetch-Site', 'same-origin')
223
273
  expect(protection.Check(sameOrigin)).toBeNull()
224
274
 
225
- const [noHeaders] = NewRequest(MethodPost, 'https://example.invalid/update', null)
275
+ const [noHeaders] = NewRequest(
276
+ MethodPost,
277
+ 'https://example.invalid/update',
278
+ null,
279
+ )
226
280
  expect(protection.Check(noHeaders)).toBeNull()
227
281
 
228
- const [safe] = NewRequest(MethodOptions, 'https://example.invalid/update', null)
282
+ const [safe] = NewRequest(
283
+ MethodOptions,
284
+ 'https://example.invalid/update',
285
+ null,
286
+ )
229
287
  Header_Set(safe!.Header, 'Sec-Fetch-Site', 'cross-site')
230
288
  expect(protection.Check(safe)).toBeNull()
231
289
 
232
- const [matchingOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
290
+ const [matchingOrigin] = NewRequest(
291
+ MethodPost,
292
+ 'https://example.invalid/update',
293
+ null,
294
+ )
233
295
  Header_Set(matchingOrigin!.Header, 'Origin', 'https://example.invalid')
234
296
  expect(protection.Check(matchingOrigin)).toBeNull()
235
297
 
236
- const [crossSite] = NewRequest(MethodPost, 'https://example.invalid/update', null)
298
+ const [crossSite] = NewRequest(
299
+ MethodPost,
300
+ 'https://example.invalid/update',
301
+ null,
302
+ )
237
303
  Header_Set(crossSite!.Header, 'Sec-Fetch-Site', 'cross-site')
238
304
  expect(protection.Check(crossSite)?.Error()).toContain('Sec-Fetch-Site')
239
305
 
240
- const [oldBrowser] = NewRequest(MethodPost, 'https://example.invalid/update', null)
306
+ const [oldBrowser] = NewRequest(
307
+ MethodPost,
308
+ 'https://example.invalid/update',
309
+ null,
310
+ )
241
311
  Header_Set(oldBrowser!.Header, 'Origin', 'https://attacker.invalid')
242
- expect(protection.Check(oldBrowser)?.Error()).toContain('Origin does not match Host')
312
+ expect(protection.Check(oldBrowser)?.Error()).toContain(
313
+ 'Origin does not match Host',
314
+ )
243
315
 
244
316
  expect(protection.AddTrustedOrigin('https://trusted.invalid')).toBeNull()
245
- expect(protection.AddTrustedOrigin('https://trusted.invalid/')).not.toBeNull()
246
- const [trusted] = NewRequest(MethodPost, 'https://example.invalid/update', null)
317
+ expect(
318
+ protection.AddTrustedOrigin('https://trusted.invalid/'),
319
+ ).not.toBeNull()
320
+ const [trusted] = NewRequest(
321
+ MethodPost,
322
+ 'https://example.invalid/update',
323
+ null,
324
+ )
247
325
  Header_Set(trusted!.Header, 'Origin', 'https://trusted.invalid')
248
326
  Header_Set(trusted!.Header, 'Sec-Fetch-Site', 'cross-site')
249
327
  expect(protection.Check(trusted)).toBeNull()
250
328
 
251
329
  protection.AddInsecureBypassPattern('/bypass/')
252
330
  protection.AddInsecureBypassPattern('POST /post-only/')
253
- const [bypass] = NewRequest(MethodPost, 'https://example.invalid/bypass/ok', null)
331
+ const [bypass] = NewRequest(
332
+ MethodPost,
333
+ 'https://example.invalid/bypass/ok',
334
+ null,
335
+ )
254
336
  Header_Set(bypass!.Header, 'Origin', 'https://attacker.invalid')
255
337
  Header_Set(bypass!.Header, 'Sec-Fetch-Site', 'cross-site')
256
338
  expect(protection.Check(bypass)).toBeNull()
257
339
 
258
- const [methodBypass] = NewRequest(MethodPost, 'https://example.invalid/post-only/', null)
340
+ const [methodBypass] = NewRequest(
341
+ MethodPost,
342
+ 'https://example.invalid/post-only/',
343
+ null,
344
+ )
259
345
  Header_Set(methodBypass!.Header, 'Origin', 'https://attacker.invalid')
260
346
  expect(protection.Check(methodBypass)).toBeNull()
261
347
  })
@@ -269,7 +355,11 @@ describe('net/http override', () => {
269
355
  w?.WriteHeader(StatusOK)
270
356
  },
271
357
  })
272
- const [blocked] = NewRequest(MethodPost, 'https://example.invalid/update', null)
358
+ const [blocked] = NewRequest(
359
+ MethodPost,
360
+ 'https://example.invalid/update',
361
+ null,
362
+ )
273
363
  Header_Set(blocked!.Header, 'Sec-Fetch-Site', 'cross-site')
274
364
  const blockedWriter = new testResponseWriter()
275
365
 
@@ -304,7 +394,9 @@ describe('net/http override', () => {
304
394
  expect(cookies?.[1]?.Value).toBe('v2')
305
395
 
306
396
  expect(ParseCookie('')[1]?.Error()).toBe('http: blank cookie')
307
- expect(ParseCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
397
+ expect(ParseCookie('missing-equals')[1]?.Error()).toBe(
398
+ "http: '=' not found in cookie",
399
+ )
308
400
  expect(ParseCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
309
401
  expect(ParseCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
310
402
 
@@ -330,9 +422,13 @@ describe('net/http override', () => {
330
422
  expect(spaced?.Quoted).toBe(true)
331
423
 
332
424
  expect(ParseSetCookie('')[1]?.Error()).toBe('http: blank cookie')
333
- expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
425
+ expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe(
426
+ "http: '=' not found in cookie",
427
+ )
334
428
  expect(ParseSetCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
335
- expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
429
+ expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe(
430
+ 'http: invalid cookie value',
431
+ )
336
432
  })
337
433
 
338
434
  it('exports no-body, limit-reader, and unsupported controller surfaces', async () => {
@@ -342,34 +438,48 @@ describe('net/http override', () => {
342
438
  expect(err).toBe(io.EOF)
343
439
  expect(NoBody.Close()).toBeNull()
344
440
 
345
- const limited = MaxBytesReader(null, {
346
- Read: (p: Uint8Array) => {
347
- p[0] = 1
348
- if (p.length > 1) {
349
- p[1] = 2
350
- }
351
- return [1, null]
441
+ const limited = MaxBytesReader(
442
+ null,
443
+ {
444
+ Read: (p: Uint8Array) => {
445
+ p[0] = 1
446
+ if (p.length > 1) {
447
+ p[1] = 2
448
+ }
449
+ return [1, null]
450
+ },
451
+ Close: () => null,
352
452
  },
353
- Close: () => null,
354
- }, 1)
453
+ 1,
454
+ )
355
455
  const limitedBuf = new Uint8Array(2)
356
456
  expect(limited.Read(limitedBuf)).toEqual([1, null])
357
457
  expect(limitedBuf[0]).toBe(1)
358
458
  const [, limitErr] = limited.Read(new Uint8Array(1))
359
459
  expect(limitErr).toBeInstanceOf(MaxBytesError)
360
460
 
361
- const exactReader = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('ok'))), 2)
461
+ const exactReader = MaxBytesReader(
462
+ null,
463
+ io.NopCloser(bytes.NewReader($.stringToBytes('ok'))),
464
+ 2,
465
+ )
362
466
  const [exactData, exactErr] = await io.ReadAll(exactReader)
363
467
  expect(exactErr).toBeNull()
364
468
  expect(Buffer.from(exactData ?? []).toString('utf8')).toBe('ok')
365
469
 
366
- const tooLarge = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))), 2)
470
+ const tooLarge = MaxBytesReader(
471
+ null,
472
+ io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))),
473
+ 2,
474
+ )
367
475
  const tooLargeBuf = new Uint8Array(8)
368
476
  const [tooLargeN, tooLargeErr] = tooLarge.Read(tooLargeBuf)
369
477
  expect(tooLargeN).toBe(2)
370
478
  expect(tooLargeErr).toBeInstanceOf(MaxBytesError)
371
479
  expect((tooLargeErr as MaxBytesError).Limit).toBe(2)
372
- expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe('to')
480
+ expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe(
481
+ 'to',
482
+ )
373
483
 
374
484
  const controller = NewResponseController(null)
375
485
  expect(controller.Hijack()[2]).toBe(ErrNotSupported)
@@ -382,12 +492,15 @@ describe('net/http override', () => {
382
492
  const body = io.NopCloser(bytes.NewReader($.stringToBytes('ok')))
383
493
  const [req] = NewRequest(MethodPost, 'http://example.invalid/upload', body)
384
494
  let servedReq: any = null
385
- const handler = MaxBytesHandler({
386
- ServeHTTP(_w, r) {
387
- servedReq = $.pointerValue(r)
388
- Header_Set(servedReq.Header, 'X-Shared', 'true')
495
+ const handler = MaxBytesHandler(
496
+ {
497
+ ServeHTTP(_w, r) {
498
+ servedReq = $.pointerValue(r)
499
+ Header_Set(servedReq.Header, 'X-Shared', 'true')
500
+ },
389
501
  },
390
- }, 1)
502
+ 1,
503
+ )
391
504
 
392
505
  handler.ServeHTTP(null, req)
393
506
 
@@ -424,7 +537,11 @@ describe('net/http override', () => {
424
537
  throw new Error('network down')
425
538
  },
426
539
  })
427
- const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid', null)
540
+ const [req, reqErr] = NewRequest(
541
+ MethodPost,
542
+ 'https://example.invalid',
543
+ null,
544
+ )
428
545
  expect(reqErr).toBeNull()
429
546
  expect((req!.URL as any).Path).toBe('/')
430
547
  expect(req!.Host).toBe('example.invalid')
@@ -440,13 +557,21 @@ describe('net/http override', () => {
440
557
  Read: (p: Uint8Array) => [p.length, null] as [number, null],
441
558
  }
442
559
 
443
- const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', reader)
560
+ const [req, reqErr] = NewRequest(
561
+ MethodPost,
562
+ 'https://example.invalid/upload',
563
+ reader,
564
+ )
444
565
 
445
566
  expect(reqErr).toBeNull()
446
567
  expect(req!.Body).not.toBeNull()
447
568
  expect(req!.Body!.Close()).toBeNull()
448
569
 
449
- const resp = new Response({ StatusCode: StatusCreated, ContentLength: -1, Request: varRef(req!) })
570
+ const resp = new Response({
571
+ StatusCode: StatusCreated,
572
+ ContentLength: -1,
573
+ Request: varRef(req!),
574
+ })
450
575
  expect(resp.ContentLength).toBe(-1)
451
576
  expect((resp.Request as any).value).toBe(req)
452
577
  })
@@ -462,7 +587,9 @@ describe('net/http override', () => {
462
587
  const headers = init?.headers as Headers
463
588
  expect(headers.get('Range')).toBe('bytes=0-9')
464
589
  expect(headers.get('Authorization')).toBe('Bearer test')
465
- expect(Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8')).toBe('payload')
590
+ expect(
591
+ Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8'),
592
+ ).toBe('payload')
466
593
  return new globalThis.Response('accepted', {
467
594
  status: StatusCreated,
468
595
  statusText: 'Created',
@@ -478,7 +605,11 @@ describe('net/http override', () => {
478
605
  return null
479
606
  },
480
607
  }
481
- const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', requestBody)
608
+ const [req] = NewRequest(
609
+ MethodPost,
610
+ 'https://example.invalid/upload',
611
+ requestBody,
612
+ )
482
613
  Header_Set(req!.Header, 'Range', 'bytes=0-9')
483
614
  Header_Set(req!.Header, 'Authorization', 'Bearer test')
484
615
 
@@ -491,6 +622,135 @@ describe('net/http override', () => {
491
622
  expect(requestBodyClosed).toBe(true)
492
623
  })
493
624
 
625
+ it('returns from fetch-backed RoundTrip after response headers', async () => {
626
+ let arrayBufferCalled = false
627
+ Object.defineProperty(globalThis, 'fetch', {
628
+ configurable: true,
629
+ writable: true,
630
+ value: async () => {
631
+ const response = new globalThis.Response(null, {
632
+ status: StatusOK,
633
+ statusText: 'OK',
634
+ headers: { 'Content-Length': '5' },
635
+ })
636
+ Object.defineProperty(response, 'arrayBuffer', {
637
+ configurable: true,
638
+ value: () => {
639
+ arrayBufferCalled = true
640
+ return new Promise<ArrayBuffer>(() => {})
641
+ },
642
+ })
643
+ return response
644
+ },
645
+ })
646
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
647
+
648
+ const [resp, err] = await Promise.race([
649
+ DefaultTransport.RoundTrip(req),
650
+ new Promise<never>((_, reject) =>
651
+ setTimeout(() => reject(new Error('RoundTrip did not return')), 25),
652
+ ),
653
+ ])
654
+
655
+ expect(err).toBeNull()
656
+ expect(resp?.StatusCode).toBe(StatusOK)
657
+ expect(resp?.ContentLength).toBe(5)
658
+ expect(arrayBufferCalled).toBe(false)
659
+ expect(resp?.Body?.Close()).toBeNull()
660
+ })
661
+
662
+ it('reads fetch-backed response bodies after RoundTrip returns', async () => {
663
+ Object.defineProperty(globalThis, 'fetch', {
664
+ configurable: true,
665
+ writable: true,
666
+ value: async () =>
667
+ new globalThis.Response('accepted', {
668
+ status: StatusCreated,
669
+ statusText: 'Created',
670
+ headers: { 'Content-Length': '8' },
671
+ }),
672
+ })
673
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
674
+
675
+ const [resp, err] = await DefaultTransport.RoundTrip(req)
676
+ const buf = new Uint8Array(16)
677
+ const [n, readErr] = await (resp!.Body!.Read(buf) as any)
678
+
679
+ expect(err).toBeNull()
680
+ expect(readErr).toBeNull()
681
+ expect(n).toBe(8)
682
+ expect(Buffer.from(buf.subarray(0, n)).toString('utf8')).toBe('accepted')
683
+ expect(resp?.Body?.Close()).toBeNull()
684
+ })
685
+
686
+ it('aborts pending fetches when the request context is canceled', async () => {
687
+ let fetchSignal: AbortSignal | undefined
688
+ let fetchStarted: (() => void) | null = null
689
+ const fetchStartedPromise = new Promise<void>((resolve) => {
690
+ fetchStarted = resolve
691
+ })
692
+ Object.defineProperty(globalThis, 'fetch', {
693
+ configurable: true,
694
+ writable: true,
695
+ value: async (_input: RequestInfo | URL, init?: RequestInit) => {
696
+ fetchSignal = init?.signal ?? undefined
697
+ fetchStarted?.()
698
+ return new Promise<globalThis.Response>((_resolve, reject) => {
699
+ fetchSignal?.addEventListener('abort', () => {
700
+ reject(new Error('aborted'))
701
+ })
702
+ })
703
+ },
704
+ })
705
+ const [ctx, cancel] = context.WithCancel(context.Background())
706
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
707
+
708
+ const roundTrip = DefaultTransport.RoundTrip(req!.WithContext(ctx))
709
+ await fetchStartedPromise
710
+ cancel?.()
711
+ const [resp, err] = await roundTrip
712
+
713
+ expect(resp).toBeNull()
714
+ expect(err).toBe(context.Canceled)
715
+ expect(fetchSignal?.aborted).toBe(true)
716
+ })
717
+
718
+ it('aborts pending fetch body reads when the request context is canceled', async () => {
719
+ let fetchSignal: AbortSignal | undefined
720
+ Object.defineProperty(globalThis, 'fetch', {
721
+ configurable: true,
722
+ writable: true,
723
+ value: async (_input: RequestInfo | URL, init?: RequestInit) => {
724
+ fetchSignal = init?.signal ?? undefined
725
+ const response = new globalThis.Response(null, { status: StatusOK })
726
+ Object.defineProperty(response, 'arrayBuffer', {
727
+ configurable: true,
728
+ value: () =>
729
+ new Promise<ArrayBuffer>((_resolve, reject) => {
730
+ fetchSignal?.addEventListener('abort', () => {
731
+ reject(new Error('body aborted'))
732
+ })
733
+ }),
734
+ })
735
+ return response
736
+ },
737
+ })
738
+ const [ctx, cancel] = context.WithCancel(context.Background())
739
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', null)
740
+
741
+ const [resp, roundTripErr] = await DefaultTransport.RoundTrip(
742
+ req!.WithContext(ctx),
743
+ )
744
+ const read = resp!.Body!.Read(new Uint8Array(8)) as any
745
+ cancel?.()
746
+ const [n, readErr] = await read
747
+
748
+ expect(roundTripErr).toBeNull()
749
+ expect(n).toBe(0)
750
+ expect(readErr).toBe(context.Canceled)
751
+ expect(fetchSignal?.aborted).toBe(true)
752
+ })
753
+
494
754
  it('closes request bodies when fetch body reads fail', async () => {
495
755
  let requestBodyClosed = false
496
756
  Object.defineProperty(globalThis, 'fetch', {
@@ -523,15 +783,20 @@ describe('net/http override', () => {
523
783
  writable: true,
524
784
  value: undefined,
525
785
  })
526
- const [unsupportedReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
527
- Read: () => [0, null],
528
- Close: () => {
529
- unsupportedClosed = true
530
- return null
786
+ const [unsupportedReq] = NewRequest(
787
+ MethodPost,
788
+ 'https://example.invalid/upload',
789
+ {
790
+ Read: () => [0, null],
791
+ Close: () => {
792
+ unsupportedClosed = true
793
+ return null
794
+ },
531
795
  },
532
- })
796
+ )
533
797
 
534
- const [unsupportedResp, unsupportedErr] = await DefaultTransport.RoundTrip(unsupportedReq)
798
+ const [unsupportedResp, unsupportedErr] =
799
+ await DefaultTransport.RoundTrip(unsupportedReq)
535
800
 
536
801
  expect(unsupportedResp).toBeNull()
537
802
  expect(unsupportedErr?.Error()).toContain('Client.Do is not implemented')
@@ -547,15 +812,21 @@ describe('net/http override', () => {
547
812
  })
548
813
  const [ctx, cancel] = context.WithCancel(context.Background())
549
814
  cancel?.()
550
- const [canceledReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
551
- Read: () => [0, null],
552
- Close: () => {
553
- canceledClosed = true
554
- return null
815
+ const [canceledReq] = NewRequest(
816
+ MethodPost,
817
+ 'https://example.invalid/upload',
818
+ {
819
+ Read: () => [0, null],
820
+ Close: () => {
821
+ canceledClosed = true
822
+ return null
823
+ },
555
824
  },
556
- })
825
+ )
557
826
 
558
- const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(canceledReq!.WithContext(ctx))
827
+ const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(
828
+ canceledReq!.WithContext(ctx),
829
+ )
559
830
 
560
831
  expect(canceledResp).toBeNull()
561
832
  expect(canceledErr).toBe(context.Canceled)
@@ -670,10 +941,14 @@ describe('net/http override', () => {
670
941
 
671
942
  it('posts URL-encoded forms through clients and package helper', async () => {
672
943
  const transport = {
673
- async RoundTrip(got: Request | $.VarRef<Request> | null): Promise<[Response | null, $.GoError]> {
944
+ async RoundTrip(
945
+ got: Request | $.VarRef<Request> | null,
946
+ ): Promise<[Response | null, $.GoError]> {
674
947
  const request = $.pointerValue<Request>(got)
675
948
  expect(request.Method).toBe(MethodPost)
676
- expect(Header_Get(request.Header, 'Content-Type')).toBe('application/x-www-form-urlencoded')
949
+ expect(Header_Get(request.Header, 'Content-Type')).toBe(
950
+ 'application/x-www-form-urlencoded',
951
+ )
677
952
  const [data, err] = await io.ReadAll(request.Body!)
678
953
  expect(err).toBeNull()
679
954
  expect($.bytesToString(data)).toBe('a=one&a=two&space=x+y')
@@ -686,7 +961,10 @@ describe('net/http override', () => {
686
961
  ])
687
962
  const client = new Client({ Transport: transport })
688
963
 
689
- const [clientResp, clientErr] = await client.PostForm('https://example.invalid/form', form)
964
+ const [clientResp, clientErr] = await client.PostForm(
965
+ 'https://example.invalid/form',
966
+ form,
967
+ )
690
968
  expect(clientErr).toBeNull()
691
969
  expect(clientResp?.StatusCode).toBe(StatusOK)
692
970
 
@@ -712,7 +990,10 @@ describe('net/http override', () => {
712
990
 
713
991
  Header_Add(header, 'x-pack-id', 'pack-1')
714
992
  Header_Add(header, 'X-Pack-Id', 'pack-2')
715
- expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual(['pack-1', 'pack-2'])
993
+ expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual([
994
+ 'pack-1',
995
+ 'pack-2',
996
+ ])
716
997
 
717
998
  Header_Del(header, 'x-pack-id')
718
999
  expect(Header_Get(header, 'X-Pack-ID')).toBe('')
@@ -721,7 +1002,7 @@ describe('net/http override', () => {
721
1002
  it('accepts server context and shutdown surfaces', () => {
722
1003
  const srv = new Server({
723
1004
  Addr: ':0',
724
- BaseContext: () => ({} as any),
1005
+ BaseContext: () => ({}) as any,
725
1006
  ReadHeaderTimeout: 10,
726
1007
  })
727
1008
 
@@ -729,7 +1010,9 @@ describe('net/http override', () => {
729
1010
  expect(srv.BaseContext?.(null)).toEqual({})
730
1011
  expect(srv.ReadHeaderTimeout).toBe(10)
731
1012
  expect(srv.Shutdown({} as any)).toBeNull()
732
- expect(srv.ListenAndServeTLS('cert.pem', 'key.pem')?.Error()).toBe('net/http: Server.ListenAndServeTLS is not implemented in GoScript')
1013
+ expect(srv.ListenAndServeTLS('cert.pem', 'key.pem')?.Error()).toBe(
1014
+ 'net/http: Server.ListenAndServeTLS is not implemented in GoScript',
1015
+ )
733
1016
  })
734
1017
 
735
1018
  it('supports handler functions and not-found responses for typechecked server tests', () => {
@@ -749,6 +1032,116 @@ describe('net/http override', () => {
749
1032
  expect(writes).toEqual(['status:404', '404 page not found\n'])
750
1033
  })
751
1034
 
1035
+ it('accepts nullable handler function receivers for generated method calls', () => {
1036
+ const writer: ResponseWriter = {
1037
+ Header: () => new Header(),
1038
+ Write: (p) => [p?.length ?? 0, null],
1039
+ WriteHeader: () => undefined,
1040
+ }
1041
+
1042
+ expect(typeof HandlerFunc_ServeHTTP).toBe('function')
1043
+ expect(writer.Header()).toBeInstanceOf(Header)
1044
+ })
1045
+
1046
+ it('registers Handler for generic runtime assertions', () => {
1047
+ const handlerFunc = (_w: ResponseWriter | null, _r: Request | null) =>
1048
+ undefined
1049
+ const handler = $.namedValueInterfaceValue(
1050
+ $.namedFunction(
1051
+ $.functionValue(handlerFunc, {
1052
+ kind: $.TypeKind.Function,
1053
+ params: [
1054
+ 'http.ResponseWriter',
1055
+ {
1056
+ kind: $.TypeKind.Pointer,
1057
+ elemType: 'http.Request',
1058
+ },
1059
+ ],
1060
+ results: [],
1061
+ }),
1062
+ 'http.HandlerFunc',
1063
+ {
1064
+ kind: $.TypeKind.Function,
1065
+ name: 'http.HandlerFunc',
1066
+ params: [
1067
+ 'http.ResponseWriter',
1068
+ {
1069
+ kind: $.TypeKind.Pointer,
1070
+ elemType: 'http.Request',
1071
+ },
1072
+ ],
1073
+ results: [],
1074
+ },
1075
+ ),
1076
+ 'http.HandlerFunc',
1077
+ {
1078
+ ServeHTTP: (receiver, ...args) =>
1079
+ HandlerFunc_ServeHTTP(
1080
+ $.isVarRef(receiver) ? receiver.value : receiver,
1081
+ ...(args as [ResponseWriter | null, Request | null]),
1082
+ ),
1083
+ },
1084
+ {
1085
+ kind: $.TypeKind.Function,
1086
+ name: 'http.HandlerFunc',
1087
+ params: [
1088
+ 'http.ResponseWriter',
1089
+ {
1090
+ kind: $.TypeKind.Pointer,
1091
+ elemType: 'http.Request',
1092
+ },
1093
+ ],
1094
+ results: [],
1095
+ },
1096
+ )
1097
+
1098
+ const [asserted, ok] = $.typeAssertTuple<{
1099
+ ServeHTTP(w: ResponseWriter | null, r: Request | null): void
1100
+ }>(handler, 'http.Handler')
1101
+
1102
+ expect(ok).toBe(true)
1103
+ expect(asserted).toBe(handler)
1104
+ })
1105
+
1106
+ it('rejects named method wrappers with incompatible interface arity', () => {
1107
+ const handler = $.namedValueInterfaceValue(
1108
+ $.namedFunction(
1109
+ $.functionValue(() => undefined, {
1110
+ kind: $.TypeKind.Function,
1111
+ params: [],
1112
+ results: [],
1113
+ }),
1114
+ 'http.HandlerFunc',
1115
+ {
1116
+ kind: $.TypeKind.Function,
1117
+ name: 'http.HandlerFunc',
1118
+ params: [],
1119
+ results: [],
1120
+ },
1121
+ ),
1122
+ 'http.HandlerFunc',
1123
+ {
1124
+ ServeHTTP: (receiver, ...args) =>
1125
+ HandlerFunc_ServeHTTP(
1126
+ $.isVarRef(receiver) ? receiver.value : receiver,
1127
+ ...(args as [ResponseWriter | null, Request | null]),
1128
+ ),
1129
+ },
1130
+ {
1131
+ kind: $.TypeKind.Function,
1132
+ name: 'http.HandlerFunc',
1133
+ params: [],
1134
+ results: [],
1135
+ },
1136
+ )
1137
+
1138
+ const [, ok] = $.typeAssertTuple<{
1139
+ ServeHTTP(w: ResponseWriter | null, r: Request | null): void
1140
+ }>(handler, 'http.Handler')
1141
+
1142
+ expect(ok).toBe(false)
1143
+ })
1144
+
752
1145
  it('routes through default mux and handler helper exports', () => {
753
1146
  const writes: string[] = []
754
1147
  const writer: ResponseWriter = {
@@ -786,15 +1179,18 @@ describe('net/http override', () => {
786
1179
  WriteHeader: () => undefined,
787
1180
  }
788
1181
 
789
- SetCookie(writer, new Cookie({
790
- Name: 'spacewave_local_capability',
791
- Value: 'token',
792
- Path: '/',
793
- MaxAge: 300,
794
- HttpOnly: true,
795
- Secure: true,
796
- SameSite: SameSiteStrictMode,
797
- }))
1182
+ SetCookie(
1183
+ writer,
1184
+ new Cookie({
1185
+ Name: 'spacewave_local_capability',
1186
+ Value: 'token',
1187
+ Path: '/',
1188
+ MaxAge: 300,
1189
+ HttpOnly: true,
1190
+ Secure: true,
1191
+ SameSite: SameSiteStrictMode,
1192
+ }),
1193
+ )
798
1194
 
799
1195
  expect(Array.from(header.get('Set-Cookie') ?? [])).toEqual([
800
1196
  'spacewave_local_capability=token; Path=/; Max-Age=300; HttpOnly; Secure; SameSite=Strict',
@@ -806,29 +1202,46 @@ describe('net/http override', () => {
806
1202
 
807
1203
  expect(err).toBeNull()
808
1204
  expect(parsed.Unix()).toBe(784111777)
809
- expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe('text/html; charset=utf-8')
1205
+ expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe(
1206
+ 'text/html; charset=utf-8',
1207
+ )
810
1208
  expect(
811
- DetectContentType(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
1209
+ DetectContentType(
1210
+ new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
1211
+ ),
812
1212
  ).toBe('image/png')
813
- expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe('image/jpeg')
814
- expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe('application/pdf')
815
- expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe('image/webp')
816
- expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe('audio/aiff')
1213
+ expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe(
1214
+ 'image/jpeg',
1215
+ )
1216
+ expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe(
1217
+ 'application/pdf',
1218
+ )
1219
+ expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe(
1220
+ 'image/webp',
1221
+ )
1222
+ expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe(
1223
+ 'audio/aiff',
1224
+ )
817
1225
  expect(DetectContentType($.stringToBytes('ID3payload'))).toBe('audio/mpeg')
818
1226
  expect(DetectContentType($.stringToBytes('wOFFpayload'))).toBe('font/woff')
819
- expect(DetectContentType(new Uint8Array([
820
- 0x00, 0x00, 0x00, 0x18,
821
- 0x66, 0x74, 0x79, 0x70,
822
- 0x69, 0x73, 0x6f, 0x6d,
823
- 0x00, 0x00, 0x00, 0x00,
824
- 0x6d, 0x70, 0x34, 0x31,
825
- 0x00, 0x00, 0x00, 0x00,
826
- ]))).toBe('video/mp4')
827
- expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe('application/octet-stream')
828
- expect(DetectContentType(new Uint8Array())).toBe('text/plain; charset=utf-8')
829
- })
830
-
831
- it('exports file server interface shapes', () => {
1227
+ expect(
1228
+ DetectContentType(
1229
+ new Uint8Array([
1230
+ 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f,
1231
+ 0x6d, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x70, 0x34, 0x31, 0x00, 0x00,
1232
+ 0x00, 0x00,
1233
+ ]),
1234
+ ),
1235
+ ).toBe('video/mp4')
1236
+ expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe(
1237
+ 'application/octet-stream',
1238
+ )
1239
+ expect(DetectContentType(new Uint8Array())).toBe(
1240
+ 'text/plain; charset=utf-8',
1241
+ )
1242
+ })
1243
+
1244
+ it('exports file server interface shapes', async () => {
832
1245
  const file: File = {
833
1246
  Close: () => null,
834
1247
  Read: (p) => [p?.length ?? 0, null],
@@ -837,11 +1250,15 @@ describe('net/http override', () => {
837
1250
  Stat: () => [null, null],
838
1251
  }
839
1252
  const fsys: FileSystem = {
840
- Open: (name) => (name === 'ok' ? [file, null] : [null, new Error('missing')]),
1253
+ Open: (name) =>
1254
+ name === 'ok' ? [file, null] : [null, new Error('missing')],
841
1255
  }
842
1256
 
843
- expect(fsys.Open('ok')[0]).toBe(file)
844
- expect(fsys.Open('missing')[1]?.message).toBe('missing')
1257
+ const [okFile] = await fsys.Open('ok')
1258
+ const [, missingErr] = await fsys.Open('missing')
1259
+
1260
+ expect(okFile).toBe(file)
1261
+ expect(missingErr?.message).toBe('missing')
845
1262
  })
846
1263
 
847
1264
  it('serves files and omits HEAD response bodies', async () => {
@@ -853,23 +1270,26 @@ describe('net/http override', () => {
853
1270
  Read: (p: Uint8Array) => reader.Read(p),
854
1271
  Seek: (offset: number, whence: number) => reader.Seek(offset, whence),
855
1272
  Readdir: () => [null, null] as [null, null],
856
- Stat: () => [
857
- {
858
- IsDir: () => false,
859
- ModTime: () => null as never,
860
- Mode: () => 0,
861
- Name: () => 'file.txt',
862
- Size: () => 5,
863
- Sys: () => null,
864
- },
865
- null,
866
- ] as const,
1273
+ Stat: () =>
1274
+ [
1275
+ {
1276
+ IsDir: () => false,
1277
+ ModTime: () => null as never,
1278
+ Mode: () => 0,
1279
+ Name: () => 'file.txt',
1280
+ Size: () => 5,
1281
+ Sys: () => null,
1282
+ },
1283
+ null,
1284
+ ] as const,
867
1285
  }
868
1286
  }
869
1287
  const root = FS({
870
1288
  Open: (name) => {
871
1289
  opened.push(name)
872
- return name === 'file.txt' ? [makeFile(), null] : [null, new Error('missing')]
1290
+ return name === 'file.txt' ?
1291
+ [makeFile(), null]
1292
+ : [null, new Error('missing')]
873
1293
  },
874
1294
  })
875
1295
  const writes: string[] = []
@@ -883,7 +1303,11 @@ describe('net/http override', () => {
883
1303
  WriteHeader: (code) => writes.push(`status:${code}`),
884
1304
  }
885
1305
  const handler = FileServer(root)
886
- const [getReq] = NewRequest(MethodGet, 'http://example.invalid/../file.txt', null)
1306
+ const [getReq] = NewRequest(
1307
+ MethodGet,
1308
+ 'http://example.invalid/../file.txt',
1309
+ null,
1310
+ )
887
1311
 
888
1312
  await handler.ServeHTTP(writer, getReq)
889
1313
 
@@ -893,7 +1317,11 @@ describe('net/http override', () => {
893
1317
 
894
1318
  writes.length = 0
895
1319
  opened.length = 0
896
- const [headReq] = NewRequest(MethodHead, 'http://example.invalid/file.txt', null)
1320
+ const [headReq] = NewRequest(
1321
+ MethodHead,
1322
+ 'http://example.invalid/file.txt',
1323
+ null,
1324
+ )
897
1325
 
898
1326
  await handler.ServeHTTP(writer, headReq)
899
1327
 
@@ -901,6 +1329,76 @@ describe('net/http override', () => {
901
1329
  expect(writes).toEqual(['status:200'])
902
1330
  })
903
1331
 
1332
+ it('serves files from async file systems and file methods', async () => {
1333
+ const closeCalls: string[] = []
1334
+ const root = {
1335
+ async Open(name: string) {
1336
+ if (name !== 'async.txt') {
1337
+ return [null, new Error('missing')]
1338
+ }
1339
+ let offset = 0
1340
+ const file = {
1341
+ async Close() {
1342
+ closeCalls.push(name)
1343
+ return null
1344
+ },
1345
+ async Read(p) {
1346
+ const data = $.stringToBytes('async-body')
1347
+ if (offset >= data.length) {
1348
+ return [0, io.EOF]
1349
+ }
1350
+ const n = Math.min(p.length, data.length - offset)
1351
+ p.set(data.subarray(offset, offset + n), 0)
1352
+ offset += n
1353
+ return [n, null]
1354
+ },
1355
+ async Seek(seekOffset) {
1356
+ offset = Number(seekOffset)
1357
+ return [offset, null]
1358
+ },
1359
+ async Readdir() {
1360
+ return [null, null]
1361
+ },
1362
+ async Stat() {
1363
+ return [
1364
+ {
1365
+ IsDir: () => false,
1366
+ ModTime: () => null as never,
1367
+ Mode: () => 0,
1368
+ Name: () => name,
1369
+ Size: () => 10,
1370
+ Sys: () => null,
1371
+ },
1372
+ null,
1373
+ ]
1374
+ },
1375
+ }
1376
+ return [file, null]
1377
+ },
1378
+ }
1379
+ const writes: string[] = []
1380
+ const header = new Header()
1381
+ const writer: ResponseWriter = {
1382
+ Header: () => header,
1383
+ Write: (p) => {
1384
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
1385
+ return [p?.length ?? 0, null]
1386
+ },
1387
+ WriteHeader: (code) => writes.push(`status:${code}`),
1388
+ }
1389
+ const [req] = NewRequest(
1390
+ MethodGet,
1391
+ 'http://example.invalid/async.txt',
1392
+ null,
1393
+ )
1394
+
1395
+ await FileServer(root).ServeHTTP(writer, req)
1396
+
1397
+ expect(writes).toEqual(['status:200', 'async-body'])
1398
+ expect(Header_Get(header, 'Content-Length')).toBe('10')
1399
+ expect(closeCalls).toEqual(['async.txt'])
1400
+ })
1401
+
904
1402
  it('awaits ServeContent writes before returning', async () => {
905
1403
  const writes: string[] = []
906
1404
  const writer: ResponseWriter = {
@@ -911,15 +1409,35 @@ describe('net/http override', () => {
911
1409
  },
912
1410
  WriteHeader: (code) => writes.push(`status:${code}`),
913
1411
  }
914
- const [req] = NewRequest(MethodGet, 'http://example.invalid/content.txt', null)
1412
+ const [req] = NewRequest(
1413
+ MethodGet,
1414
+ 'http://example.invalid/content.txt',
1415
+ null,
1416
+ )
915
1417
 
916
- await ServeContent(writer, req, 'content.txt', null as never, bytes.NewReader($.stringToBytes('served')))
1418
+ await ServeContent(
1419
+ writer,
1420
+ req,
1421
+ 'content.txt',
1422
+ null as never,
1423
+ bytes.NewReader($.stringToBytes('served')),
1424
+ )
917
1425
 
918
1426
  expect(writes).toEqual(['status:200', 'served'])
919
1427
 
920
1428
  writes.length = 0
921
- const [headReq] = NewRequest(MethodHead, 'http://example.invalid/content.txt', null)
922
- await ServeContent(writer, headReq, 'content.txt', null as never, bytes.NewReader($.stringToBytes('hidden')))
1429
+ const [headReq] = NewRequest(
1430
+ MethodHead,
1431
+ 'http://example.invalid/content.txt',
1432
+ null,
1433
+ )
1434
+ await ServeContent(
1435
+ writer,
1436
+ headReq,
1437
+ 'content.txt',
1438
+ null as never,
1439
+ bytes.NewReader($.stringToBytes('hidden')),
1440
+ )
923
1441
 
924
1442
  expect(writes).toEqual(['status:200'])
925
1443
  })
@@ -954,7 +1472,11 @@ describe('net/http override', () => {
954
1472
  },
955
1473
  WriteHeader: (code) => writes.push(`status:${code}`),
956
1474
  }
957
- const [req] = NewRequest(MethodGet, 'http://example.invalid/eval/missing.js', null)
1475
+ const [req] = NewRequest(
1476
+ MethodGet,
1477
+ 'http://example.invalid/eval/missing.js',
1478
+ null,
1479
+ )
958
1480
 
959
1481
  ServeFile(writer, req, '/host-only/missing.js')
960
1482