@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.
Files changed (213) hide show
  1. package/README.md +36 -3
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/rust-toolchain.toml +3 -0
  5. package/crate/src/build/emit.rs +103 -0
  6. package/crate/src/build.rs +367 -0
  7. package/crate/src/ctx.rs +186 -0
  8. package/crate/src/envelope.rs +163 -0
  9. package/crate/src/functions.rs +173 -0
  10. package/crate/src/lib.rs +118 -0
  11. package/crate/src/native.rs +212 -0
  12. package/crate/src/otel.rs +62 -0
  13. package/crate/src/server.rs +203 -0
  14. package/package.json +46 -37
  15. package/src/app.mjs +51 -10
  16. package/src/asset-receipt.d.mts +13 -0
  17. package/src/asset-receipt.mjs +61 -0
  18. package/src/auth/better-auth.ts +215 -17
  19. package/src/auth/drizzle.ts +1 -1
  20. package/src/auth.ts +21 -6
  21. package/src/boot.ts +19 -8
  22. package/src/bus/postgres.ts +32 -19
  23. package/src/bus.ts +7 -4
  24. package/src/{capture.mjs → capture.ts} +34 -1
  25. package/src/changes-sql.d.mts +8 -2
  26. package/src/changes-sql.mjs +26 -10
  27. package/src/changes.ts +165 -66
  28. package/src/channel/connections.ts +216 -0
  29. package/src/channel/socket.ts +396 -0
  30. package/src/channel-fake.ts +19 -4
  31. package/src/channel.ts +124 -490
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +146 -155
  35. package/src/data/cache.ts +354 -0
  36. package/src/data/definitions.ts +94 -0
  37. package/src/data/draft.ts +104 -0
  38. package/src/data/foundation.ts +95 -0
  39. package/src/data/live-api.ts +22 -0
  40. package/src/data/mutation.ts +138 -0
  41. package/src/data/primitives.ts +135 -0
  42. package/src/data/query.ts +106 -0
  43. package/src/data/transport.ts +121 -0
  44. package/src/data/types.ts +303 -0
  45. package/src/data.ts +69 -894
  46. package/src/db/bun-sqlite.d.ts +4 -1
  47. package/src/db/changes-mysql.ts +61 -21
  48. package/src/db/changes-postgres.ts +197 -118
  49. package/src/db/changes-sqlite.ts +131 -64
  50. package/src/db/core.ts +38 -13
  51. package/src/db/index.ts +20 -4
  52. package/src/db/mysql.ts +22 -6
  53. package/src/db/postgres.ts +49 -13
  54. package/src/db/sqlite.ts +53 -27
  55. package/src/derive.d.mts +4 -1
  56. package/src/derive.mjs +136 -39
  57. package/src/dev-runtime.mjs +8 -4
  58. package/src/exec/errors.ts +57 -0
  59. package/src/exec/landing.ts +66 -0
  60. package/src/exec/scope.ts +174 -0
  61. package/src/exec/touch.ts +80 -0
  62. package/src/exec/validation.ts +176 -0
  63. package/src/exec.ts +191 -341
  64. package/src/extension.ts +8 -2
  65. package/src/first-paint.ts +26 -9
  66. package/src/form/value.ts +15 -0
  67. package/src/form.tsx +132 -43
  68. package/src/hub/channels.ts +102 -0
  69. package/src/hub/connections.ts +147 -0
  70. package/src/hub/delivery.ts +62 -0
  71. package/src/hub/feed.ts +168 -0
  72. package/src/hub/presence.ts +64 -0
  73. package/src/hub/route.ts +158 -0
  74. package/src/hub/subscriptions.ts +212 -0
  75. package/src/hub/topic.ts +98 -0
  76. package/src/hub/tracked.ts +12 -0
  77. package/src/hub/types.ts +190 -0
  78. package/src/hub/view.ts +147 -0
  79. package/src/hub.ts +132 -758
  80. package/src/index.ts +84 -10
  81. package/src/inline.mjs +356 -0
  82. package/src/inspect/compare.mjs +162 -0
  83. package/src/inspect/routes.mjs +154 -0
  84. package/src/inspect/samples.mjs +59 -0
  85. package/src/inspect/url.mjs +2 -0
  86. package/src/inspect.mjs +146 -187
  87. package/src/link/election.ts +178 -0
  88. package/src/link/lines.ts +27 -0
  89. package/src/link.ts +97 -168
  90. package/src/list-drizzle.ts +104 -18
  91. package/src/list.ts +127 -36
  92. package/src/listen.ts +32 -6
  93. package/src/live/api.ts +18 -0
  94. package/src/live/backoff.ts +6 -0
  95. package/src/live/frames.ts +42 -0
  96. package/src/live/read.ts +77 -0
  97. package/src/live/sub-types.ts +24 -0
  98. package/src/live/transport.ts +86 -0
  99. package/src/live/types.ts +52 -0
  100. package/src/live-fake.ts +20 -5
  101. package/src/live-target.mjs +113 -35
  102. package/src/live.ts +116 -225
  103. package/src/media.ts +8 -1
  104. package/src/migrations.mjs +14 -6
  105. package/src/mux.ts +161 -64
  106. package/src/og/DefaultCard.tsx +40 -9
  107. package/src/og/fonts.mjs +16 -8
  108. package/src/og/images.mjs +43 -22
  109. package/src/og/index.mjs +119 -43
  110. package/src/og/native.mjs +11 -4
  111. package/src/range.mjs +15 -3
  112. package/src/router.ts +56 -6
  113. package/src/routes.mjs +73 -22
  114. package/src/run/admission.ts +80 -0
  115. package/src/run/boot.tsx +115 -0
  116. package/src/run/cache-presentation.ts +67 -0
  117. package/src/run/cache.ts +254 -0
  118. package/src/run/helpers.ts +197 -0
  119. package/src/run/listeners.ts +155 -0
  120. package/src/run/navigation.ts +382 -0
  121. package/src/run/page-skeleton.tsx +35 -0
  122. package/src/run/prefetch.ts +128 -0
  123. package/src/run/retain-view.ts +18 -0
  124. package/src/run/shape-preparation.ts +97 -0
  125. package/src/run/shape-styles.ts +72 -0
  126. package/src/run/source-context/pin.ts +124 -0
  127. package/src/run/source-context.ts +311 -0
  128. package/src/run.tsx +50 -174
  129. package/src/rust/common.ts +163 -0
  130. package/src/rust/native.ts +282 -0
  131. package/src/rust.ts +103 -229
  132. package/src/server/card.tsx +65 -0
  133. package/src/server/compression.ts +115 -0
  134. package/src/server/core.ts +70 -0
  135. package/src/server/dispatch.ts +70 -0
  136. package/src/server/form.ts +85 -0
  137. package/src/server/functions.ts +129 -0
  138. package/src/server/handler.ts +39 -0
  139. package/src/server/head.ts +96 -0
  140. package/src/server/media.ts +60 -0
  141. package/src/server/navigation-cache.ts +80 -0
  142. package/src/server/navigation.tsx +85 -0
  143. package/src/server/page-cold.ts +95 -0
  144. package/src/server/page-stream.tsx +181 -0
  145. package/src/server/page-types.ts +51 -0
  146. package/src/server/page-warm.ts +130 -0
  147. package/src/server/page.ts +69 -0
  148. package/src/server/payload.ts +89 -0
  149. package/src/server/prerender.ts +60 -0
  150. package/src/server/preview.tsx +67 -0
  151. package/src/server/render.ts +167 -0
  152. package/src/server/request.ts +98 -0
  153. package/src/server/resolver.ts +228 -0
  154. package/src/server/routes.ts +150 -0
  155. package/src/server/services.ts +69 -0
  156. package/src/server/text.ts +29 -0
  157. package/src/server/types.ts +160 -0
  158. package/src/server.tsx +82 -964
  159. package/src/shape.mjs +30 -6
  160. package/src/share/display.ts +50 -0
  161. package/src/share/network.ts +119 -0
  162. package/src/share/receive.ts +70 -0
  163. package/src/share/state.ts +30 -0
  164. package/src/share/tab-queue.ts +28 -0
  165. package/src/share-server.ts +128 -43
  166. package/src/share.ts +103 -209
  167. package/src/sheets.mjs +30 -18
  168. package/src/sidecar/build.mjs +170 -0
  169. package/src/sidecar/cargo.mjs +164 -0
  170. package/src/sidecar/common.mjs +17 -0
  171. package/src/sidecar/dev-build.mjs +91 -0
  172. package/src/sidecar/dev-watch.mjs +88 -0
  173. package/src/sidecar/manifest.mjs +19 -0
  174. package/src/sidecar/process.mjs +58 -0
  175. package/src/sidecar/refresh.mjs +19 -0
  176. package/src/sidecar.mjs +75 -416
  177. package/src/socket/check.ts +50 -0
  178. package/src/socket/deliver.ts +16 -0
  179. package/src/socket/publish.ts +56 -0
  180. package/src/socket/rooms.ts +41 -0
  181. package/src/socket/route.ts +79 -0
  182. package/src/socket/shared.ts +26 -0
  183. package/src/socket/types.ts +48 -0
  184. package/src/socket.ts +205 -194
  185. package/src/stream.ts +10 -1
  186. package/src/verify/app.ts +39 -0
  187. package/src/verify/cleanup.ts +27 -0
  188. package/src/verify/database.ts +11 -0
  189. package/src/verify/live.ts +63 -0
  190. package/src/verify/notify.ts +35 -0
  191. package/src/verify/skip.ts +19 -0
  192. package/src/verify/support.ts +149 -0
  193. package/src/verify/timing.ts +15 -0
  194. package/src/verify/trigger.ts +14 -0
  195. package/src/verify.ts +77 -176
  196. package/src/view.tsx +96 -11
  197. package/src/vite/asset-bytes.mjs +95 -0
  198. package/src/vite/assets.mjs +117 -0
  199. package/src/vite/config-hooks.mjs +166 -0
  200. package/src/vite/dev-hooks.mjs +103 -0
  201. package/src/vite/dev.mjs +102 -0
  202. package/src/vite/entries.mjs +145 -0
  203. package/src/vite/module-hooks.mjs +288 -0
  204. package/src/vite/route-modules.mjs +289 -0
  205. package/src/vite/server-functions.mjs +222 -0
  206. package/src/vite/shape-entry-css.mjs +187 -0
  207. package/src/vite/shape-override.mjs +226 -0
  208. package/src/vite/shared.mjs +51 -0
  209. package/src/vite/sheets.mjs +92 -0
  210. package/src/vite/state.mjs +89 -0
  211. package/src/vite/syntax.mjs +139 -0
  212. package/src/vite.mjs +152 -853
  213. 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 { NONE, seams, type Counts, type ShareLine } from '@weft-js/core/interaction'
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 = { role: 'none' as 'none' | 'leader' | 'follower', tab, conn: undefined as string | undefined, handed: 0, steals: 0, frames: 0, keysSent: 0, bc: 0 }
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
- function receive(m: Msg) {
153
- if (m.t === 'tab') {
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
- const wakeIfLeading = () => { if (leading()) wakeSocket() }
189
-
190
- function announce() { post({ t: 'tab', tab, keys: [...mine], shown: looking(), ...(locks() ? {} : { k: shareStats.conn }) }) }
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
- let tabLast = 0
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
- /** One frame per GAP_NET, `u` first: the bucket is 20 a second, and a frame carries every key that's waiting. */
255
- function netTick() {
256
- netTimer = undefined
257
- if (!conn || !open) return
258
- let name: 'u' | 'set' | undefined, msg: unknown, size = 0
259
- if (toAdd.size || toDrop.size) {
260
- const add = [...toAdd].slice(0, BATCH), drop = [...toDrop].slice(0, BATCH - add.length)
261
- for (const k of add) toAdd.delete(k)
262
- for (const k of drop) toDrop.delete(k)
263
- name = 'u'
264
- msg = { add, drop }
265
- size = add.length + drop.length
266
- } else {
267
- const e: Pair[] = []
268
- for (const key of dirtyNet) {
269
- if (e.length === BATCH) break
270
- dirtyNet.delete(key)
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 idle ??= setTimeout(() => {
313
- idle = undefined
314
- if (leading() && !wanted().size) hangUp()
315
- }, IDLE)
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
- clearTimeout(netTimer)
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(() => conn!.status.value, s => linked(s === 'open'))
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(CHANNEL, lead, () => {
411
- hangUp()
412
- shareStats.role = 'follower'
413
- }, () => void shareStats.steals++)
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) if (bits) {
436
- sent.set(key, -1)
437
- dirtyNet.add(key)
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 (!(e).persisted || !lines.size) return
359
+ addEventListener('pageshow', (e) => {
360
+ if (!e.persisted || !lines.size) return
469
361
  up()
470
362
  renderAll()
471
363
  })
472
- document.addEventListener('visibilitychange', () => { if (running) post({ t: 'shown', tab, shown: looking() }) })
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 = css => !/url\(\s*(?!["']?(?:\/|data:|https?:|#|%23))/.test(css) && !/@import\b/.test(css)
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(c => /\.[jt]sx?$/.test(c) && existsSync(c))
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, most = 0
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))) { best = r; most = hits }
65
- }
66
- if (!best) for (const r of plan.routes) { // a route's sheet may hold a little more than this page showed: never another route's
67
- const hits = r.has.filter(n => left.has(n)).length
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
  }