@amritk/nish-x86_64-linux 0.30.0 → 0.32.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.
- package/bin/nish +0 -0
- package/docs/AI.md +66 -8
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -0
- package/runtime/core/host.mjs +9 -0
- package/runtime/nish.d.ts +48 -2
- package/runtime/nish.h +9 -1
- package/runtime/nish.mjs +36 -0
- package/runtime/runtime-net.c +32 -3
- package/runtime/runtime-os.c +33 -1
- package/runtime/runtime-region.c +15 -3
- package/runtime/runtime.c +9 -2
- package/std/README.md +9 -7
- package/std/crypto/bcrypt.ts +152 -101
- package/std/net/{http-client.ts → fetch.ts} +17 -17
- package/std/net/handler.ts +1 -1
- package/std/net/{http1-client.ts → http1-client-codec.ts} +6 -6
- package/std/net/{http1.ts → http1-codec.ts} +2 -2
- package/std/net/http1-server.ts +4 -4
- package/std/net/outbound.ts +2 -2
- package/std/net/postgres.ts +383 -49
- package/std/net/prefork.ts +149 -13
- package/std/net/router.ts +1 -1
- package/std/net/upstream.ts +3 -3
- package/std/net/websocket.ts +5 -5
- package/std/region.ts +95 -0
- package/std/sort.ts +178 -0
package/std/net/prefork.ts
CHANGED
|
@@ -51,6 +51,26 @@
|
|
|
51
51
|
* gone longer than that. With `restart` off nothing is signalled; a worker sees
|
|
52
52
|
* its peer's exit as a hang-up.
|
|
53
53
|
*
|
|
54
|
+
* **Listeners and pinning.** With `listenPort` set (0 or above), the
|
|
55
|
+
* supervisor binds one `SO_REUSEPORT` listener per worker itself, in index
|
|
56
|
+
* order, keeps them for its whole run and hands listener `k` to every start of
|
|
57
|
+
* worker `k` after the channel ends; `preforkListener(args)` answers it in the
|
|
58
|
+
* worker (`--nish-prefork-listener=`, before the channels flag), so a worker
|
|
59
|
+
* serves that descriptor instead of binding its own. Port 0 binds the first
|
|
60
|
+
* listener anywhere and the rest to its port. With `pin` on as well, each
|
|
61
|
+
* listener steers by CPU (`tcpListenFlags`' flag 4): the kernel hands a
|
|
62
|
+
* connection that arrived on CPU `k` to listener `k`, and a worker that is
|
|
63
|
+
* restarted takes the same listener, so the order holds across restarts,
|
|
64
|
+
* which it would not if each worker bound its own. `pin` alone makes each
|
|
65
|
+
* worker call `setCpuAffinity(k)` before `preforkWith` answers it, so worker
|
|
66
|
+
* `k` runs on CPU `k` only; a worker the kernel refuses (CPU `k` outside the
|
|
67
|
+
* process's mask) says so on stderr and runs unpinned. More workers than
|
|
68
|
+
* `availableParallelism()` is refused with a panic, since a worker past the
|
|
69
|
+
* last CPU would be pinned nowhere and, steered, handed nothing. Pinning and
|
|
70
|
+
* steering are Linux's: elsewhere the supervisor says once that the workers
|
|
71
|
+
* run unpinned, and the listeners are plain `SO_REUSEPORT` ones. Both are off
|
|
72
|
+
* by default.
|
|
73
|
+
*
|
|
54
74
|
* **How many.** `NISH_WORKERS` when it is a positive whole number, else
|
|
55
75
|
* `PreforkConfig.workers` when that is positive, else
|
|
56
76
|
* `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
|
|
@@ -83,10 +103,12 @@ import {
|
|
|
83
103
|
NET_UNIX_SEQPACKET,
|
|
84
104
|
NET_UNIX_STREAM,
|
|
85
105
|
netClose,
|
|
106
|
+
netLocalPort,
|
|
86
107
|
netRead,
|
|
87
108
|
pollAdd,
|
|
88
109
|
pollCreate,
|
|
89
110
|
pollWait,
|
|
111
|
+
tcpListenFlags,
|
|
90
112
|
unixSocketPair,
|
|
91
113
|
} from "nish:net"
|
|
92
114
|
import {
|
|
@@ -96,6 +118,7 @@ import {
|
|
|
96
118
|
monotonicNanos,
|
|
97
119
|
platform,
|
|
98
120
|
readSignal,
|
|
121
|
+
setCpuAffinity,
|
|
99
122
|
signalFd,
|
|
100
123
|
spawn,
|
|
101
124
|
spawnWithFds,
|
|
@@ -111,6 +134,12 @@ export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
|
|
|
111
134
|
* for none.
|
|
112
135
|
*/
|
|
113
136
|
export const PREFORK_CHANNELS_FLAG: string = "--nish-prefork-channels="
|
|
137
|
+
/**
|
|
138
|
+
* The argument before the channels flag, or before the worker flag when there
|
|
139
|
+
* is none, when the supervisor holds the listeners: the worker's listener's
|
|
140
|
+
* descriptor.
|
|
141
|
+
*/
|
|
142
|
+
export const PREFORK_LISTENER_FLAG: string = "--nish-prefork-listener="
|
|
114
143
|
|
|
115
144
|
/** What `preforkWith` answers in the supervisor once its workers are gone. */
|
|
116
145
|
export const PREFORK_STOPPED: i32 = -1
|
|
@@ -127,6 +156,8 @@ export const PREFORK_WORKERS_EXITED: i32 = -3
|
|
|
127
156
|
|
|
128
157
|
/** The supervisor's numbers; every one is checked by `preforkWith`. */
|
|
129
158
|
export class PreforkConfig {
|
|
159
|
+
/** The address the listeners of `listenPort` bind. */
|
|
160
|
+
listenHost: string = "0.0.0.0"
|
|
130
161
|
/** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
|
|
131
162
|
workers: i32 = 0
|
|
132
163
|
/** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
|
|
@@ -139,6 +170,23 @@ export class PreforkConfig {
|
|
|
139
170
|
quickExitMs: i32 = 1000
|
|
140
171
|
/** How long the workers get to exit after SIGTERM before SIGKILL. */
|
|
141
172
|
graceMs: i32 = 5000
|
|
173
|
+
/** The worker every other worker gets a socketpair to; -1 for none. */
|
|
174
|
+
serviceWorker: i32 = -1
|
|
175
|
+
/**
|
|
176
|
+
* What every channel is: `NET_UNIX_STREAM` (1), or `NET_UNIX_SEQPACKET` (2),
|
|
177
|
+
* which keeps each write one message but only Linux has. Stream is the
|
|
178
|
+
* default because it works on Darwin too.
|
|
179
|
+
*/
|
|
180
|
+
channelKind: i32 = 1
|
|
181
|
+
/**
|
|
182
|
+
* The port the supervisor binds one listener per worker on, 0 for any; -1,
|
|
183
|
+
* the default, leaves each worker to bind its own.
|
|
184
|
+
*/
|
|
185
|
+
listenPort: i32 = -1
|
|
186
|
+
/** Their backlog. */
|
|
187
|
+
listenBacklog: i32 = 1024
|
|
188
|
+
/** Their `tcpListenFlags` beyond `SO_REUSEPORT`, which is always set: 0, or 2 for `TCP_NODELAY`. */
|
|
189
|
+
listenFlags: i32 = 0
|
|
142
190
|
/**
|
|
143
191
|
* Start a worker that exited again. Off, a worker that exits stays gone,
|
|
144
192
|
* and the supervisor returns once none is left.
|
|
@@ -146,14 +194,14 @@ export class PreforkConfig {
|
|
|
146
194
|
restart: boolean = true
|
|
147
195
|
/** Give each worker a socketpair to the supervisor (`preforkSupervise`). */
|
|
148
196
|
supervisorChannels: boolean = false
|
|
149
|
-
/** The worker every other worker gets a socketpair to; -1 for none. */
|
|
150
|
-
serviceWorker: i32 = -1
|
|
151
197
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
198
|
+
* Pin worker `k` to CPU `k` (`setCpuAffinity`), and with `listenPort`, steer
|
|
199
|
+
* each connection to the listener of the CPU that received it. Linux only;
|
|
200
|
+
* elsewhere the workers run unpinned. `k` is the CPU number, not the `k`-th
|
|
201
|
+
* allowed CPU: under a mask that skips CPU `k`, worker `k` runs unpinned and
|
|
202
|
+
* a connection received past the listeners falls back to the kernel's hash.
|
|
155
203
|
*/
|
|
156
|
-
|
|
204
|
+
pin: boolean = false
|
|
157
205
|
}
|
|
158
206
|
|
|
159
207
|
/** True when `s` is one or more ASCII digits and nothing else. */
|
|
@@ -206,8 +254,8 @@ export const preforkIndex = (args: string[]): i32 => {
|
|
|
206
254
|
}
|
|
207
255
|
|
|
208
256
|
/**
|
|
209
|
-
* `args` (`process.argv`) without the worker flag and the channels
|
|
210
|
-
* it, the same in every process.
|
|
257
|
+
* `args` (`process.argv`) without the worker flag and the channels and
|
|
258
|
+
* listener flags before it, the same in every process.
|
|
211
259
|
*/
|
|
212
260
|
export const preforkArgs = (args: string[]): string[] => {
|
|
213
261
|
let keep: i32 = toI32(args.length)
|
|
@@ -216,6 +264,9 @@ export const preforkArgs = (args: string[]): string[] => {
|
|
|
216
264
|
if (keep > 1 && args[keep - 1].startsWith(PREFORK_CHANNELS_FLAG)) {
|
|
217
265
|
keep = keep - 1
|
|
218
266
|
}
|
|
267
|
+
if (keep > 1 && args[keep - 1].startsWith(PREFORK_LISTENER_FLAG)) {
|
|
268
|
+
keep = keep - 1
|
|
269
|
+
}
|
|
219
270
|
}
|
|
220
271
|
const out: string[] = []
|
|
221
272
|
for (let i: i32 = 0; i < toI32(args.length); i++) {
|
|
@@ -258,6 +309,21 @@ export const preforkServiceChannel = (args: string[]): i32 => preforkChannel(arg
|
|
|
258
309
|
export const preforkServiceChannelOf = (args: string[], index: i32): i32 =>
|
|
259
310
|
index < 0 || index > 2147483645 ? -1 : preforkChannel(args, 2 + index)
|
|
260
311
|
|
|
312
|
+
/** In a worker, the listener the supervisor bound for it (`listenPort`); -1 when none. */
|
|
313
|
+
export const preforkListener = (args: string[]): i32 => {
|
|
314
|
+
if (preforkIndex(args) < 0) {
|
|
315
|
+
return -1
|
|
316
|
+
}
|
|
317
|
+
for (let back: i32 = 2; back <= 3 && back < toI32(args.length); back++) {
|
|
318
|
+
const flag: string = args[toI32(args.length) - back]
|
|
319
|
+
if (flag.startsWith(PREFORK_LISTENER_FLAG)) {
|
|
320
|
+
const digits: string = flag.substring(PREFORK_LISTENER_FLAG.length)
|
|
321
|
+
return preforkDigits(digits) ? parseInt(digits) : -1
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return -1
|
|
325
|
+
}
|
|
326
|
+
|
|
261
327
|
/** Milliseconds on the monotonic clock. */
|
|
262
328
|
const preforkNowMs = (): i64 => monotonicNanos() / 1000000
|
|
263
329
|
|
|
@@ -298,6 +364,8 @@ class PreforkSupervisor {
|
|
|
298
364
|
service: i32
|
|
299
365
|
/** The group is being stopped, to be started again once none of it is left. */
|
|
300
366
|
cascading: boolean = false
|
|
367
|
+
/** Worker i's listener at i, held for the whole run; empty without `listenPort`. */
|
|
368
|
+
listeners: i32[]
|
|
301
369
|
|
|
302
370
|
constructor(config: PreforkConfig, args: string[], count: i32, loop: i32) {
|
|
303
371
|
this.config = config
|
|
@@ -305,6 +373,7 @@ class PreforkSupervisor {
|
|
|
305
373
|
this.service = config.serviceWorker
|
|
306
374
|
this.toService = []
|
|
307
375
|
this.fromWorkers = []
|
|
376
|
+
this.listeners = []
|
|
308
377
|
this.args = preforkArgs(args)
|
|
309
378
|
const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
|
|
310
379
|
this.program = self !== null ? self : this.args[0]
|
|
@@ -359,7 +428,12 @@ class PreforkSupervisor {
|
|
|
359
428
|
for (let i: i32 = 1; i < toI32(this.args.length); i++) {
|
|
360
429
|
argv.push(this.args[i])
|
|
361
430
|
}
|
|
362
|
-
|
|
431
|
+
const channels: boolean = fds.length > 0
|
|
432
|
+
if (this.listeners.length > 0) {
|
|
433
|
+
fds.push(this.listeners[index])
|
|
434
|
+
argv.push(`${PREFORK_LISTENER_FLAG}${2 + fds.length}`)
|
|
435
|
+
}
|
|
436
|
+
if (channels) {
|
|
363
437
|
argv.push(`${PREFORK_CHANNELS_FLAG}${fields.join(",")}`)
|
|
364
438
|
}
|
|
365
439
|
argv.push(`${PREFORK_WORKER_FLAG}${index}`)
|
|
@@ -576,6 +650,37 @@ class PreforkSupervisor {
|
|
|
576
650
|
return true
|
|
577
651
|
}
|
|
578
652
|
|
|
653
|
+
/**
|
|
654
|
+
* One listener per worker, in index order, which is the order steering
|
|
655
|
+
* counts CPUs in; false, with the ones made closed, when one cannot bind.
|
|
656
|
+
*/
|
|
657
|
+
listen(steer: boolean): boolean {
|
|
658
|
+
const config: PreforkConfig = this.config
|
|
659
|
+
const flags: i32 = config.listenFlags | 1 | (steer ? 4 : 0)
|
|
660
|
+
let port: i32 = config.listenPort
|
|
661
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
662
|
+
const fd: i32 = tcpListenFlags(config.listenHost, port, config.listenBacklog, flags)
|
|
663
|
+
if (fd < 0) {
|
|
664
|
+
console.error(`nish/net/prefork: cannot listen on ${config.listenHost}:${port} (${fd})`)
|
|
665
|
+
this.closeListeners()
|
|
666
|
+
return false
|
|
667
|
+
}
|
|
668
|
+
this.listeners.push(fd)
|
|
669
|
+
if (port === 0) {
|
|
670
|
+
port = netLocalPort(fd)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return true
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Close the listeners `listen` made. */
|
|
677
|
+
closeListeners(): void {
|
|
678
|
+
for (let i: i32 = 0; i < toI32(this.listeners.length); i++) {
|
|
679
|
+
netClose(this.listeners[i])
|
|
680
|
+
}
|
|
681
|
+
this.listeners = []
|
|
682
|
+
}
|
|
683
|
+
|
|
579
684
|
/** Send `sig` to every running worker. */
|
|
580
685
|
signal(sig: i32): void {
|
|
581
686
|
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
@@ -644,6 +749,15 @@ const preforkInvalid = (config: PreforkConfig): string => {
|
|
|
644
749
|
if (config.serviceWorker < -1) {
|
|
645
750
|
return "serviceWorker below -1"
|
|
646
751
|
}
|
|
752
|
+
if (config.listenPort < -1 || config.listenPort > 65535) {
|
|
753
|
+
return "listenPort not -1 or 0..65535"
|
|
754
|
+
}
|
|
755
|
+
if (config.listenBacklog <= 0) {
|
|
756
|
+
return "listenBacklog not above 0"
|
|
757
|
+
}
|
|
758
|
+
if (config.listenFlags !== 0 && config.listenFlags !== 2) {
|
|
759
|
+
return "listenFlags not 0 or 2"
|
|
760
|
+
}
|
|
647
761
|
return ""
|
|
648
762
|
}
|
|
649
763
|
|
|
@@ -679,8 +793,11 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
|
|
|
679
793
|
* supervisor closes it after a hang-up, when the worker is reaped and when it
|
|
680
794
|
* stops, and a restarted worker comes with a new one. Panics, besides, when
|
|
681
795
|
* `serviceWorker` is not below the worker count or the count is above 64 with
|
|
682
|
-
* a service worker, the most `spawnWithFds` hands one
|
|
683
|
-
* `
|
|
796
|
+
* a service worker (63 with `listenPort`), the most `spawnWithFds` hands one
|
|
797
|
+
* child; when `pin`, on Linux, asks for more workers than
|
|
798
|
+
* `availableParallelism()`; and, through `preforkInvalid`, on seqpacket
|
|
799
|
+
* channels off Linux. A listener that cannot bind answers
|
|
800
|
+
* `PREFORK_SPAWN_FAILED` before any worker starts.
|
|
684
801
|
*/
|
|
685
802
|
export const preforkSupervise = <S>(
|
|
686
803
|
args: string[],
|
|
@@ -692,13 +809,27 @@ export const preforkSupervise = <S>(
|
|
|
692
809
|
if (invalid !== "") {
|
|
693
810
|
panic(`nish/net/prefork: ${invalid}`)
|
|
694
811
|
}
|
|
812
|
+
const pin: boolean = config.pin && platform === "linux"
|
|
695
813
|
const index: i32 = preforkIndex(args)
|
|
696
814
|
if (index >= 0) {
|
|
815
|
+
const pinned: i32 = pin ? setCpuAffinity(index) : 0
|
|
816
|
+
if (pinned < 0) {
|
|
817
|
+
console.error(`nish/net/prefork: worker ${index} runs unpinned (${pinned})`)
|
|
818
|
+
}
|
|
697
819
|
return index
|
|
698
820
|
}
|
|
699
821
|
const count: i32 = preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
|
|
700
|
-
|
|
701
|
-
|
|
822
|
+
// A held listener is one more descriptor for every worker, the service one too.
|
|
823
|
+
const fdMax: i32 = config.listenPort >= 0 ? 63 : 64
|
|
824
|
+
if (config.serviceWorker >= count || (config.serviceWorker >= 0 && count > fdMax)) {
|
|
825
|
+
panic(`nish/net/prefork: serviceWorker not below ${count} workers, or above ${fdMax} workers`)
|
|
826
|
+
}
|
|
827
|
+
const cpus: i32 = pin ? availableParallelism() : 0
|
|
828
|
+
if (pin && count > cpus) {
|
|
829
|
+
panic(`nish/net/prefork: pin with ${count} workers on ${cpus} CPUs`)
|
|
830
|
+
}
|
|
831
|
+
if (config.pin && !pin) {
|
|
832
|
+
console.error("nish/net/prefork: pinning needs Linux; the workers run unpinned")
|
|
702
833
|
}
|
|
703
834
|
const signals: i32 = signalFd()
|
|
704
835
|
const loop: i32 = pollCreate()
|
|
@@ -712,6 +843,10 @@ export const preforkSupervise = <S>(
|
|
|
712
843
|
return PREFORK_SPAWN_FAILED
|
|
713
844
|
}
|
|
714
845
|
const supervisor = new PreforkSupervisor(config, args, count, loop)
|
|
846
|
+
if (config.listenPort >= 0 && !supervisor.listen(pin)) {
|
|
847
|
+
netClose(loop)
|
|
848
|
+
return PREFORK_SPAWN_FAILED
|
|
849
|
+
}
|
|
715
850
|
// Token 0 is the signal pipe, token i + 1 worker i's channel.
|
|
716
851
|
const ready: i32[] = new Array<i32>(2 * count + 2)
|
|
717
852
|
let result: i32 = PREFORK_STOPPED
|
|
@@ -748,6 +883,7 @@ export const preforkSupervise = <S>(
|
|
|
748
883
|
}
|
|
749
884
|
}
|
|
750
885
|
supervisor.stop(loop, signals, ready)
|
|
886
|
+
supervisor.closeListeners()
|
|
751
887
|
netClose(loop)
|
|
752
888
|
return result
|
|
753
889
|
}
|
package/std/net/router.ts
CHANGED
|
@@ -69,7 +69,7 @@ export const ROUTER_BAD_ESCAPE: i32 = -10
|
|
|
69
69
|
/** `routerDecode`: the decoded bytes do not fit `out`; nothing was written. */
|
|
70
70
|
export const ROUTER_NO_ROOM: i32 = -11
|
|
71
71
|
|
|
72
|
-
/** The longest method `add` takes, as `nish/net/http1`'s `HTTP1_MAX_METHOD`. */
|
|
72
|
+
/** The longest method `add` takes, as `nish/net/http1-codec`'s `HTTP1_MAX_METHOD`. */
|
|
73
73
|
const ROUTER_MAX_METHOD: i32 = 32
|
|
74
74
|
/** No node or route: an `i32` spelled once, so that `--number-mode f64` does not read a bare `-1` as a double. */
|
|
75
75
|
const ROUTER_NONE: i32 = -1
|
package/std/net/upstream.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `nish/net/upstream` — outbound HTTP/1.1 calls from a route handler that
|
|
3
3
|
* never block its worker: a pool of keep-alive connections to one origin,
|
|
4
4
|
* registered on the `HttpHandler`'s own poll loop and read through the
|
|
5
|
-
* `nish/net/http1-client` codec.
|
|
5
|
+
* `nish/net/http1-client-codec` codec.
|
|
6
6
|
*
|
|
7
7
|
* const h = new HttpHandler(new HandlerLimits(), listener, router)
|
|
8
8
|
* const users = new Upstream(h, "http://127.0.0.1:9000", new UpstreamLimits())
|
|
@@ -87,8 +87,8 @@ import {
|
|
|
87
87
|
HTTP1_HEAD,
|
|
88
88
|
HTTP1_NEED_MORE,
|
|
89
89
|
HTTP1_NO_BODY,
|
|
90
|
-
} from "nish/net/http1"
|
|
91
|
-
import { HTTP1_INFORMATIONAL, Http1ResponseParser, http1WriteRequestHead } from "nish/net/http1-client"
|
|
90
|
+
} from "nish/net/http1-codec"
|
|
91
|
+
import { HTTP1_INFORMATIONAL, Http1ResponseParser, http1WriteRequestHead } from "nish/net/http1-client-codec"
|
|
92
92
|
import { OutboundPolicy, outboundAddressText, outboundFilter, outboundHostAllowed } from "nish/net/outbound"
|
|
93
93
|
import { Url, urlParse } from "nish/net/url"
|
|
94
94
|
import {
|
package/std/net/websocket.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `nish/net/websocket` — RFC 6455 WebSocket framing and both sides of the
|
|
3
3
|
* opening handshake, sans-IO: bytes in, events and bytes out.
|
|
4
4
|
*
|
|
5
|
-
* **The server's handshake.** `nish/net/http1` reads the client's request;
|
|
5
|
+
* **The server's handshake.** `nish/net/http1-codec` reads the client's request;
|
|
6
6
|
* `websocketRequestKey(parser)` answers its `Sec-WebSocket-Key` when the head
|
|
7
7
|
* is a valid opening handshake (§4.2.1), `websocketOfferedProtocols(parser)`
|
|
8
8
|
* the subprotocols it offers, and `websocketUpgradeResponse(key, protocol)`
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* random bytes as the key, `websocketUpgradeRequest(host, target, key,
|
|
16
16
|
* protocols)` writes the GET that asks to upgrade, and
|
|
17
17
|
* `websocketCheckResponse(parser, key, offered)` reads the server's answer,
|
|
18
|
-
* parsed by `nish/net/http1-client` after `expectUpgrade()`: it answers the
|
|
18
|
+
* parsed by `nish/net/http1-client-codec` after `expectUpgrade()`: it answers the
|
|
19
19
|
* negotiated subprotocol, or which rule of §4.1 the response broke. What
|
|
20
20
|
* `HTTP1_UPGRADE`'s `tunnelBytes()` answers is the first input for
|
|
21
21
|
* `WsDecoder(false, …)`. This module draws no randomness: the nonce and every
|
|
@@ -77,8 +77,8 @@ import {
|
|
|
77
77
|
http1ResponseHead,
|
|
78
78
|
http1SameWord,
|
|
79
79
|
http1Text,
|
|
80
|
-
} from "nish/net/http1"
|
|
81
|
-
import { Http1ResponseParser, http1UpgradeRequestHead } from "nish/net/http1-client"
|
|
80
|
+
} from "nish/net/http1-codec"
|
|
81
|
+
import { Http1ResponseParser, http1UpgradeRequestHead } from "nish/net/http1-client-codec"
|
|
82
82
|
|
|
83
83
|
/** The GUID RFC 6455 §1.3 appends to the client's key before hashing it. */
|
|
84
84
|
export const WS_GUID: string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
@@ -954,7 +954,7 @@ export const websocketClientKey = (nonce: u8[]): string | null => {
|
|
|
954
954
|
* with `Host`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`, the offered
|
|
955
955
|
* `protocols` in one `Sec-WebSocket-Protocol` field when there are any, and
|
|
956
956
|
* `Upgrade: websocket` and `Connection: Upgrade`, written by
|
|
957
|
-
* `nish/net/http1-client`'s `http1UpgradeRequestHead`. Answers `null` for a
|
|
957
|
+
* `nish/net/http1-client-codec`'s `http1UpgradeRequestHead`. Answers `null` for a
|
|
958
958
|
* key `websocketKeyIsValid` refuses, a protocol that is not a token (an empty
|
|
959
959
|
* one, or one holding a comma, a space, CR or LF), a protocol offered twice, an
|
|
960
960
|
* empty host, and everything the request writer refuses (CR, LF or another
|
package/std/region.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish:region` — memory a program frees when it says so (docs/LANGUAGE.md,
|
|
3
|
+
* "Regions").
|
|
4
|
+
*
|
|
5
|
+
* import { Region, enterWith, free, regionWith } from "nish:region"
|
|
6
|
+
*
|
|
7
|
+
* const r: Region<Session> = regionWith(id, open)
|
|
8
|
+
* const n: i32 = enterWith(r, out, serve)
|
|
9
|
+
* free(r)
|
|
10
|
+
*
|
|
11
|
+
* This file is the source behind the builtin module, and it is imported only
|
|
12
|
+
* by that name: `nish/region` is refused, so there is one spelling for the
|
|
13
|
+
* checker's rules to be read under (`src/region-check.ts`). What is written
|
|
14
|
+
* here is the meaning, over five intrinsics the compiler gives bodies of their
|
|
15
|
+
* own (`src/emit-region.ts`), which swap the thread's arena for the region's
|
|
16
|
+
* around the function they run:
|
|
17
|
+
*
|
|
18
|
+
* - `region(f)` makes a region, runs `f` with every allocation landing in
|
|
19
|
+
* it, and keeps what `f` answers as the region's root;
|
|
20
|
+
* - `enter(r, f)` runs `f(root)` the same way, and answers `f`'s scalar;
|
|
21
|
+
* - `free(r)` gives the region's memory back. A handle used after it is
|
|
22
|
+
* freed, or entered twice, or freed while entered, stops the program.
|
|
23
|
+
*
|
|
24
|
+
* A `Region<T>` is a handle, a 64-bit integer, and not a pointer: the root
|
|
25
|
+
* never leaves the region except through `enter`'s function, and that
|
|
26
|
+
* function answers a scalar, so nothing outside a region can point into it.
|
|
27
|
+
*
|
|
28
|
+
* Under Node the module is answered by `runtime/nish.mjs`, where a region is
|
|
29
|
+
* an entry in a table and `free` drops it.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A region whose root is a `T`. The compiler resolves the name to a type of
|
|
34
|
+
* its own, a 64-bit handle; this declaration is what `tsc` and the editor
|
|
35
|
+
* read through `runtime/nish.d.ts`.
|
|
36
|
+
*/
|
|
37
|
+
export class Region<T> {
|
|
38
|
+
/** Never read: the compiler's type has no fields. */
|
|
39
|
+
root: T | null = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The five intrinsics below are the runtime's region calls. Each is written
|
|
43
|
+
// here as a call of itself, so it has a type and nothing more; the compiler
|
|
44
|
+
// gives every instance a body of its own (`src/emit-region.ts`), one call into
|
|
45
|
+
// `runtime/runtime-region.c`.
|
|
46
|
+
|
|
47
|
+
/** `nish_region_new`: a fresh handle, never 0. */
|
|
48
|
+
const handleNew = (): i64 => handleNew()
|
|
49
|
+
|
|
50
|
+
/** `nish_region_enter` on a new region, whose root is still null. */
|
|
51
|
+
const open = (h: i64): void => open(h)
|
|
52
|
+
|
|
53
|
+
/** `nish_region_leave` keeping `root`: the handle, typed by its root. */
|
|
54
|
+
const close = <T>(h: i64, root: T): Region<T> => close(h, root)
|
|
55
|
+
|
|
56
|
+
/** `nish_region_enter` on a made region: its root. */
|
|
57
|
+
const reopen = <T>(r: Region<T>): T => reopen(r)
|
|
58
|
+
|
|
59
|
+
/** `nish_region_leave` with the root `reopen` gave. */
|
|
60
|
+
const leave = <T>(r: Region<T>, root: T): void => leave(r, root)
|
|
61
|
+
|
|
62
|
+
/** A new region, whose root is what `f` answers; everything `f` allocates is in it. */
|
|
63
|
+
export const region = <T>(f: () => T): Region<T> => {
|
|
64
|
+
const h: i64 = handleNew()
|
|
65
|
+
open(h)
|
|
66
|
+
return close(h, f())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `region` with one argument for `f`, because the arrow `region` runs captures nothing. */
|
|
70
|
+
export const regionWith = <A, T>(arg: A, f: (arg: A) => T): Region<T> => {
|
|
71
|
+
const h: i64 = handleNew()
|
|
72
|
+
open(h)
|
|
73
|
+
return close(h, f(arg))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `f(root)`, allocating in the region: the one way to reach what a region holds. */
|
|
77
|
+
export const enter = <T, R>(r: Region<T>, f: (root: T) => R): R => {
|
|
78
|
+
const root: T = reopen(r)
|
|
79
|
+
const result: R = f(root)
|
|
80
|
+
leave(r, root)
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `enter` with one argument for `f`. */
|
|
85
|
+
export const enterWith = <T, A, R>(r: Region<T>, arg: A, f: (root: T, arg: A) => R): R => {
|
|
86
|
+
const root: T = reopen(r)
|
|
87
|
+
const result: R = f(root, arg)
|
|
88
|
+
leave(r, root)
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Give the region's memory back. The handle is dead after it. The compiler writes the body. */
|
|
93
|
+
export const free = <T>(_r: Region<T>): void => {
|
|
94
|
+
// `nish_region_free`: see above.
|
|
95
|
+
}
|
package/std/sort.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/sort` — `xs.sort(cmp)` on an array, in Nish: a stable merge sort,
|
|
3
|
+
* compiled once per comparator.
|
|
4
|
+
*
|
|
5
|
+
* A program does not import this module. A `.sort(cmp)` call is what loads it
|
|
6
|
+
* (the checker's `importSort`), and the checker routes the call to `sortWith`
|
|
7
|
+
* here. `cmp` is a compile-time function parameter, so each comparator is an
|
|
8
|
+
* instantiation of its own whose body calls the comparator directly — there is
|
|
9
|
+
* no function pointer. Under Node and `tsc` the call is the platform's
|
|
10
|
+
* `Array.prototype.sort`, so nothing here runs there.
|
|
11
|
+
*
|
|
12
|
+
* **Same order as Node.** `Array.prototype.sort` is stable (ES2019), so for a
|
|
13
|
+
* comparator that is a consistent total preorder there is exactly one answer,
|
|
14
|
+
* and any stable sort gives it. A comparator's result is read only as "greater
|
|
15
|
+
* than zero or not": a later element moves before an earlier one only when
|
|
16
|
+
* the comparator answers a positive number for (earlier, later), so a negative
|
|
17
|
+
* zero and NaN are zero, as Node treats them.
|
|
18
|
+
*
|
|
19
|
+
* **Shape.** The array is copied once into a scratch array of the same
|
|
20
|
+
* length, the only allocation, which the function's arena scope gives back
|
|
21
|
+
* before it returns, so a sort leaves `Arena.used()` where it was. Runs of
|
|
22
|
+
* `RUN` are sorted by insertion from the scratch into the array (a strictly
|
|
23
|
+
* descending run is copied reversed instead), then merged
|
|
24
|
+
* bottom-up, alternating between the two, and copied back once if the last
|
|
25
|
+
* pass ended in the scratch. A pair of runs already in order is copied without
|
|
26
|
+
* a merge. Every merge takes from the left run on a tie, which is what makes
|
|
27
|
+
* it stable.
|
|
28
|
+
*
|
|
29
|
+
* **No element is held across a write.** An `interface` element is stored
|
|
30
|
+
* inline, and `const v = xs[i]` is a pointer into the array, so an insertion
|
|
31
|
+
* sort in place would overwrite the element it is inserting. Every step here
|
|
32
|
+
* reads from one array and writes the other, and moves a whole slot, which for
|
|
33
|
+
* an inline record is one copy of its bytes.
|
|
34
|
+
*
|
|
35
|
+
* **Proofs.** Every index is proven in range by the guard written beside it,
|
|
36
|
+
* so no access keeps its bounds check. The run arithmetic wraps only where
|
|
37
|
+
* the operands are proven to lie in `0..n`, so it cannot wrap: a difference
|
|
38
|
+
* of two of them, or a sum taken only when it stays below `n`.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { wrappingAdd, wrappingSub } from "nish:unsafe"
|
|
42
|
+
|
|
43
|
+
/** The length of the runs insertion sort makes before the first merge. */
|
|
44
|
+
const RUN: i32 = 32
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `lo + step`, or `n` when that is not below `n`. Every caller has
|
|
48
|
+
* `0 <= lo <= n`, so `n - lo` cannot wrap, and the sum is taken only below `n`.
|
|
49
|
+
*/
|
|
50
|
+
const stepTo = (lo: i32, step: i32, n: i32): i32 => (wrappingSub(n, lo) > step ? wrappingAdd(lo, step) : n)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `src[lo..hi)` sorted by insertion into `dst[lo..hi)`. The comparator call
|
|
54
|
+
* forgets every array length, so each access after it repeats its guard.
|
|
55
|
+
*/
|
|
56
|
+
const insertionRun = <T>(src: T[], dst: T[], cmp: (a: T, b: T) => number, lo: i32, hi: i32): void => {
|
|
57
|
+
// A strictly descending run is copied reversed: no two of its elements are
|
|
58
|
+
// equal, so reversing it keeps the sort stable, and costs one comparison each.
|
|
59
|
+
// `lo < hi`, and `d` steps only while below `hi`, so neither `+ 1` wraps.
|
|
60
|
+
let d = wrappingAdd(lo, 1)
|
|
61
|
+
while (d > 0 && d < hi && d < toI32(src.length)) {
|
|
62
|
+
const p = d - 1
|
|
63
|
+
if (!(cmp(src[p], src[d]) > 0)) {
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
d = wrappingAdd(d, 1)
|
|
67
|
+
}
|
|
68
|
+
if (d >= hi) {
|
|
69
|
+
// `hi > lo >= 0`, so `hi - 1` does not wrap, and `r` steps down only from `r >= 0`.
|
|
70
|
+
let r = wrappingSub(hi, 1)
|
|
71
|
+
for (let k: i32 = lo; k >= 0 && k < hi && k < toI32(dst.length) && r >= 0 && r < toI32(src.length); k++) {
|
|
72
|
+
dst[k] = src[r]
|
|
73
|
+
r = wrappingSub(r, 1)
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
for (let i: i32 = lo; i >= 0 && i < hi; i++) {
|
|
78
|
+
let j = i
|
|
79
|
+
while (j > lo && j > 0 && j < toI32(dst.length) && i < toI32(src.length)) {
|
|
80
|
+
const k = j - 1
|
|
81
|
+
if (!(cmp(dst[k], src[i]) > 0) || k < 0 || k >= toI32(dst.length) || j >= toI32(dst.length)) {
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
dst[j] = dst[k]
|
|
85
|
+
j = k
|
|
86
|
+
}
|
|
87
|
+
if (i < toI32(src.length) && j >= 0 && j < toI32(dst.length)) {
|
|
88
|
+
dst[j] = src[i]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `src[lo..hi)` copied into `dst[lo..hi)`. */
|
|
94
|
+
const copyRun = <T>(src: T[], dst: T[], lo: i32, hi: i32): void => {
|
|
95
|
+
for (let k: i32 = lo; k >= 0 && k < hi && k < toI32(src.length) && k < toI32(dst.length); k++) {
|
|
96
|
+
dst[k] = src[k]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The sorted runs `src[lo..mid)` and `src[mid..hi)` merged into `dst[lo..hi)`,
|
|
102
|
+
* or copied when the last of the left run is not after the first of the right.
|
|
103
|
+
*/
|
|
104
|
+
const mergeRuns = <T>(src: T[], dst: T[], cmp: (a: T, b: T) => number, lo: i32, mid: i32, hi: i32): void => {
|
|
105
|
+
if (mid > 0 && mid < hi && mid < toI32(src.length)) {
|
|
106
|
+
const last = mid - 1
|
|
107
|
+
if (last < toI32(src.length) && !(cmp(src[last], src[mid]) > 0)) {
|
|
108
|
+
copyRun(src, dst, lo, hi)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let i = lo
|
|
113
|
+
let j = mid
|
|
114
|
+
for (let k: i32 = lo; k >= 0 && k < hi; k++) {
|
|
115
|
+
let right = i >= mid
|
|
116
|
+
if (!right && j < hi && i >= 0 && i < toI32(src.length) && j >= 0 && j < toI32(src.length)) {
|
|
117
|
+
right = cmp(src[i], src[j]) > 0
|
|
118
|
+
}
|
|
119
|
+
if (right) {
|
|
120
|
+
if (j >= 0 && j < toI32(src.length) && k < toI32(dst.length)) {
|
|
121
|
+
dst[k] = src[j]
|
|
122
|
+
}
|
|
123
|
+
// `j` steps at most `hi - mid` times from `mid`, so it stays at most `hi`.
|
|
124
|
+
j = wrappingAdd(j, 1)
|
|
125
|
+
} else {
|
|
126
|
+
if (i >= 0 && i < toI32(src.length) && k < toI32(dst.length)) {
|
|
127
|
+
dst[k] = src[i]
|
|
128
|
+
}
|
|
129
|
+
// `i` steps at most `mid - lo` times from `lo`, so it stays at most `mid`.
|
|
130
|
+
i = wrappingAdd(i, 1)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One bottom-up pass: every pair of adjacent `width`-runs of `src` merged into `dst`. */
|
|
136
|
+
const mergePass = <T>(src: T[], dst: T[], cmp: (a: T, b: T) => number, n: i32, width: i32): void => {
|
|
137
|
+
let lo: i32 = 0
|
|
138
|
+
while (lo >= 0 && lo < n) {
|
|
139
|
+
const mid = stepTo(lo, width, n)
|
|
140
|
+
const hi = stepTo(mid, width, n)
|
|
141
|
+
mergeRuns(src, dst, cmp, lo, mid, hi)
|
|
142
|
+
lo = hi
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `xs` sorted in place by `cmp`, stably, and answered: what `xs.sort(cmp)` compiles to. */
|
|
147
|
+
export const sortWith = <T>(xs: T[], cmp: (a: T, b: T) => number): T[] => {
|
|
148
|
+
const n: i32 = toI32(xs.length)
|
|
149
|
+
if (n < 2) {
|
|
150
|
+
return xs
|
|
151
|
+
}
|
|
152
|
+
// One allocation at full size, every slot written before any is read: the
|
|
153
|
+
// checker lets this `new Array<T>(n)` hold any `T`, and neither emitter
|
|
154
|
+
// zero-fills it (`isSortInstance`).
|
|
155
|
+
const tmp = new Array<T>(n)
|
|
156
|
+
copyRun(xs, tmp, 0, n)
|
|
157
|
+
let lo: i32 = 0
|
|
158
|
+
while (lo >= 0 && lo < n) {
|
|
159
|
+
const hi = stepTo(lo, RUN, n)
|
|
160
|
+
insertionRun(tmp, xs, cmp, lo, hi)
|
|
161
|
+
lo = hi
|
|
162
|
+
}
|
|
163
|
+
let inTmp = false
|
|
164
|
+
let width: i32 = RUN
|
|
165
|
+
while (width > 0 && width < n) {
|
|
166
|
+
if (inTmp) {
|
|
167
|
+
mergePass(tmp, xs, cmp, n, width)
|
|
168
|
+
} else {
|
|
169
|
+
mergePass(xs, tmp, cmp, n, width)
|
|
170
|
+
}
|
|
171
|
+
inTmp = !inTmp
|
|
172
|
+
width = stepTo(width, width, n)
|
|
173
|
+
}
|
|
174
|
+
if (inTmp) {
|
|
175
|
+
copyRun(tmp, xs, 0, n)
|
|
176
|
+
}
|
|
177
|
+
return xs
|
|
178
|
+
}
|