@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
package/src/share.ts
CHANGED
|
@@ -13,9 +13,25 @@
|
|
|
13
13
|
// no path. A tab is a pure function of `tabs` and `clients`, and the socket a pure diff against them (`reconcile`), so a
|
|
14
14
|
// new leader runs the code an old one ran.
|
|
15
15
|
|
|
16
|
+
import { tabQueue } from './share/tab-queue'
|
|
17
|
+
import { network, upstream as upstreamOf, wanted as wantedKeys } from './share/network'
|
|
18
|
+
import { receiver } from './share/receive'
|
|
19
|
+
import { render, renderAll, hear, dropClients } from './share/display'
|
|
16
20
|
import { createRoot, effect, untrack } from '@weft-js/core'
|
|
17
|
-
import {
|
|
21
|
+
import { seams, type ShareLine } from '@weft-js/core/interaction'
|
|
18
22
|
import { connectBuiltIn, keepOpen, wakeSocket } from './channel'
|
|
23
|
+
import {
|
|
24
|
+
lines,
|
|
25
|
+
shown,
|
|
26
|
+
tabs,
|
|
27
|
+
tabAt,
|
|
28
|
+
visible,
|
|
29
|
+
mine,
|
|
30
|
+
watched,
|
|
31
|
+
clients,
|
|
32
|
+
type Triple,
|
|
33
|
+
type Msg,
|
|
34
|
+
} from './share/state'
|
|
19
35
|
import { BEAT, campaign, freeTab, holdTab, probeLink, tab, whenGone, type Campaign } from './link'
|
|
20
36
|
|
|
21
37
|
/** Ms between flushes to the other tabs. */
|
|
@@ -35,35 +51,20 @@ export const IDLE = 5000
|
|
|
35
51
|
const WIRE = 1
|
|
36
52
|
const CHANNEL = `weft:ui:${WIRE}`
|
|
37
53
|
|
|
38
|
-
type Pair = [key: string, bits: number]
|
|
39
|
-
type Triple = [id: string, key: string, bits: number]
|
|
40
|
-
type Msg =
|
|
41
|
-
| { t: 'tab'; tab: string; keys: Pair[]; shown: boolean; k?: string | undefined }
|
|
42
|
-
| { t: 'bits'; tab: string; e: Pair[] }
|
|
43
|
-
| { t: 'shown'; tab: string; shown: boolean }
|
|
44
|
-
| { t: 'net'; s: Triple[] }
|
|
45
|
-
| { t: 'link'; up: boolean }
|
|
46
|
-
| { t: 'roll' } | { t: 'bye'; tab: string }
|
|
47
|
-
|
|
48
|
-
interface Client { bits: number; at: number; stale?: boolean }
|
|
49
|
-
|
|
50
54
|
/** What a test reads: `role`, and how much went where. */
|
|
51
|
-
export const shareStats = {
|
|
55
|
+
export const shareStats = {
|
|
56
|
+
role: 'none' as 'none' | 'leader' | 'follower',
|
|
57
|
+
tab,
|
|
58
|
+
conn: undefined as string | undefined,
|
|
59
|
+
handed: 0,
|
|
60
|
+
steals: 0,
|
|
61
|
+
frames: 0,
|
|
62
|
+
keysSent: 0,
|
|
63
|
+
bc: 0,
|
|
64
|
+
}
|
|
52
65
|
|
|
53
66
|
const noLocks = () => typeof navigator === 'undefined' || !navigator.locks
|
|
54
67
|
|
|
55
|
-
const lines = new Map<string, (c: Counts) => void>()
|
|
56
|
-
const shown = new Map<string, Counts>()
|
|
57
|
-
/** key → bits, per tab, mine included. */
|
|
58
|
-
const tabs = new Map<string, Map<string, number>>()
|
|
59
|
-
const tabAt = new Map<string, number>()
|
|
60
|
-
const visible = new Map<string, boolean>()
|
|
61
|
-
const mine = new Map<string, number>()
|
|
62
|
-
const watched = new Set<string>()
|
|
63
|
-
const siblings = new Set<string>()
|
|
64
|
-
/** key → other clients. Everything the leader joined; a follower keeps what its own lines want. */
|
|
65
|
-
const clients = new Map<string, Map<string, Client>>()
|
|
66
|
-
|
|
67
68
|
const locks = () => !noLocks()
|
|
68
69
|
const leading = () => shareStats.role === 'leader'
|
|
69
70
|
const looking = () => typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
|
@@ -83,25 +84,6 @@ function post(m: Msg) {
|
|
|
83
84
|
|
|
84
85
|
// ---- what a tab shows ------------------------------------------------------------------------------------------------
|
|
85
86
|
|
|
86
|
-
function render(key: string) {
|
|
87
|
-
const heard = lines.get(key)
|
|
88
|
-
if (!heard) return
|
|
89
|
-
let hover = 0, focus = 0, pressed = 0
|
|
90
|
-
const add = (bits: number) => {
|
|
91
|
-
if (bits & 1) hover++
|
|
92
|
-
if (bits & 2) focus++
|
|
93
|
-
if (bits & 4) pressed++
|
|
94
|
-
}
|
|
95
|
-
for (const [t, keys] of tabs) if (t !== tab) add(keys.get(key) ?? 0)
|
|
96
|
-
for (const e of clients.get(key)?.values() ?? []) add(e.bits)
|
|
97
|
-
const was = shown.get(key) ?? NONE
|
|
98
|
-
if (was.hover === hover && was.focus === focus && was.pressed === pressed) return
|
|
99
|
-
const now = hover || focus || pressed ? { hover, focus, pressed } : NONE
|
|
100
|
-
shown.set(key, now)
|
|
101
|
-
heard(now)
|
|
102
|
-
}
|
|
103
|
-
const renderAll = () => { for (const k of lines.keys()) render(k) }
|
|
104
|
-
|
|
105
87
|
function dropTab(t: string) {
|
|
106
88
|
visible.delete(t)
|
|
107
89
|
if (!tabs.delete(t)) return
|
|
@@ -118,76 +100,27 @@ function watch(t: string) {
|
|
|
118
100
|
whenGone(t, () => dropTab(t))
|
|
119
101
|
}
|
|
120
102
|
|
|
121
|
-
function hear(s: Triple[]) {
|
|
122
|
-
const at = Date.now(), touched = new Set<string>()
|
|
123
|
-
for (const [id, key, bits] of s) {
|
|
124
|
-
if (siblings.has(id) || (!leading() && !lines.has(key))) continue
|
|
125
|
-
let held = clients.get(key)
|
|
126
|
-
if (bits) {
|
|
127
|
-
if (!held) clients.set(key, (held = new Map()))
|
|
128
|
-
held.set(id, { bits, at })
|
|
129
|
-
} else if (!held?.delete(id)) continue
|
|
130
|
-
touched.add(key)
|
|
131
|
-
}
|
|
132
|
-
for (const k of touched) render(k)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function dropClients(which: (e: Client, id: string) => boolean) {
|
|
136
|
-
for (const [key, held] of clients) {
|
|
137
|
-
let changed = false
|
|
138
|
-
for (const [id, e] of held) if (which(e, id)) changed = held.delete(id)
|
|
139
|
-
if (!held.size) clients.delete(key)
|
|
140
|
-
if (changed) render(key)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
103
|
/** Down: what's held stands for GRACE, so a handover doesn't flash. Up: holders re-say, and what nobody re-says goes. */
|
|
145
104
|
let settling: ReturnType<typeof setTimeout> | undefined
|
|
146
105
|
function link(on: boolean) {
|
|
147
106
|
clearTimeout(settling)
|
|
148
107
|
if (on) for (const held of clients.values()) for (const e of held.values()) e.stale = true
|
|
149
|
-
settling = setTimeout(() => dropClients(e => !on || !!e.stale), GRACE)
|
|
108
|
+
settling = setTimeout(() => dropClients((e) => !on || !!e.stale), GRACE)
|
|
150
109
|
}
|
|
151
110
|
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
tabs.set(m.tab, new Map(m.keys))
|
|
155
|
-
tabAt.set(m.tab, Date.now())
|
|
156
|
-
visible.set(m.tab, m.shown)
|
|
157
|
-
if (m.k && !siblings.has(m.k)) {
|
|
158
|
-
siblings.add(m.k)
|
|
159
|
-
dropClients((_, id) => id === m.k)
|
|
160
|
-
}
|
|
161
|
-
watch(m.tab)
|
|
162
|
-
renderAll()
|
|
163
|
-
reconcile()
|
|
164
|
-
if (m.shown) wakeIfLeading()
|
|
165
|
-
// A tab that arrived late has what the socket already heard.
|
|
166
|
-
if (leading()) {
|
|
167
|
-
const s: Triple[] = []
|
|
168
|
-
for (const [key] of m.keys) for (const [id, e] of clients.get(key) ?? []) s.push([id, key, e.bits])
|
|
169
|
-
if (s.length) post({ t: 'net', s })
|
|
170
|
-
}
|
|
171
|
-
} else if (m.t === 'bits') {
|
|
172
|
-
const keys = tabs.get(m.tab) ?? new Map<string, number>()
|
|
173
|
-
tabs.set(m.tab, keys)
|
|
174
|
-
tabAt.set(m.tab, Date.now())
|
|
175
|
-
for (const [key, bits] of m.e) keys.set(key, bits)
|
|
176
|
-
watch(m.tab)
|
|
177
|
-
for (const [key] of m.e) render(key)
|
|
178
|
-
reconcile()
|
|
179
|
-
} else if (m.t === 'shown') {
|
|
180
|
-
visible.set(m.tab, m.shown)
|
|
181
|
-
if (m.shown) wakeIfLeading()
|
|
182
|
-
} else if (m.t === 'roll') announce()
|
|
183
|
-
else if (m.t === 'bye') dropTab(m.tab)
|
|
184
|
-
else if (m.t === 'net') hear(m.s)
|
|
185
|
-
else if (m.t === 'link') link(m.up)
|
|
111
|
+
const wakeIfLeading = () => {
|
|
112
|
+
if (leading()) wakeSocket()
|
|
186
113
|
}
|
|
187
114
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
115
|
+
function announce() {
|
|
116
|
+
post({
|
|
117
|
+
t: 'tab',
|
|
118
|
+
tab,
|
|
119
|
+
keys: [...mine],
|
|
120
|
+
shown: looking(),
|
|
121
|
+
...(locks() ? {} : { k: shareStats.conn }),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
191
124
|
|
|
192
125
|
/** 200 rows mounting are one post and one frame. */
|
|
193
126
|
let queued = false
|
|
@@ -205,21 +138,7 @@ function changedKeys() {
|
|
|
205
138
|
// ---- to the other tabs, once a frame ----------------------------------------------------------------------------------
|
|
206
139
|
|
|
207
140
|
const dirtyTab = new Set<string>()
|
|
208
|
-
|
|
209
|
-
let tabTimer: ReturnType<typeof setTimeout> | undefined
|
|
210
|
-
function flushTab() {
|
|
211
|
-
tabTimer = undefined
|
|
212
|
-
tabLast = performance.now()
|
|
213
|
-
if (dirtyTab.size) post({ t: 'bits', tab, e: [...dirtyTab].map(key => [key, mine.get(key) ?? 0]) })
|
|
214
|
-
dirtyTab.clear()
|
|
215
|
-
}
|
|
216
|
-
function queueTab(key: string) {
|
|
217
|
-
dirtyTab.add(key)
|
|
218
|
-
if (tabTimer !== undefined) return
|
|
219
|
-
const wait = GAP_TAB - (performance.now() - tabLast)
|
|
220
|
-
if (wait <= 0) flushTab()
|
|
221
|
-
else tabTimer = setTimeout(flushTab, wait)
|
|
222
|
-
}
|
|
141
|
+
const queueTab = tabQueue(dirtyTab, mine, post, tab, GAP_TAB)
|
|
223
142
|
|
|
224
143
|
// ---- the leader: the OR of every tab, on one join ---------------------------------------------------------------------
|
|
225
144
|
|
|
@@ -233,73 +152,25 @@ const refused = new Set<string>()
|
|
|
233
152
|
const dirtyNet = new Set<string>()
|
|
234
153
|
const toAdd = new Set<string>()
|
|
235
154
|
const toDrop = new Set<string>()
|
|
236
|
-
let netLast = 0
|
|
237
155
|
let idle: ReturnType<typeof setTimeout> | undefined
|
|
238
|
-
let netTimer: ReturnType<typeof setTimeout> | undefined
|
|
239
|
-
|
|
240
|
-
const upstream = (key: string) => {
|
|
241
|
-
if (!locks()) return mine.get(key) ?? 0
|
|
242
|
-
let bits = 0
|
|
243
|
-
for (const keys of tabs.values()) bits |= keys.get(key) ?? 0
|
|
244
|
-
return bits
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function wanted() {
|
|
248
|
-
if (!locks()) return new Set(mine.keys())
|
|
249
|
-
const out = new Set<string>()
|
|
250
|
-
for (const keys of tabs.values()) for (const k of keys.keys()) out.add(k)
|
|
251
|
-
return out
|
|
252
|
-
}
|
|
253
156
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!sent.has(key) || refused.has(key)) continue
|
|
272
|
-
const bits = upstream(key)
|
|
273
|
-
// Off and on again inside the gap: nothing to say. The latest value is all that's kept.
|
|
274
|
-
if (bits !== sent.get(key)) {
|
|
275
|
-
sent.set(key, bits)
|
|
276
|
-
e.push([key, bits])
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (e.length) {
|
|
280
|
-
name = 'set'
|
|
281
|
-
msg = { e }
|
|
282
|
-
size = e.length
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (name) {
|
|
286
|
-
netLast = performance.now()
|
|
287
|
-
shareStats.frames++
|
|
288
|
-
shareStats.keysSent += size
|
|
289
|
-
conn.send(name as never, msg as never).catch(() => {}) // a dropped link re-says everything
|
|
290
|
-
}
|
|
291
|
-
if (toAdd.size || toDrop.size || dirtyNet.size) netSoon()
|
|
292
|
-
}
|
|
293
|
-
/** Not a timer when the gap has passed: the leader is often a hidden tab, where a timer waits a second or more. */
|
|
294
|
-
function netSoon() {
|
|
295
|
-
const wait = GAP_NET - (performance.now() - netLast)
|
|
296
|
-
if (wait > 0) {
|
|
297
|
-
netTimer ??= setTimeout(netTick, wait)
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
clearTimeout(netTimer)
|
|
301
|
-
netTick()
|
|
302
|
-
}
|
|
157
|
+
const upstream = (key: string) => upstreamOf(key, locks())
|
|
158
|
+
const wanted = () => wantedKeys(locks())
|
|
159
|
+
|
|
160
|
+
const net = network({
|
|
161
|
+
connection: () => conn,
|
|
162
|
+
isOpen: () => open,
|
|
163
|
+
sent,
|
|
164
|
+
refused,
|
|
165
|
+
dirtyNet,
|
|
166
|
+
toAdd,
|
|
167
|
+
toDrop,
|
|
168
|
+
upstream,
|
|
169
|
+
shareStats,
|
|
170
|
+
GAP_NET,
|
|
171
|
+
BATCH,
|
|
172
|
+
})
|
|
173
|
+
const netSoon = net.netSoon
|
|
303
174
|
|
|
304
175
|
/** Whatever changed in `tabs`, the socket follows. Nothing wanted for a while, no socket: a page left its rows behind. */
|
|
305
176
|
function reconcile() {
|
|
@@ -309,10 +180,11 @@ function reconcile() {
|
|
|
309
180
|
clearTimeout(idle)
|
|
310
181
|
idle = undefined
|
|
311
182
|
dial()
|
|
312
|
-
} else
|
|
313
|
-
idle
|
|
314
|
-
|
|
315
|
-
|
|
183
|
+
} else
|
|
184
|
+
idle ??= setTimeout(() => {
|
|
185
|
+
idle = undefined
|
|
186
|
+
if (leading() && !wanted().size) hangUp()
|
|
187
|
+
}, IDLE)
|
|
316
188
|
if (!open) return
|
|
317
189
|
for (const key of want) {
|
|
318
190
|
if (sent.has(key)) {
|
|
@@ -343,8 +215,7 @@ function linked(live: boolean) {
|
|
|
343
215
|
toDrop.clear()
|
|
344
216
|
dirtyNet.clear()
|
|
345
217
|
refused.clear()
|
|
346
|
-
|
|
347
|
-
netTimer = undefined
|
|
218
|
+
net.clear()
|
|
348
219
|
}
|
|
349
220
|
link(live)
|
|
350
221
|
post({ t: 'link', up: live })
|
|
@@ -353,10 +224,10 @@ function linked(live: boolean) {
|
|
|
353
224
|
|
|
354
225
|
function dial() {
|
|
355
226
|
if (conn) return
|
|
356
|
-
stopConn = createRoot(dispose => {
|
|
227
|
+
stopConn = createRoot((dispose) => {
|
|
357
228
|
conn = connectBuiltIn('$share')
|
|
358
229
|
conn.on('b', ({ s }: { s: Triple[] }) => {
|
|
359
|
-
hear(s)
|
|
230
|
+
hear(s, leading())
|
|
360
231
|
if (locks()) post({ t: 'net', s })
|
|
361
232
|
})
|
|
362
233
|
conn.on('ok', (m: { id: string; no: string[] }) => {
|
|
@@ -368,12 +239,15 @@ function dial() {
|
|
|
368
239
|
for (const id of clients.get(k)?.keys() ?? []) lost.push([id, k, 0])
|
|
369
240
|
}
|
|
370
241
|
if (lost.length) {
|
|
371
|
-
hear(lost)
|
|
242
|
+
hear(lost, leading())
|
|
372
243
|
if (locks()) post({ t: 'net', s: lost })
|
|
373
244
|
}
|
|
374
245
|
if (!locks()) announce()
|
|
375
246
|
})
|
|
376
|
-
effect(
|
|
247
|
+
effect(
|
|
248
|
+
() => conn!.status.value,
|
|
249
|
+
(s) => linked(s === 'open'),
|
|
250
|
+
)
|
|
377
251
|
return dispose
|
|
378
252
|
}, null)
|
|
379
253
|
}
|
|
@@ -400,17 +274,22 @@ function up() {
|
|
|
400
274
|
if (running) return
|
|
401
275
|
running = true
|
|
402
276
|
bc = new BroadcastChannel(CHANNEL)
|
|
403
|
-
bc.onmessage = e => receive(e.data)
|
|
277
|
+
bc.onmessage = (e) => receive(e.data)
|
|
404
278
|
tabs.set(tab, mine)
|
|
405
279
|
holdTab(() => {
|
|
406
280
|
lockReady = true
|
|
407
281
|
for (const m of early.splice(0)) post(m)
|
|
408
282
|
})
|
|
409
283
|
shareStats.role = 'follower'
|
|
410
|
-
election = campaign(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
284
|
+
election = campaign(
|
|
285
|
+
CHANNEL,
|
|
286
|
+
lead,
|
|
287
|
+
() => {
|
|
288
|
+
hangUp()
|
|
289
|
+
shareStats.role = 'follower'
|
|
290
|
+
},
|
|
291
|
+
() => void shareStats.steals++,
|
|
292
|
+
)
|
|
414
293
|
election.pinged = sayHeld // a hidden leader's own beat is throttled; a sibling's ping isn't
|
|
415
294
|
announce()
|
|
416
295
|
post({ t: 'roll' })
|
|
@@ -419,7 +298,7 @@ function up() {
|
|
|
419
298
|
|
|
420
299
|
function beat() {
|
|
421
300
|
const now = Date.now()
|
|
422
|
-
dropClients(e => now - e.at > TTL)
|
|
301
|
+
dropClients((e) => now - e.at > TTL)
|
|
423
302
|
if (!locks()) {
|
|
424
303
|
announce()
|
|
425
304
|
for (const [t, at] of tabAt) if (now - at > TTL) dropTab(t)
|
|
@@ -432,10 +311,11 @@ let saidAt = 0
|
|
|
432
311
|
function sayHeld() {
|
|
433
312
|
if (Date.now() - saidAt < BEAT - 1000) return
|
|
434
313
|
saidAt = Date.now()
|
|
435
|
-
for (const [key, bits] of sent)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
314
|
+
for (const [key, bits] of sent)
|
|
315
|
+
if (bits) {
|
|
316
|
+
sent.set(key, -1)
|
|
317
|
+
dirtyNet.add(key)
|
|
318
|
+
}
|
|
439
319
|
if (dirtyNet.size) netSoon()
|
|
440
320
|
}
|
|
441
321
|
|
|
@@ -462,14 +342,28 @@ function halt() {
|
|
|
462
342
|
clients.clear()
|
|
463
343
|
}
|
|
464
344
|
|
|
345
|
+
const receive = receiver({
|
|
346
|
+
dropClients,
|
|
347
|
+
watch,
|
|
348
|
+
reconcile,
|
|
349
|
+
wakeIfLeading,
|
|
350
|
+
leading,
|
|
351
|
+
post,
|
|
352
|
+
announce,
|
|
353
|
+
dropTab,
|
|
354
|
+
link,
|
|
355
|
+
})
|
|
356
|
+
|
|
465
357
|
if (typeof window !== 'undefined') {
|
|
466
358
|
addEventListener('pagehide', halt)
|
|
467
|
-
addEventListener('pageshow', e => {
|
|
468
|
-
if (!
|
|
359
|
+
addEventListener('pageshow', (e) => {
|
|
360
|
+
if (!e.persisted || !lines.size) return
|
|
469
361
|
up()
|
|
470
362
|
renderAll()
|
|
471
363
|
})
|
|
472
|
-
document.addEventListener('visibilitychange', () => {
|
|
364
|
+
document.addEventListener('visibilitychange', () => {
|
|
365
|
+
if (running) post({ t: 'shown', tab, shown: looking() })
|
|
366
|
+
})
|
|
473
367
|
keepOpen.stay = () => [...visible.values()].some(Boolean)
|
|
474
368
|
}
|
|
475
369
|
|
package/src/sheets.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
7
7
|
import { dirname, resolve } from 'node:path'
|
|
8
8
|
|
|
9
9
|
/** A relative `url()` means something else from another file, and that file keeps its own link. */
|
|
10
|
-
const movable =
|
|
10
|
+
const movable = (css) =>
|
|
11
|
+
!/url\(\s*(?!["']?(?:\/|data:|https?:|#|%23))/.test(css) && !/@import\b/.test(css)
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @param {string[][]} routes each route's sheets, in cascade order
|
|
@@ -15,18 +16,18 @@ const movable = css => !/url\(\s*(?!["']?(?:\/|data:|https?:|#|%23))/.test(css)
|
|
|
15
16
|
*/
|
|
16
17
|
export function planSheets(routes, text) {
|
|
17
18
|
const files = {}
|
|
18
|
-
const join = has => {
|
|
19
|
+
const join = (has) => {
|
|
19
20
|
if (!has.length) return undefined
|
|
20
21
|
const css = has.map(text).join('')
|
|
21
22
|
const file = `assets/sheet-${createHash('sha256').update(css).digest('base64url').slice(0, 8)}.css`
|
|
22
23
|
files[file] = css
|
|
23
24
|
return { file, has }
|
|
24
25
|
}
|
|
25
|
-
const everywhere = n => routes.every(r => r.includes(n))
|
|
26
|
-
const shared = join([...new Set(routes.flat())].filter(n => everywhere(n) && movable(text(n))))
|
|
26
|
+
const everywhere = (n) => routes.every((r) => r.includes(n))
|
|
27
|
+
const shared = join([...new Set(routes.flat())].filter((n) => everywhere(n) && movable(text(n))))
|
|
27
28
|
const seen = new Map()
|
|
28
29
|
for (const list of routes) {
|
|
29
|
-
const route = join([...new Set(list)].filter(n => !everywhere(n) && movable(text(n))))
|
|
30
|
+
const route = join([...new Set(list)].filter((n) => !everywhere(n) && movable(text(n))))
|
|
30
31
|
if (route) seen.set(route.file, route)
|
|
31
32
|
}
|
|
32
33
|
return { shared, routes: [...seen.values()], files }
|
|
@@ -38,11 +39,13 @@ const EXTS = ['', '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts']
|
|
|
38
39
|
/** The app's own source files `files` import: where the islands a route can show are declared. */
|
|
39
40
|
export function reach(files) {
|
|
40
41
|
const seen = new Set()
|
|
41
|
-
const walk = f => {
|
|
42
|
+
const walk = (f) => {
|
|
42
43
|
if (seen.has(f)) return
|
|
43
44
|
seen.add(f)
|
|
44
45
|
for (const [, spec] of readFileSync(f, 'utf8').matchAll(FROM)) {
|
|
45
|
-
const to = EXTS.map(e => resolve(dirname(f), spec) + e).find(
|
|
46
|
+
const to = EXTS.map((e) => resolve(dirname(f), spec) + e).find(
|
|
47
|
+
(c) => /\.[jt]sx?$/.test(c) && existsSync(c),
|
|
48
|
+
)
|
|
46
49
|
if (to) walk(to)
|
|
47
50
|
}
|
|
48
51
|
}
|
|
@@ -54,22 +57,31 @@ export function reach(files) {
|
|
|
54
57
|
export function pick(plan, list) {
|
|
55
58
|
const left = new Set(list)
|
|
56
59
|
const out = []
|
|
57
|
-
if (plan.shared?.has.some(n => left.has(n))) {
|
|
60
|
+
if (plan.shared?.has.some((n) => left.has(n))) {
|
|
58
61
|
out.push(plan.shared)
|
|
59
|
-
plan.shared.has.forEach(n => left.delete(n))
|
|
62
|
+
plan.shared.has.forEach((n) => left.delete(n))
|
|
60
63
|
}
|
|
61
|
-
let best,
|
|
64
|
+
let best,
|
|
65
|
+
most = 0
|
|
62
66
|
for (const r of plan.routes) {
|
|
63
|
-
const hits = r.has.filter(n => left.has(n)).length
|
|
64
|
-
if (hits > most && r.has.every(n => list.includes(n) || left.has(n))) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (hits > most) { best = r; most = hits }
|
|
67
|
+
const hits = r.has.filter((n) => left.has(n)).length
|
|
68
|
+
if (hits > most && r.has.every((n) => list.includes(n) || left.has(n))) {
|
|
69
|
+
best = r
|
|
70
|
+
most = hits
|
|
71
|
+
}
|
|
69
72
|
}
|
|
73
|
+
if (!best)
|
|
74
|
+
for (const r of plan.routes) {
|
|
75
|
+
// a route's sheet may hold a little more than this page showed: never another route's
|
|
76
|
+
const hits = r.has.filter((n) => left.has(n)).length
|
|
77
|
+
if (hits > most) {
|
|
78
|
+
best = r
|
|
79
|
+
most = hits
|
|
80
|
+
}
|
|
81
|
+
}
|
|
70
82
|
if (best) {
|
|
71
83
|
out.push(best)
|
|
72
|
-
best.has.forEach(n => left.delete(n))
|
|
84
|
+
best.has.forEach((n) => left.delete(n))
|
|
73
85
|
}
|
|
74
|
-
return { sheets: out, rest: list.filter(n => left.has(n)) }
|
|
86
|
+
return { sheets: out, rest: list.filter((n) => left.has(n)) }
|
|
75
87
|
}
|