@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,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,137 +12,46 @@
|
|
|
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
|
|
|
148
57
|
declare const __WEFT_SOCKETS__: boolean | undefined
|
|
@@ -160,25 +69,13 @@ export const noSockets = () => {
|
|
|
160
69
|
}
|
|
161
70
|
export const probeLink = () => election?.probe()
|
|
162
71
|
|
|
163
|
-
function end(l: Mine, code: number, reason?: string) {
|
|
164
|
-
lines.delete(l.id)
|
|
165
|
-
l.closed(code, reason)
|
|
166
|
-
}
|
|
167
|
-
const endAll = (code: number) => { for (const l of Array.from(lines.values())) end(l, code) }
|
|
168
|
-
|
|
169
|
-
function arrive(text: string) {
|
|
170
|
-
const at = text.indexOf(':', 2)
|
|
171
|
-
const l = lines.get(text.slice(2, at))
|
|
172
|
-
if (!l) return
|
|
173
|
-
if (text[0] === 'x') {
|
|
174
|
-
const split = text.indexOf(':', at + 1)
|
|
175
|
-
return end(l, +text.slice(at + 1, split), text.slice(split + 1))
|
|
176
|
-
}
|
|
177
|
-
l.frame(text.slice(at + 1))
|
|
178
|
-
}
|
|
179
|
-
|
|
180
72
|
/** `c`: a socket's frames (socket.ts). `r`: realtime events, opened with a `/_weft/rt` body (hub.ts). */
|
|
181
|
-
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 {
|
|
182
79
|
start()
|
|
183
80
|
const id = `${tab}.${++nth}`
|
|
184
81
|
const l: Mine = { id, hello: `o:${id}:${kind}:${hello}`, frame, closed }
|
|
@@ -186,7 +83,9 @@ export function openLine(kind: 'c' | 'r', hello: string, frame: (text: string) =
|
|
|
186
83
|
say(id, 'o', l.hello)
|
|
187
84
|
if (!election?.leading()) election?.probe()
|
|
188
85
|
return {
|
|
189
|
-
send(text) {
|
|
86
|
+
send(text) {
|
|
87
|
+
if (lines.has(id)) say(id, 'f', `f:${id}:${text}`)
|
|
88
|
+
},
|
|
190
89
|
close() {
|
|
191
90
|
if (!lines.has(id)) return
|
|
192
91
|
say(id, 'x', `x:${id}:`)
|
|
@@ -199,7 +98,11 @@ export function openLine(kind: 'c' | 'r', hello: string, frame: (text: string) =
|
|
|
199
98
|
|
|
200
99
|
type Up = { tab: string; id: string; k: 'o' | 'x' | 'f'; l: string }
|
|
201
100
|
type Note =
|
|
202
|
-
| { 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' }
|
|
203
106
|
| { t: 'bye'; tab: string }
|
|
204
107
|
|
|
205
108
|
let running = false
|
|
@@ -223,13 +126,18 @@ function start() {
|
|
|
223
126
|
running = true
|
|
224
127
|
if (shareable()) {
|
|
225
128
|
notes = new BroadcastChannel(NAME)
|
|
226
|
-
notes.onmessage = e => note(e.data)
|
|
129
|
+
notes.onmessage = (e) => note(e.data)
|
|
227
130
|
down = new BroadcastChannel(`${NAME}:down:${tab}`)
|
|
228
|
-
down.onmessage = e => arrive(e.data)
|
|
131
|
+
down.onmessage = (e) => arrive(e.data)
|
|
229
132
|
upTo = new BroadcastChannel(`${NAME}:up`)
|
|
230
133
|
}
|
|
231
134
|
listen()
|
|
232
|
-
election = campaign(
|
|
135
|
+
election = campaign(
|
|
136
|
+
NAME,
|
|
137
|
+
lead,
|
|
138
|
+
() => hangUp(HANDOVER),
|
|
139
|
+
() => void linkStats.steals++,
|
|
140
|
+
)
|
|
233
141
|
linkStats.role = 'follower'
|
|
234
142
|
if (!shareable() || session) return
|
|
235
143
|
gate = []
|
|
@@ -244,11 +152,12 @@ function note(m: Note) {
|
|
|
244
152
|
if (election?.leading()) return
|
|
245
153
|
if (m.never) socketFailures++
|
|
246
154
|
endAll(m.code)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
else if (m.t === 'you' && m.to === tab) theirs?.(m.u)
|
|
250
|
-
else if (m.t === 'redial') {
|
|
251
|
-
|
|
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)
|
|
252
161
|
}
|
|
253
162
|
|
|
254
163
|
let session: Promise<void> | undefined
|
|
@@ -256,8 +165,15 @@ let theirs: ((u: string | undefined) => void) | undefined
|
|
|
256
165
|
|
|
257
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. */
|
|
258
167
|
function checkSession() {
|
|
259
|
-
const mine = fetch('/_weft/rt', {
|
|
260
|
-
|
|
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) => {
|
|
261
177
|
theirs = ok
|
|
262
178
|
notes!.postMessage({ t: 'who', tab })
|
|
263
179
|
setTimeout(ok, 1000)
|
|
@@ -288,7 +204,7 @@ const opened = new Map<string, Set<string>>()
|
|
|
288
204
|
const ears = new Map<string, BroadcastChannel>()
|
|
289
205
|
let upFrom: BroadcastChannel | undefined
|
|
290
206
|
|
|
291
|
-
const offline = () => !
|
|
207
|
+
const offline = () => !navigator.onLine
|
|
292
208
|
const send = (line: string) => {
|
|
293
209
|
if (open) ws!.send(line)
|
|
294
210
|
else {
|
|
@@ -310,13 +226,14 @@ function take(m: Up) {
|
|
|
310
226
|
} else if (m.k === 'x') set?.delete(m.id)
|
|
311
227
|
// A line that closed before the socket came never has to be opened.
|
|
312
228
|
if (m.k === 'x' && !open) {
|
|
313
|
-
const hello = queue.findIndex(q => q.startsWith(`o:${m.id}:`))
|
|
229
|
+
const hello = queue.findIndex((q) => q.startsWith(`o:${m.id}:`))
|
|
314
230
|
if (hello >= 0) queue.splice(hello, 1)
|
|
315
231
|
} else send(m.l)
|
|
316
|
-
if (![...opened.values()].some(s => s.size))
|
|
317
|
-
idle
|
|
318
|
-
|
|
319
|
-
|
|
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)
|
|
320
237
|
}
|
|
321
238
|
|
|
322
239
|
function dropTab(t: string) {
|
|
@@ -358,20 +275,27 @@ function forward(owner: string, text: string) {
|
|
|
358
275
|
function wantSocket() {
|
|
359
276
|
if (!SOCKETS || ws || retry !== undefined || !election?.leading() || offline()) return
|
|
360
277
|
if (!attempt) return dial()
|
|
361
|
-
retry = setTimeout(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
278
|
+
retry = setTimeout(
|
|
279
|
+
() => {
|
|
280
|
+
retry = undefined
|
|
281
|
+
dial()
|
|
282
|
+
},
|
|
283
|
+
Math.random() * Math.min(CAP, BASE * 2 ** attempt),
|
|
284
|
+
)
|
|
365
285
|
}
|
|
366
286
|
|
|
367
287
|
function dial() {
|
|
368
288
|
if (ws || !election?.leading()) return
|
|
369
289
|
linkStats.dials++
|
|
370
|
-
const s = new WebSocket(
|
|
290
|
+
const s = new WebSocket(
|
|
291
|
+
`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_weft/ws?mux=1`,
|
|
292
|
+
)
|
|
371
293
|
ws = s
|
|
372
|
-
s.onmessage = e => arriveFrom(e.data)
|
|
294
|
+
s.onmessage = (e) => arriveFrom(e.data)
|
|
373
295
|
s.onclose = () => lost(s, 1006)
|
|
374
|
-
ping = setInterval(() => {
|
|
296
|
+
ping = setInterval(() => {
|
|
297
|
+
if (open) s.send('p')
|
|
298
|
+
}, PING)
|
|
375
299
|
dead = setTimeout(() => lost(s, 1006), DEAD)
|
|
376
300
|
}
|
|
377
301
|
|
|
@@ -418,7 +342,7 @@ function lead() {
|
|
|
418
342
|
linkStats.role = 'leader'
|
|
419
343
|
if (shareable()) {
|
|
420
344
|
upFrom = new BroadcastChannel(`${NAME}:up`)
|
|
421
|
-
upFrom.onmessage = e => take(e.data)
|
|
345
|
+
upFrom.onmessage = (e) => take(e.data)
|
|
422
346
|
notes!.postMessage({ t: 'lead' })
|
|
423
347
|
}
|
|
424
348
|
// What this tab held for the session check goes straight out, and its lines were waiting on nobody.
|