@weft-js/start 0.1.1 → 0.1.2
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/README.md +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +38 -8
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +120 -492
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +144 -157
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +90 -166
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +112 -37
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -178
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -971
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { DEV, batch, createRoot, state, untrack, type State } from '@weft-js/core'
|
|
2
|
+
import { HANDOVER, SOCKETS, openLine, socketDown } from '../link'
|
|
3
|
+
import { FnError, keyOf, net, rt, track, type Fn } from '../data'
|
|
4
|
+
import type { ChannelStatus } from '../channel'
|
|
5
|
+
|
|
6
|
+
// ---- joins ----------------------------------------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export interface ListSpec {
|
|
9
|
+
name: string
|
|
10
|
+
max: number
|
|
11
|
+
key?: ((m: never) => unknown) | undefined
|
|
12
|
+
}
|
|
13
|
+
export interface List extends ListSpec {
|
|
14
|
+
box: State<unknown[]>
|
|
15
|
+
}
|
|
16
|
+
export interface Handle {
|
|
17
|
+
on: Map<string, Set<(m: never) => void>>
|
|
18
|
+
resets: Set<() => void>
|
|
19
|
+
value?(v: { v: unknown; at: number }): void
|
|
20
|
+
}
|
|
21
|
+
export interface Join {
|
|
22
|
+
n: number
|
|
23
|
+
key: string
|
|
24
|
+
fn: Fn
|
|
25
|
+
params: unknown
|
|
26
|
+
refs: number
|
|
27
|
+
timer?: ReturnType<typeof setTimeout> | undefined
|
|
28
|
+
cursor?: string | undefined
|
|
29
|
+
joined: boolean
|
|
30
|
+
refused: boolean
|
|
31
|
+
status: State<ChannelStatus>
|
|
32
|
+
error: State<unknown>
|
|
33
|
+
resets: State<number>
|
|
34
|
+
lists: Map<string, List>
|
|
35
|
+
handles: Set<Handle>
|
|
36
|
+
/** `send`s waiting for the join. */
|
|
37
|
+
waiting: Set<() => void>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const joins = new Map<string, Join>()
|
|
41
|
+
export const byN = new Map<number, Join>()
|
|
42
|
+
let nth = 0
|
|
43
|
+
|
|
44
|
+
const KEEP = 30_000
|
|
45
|
+
const ms = (keep: string | number = KEEP): number => {
|
|
46
|
+
if (typeof keep === 'number') return keep
|
|
47
|
+
const m = /^(\d+)(ms|s|m)$/.exec(keep)
|
|
48
|
+
return m ? +m[1]! * { ms: 1, s: 1000, m: 60_000 }[m[2] as 'ms' | 's' | 'm'] : Infinity
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function acquire(fn: Fn, params: unknown): Join {
|
|
52
|
+
const key = keyOf(fn.$id, params)
|
|
53
|
+
let j = joins.get(key)
|
|
54
|
+
if (!j) {
|
|
55
|
+
j = createRoot(
|
|
56
|
+
() => ({
|
|
57
|
+
n: ++nth,
|
|
58
|
+
key,
|
|
59
|
+
fn,
|
|
60
|
+
params,
|
|
61
|
+
refs: 0,
|
|
62
|
+
joined: false,
|
|
63
|
+
refused: false,
|
|
64
|
+
status: state<ChannelStatus>('connecting'),
|
|
65
|
+
error: state<unknown>(undefined),
|
|
66
|
+
resets: state(0),
|
|
67
|
+
lists: new Map(),
|
|
68
|
+
handles: new Set(),
|
|
69
|
+
waiting: new Set(),
|
|
70
|
+
}),
|
|
71
|
+
null,
|
|
72
|
+
)
|
|
73
|
+
if (DEV && net() && !net()!.resets.includes(reset)) net()!.resets.push(reset)
|
|
74
|
+
joins.set(key, j)
|
|
75
|
+
byN.set(j.n, j)
|
|
76
|
+
if (live) enter(j)
|
|
77
|
+
else start()
|
|
78
|
+
}
|
|
79
|
+
clearTimeout(j.timer)
|
|
80
|
+
j.timer = undefined
|
|
81
|
+
j.refs++
|
|
82
|
+
return j
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function release(j: Join, keep?: string | number) {
|
|
86
|
+
if (--j.refs) return
|
|
87
|
+
const t = ms(keep)
|
|
88
|
+
if (t === Infinity) return
|
|
89
|
+
if (t) j.timer = setTimeout(() => drop(j), t)
|
|
90
|
+
else drop(j)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function drop(j: Join) {
|
|
94
|
+
joins.delete(j.key)
|
|
95
|
+
byN.delete(j.n)
|
|
96
|
+
j.status.value = 'closed'
|
|
97
|
+
if (live) send(['l', j.n])
|
|
98
|
+
for (const w of Array.from(j.waiting)) w()
|
|
99
|
+
if (!joins.size) live?.close()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hold(j: Join, keep?: string | number): () => void {
|
|
103
|
+
const token = {}
|
|
104
|
+
const stop = () => {
|
|
105
|
+
net()?.open.delete(token)
|
|
106
|
+
release(j, keep)
|
|
107
|
+
}
|
|
108
|
+
if (DEV) net()?.open.set(token, [`channel \`${j.key}\``, stop])
|
|
109
|
+
return stop
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function listOf(j: Join, spec: ListSpec): List {
|
|
113
|
+
const id = `${spec.name}:${spec.max}:${spec.key ? String(spec.key) : ''}`
|
|
114
|
+
let l = j.lists.get(id)
|
|
115
|
+
if (!l) j.lists.set(id, (l = createRoot(() => ({ ...spec, box: state<unknown[]>([]) }), null)))
|
|
116
|
+
return l
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---- sends ----------------------------------------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
const ACK_WAIT = 10_000
|
|
122
|
+
const acks = new Map<number, { ok: () => void; no: (e: unknown) => void; n: number }>()
|
|
123
|
+
let ackId = 0
|
|
124
|
+
|
|
125
|
+
export async function post(j: Join, name: string, message: unknown): Promise<void> {
|
|
126
|
+
if (!SOCKETS && !(DEV && net()))
|
|
127
|
+
throw new FnError({ code: 'R024', message: "this deploy can't hold a socket: not sent" }, 503)
|
|
128
|
+
if (!j.joined && !j.refused) {
|
|
129
|
+
await new Promise<void>((ok) => {
|
|
130
|
+
const done = () => {
|
|
131
|
+
clearTimeout(t)
|
|
132
|
+
j.waiting.delete(done)
|
|
133
|
+
ok()
|
|
134
|
+
}
|
|
135
|
+
const t = setTimeout(done, ACK_WAIT)
|
|
136
|
+
j.waiting.add(done)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
if (!j.joined)
|
|
140
|
+
throw new FnError(
|
|
141
|
+
{
|
|
142
|
+
...(j.refused && { code: 'R011' }),
|
|
143
|
+
message: j.refused ? 'not allowed in this channel' : 'no connection: not sent',
|
|
144
|
+
},
|
|
145
|
+
j.refused ? 403 : 503,
|
|
146
|
+
)
|
|
147
|
+
const ack = ++ackId
|
|
148
|
+
const p = new Promise<void>((ok, no) => {
|
|
149
|
+
acks.set(ack, { ok, no, n: j.n })
|
|
150
|
+
})
|
|
151
|
+
send(['m', j.n, ack, name, message])
|
|
152
|
+
return track(p)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- the socket ------------------------------------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/** What the server sends a channel's client: the first element says which. */
|
|
158
|
+
export type Frame =
|
|
159
|
+
| ['h', { b?: string }]
|
|
160
|
+
| ['j', number, string | undefined]
|
|
161
|
+
| ['m', number, string | undefined, string, never]
|
|
162
|
+
| ['r', number, string | undefined]
|
|
163
|
+
| ['e', number, { code?: string; message?: string }]
|
|
164
|
+
| ['a', number, number, { code?: string; message?: string } | undefined]
|
|
165
|
+
|
|
166
|
+
/** One end of a socket, as a channel holds it. */
|
|
167
|
+
export interface SocketLink {
|
|
168
|
+
send(text: string): void
|
|
169
|
+
close(): void
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** This tab's line on the browser's one socket (link.ts). `done` gets the close code. */
|
|
173
|
+
function browser(onFrame: (d: Frame) => void, closed: (code: number) => void): SocketLink {
|
|
174
|
+
return openLine('c', '', (text) => onFrame(JSON.parse(text)), closed)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let fake: (onFrame: (d: Frame) => void, closed: (code: number) => void) => SocketLink
|
|
178
|
+
/** Keep the test transport resolved from the original entry. */
|
|
179
|
+
export function setFake(value: typeof fake) {
|
|
180
|
+
fake = value
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const BASE = 500,
|
|
184
|
+
CAP = 30_000,
|
|
185
|
+
PING = 20_000,
|
|
186
|
+
DEAD = 45_000,
|
|
187
|
+
HIDDEN = 300_000
|
|
188
|
+
|
|
189
|
+
let live: SocketLink | undefined
|
|
190
|
+
let open = false
|
|
191
|
+
let running = false
|
|
192
|
+
let attempt = 0
|
|
193
|
+
let build: string | undefined
|
|
194
|
+
let nudge: (() => void) | undefined
|
|
195
|
+
let parked = false
|
|
196
|
+
let hiddenAt: ReturnType<typeof setTimeout> | undefined
|
|
197
|
+
|
|
198
|
+
/** A leader tab can be hidden while a sibling is looking: `@weft-js/start/share` says when this socket must stay. */
|
|
199
|
+
export const keepOpen: { stay?: (() => boolean) | undefined } = {}
|
|
200
|
+
|
|
201
|
+
const send = (f: unknown[]) => live?.send(JSON.stringify(f))
|
|
202
|
+
const statusAll = (s: ChannelStatus) => {
|
|
203
|
+
for (const j of joins.values()) if (!j.refused) j.status.value = s
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function enter(j: Join) {
|
|
207
|
+
if (!open || !live) return
|
|
208
|
+
send(['j', j.n, j.fn.$id, j.params, j.cursor])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function rejoin(j: Join) {
|
|
212
|
+
j.refused = false
|
|
213
|
+
j.status.value = 'connecting'
|
|
214
|
+
if (running) enter(j)
|
|
215
|
+
else start()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function start() {
|
|
219
|
+
if (running) return
|
|
220
|
+
// a function host: no socket to dial, so say so once instead of retrying forever
|
|
221
|
+
if (!SOCKETS && !(DEV && net())) return statusAll('unavailable')
|
|
222
|
+
running = true
|
|
223
|
+
queueMicrotask(() => void run())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function dispatch(j: Join, name: string, msg: never) {
|
|
227
|
+
if (name === '$v') {
|
|
228
|
+
for (const h of j.handles) h.value?.(msg)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
for (const l of j.lists.values()) {
|
|
232
|
+
if (l.name !== name) continue
|
|
233
|
+
const was = untrack(() => l.box.value)
|
|
234
|
+
const key = l.key?.(msg)
|
|
235
|
+
const at = l.key ? was.findIndex((m) => l.key!(m as never) === key) : -1
|
|
236
|
+
const next = at >= 0 ? was.map((m, i) => (i === at ? msg : m)) : [...was, msg]
|
|
237
|
+
l.box.value = next.length > l.max ? next.slice(-l.max) : next
|
|
238
|
+
}
|
|
239
|
+
for (const h of j.handles) for (const f of h.on.get(name) ?? []) f(msg)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function frame(d: Frame) {
|
|
243
|
+
batch(() => {
|
|
244
|
+
const j = typeof d[1] === 'number' ? byN.get(d[1]) : undefined
|
|
245
|
+
if (d[0] === 'h') {
|
|
246
|
+
if (build && d[1].b && d[1].b !== build) rt.stale.value = true
|
|
247
|
+
build = d[1].b
|
|
248
|
+
open = true
|
|
249
|
+
attempt = 0
|
|
250
|
+
for (const x of joins.values()) if (!x.refused) enter(x)
|
|
251
|
+
} else if (d[0] === 'j' && j) {
|
|
252
|
+
j.cursor ??= d[2]
|
|
253
|
+
j.joined = true
|
|
254
|
+
j.error.value = undefined
|
|
255
|
+
j.status.value = 'open'
|
|
256
|
+
for (const w of Array.from(j.waiting)) w()
|
|
257
|
+
} else if (d[0] === 'm' && j) {
|
|
258
|
+
j.cursor = d[2]
|
|
259
|
+
dispatch(j, d[3], d[4])
|
|
260
|
+
} else if (d[0] === 'r' && j) {
|
|
261
|
+
j.cursor = d[2]
|
|
262
|
+
for (const l of j.lists.values()) l.box.value = []
|
|
263
|
+
j.resets.value++
|
|
264
|
+
for (const h of j.handles) for (const f of h.resets) f()
|
|
265
|
+
} else if (d[0] === 'e' && j) {
|
|
266
|
+
j.joined = false
|
|
267
|
+
j.refused = true
|
|
268
|
+
j.error.value = new FnError(d[2], d[2].code === 'R011' ? 403 : 400)
|
|
269
|
+
j.status.value = d[2].code === 'R011' ? 'unauthorized' : 'closed'
|
|
270
|
+
for (const w of Array.from(j.waiting)) w()
|
|
271
|
+
} else if (d[0] === 'a') {
|
|
272
|
+
const p = acks.get(d[2])
|
|
273
|
+
acks.delete(d[2])
|
|
274
|
+
if (d[3]) p?.no(new FnError(d[3], d[3].code === 'R011' ? 403 : 400))
|
|
275
|
+
else p?.ok()
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const sleep = (t: number) =>
|
|
281
|
+
new Promise<void>((ok) => {
|
|
282
|
+
const timer = setTimeout(ok, t)
|
|
283
|
+
nudge = () => {
|
|
284
|
+
clearTimeout(timer)
|
|
285
|
+
nudge = undefined
|
|
286
|
+
ok()
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
async function run() {
|
|
291
|
+
listen(true)
|
|
292
|
+
const fakeNet = () => (DEV ? net() : undefined)
|
|
293
|
+
while (joins.size) {
|
|
294
|
+
if (parked || !navigator.onLine) {
|
|
295
|
+
statusAll('reconnecting')
|
|
296
|
+
await sleep(CAP)
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
let code = 0
|
|
300
|
+
let dead: ReturnType<typeof setTimeout> | undefined
|
|
301
|
+
let ping: ReturnType<typeof setInterval> | undefined
|
|
302
|
+
const alive = () => {
|
|
303
|
+
clearTimeout(dead)
|
|
304
|
+
if (!fakeNet()) dead = setTimeout(() => live?.close(), DEAD)
|
|
305
|
+
}
|
|
306
|
+
await new Promise<void>((done) => {
|
|
307
|
+
const take = (d: Frame) => {
|
|
308
|
+
alive()
|
|
309
|
+
frame(d)
|
|
310
|
+
}
|
|
311
|
+
live = (fakeNet() ? fake : browser)(take, (c) => {
|
|
312
|
+
code = c
|
|
313
|
+
done()
|
|
314
|
+
})
|
|
315
|
+
alive()
|
|
316
|
+
if (!fakeNet()) ping = setInterval(() => send(['p']), PING)
|
|
317
|
+
})
|
|
318
|
+
clearTimeout(dead)
|
|
319
|
+
clearInterval(ping)
|
|
320
|
+
live = undefined
|
|
321
|
+
open = false
|
|
322
|
+
for (const j of joins.values()) j.joined = false
|
|
323
|
+
for (const [id, p] of acks) {
|
|
324
|
+
acks.delete(id)
|
|
325
|
+
p.no(
|
|
326
|
+
new FnError({ message: 'the connection dropped: this may or may not have been sent' }, 503),
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if (!joins.size) break
|
|
330
|
+
const n = fakeNet()
|
|
331
|
+
if (n) n.reconnects++
|
|
332
|
+
if (code === 4401) for (const j of joins.values()) j.status.value = 'unauthorized'
|
|
333
|
+
else statusAll('reconnecting')
|
|
334
|
+
if (!n) await sleep(code === HANDOVER ? 0 : backoff())
|
|
335
|
+
}
|
|
336
|
+
running = false
|
|
337
|
+
attempt = 0
|
|
338
|
+
listen(false)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Full jitter, 0.5 s doubling to 30 s (research 30 §6). */
|
|
342
|
+
const backoff = () => Math.random() * Math.min(CAP, BASE * 2 ** attempt++)
|
|
343
|
+
|
|
344
|
+
// ---- the page -------------------------------------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
const wake = () => {
|
|
347
|
+
parked = false
|
|
348
|
+
nudge?.()
|
|
349
|
+
}
|
|
350
|
+
function park() {
|
|
351
|
+
parked = true
|
|
352
|
+
live?.close()
|
|
353
|
+
}
|
|
354
|
+
const parkHidden = () => {
|
|
355
|
+
if (keepOpen.stay?.()) hiddenAt = setTimeout(parkHidden, HIDDEN)
|
|
356
|
+
else park()
|
|
357
|
+
}
|
|
358
|
+
function onVisibility() {
|
|
359
|
+
clearTimeout(hiddenAt)
|
|
360
|
+
if (document.visibilityState === 'hidden') hiddenAt = setTimeout(parkHidden, HIDDEN)
|
|
361
|
+
else wake()
|
|
362
|
+
}
|
|
363
|
+
/** A sibling came back while this tab's socket was parked. */
|
|
364
|
+
export const wakeSocket = () => wake()
|
|
365
|
+
const onOffline = () => {
|
|
366
|
+
socketDown()
|
|
367
|
+
live?.close()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function listen(on: boolean) {
|
|
371
|
+
if (typeof window === 'undefined') return
|
|
372
|
+
const m = on ? 'addEventListener' : 'removeEventListener'
|
|
373
|
+
window[m]('online', wake)
|
|
374
|
+
window[m]('offline', onOffline)
|
|
375
|
+
window[m]('pagehide', park)
|
|
376
|
+
window[m]('pageshow', wake)
|
|
377
|
+
document[m]('visibilitychange', onVisibility)
|
|
378
|
+
if (!on) clearTimeout(hiddenAt)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @weft-js/core/test, after each test. */
|
|
382
|
+
function reset() {
|
|
383
|
+
for (const j of joins.values()) clearTimeout(j.timer)
|
|
384
|
+
joins.clear()
|
|
385
|
+
byN.clear()
|
|
386
|
+
acks.clear()
|
|
387
|
+
live?.close()
|
|
388
|
+
nudge?.()
|
|
389
|
+
build = undefined
|
|
390
|
+
parked = false
|
|
391
|
+
attempt = 0
|
|
392
|
+
net()!.reconnects = 0
|
|
393
|
+
untrack(() => {
|
|
394
|
+
rt.stale.value = false
|
|
395
|
+
})
|
|
396
|
+
}
|
package/src/channel-fake.ts
CHANGED
|
@@ -6,9 +6,17 @@ import type { Frame, SocketLink } from './channel'
|
|
|
6
6
|
/** A socket to the real server code in this process. */
|
|
7
7
|
const exec_ = async () => (runtime.exec ??= await import('./exec'))
|
|
8
8
|
|
|
9
|
-
export async function open(
|
|
9
|
+
export async function open(
|
|
10
|
+
frame: (d: Frame) => void,
|
|
11
|
+
done: (code: number) => void,
|
|
12
|
+
up: () => void,
|
|
13
|
+
): Promise<SocketLink> {
|
|
10
14
|
const n = net()!
|
|
11
|
-
const [{ hub, resetHub }, { attach, publish }, exec] = await Promise.all([
|
|
15
|
+
const [{ hub, resetHub }, { attach, publish }, exec] = await Promise.all([
|
|
16
|
+
import('./hub'),
|
|
17
|
+
import('./socket'),
|
|
18
|
+
exec_(),
|
|
19
|
+
])
|
|
12
20
|
if (!n.online()) throw new Error('offline')
|
|
13
21
|
if (!n.resets.includes(resetHub)) n.resets.push(resetHub)
|
|
14
22
|
const h = hub()
|
|
@@ -36,7 +44,10 @@ export async function open(frame: (d: Frame) => void, done: (code: number) => vo
|
|
|
36
44
|
const socket = await attach(peer, request, fakeScope(exec, n, request), h)
|
|
37
45
|
n.ws = {
|
|
38
46
|
publish: (id, params, name, msg) => publish(fns.get(id)!, params, name, msg),
|
|
39
|
-
drop: () => {
|
|
47
|
+
drop: () => {
|
|
48
|
+
peer.close(1006)
|
|
49
|
+
socket.close()
|
|
50
|
+
}, // both ends, as a real cut: the server runs `left`
|
|
40
51
|
recheck: () => socket.recheck(),
|
|
41
52
|
}
|
|
42
53
|
up()
|
|
@@ -46,7 +57,11 @@ export async function open(frame: (d: Frame) => void, done: (code: number) => vo
|
|
|
46
57
|
if (f[0] === 'm') n.sent.push({ id: rooms.get(f[1]) ?? '', name: f[3], msg: f[4] })
|
|
47
58
|
if (f[0] === 'j') {
|
|
48
59
|
rooms.set(f[1], f[2])
|
|
49
|
-
void track(
|
|
60
|
+
void track(
|
|
61
|
+
new Promise<void>((ok) => {
|
|
62
|
+
joining.set(f[1], ok)
|
|
63
|
+
}),
|
|
64
|
+
) // `net.settle()` waits for the join's answer
|
|
50
65
|
}
|
|
51
66
|
socket.message(text)
|
|
52
67
|
},
|