@weft-js/start 0.1.0 → 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 +51 -10
- 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 +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- 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 +97 -168
- 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 +113 -35
- 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 -174
- 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 -964
- 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,178 @@
|
|
|
1
|
+
/** Bump when a message here changes shape: tabs of two shapes share nothing, and each is its own client. */
|
|
2
|
+
const WIRE = 1
|
|
3
|
+
export const NAME = `weft:link:${WIRE}`
|
|
4
|
+
export const BEAT = 5000
|
|
5
|
+
const PONG = 1500
|
|
6
|
+
// getRandomValues, not randomUUID: a page on plain http (a LAN address) has no randomUUID, and this runs on import.
|
|
7
|
+
export const tab =
|
|
8
|
+
typeof crypto === 'undefined' ? '' : crypto.getRandomValues(new Uint32Array(1))[0]!.toString(36)
|
|
9
|
+
const locks = (): LockManager | undefined =>
|
|
10
|
+
typeof navigator !== 'undefined' ? navigator.locks : undefined
|
|
11
|
+
export const shareable = () => !!locks() && typeof BroadcastChannel !== 'undefined'
|
|
12
|
+
|
|
13
|
+
// ---- a lock, a queue and a ping ---------------------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface Campaign {
|
|
16
|
+
probe(): void
|
|
17
|
+
stop(): void
|
|
18
|
+
leading(): boolean
|
|
19
|
+
pinged?: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `lead` when this tab takes the lock; `lose` when another tab takes it from under this one. */
|
|
23
|
+
export function campaign(
|
|
24
|
+
name: string,
|
|
25
|
+
won: () => void,
|
|
26
|
+
lose: () => void,
|
|
27
|
+
stolen?: () => void,
|
|
28
|
+
): Campaign {
|
|
29
|
+
const l = locks()
|
|
30
|
+
const lockName = `${name}:leader`
|
|
31
|
+
const state: ElectionState = { on: true, leads: false, waiting: undefined }
|
|
32
|
+
const hold = () => {
|
|
33
|
+
state.leads = true
|
|
34
|
+
clearTimeout(state.waiting) // a ping that went out before there was anyone to answer it
|
|
35
|
+
state.waiting = undefined
|
|
36
|
+
won()
|
|
37
|
+
}
|
|
38
|
+
if (!l || typeof BroadcastChannel === 'undefined') {
|
|
39
|
+
queueMicrotask(() => {
|
|
40
|
+
if (state.on) hold()
|
|
41
|
+
})
|
|
42
|
+
return {
|
|
43
|
+
probe() {},
|
|
44
|
+
leading: () => state.leads,
|
|
45
|
+
stop() {
|
|
46
|
+
state.on = state.leads = false
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return queuedCampaign(name, lockName, l, state, { hold, lose, stolen })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ElectionState {
|
|
54
|
+
on: boolean
|
|
55
|
+
leads: boolean
|
|
56
|
+
waiting: ReturnType<typeof setTimeout> | undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function queuedCampaign(
|
|
60
|
+
name: string,
|
|
61
|
+
lockName: string,
|
|
62
|
+
l: LockManager,
|
|
63
|
+
state: ElectionState,
|
|
64
|
+
on: { hold: () => void; lose: () => void; stolen?: (() => void) | undefined },
|
|
65
|
+
): Campaign {
|
|
66
|
+
const { hold, lose, stolen } = on
|
|
67
|
+
let release: (() => void) | undefined
|
|
68
|
+
let abort: AbortController | undefined
|
|
69
|
+
let pongAt = 0
|
|
70
|
+
const bc = new BroadcastChannel(`${name}:e`)
|
|
71
|
+
const me: Campaign = {
|
|
72
|
+
leading: () => state.leads,
|
|
73
|
+
probe() {
|
|
74
|
+
// Each hover asks, so a recent answer has to do: one message a second, not one a move.
|
|
75
|
+
if (state.leads || state.waiting !== undefined || !state.on || Date.now() - pongAt < 1000)
|
|
76
|
+
return
|
|
77
|
+
bc.postMessage({ ping: tab })
|
|
78
|
+
const asked = Date.now()
|
|
79
|
+
state.waiting = setTimeout(() => {
|
|
80
|
+
state.waiting = undefined
|
|
81
|
+
// Late means this tab was frozen, and the answer is queued behind this timer: ask again.
|
|
82
|
+
if (Date.now() - asked > PONG * 1.5) return me.probe()
|
|
83
|
+
stolen?.()
|
|
84
|
+
const queued = abort
|
|
85
|
+
elect(true)
|
|
86
|
+
queued?.abort()
|
|
87
|
+
}, PONG)
|
|
88
|
+
},
|
|
89
|
+
stop() {
|
|
90
|
+
state.on = false
|
|
91
|
+
state.leads = false
|
|
92
|
+
clearTimeout(state.waiting)
|
|
93
|
+
clearInterval(timer)
|
|
94
|
+
abort?.abort()
|
|
95
|
+
release?.()
|
|
96
|
+
bc.close()
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
bc.onmessage = ({ data }) => {
|
|
100
|
+
if (data.ping && state.leads) {
|
|
101
|
+
bc.postMessage({ pong: data.ping })
|
|
102
|
+
me.pinged?.()
|
|
103
|
+
} else if (data.pong === tab) {
|
|
104
|
+
pongAt = Date.now()
|
|
105
|
+
clearTimeout(state.waiting)
|
|
106
|
+
state.waiting = undefined
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function elect(steal = false) {
|
|
110
|
+
const ctl = new AbortController()
|
|
111
|
+
abort = ctl
|
|
112
|
+
l.request(
|
|
113
|
+
lockName,
|
|
114
|
+
steal ? { steal: true } : { signal: ctl.signal },
|
|
115
|
+
// A grant that comes after stop() (a steal can't be aborted) is handed straight back.
|
|
116
|
+
() =>
|
|
117
|
+
state.on
|
|
118
|
+
? new Promise<void>((ok) => {
|
|
119
|
+
release = ok
|
|
120
|
+
hold()
|
|
121
|
+
})
|
|
122
|
+
: undefined,
|
|
123
|
+
).catch(() => {
|
|
124
|
+
if (!state.on || abort !== ctl) return // this tab left the queue on purpose
|
|
125
|
+
if (state.leads) {
|
|
126
|
+
state.leads = false
|
|
127
|
+
lose()
|
|
128
|
+
}
|
|
129
|
+
elect()
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
const timer = setInterval(() => me.probe(), BEAT)
|
|
133
|
+
elect()
|
|
134
|
+
return me
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- this tab's own lock ----------------------------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
let tabHeld = false
|
|
140
|
+
let tabFree: (() => void) | undefined
|
|
141
|
+
/** Bumped by freeTab(): a grant for an earlier turn is handed straight back. */
|
|
142
|
+
let tabTurn = 0
|
|
143
|
+
const whenHeld: (() => void)[] = []
|
|
144
|
+
|
|
145
|
+
/** Held before the tab says anything, so a sibling that queues on it can't read a live tab as dead. */
|
|
146
|
+
export function holdTab(then: () => void) {
|
|
147
|
+
if (tabHeld || !locks()) return then()
|
|
148
|
+
whenHeld.push(then)
|
|
149
|
+
if (whenHeld.length > 1) return
|
|
150
|
+
const turn = tabTurn
|
|
151
|
+
locks()!
|
|
152
|
+
.request(`${NAME}:tab:${tab}`, () => {
|
|
153
|
+
if (turn !== tabTurn) return undefined // freed while it was asked for
|
|
154
|
+
return new Promise<void>((ok) => {
|
|
155
|
+
tabFree = ok
|
|
156
|
+
tabHeld = true
|
|
157
|
+
for (const f of whenHeld.splice(0)) f()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
// No lock after all: go on without one rather than leave the callers (and every later one) waiting.
|
|
161
|
+
.catch(() => {
|
|
162
|
+
for (const f of whenHeld.splice(0)) f()
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Comes through when that tab is gone, crash included. */
|
|
167
|
+
export function freeTab() {
|
|
168
|
+
tabTurn++
|
|
169
|
+
tabHeld = false
|
|
170
|
+
whenHeld.length = 0
|
|
171
|
+
tabFree?.()
|
|
172
|
+
tabFree = undefined
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const whenGone = (t: string, fn: () => void) =>
|
|
176
|
+
void locks()
|
|
177
|
+
?.request(`${NAME}:tab:${t}`, () => fn())
|
|
178
|
+
.catch(() => {})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Mine {
|
|
2
|
+
id: string
|
|
3
|
+
hello: string
|
|
4
|
+
frame(text: string): void
|
|
5
|
+
closed(code: number, reason?: string): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const lines = new Map<string, Mine>()
|
|
9
|
+
|
|
10
|
+
export function end(l: Mine, code: number, reason?: string) {
|
|
11
|
+
lines.delete(l.id)
|
|
12
|
+
l.closed(code, reason)
|
|
13
|
+
}
|
|
14
|
+
export const endAll = (code: number) => {
|
|
15
|
+
for (const l of Array.from(lines.values())) end(l, code)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function arrive(text: string) {
|
|
19
|
+
const at = text.indexOf(':', 2)
|
|
20
|
+
const l = lines.get(text.slice(2, at))
|
|
21
|
+
if (!l) return
|
|
22
|
+
if (text[0] === 'x') {
|
|
23
|
+
const split = text.indexOf(':', at + 1)
|
|
24
|
+
return end(l, +text.slice(at + 1, split), text.slice(split + 1))
|
|
25
|
+
}
|
|
26
|
+
l.frame(text.slice(at + 1))
|
|
27
|
+
}
|
package/src/link.ts
CHANGED
|
@@ -12,168 +12,70 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Without Web Locks every tab leads itself: its own socket, which is what every tab used to have.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
import {
|
|
16
|
+
NAME,
|
|
17
|
+
campaign,
|
|
18
|
+
freeTab,
|
|
19
|
+
holdTab,
|
|
20
|
+
shareable,
|
|
21
|
+
tab,
|
|
22
|
+
whenGone,
|
|
23
|
+
type Campaign,
|
|
24
|
+
} from './link/election'
|
|
25
|
+
import { arrive, end, endAll, lines, type Mine } from './link/lines'
|
|
26
|
+
export {
|
|
27
|
+
BEAT,
|
|
28
|
+
campaign,
|
|
29
|
+
freeTab,
|
|
30
|
+
holdTab,
|
|
31
|
+
shareable,
|
|
32
|
+
tab,
|
|
33
|
+
whenGone,
|
|
34
|
+
type Campaign,
|
|
35
|
+
} from './link/election'
|
|
36
|
+
|
|
37
|
+
const PING = 20_000,
|
|
38
|
+
DEAD = 45_000,
|
|
39
|
+
BASE = 200,
|
|
40
|
+
CAP = 30_000,
|
|
41
|
+
IDLE = 1000
|
|
21
42
|
/** The line went because the leader did: not the network's fault, so reopen at once. */
|
|
22
43
|
export const HANDOVER = 4900
|
|
23
44
|
|
|
24
|
-
// getRandomValues, not randomUUID: a page on plain http (a LAN address) has no randomUUID, and this runs on import.
|
|
25
|
-
export const tab = typeof crypto === 'undefined' ? '' : crypto.getRandomValues(new Uint32Array(1))[0]!.toString(36)
|
|
26
|
-
const locks = (): LockManager | undefined => (typeof navigator !== 'undefined' ? navigator.locks : undefined)
|
|
27
|
-
export const shareable = () => !!locks() && typeof BroadcastChannel !== 'undefined'
|
|
28
|
-
|
|
29
45
|
/** What a test reads. */
|
|
30
46
|
export const linkStats = { role: 'none' as 'none' | 'leader' | 'follower', steals: 0, dials: 0 }
|
|
31
47
|
|
|
32
|
-
// ---- a lock, a queue and a ping ---------------------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
export interface Campaign { probe(): void; stop(): void; leading(): boolean; pinged?: () => void }
|
|
35
|
-
|
|
36
|
-
/** `lead` when this tab takes the lock; `lose` when another tab takes it from under this one. */
|
|
37
|
-
export function campaign(name: string, won: () => void, lose: () => void, stolen?: () => void): Campaign {
|
|
38
|
-
const l = locks()
|
|
39
|
-
const lockName = `${name}:leader`
|
|
40
|
-
let on = true, leads = false
|
|
41
|
-
let release: (() => void) | undefined
|
|
42
|
-
let abort: AbortController | undefined
|
|
43
|
-
let waiting: ReturnType<typeof setTimeout> | undefined
|
|
44
|
-
let pongAt = 0
|
|
45
|
-
const hold = () => {
|
|
46
|
-
leads = true
|
|
47
|
-
clearTimeout(waiting) // a ping that went out before there was anyone to answer it
|
|
48
|
-
waiting = undefined
|
|
49
|
-
won()
|
|
50
|
-
}
|
|
51
|
-
if (!l || typeof BroadcastChannel === 'undefined') {
|
|
52
|
-
queueMicrotask(() => { if (on) hold() })
|
|
53
|
-
return { probe() {}, leading: () => leads, stop() { on = leads = false } }
|
|
54
|
-
}
|
|
55
|
-
const bc = new BroadcastChannel(`${name}:e`)
|
|
56
|
-
const me: Campaign = {
|
|
57
|
-
leading: () => leads,
|
|
58
|
-
probe() {
|
|
59
|
-
// Each hover asks, so a recent answer has to do: one message a second, not one a move.
|
|
60
|
-
if (leads || waiting !== undefined || !on || Date.now() - pongAt < 1000) return
|
|
61
|
-
bc.postMessage({ ping: tab })
|
|
62
|
-
const asked = Date.now()
|
|
63
|
-
waiting = setTimeout(() => {
|
|
64
|
-
waiting = undefined
|
|
65
|
-
// Late means this tab was the frozen one, and the answer is queued behind this timer: ask again.
|
|
66
|
-
if (Date.now() - asked > PONG * 1.5) return me.probe()
|
|
67
|
-
stolen?.()
|
|
68
|
-
const queued = abort
|
|
69
|
-
elect(true)
|
|
70
|
-
queued?.abort()
|
|
71
|
-
}, PONG)
|
|
72
|
-
},
|
|
73
|
-
stop() {
|
|
74
|
-
on = false
|
|
75
|
-
leads = false
|
|
76
|
-
clearTimeout(waiting)
|
|
77
|
-
clearInterval(timer)
|
|
78
|
-
abort?.abort()
|
|
79
|
-
release?.()
|
|
80
|
-
bc.close()
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
bc.onmessage = ({ data }) => {
|
|
84
|
-
if (data.ping && leads) {
|
|
85
|
-
bc.postMessage({ pong: data.ping })
|
|
86
|
-
me.pinged?.()
|
|
87
|
-
} else if (data.pong === tab) {
|
|
88
|
-
pongAt = Date.now()
|
|
89
|
-
clearTimeout(waiting)
|
|
90
|
-
waiting = undefined
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function elect(steal = false) {
|
|
94
|
-
const ctl = new AbortController()
|
|
95
|
-
abort = ctl
|
|
96
|
-
l!.request(lockName, steal ? { steal: true } : { signal: ctl.signal }, () => new Promise<void>(ok => {
|
|
97
|
-
release = ok
|
|
98
|
-
hold()
|
|
99
|
-
})).catch(() => {
|
|
100
|
-
if (!on || abort !== ctl) return // this tab left the queue on purpose
|
|
101
|
-
if (leads) {
|
|
102
|
-
leads = false
|
|
103
|
-
lose()
|
|
104
|
-
}
|
|
105
|
-
elect()
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
const timer = setInterval(() => me.probe(), BEAT)
|
|
109
|
-
elect()
|
|
110
|
-
return me
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ---- this tab's own lock ----------------------------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
let tabHeld = false
|
|
116
|
-
let tabFree: (() => void) | undefined
|
|
117
|
-
const whenHeld: (() => void)[] = []
|
|
118
|
-
|
|
119
|
-
/** Held before the tab says anything, so a sibling that queues on it can't read a live tab as dead. */
|
|
120
|
-
export function holdTab(then: () => void) {
|
|
121
|
-
if (tabHeld || !locks()) return then()
|
|
122
|
-
whenHeld.push(then)
|
|
123
|
-
if (whenHeld.length > 1) return
|
|
124
|
-
locks()!.request(`${NAME}:tab:${tab}`, () => new Promise<void>(ok => {
|
|
125
|
-
tabFree = ok
|
|
126
|
-
tabHeld = true
|
|
127
|
-
for (const f of whenHeld.splice(0)) f()
|
|
128
|
-
})).catch(() => {})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Comes through when that tab is gone, crash included. */
|
|
132
|
-
export function freeTab() {
|
|
133
|
-
tabHeld = false
|
|
134
|
-
tabFree?.()
|
|
135
|
-
tabFree = undefined
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export const whenGone = (t: string, fn: () => void) => void locks()?.request(`${NAME}:tab:${t}`, () => fn()).catch(() => {})
|
|
139
|
-
|
|
140
48
|
// ---- lines, as a tab sees them ----------------------------------------------------------------------------------------
|
|
141
49
|
|
|
142
|
-
export interface Line {
|
|
143
|
-
|
|
50
|
+
export interface Line {
|
|
51
|
+
send(text: string): void
|
|
52
|
+
close(): void
|
|
53
|
+
}
|
|
144
54
|
|
|
145
|
-
const lines = new Map<string, Mine>()
|
|
146
55
|
let nth = 0
|
|
147
56
|
|
|
57
|
+
declare const __WEFT_SOCKETS__: boolean | undefined
|
|
58
|
+
/** Whether this deploy can hold a socket. `weft build` says no for a function host (Netlify, Vercel, Lambda…). */
|
|
59
|
+
export const SOCKETS = typeof __WEFT_SOCKETS__ === 'undefined' || __WEFT_SOCKETS__
|
|
60
|
+
|
|
148
61
|
/** Sockets that never said hello in a row: the page's way to learn this host has none. */
|
|
149
62
|
export let socketFailures = 0
|
|
150
|
-
/** The one who asked goes round SSE once, and the socket gets another chance after. */
|
|
63
|
+
/** The one who asked goes round SSE once, and the socket gets another chance after. Where the build said none, always. */
|
|
151
64
|
export const noSockets = () => {
|
|
65
|
+
if (!SOCKETS) return true
|
|
152
66
|
const none = socketFailures >= 2
|
|
153
67
|
if (none) socketFailures = 0
|
|
154
68
|
return none
|
|
155
69
|
}
|
|
156
70
|
export const probeLink = () => election?.probe()
|
|
157
71
|
|
|
158
|
-
function end(l: Mine, code: number, reason?: string) {
|
|
159
|
-
lines.delete(l.id)
|
|
160
|
-
l.closed(code, reason)
|
|
161
|
-
}
|
|
162
|
-
const endAll = (code: number) => { for (const l of Array.from(lines.values())) end(l, code) }
|
|
163
|
-
|
|
164
|
-
function arrive(text: string) {
|
|
165
|
-
const at = text.indexOf(':', 2)
|
|
166
|
-
const l = lines.get(text.slice(2, at))
|
|
167
|
-
if (!l) return
|
|
168
|
-
if (text[0] === 'x') {
|
|
169
|
-
const split = text.indexOf(':', at + 1)
|
|
170
|
-
return end(l, +text.slice(at + 1, split), text.slice(split + 1))
|
|
171
|
-
}
|
|
172
|
-
l.frame(text.slice(at + 1))
|
|
173
|
-
}
|
|
174
|
-
|
|
175
72
|
/** `c`: a socket's frames (socket.ts). `r`: realtime events, opened with a `/_weft/rt` body (hub.ts). */
|
|
176
|
-
export function openLine(
|
|
73
|
+
export function openLine(
|
|
74
|
+
kind: 'c' | 'r',
|
|
75
|
+
hello: string,
|
|
76
|
+
frame: (text: string) => void,
|
|
77
|
+
closed: (code: number, reason?: string) => void,
|
|
78
|
+
): Line {
|
|
177
79
|
start()
|
|
178
80
|
const id = `${tab}.${++nth}`
|
|
179
81
|
const l: Mine = { id, hello: `o:${id}:${kind}:${hello}`, frame, closed }
|
|
@@ -181,7 +83,9 @@ export function openLine(kind: 'c' | 'r', hello: string, frame: (text: string) =
|
|
|
181
83
|
say(id, 'o', l.hello)
|
|
182
84
|
if (!election?.leading()) election?.probe()
|
|
183
85
|
return {
|
|
184
|
-
send(text) {
|
|
86
|
+
send(text) {
|
|
87
|
+
if (lines.has(id)) say(id, 'f', `f:${id}:${text}`)
|
|
88
|
+
},
|
|
185
89
|
close() {
|
|
186
90
|
if (!lines.has(id)) return
|
|
187
91
|
say(id, 'x', `x:${id}:`)
|
|
@@ -194,7 +98,11 @@ export function openLine(kind: 'c' | 'r', hello: string, frame: (text: string) =
|
|
|
194
98
|
|
|
195
99
|
type Up = { tab: string; id: string; k: 'o' | 'x' | 'f'; l: string }
|
|
196
100
|
type Note =
|
|
197
|
-
| { t: 'lead' }
|
|
101
|
+
| { t: 'lead' }
|
|
102
|
+
| { t: 'lost'; code: number; never: boolean }
|
|
103
|
+
| { t: 'who'; tab: string }
|
|
104
|
+
| { t: 'you'; to: string; u?: string }
|
|
105
|
+
| { t: 'redial' }
|
|
198
106
|
| { t: 'bye'; tab: string }
|
|
199
107
|
|
|
200
108
|
let running = false
|
|
@@ -218,13 +126,18 @@ function start() {
|
|
|
218
126
|
running = true
|
|
219
127
|
if (shareable()) {
|
|
220
128
|
notes = new BroadcastChannel(NAME)
|
|
221
|
-
notes.onmessage = e => note(e.data)
|
|
129
|
+
notes.onmessage = (e) => note(e.data)
|
|
222
130
|
down = new BroadcastChannel(`${NAME}:down:${tab}`)
|
|
223
|
-
down.onmessage = e => arrive(e.data)
|
|
131
|
+
down.onmessage = (e) => arrive(e.data)
|
|
224
132
|
upTo = new BroadcastChannel(`${NAME}:up`)
|
|
225
133
|
}
|
|
226
134
|
listen()
|
|
227
|
-
election = campaign(
|
|
135
|
+
election = campaign(
|
|
136
|
+
NAME,
|
|
137
|
+
lead,
|
|
138
|
+
() => hangUp(HANDOVER),
|
|
139
|
+
() => void linkStats.steals++,
|
|
140
|
+
)
|
|
228
141
|
linkStats.role = 'follower'
|
|
229
142
|
if (!shareable() || session) return
|
|
230
143
|
gate = []
|
|
@@ -239,11 +152,12 @@ function note(m: Note) {
|
|
|
239
152
|
if (election?.leading()) return
|
|
240
153
|
if (m.never) socketFailures++
|
|
241
154
|
endAll(m.code)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
else if (m.t === 'you' && m.to === tab) theirs?.(m.u)
|
|
245
|
-
else if (m.t === 'redial') {
|
|
246
|
-
|
|
155
|
+
} else if (m.t === 'who') {
|
|
156
|
+
if (election?.leading()) notes!.postMessage({ t: 'you', to: m.tab, u: open ? user : undefined })
|
|
157
|
+
} else if (m.t === 'you' && m.to === tab) theirs?.(m.u)
|
|
158
|
+
else if (m.t === 'redial') {
|
|
159
|
+
if (election?.leading() && ws) lost(ws, HANDOVER, true)
|
|
160
|
+
} else if (m.t === 'bye') dropTab(m.tab)
|
|
247
161
|
}
|
|
248
162
|
|
|
249
163
|
let session: Promise<void> | undefined
|
|
@@ -251,8 +165,15 @@ let theirs: ((u: string | undefined) => void) | undefined
|
|
|
251
165
|
|
|
252
166
|
/** One round trip, once, the first time a tab that didn't dial opens a line: a new socket used to cost the same. */
|
|
253
167
|
function checkSession() {
|
|
254
|
-
const mine = fetch('/_weft/rt', {
|
|
255
|
-
|
|
168
|
+
const mine = fetch('/_weft/rt', {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: { 'content-type': 'application/json', 'x-weft': '1' },
|
|
171
|
+
body: '{"w":1}',
|
|
172
|
+
})
|
|
173
|
+
.then((r) => (r.ok ? r.json() : undefined))
|
|
174
|
+
.then((r) => r?.u as string | undefined)
|
|
175
|
+
.catch(() => undefined)
|
|
176
|
+
const leader = new Promise<string | undefined>((ok) => {
|
|
256
177
|
theirs = ok
|
|
257
178
|
notes!.postMessage({ t: 'who', tab })
|
|
258
179
|
setTimeout(ok, 1000)
|
|
@@ -283,7 +204,7 @@ const opened = new Map<string, Set<string>>()
|
|
|
283
204
|
const ears = new Map<string, BroadcastChannel>()
|
|
284
205
|
let upFrom: BroadcastChannel | undefined
|
|
285
206
|
|
|
286
|
-
const offline = () => !
|
|
207
|
+
const offline = () => !navigator.onLine
|
|
287
208
|
const send = (line: string) => {
|
|
288
209
|
if (open) ws!.send(line)
|
|
289
210
|
else {
|
|
@@ -305,13 +226,14 @@ function take(m: Up) {
|
|
|
305
226
|
} else if (m.k === 'x') set?.delete(m.id)
|
|
306
227
|
// A line that closed before the socket came never has to be opened.
|
|
307
228
|
if (m.k === 'x' && !open) {
|
|
308
|
-
const hello = queue.findIndex(q => q.startsWith(`o:${m.id}:`))
|
|
229
|
+
const hello = queue.findIndex((q) => q.startsWith(`o:${m.id}:`))
|
|
309
230
|
if (hello >= 0) queue.splice(hello, 1)
|
|
310
231
|
} else send(m.l)
|
|
311
|
-
if (![...opened.values()].some(s => s.size))
|
|
312
|
-
idle
|
|
313
|
-
|
|
314
|
-
|
|
232
|
+
if (![...opened.values()].some((s) => s.size))
|
|
233
|
+
idle ??= setTimeout(() => {
|
|
234
|
+
idle = undefined
|
|
235
|
+
if (ws && ![...opened.values()].some((s) => s.size)) lost(ws, 1000)
|
|
236
|
+
}, IDLE)
|
|
315
237
|
}
|
|
316
238
|
|
|
317
239
|
function dropTab(t: string) {
|
|
@@ -351,22 +273,29 @@ function forward(owner: string, text: string) {
|
|
|
351
273
|
}
|
|
352
274
|
|
|
353
275
|
function wantSocket() {
|
|
354
|
-
if (ws || retry !== undefined || !election?.leading() || offline()) return
|
|
276
|
+
if (!SOCKETS || ws || retry !== undefined || !election?.leading() || offline()) return
|
|
355
277
|
if (!attempt) return dial()
|
|
356
|
-
retry = setTimeout(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
278
|
+
retry = setTimeout(
|
|
279
|
+
() => {
|
|
280
|
+
retry = undefined
|
|
281
|
+
dial()
|
|
282
|
+
},
|
|
283
|
+
Math.random() * Math.min(CAP, BASE * 2 ** attempt),
|
|
284
|
+
)
|
|
360
285
|
}
|
|
361
286
|
|
|
362
287
|
function dial() {
|
|
363
288
|
if (ws || !election?.leading()) return
|
|
364
289
|
linkStats.dials++
|
|
365
|
-
const s = new WebSocket(
|
|
290
|
+
const s = new WebSocket(
|
|
291
|
+
`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_weft/ws?mux=1`,
|
|
292
|
+
)
|
|
366
293
|
ws = s
|
|
367
|
-
s.onmessage = e => arriveFrom(e.data)
|
|
294
|
+
s.onmessage = (e) => arriveFrom(e.data)
|
|
368
295
|
s.onclose = () => lost(s, 1006)
|
|
369
|
-
ping = setInterval(() => {
|
|
296
|
+
ping = setInterval(() => {
|
|
297
|
+
if (open) s.send('p')
|
|
298
|
+
}, PING)
|
|
370
299
|
dead = setTimeout(() => lost(s, 1006), DEAD)
|
|
371
300
|
}
|
|
372
301
|
|
|
@@ -413,7 +342,7 @@ function lead() {
|
|
|
413
342
|
linkStats.role = 'leader'
|
|
414
343
|
if (shareable()) {
|
|
415
344
|
upFrom = new BroadcastChannel(`${NAME}:up`)
|
|
416
|
-
upFrom.onmessage = e => take(e.data)
|
|
345
|
+
upFrom.onmessage = (e) => take(e.data)
|
|
417
346
|
notes!.postMessage({ t: 'lead' })
|
|
418
347
|
}
|
|
419
348
|
// What this tab held for the session check goes straight out, and its lines were waiting on nobody.
|