@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.
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 +38 -8
  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 +120 -492
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +144 -157
  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 +90 -166
  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 +112 -37
  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 -178
  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 -971
  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
@@ -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
- /** Bump when a message here changes shape: tabs of two shapes share nothing, and each is its own client. */
16
- const WIRE = 1
17
- const NAME = `weft:link:${WIRE}`
18
- const PING = 20_000, DEAD = 45_000, BASE = 200, CAP = 30_000, IDLE = 1000
19
- export const BEAT = 5000
20
- const PONG = 1500
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 { send(text: string): void; close(): void }
143
- interface Mine { id: string; hello: string; frame(text: string): void; closed(code: number, reason?: string): void }
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(kind: 'c' | 'r', hello: string, frame: (text: string) => void, closed: (code: number, reason?: string) => void): Line {
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) { if (lines.has(id)) say(id, 'f', `f:${id}:${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' } | { t: 'lost'; code: number; never: boolean } | { t: 'who'; tab: string } | { t: 'you'; to: string; u?: string } | { t: 'redial' }
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(NAME, lead, () => hangUp(HANDOVER), () => void linkStats.steals++)
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
- else if (m.t === 'who') { if (election?.leading()) notes!.postMessage({ t: 'you', to: m.tab, u: open ? user : undefined }) }
249
- else if (m.t === 'you' && m.to === tab) theirs?.(m.u)
250
- else if (m.t === 'redial') { if (election?.leading() && ws) lost(ws, HANDOVER, true) }
251
- else if (m.t === 'bye') dropTab(m.tab)
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', { method: 'POST', headers: { 'content-type': 'application/json', 'x-weft': '1' }, body: '{"w":1}' }).then(r => (r.ok ? r.json() : undefined)).then(r => r?.u as string | undefined).catch(() => undefined)
260
- const leader = new Promise<string | undefined>(ok => {
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 = () => ! navigator.onLine
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)) idle ??= setTimeout(() => {
317
- idle = undefined
318
- if (ws && ![...opened.values()].some(s => s.size)) lost(ws, 1000)
319
- }, IDLE)
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
- retry = undefined
363
- dial()
364
- }, Math.random() * Math.min(CAP, BASE * 2 ** attempt))
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(`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/_weft/ws?mux=1`)
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(() => { if (open) s.send('p') }, PING)
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.