@weft-js/start 0.1.1 → 0.1.3

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 (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. package/src/capture.d.mts +0 -1
package/src/shape.mjs CHANGED
@@ -12,7 +12,20 @@ export function toJson(schema, side) {
12
12
  }
13
13
 
14
14
  /** Typical lengths by field name, for strings with no hint (research 11 §7.1). */
15
- const NAMES = { title: 40, name: 16, label: 16, email: 22, body: 200, content: 200, text: 120, description: 120, summary: 120, slug: 24, url: 40, id: 8 }
15
+ const NAMES = {
16
+ title: 40,
17
+ name: 16,
18
+ label: 16,
19
+ email: 22,
20
+ body: 200,
21
+ content: 200,
22
+ text: 120,
23
+ description: 120,
24
+ summary: 120,
25
+ slug: 24,
26
+ url: 40,
27
+ id: 8,
28
+ }
16
29
  const FORMATS = { email: 22, uuid: 36, url: 40, uri: 40, 'date-time': 24, date: 10, time: 8 }
17
30
  /** A string with no hint: X036 if a skeleton shows one. */
18
31
  const UNHINTED = 16
@@ -35,7 +48,7 @@ export function schemaTree(s, name = '') {
35
48
  if (s.enum) return s.enum[0]
36
49
  const union = s.anyOf ?? s.oneOf
37
50
  if (union) {
38
- const some = union.filter(x => x.type !== 'null')
51
+ const some = union.filter((x) => x.type !== 'null')
39
52
  return some.length < union.length && !(s.probability >= 0.5) ? null : schemaTree(some[0], name)
40
53
  }
41
54
  switch (s.type) {
@@ -47,10 +60,14 @@ export function schemaTree(s, name = '') {
47
60
  case 'boolean':
48
61
  return false
49
62
  case 'array':
50
- return { ...(s.maxItems !== undefined ? { $a: s.maxItems } : {}), $of: schemaTree(s.items, name) }
63
+ return {
64
+ ...(s.maxItems !== undefined ? { $a: s.maxItems } : {}),
65
+ $of: schemaTree(s.items, name),
66
+ }
51
67
  case 'object': {
52
68
  const out = {}
53
- for (const [k, v] of Object.entries(s.properties ?? {})) if (s.required?.includes(k) || v.probability >= 0.5) out[k] = schemaTree(v, k)
69
+ for (const [k, v] of Object.entries(s.properties ?? {}))
70
+ if (s.required?.includes(k) || v.probability >= 0.5) out[k] = schemaTree(v, k)
54
71
  return out
55
72
  }
56
73
  }
@@ -60,8 +77,15 @@ export function schemaTree(s, name = '') {
60
77
  /** `example` (a declared `shape`) over `tree`. */
61
78
  function merge(tree, example) {
62
79
  if (example === undefined) return tree
63
- if (Array.isArray(example)) return example.length > 1 || !tree?.$of ? example : { ...tree, $of: merge(tree.$of, example[0]) }
64
- if (example && typeof example === 'object') return Object.fromEntries([...Object.entries(tree ?? {}), ...Object.entries(example).map(([k, v]) => [k, merge(tree?.[k], v)])])
80
+ if (Array.isArray(example))
81
+ return example.length > 1 || !tree?.$of
82
+ ? example
83
+ : { ...tree, $of: merge(tree.$of, example[0]) }
84
+ if (example && typeof example === 'object')
85
+ return Object.fromEntries([
86
+ ...Object.entries(tree ?? {}),
87
+ ...Object.entries(example).map(([k, v]) => [k, merge(tree?.[k], v)]),
88
+ ])
65
89
  return example
66
90
  }
67
91
 
@@ -0,0 +1,50 @@
1
+ import { NONE } from '@weft-js/core/interaction'
2
+ import { tab } from '../link'
3
+ import { clients, lines, shown, siblings, tabs, type Client, type Triple } from './state'
4
+
5
+ export function render(key: string) {
6
+ const heard = lines.get(key)
7
+ if (!heard) return
8
+ let hover = 0,
9
+ focus = 0,
10
+ pressed = 0
11
+ const add = (bits: number) => {
12
+ if (bits & 1) hover++
13
+ if (bits & 2) focus++
14
+ if (bits & 4) pressed++
15
+ }
16
+ for (const [t, keys] of tabs) if (t !== tab) add(keys.get(key) ?? 0)
17
+ for (const e of clients.get(key)?.values() ?? []) add(e.bits)
18
+ const was = shown.get(key) ?? NONE
19
+ if (was.hover === hover && was.focus === focus && was.pressed === pressed) return
20
+ const now = hover || focus || pressed ? { hover, focus, pressed } : NONE
21
+ shown.set(key, now)
22
+ heard(now)
23
+ }
24
+ export const renderAll = () => {
25
+ for (const k of lines.keys()) render(k)
26
+ }
27
+
28
+ export function hear(s: Triple[], leading: boolean) {
29
+ const at = Date.now(),
30
+ touched = new Set<string>()
31
+ for (const [id, key, bits] of s) {
32
+ if (siblings.has(id) || (!leading && !lines.has(key))) continue
33
+ let held = clients.get(key)
34
+ if (bits) {
35
+ if (!held) clients.set(key, (held = new Map()))
36
+ held.set(id, { bits, at })
37
+ } else if (!held?.delete(id)) continue
38
+ touched.add(key)
39
+ }
40
+ for (const k of touched) render(k)
41
+ }
42
+
43
+ export function dropClients(which: (e: Client, id: string) => boolean) {
44
+ for (const [key, held] of clients) {
45
+ let changed = false
46
+ for (const [id, e] of held) if (which(e, id)) changed = held.delete(id)
47
+ if (!held.size) clients.delete(key)
48
+ if (changed) render(key)
49
+ }
50
+ }
@@ -0,0 +1,119 @@
1
+ import type { connectBuiltIn } from '../channel'
2
+ import type { shareStats as ShareStats } from '../share'
3
+ import { mine, tabs, type Pair } from './state'
4
+
5
+ type Conn = ReturnType<typeof connectBuiltIn>
6
+
7
+ function changedPairs(o: {
8
+ dirtyNet: Set<string>
9
+ sent: Map<string, number>
10
+ refused: Set<string>
11
+ upstream: (key: string) => number
12
+ BATCH: number
13
+ }) {
14
+ const { dirtyNet, sent, refused, upstream, BATCH } = o
15
+ const e: Pair[] = []
16
+ for (const key of dirtyNet) {
17
+ if (e.length === BATCH) break
18
+ dirtyNet.delete(key)
19
+ if (!sent.has(key) || refused.has(key)) continue
20
+ const bits = upstream(key)
21
+ // Off and on again inside the gap: nothing to say. The latest value is all that's kept.
22
+ if (bits !== sent.get(key)) {
23
+ sent.set(key, bits)
24
+ e.push([key, bits])
25
+ }
26
+ }
27
+ return e
28
+ }
29
+
30
+ export function network({
31
+ connection,
32
+ isOpen,
33
+ sent,
34
+ refused,
35
+ dirtyNet,
36
+ toAdd,
37
+ toDrop,
38
+ upstream,
39
+ shareStats,
40
+ GAP_NET,
41
+ BATCH,
42
+ }: {
43
+ connection: () => Conn | undefined
44
+ isOpen: () => boolean
45
+ sent: Map<string, number>
46
+ refused: Set<string>
47
+ dirtyNet: Set<string>
48
+ toAdd: Set<string>
49
+ toDrop: Set<string>
50
+ upstream: (key: string) => number
51
+ shareStats: typeof ShareStats
52
+ GAP_NET: number
53
+ BATCH: number
54
+ }) {
55
+ let netLast = 0
56
+ let netTimer: ReturnType<typeof setTimeout> | undefined
57
+ /** One frame per GAP_NET, `u` first: the bucket is 20 a second, and a frame carries every key that's waiting. */
58
+ function netTick() {
59
+ netTimer = undefined
60
+ const conn = connection()
61
+ if (!conn || !isOpen()) return
62
+ let name: 'u' | 'set' | undefined,
63
+ msg: unknown,
64
+ size = 0
65
+ if (toAdd.size || toDrop.size) {
66
+ const add = [...toAdd].slice(0, BATCH),
67
+ drop = [...toDrop].slice(0, BATCH - add.length)
68
+ for (const k of add) toAdd.delete(k)
69
+ for (const k of drop) toDrop.delete(k)
70
+ name = 'u'
71
+ msg = { add, drop }
72
+ size = add.length + drop.length
73
+ } else {
74
+ const e = changedPairs({ dirtyNet, sent, refused, upstream, BATCH })
75
+ if (e.length) {
76
+ name = 'set'
77
+ msg = { e }
78
+ size = e.length
79
+ }
80
+ }
81
+ if (name) {
82
+ netLast = performance.now()
83
+ shareStats.frames++
84
+ shareStats.keysSent += size
85
+ conn.send(name as never, msg as never).catch(() => {}) // a dropped link re-says everything
86
+ }
87
+ if (toAdd.size || toDrop.size || dirtyNet.size) netSoon()
88
+ }
89
+ /** Not a timer when the gap has passed: the leader is often a hidden tab, where a timer waits a second or more. */
90
+ function netSoon() {
91
+ const wait = GAP_NET - (performance.now() - netLast)
92
+ if (wait > 0) {
93
+ netTimer ??= setTimeout(netTick, wait)
94
+ return
95
+ }
96
+ clearTimeout(netTimer)
97
+ netTick()
98
+ }
99
+
100
+ const clear = () => {
101
+ clearTimeout(netTimer)
102
+ netTimer = undefined
103
+ }
104
+ return { netSoon, clear }
105
+ }
106
+
107
+ export const upstream = (key: string, locks: boolean) => {
108
+ if (!locks) return mine.get(key) ?? 0
109
+ let bits = 0
110
+ for (const keys of tabs.values()) bits |= keys.get(key) ?? 0
111
+ return bits
112
+ }
113
+
114
+ export function wanted(locks: boolean) {
115
+ if (!locks) return new Set(mine.keys())
116
+ const out = new Set<string>()
117
+ for (const keys of tabs.values()) for (const k of keys.keys()) out.add(k)
118
+ return out
119
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ clients,
3
+ siblings,
4
+ tabAt,
5
+ tabs,
6
+ visible,
7
+ type Client,
8
+ type Msg,
9
+ type Triple,
10
+ } from './state'
11
+ import { hear, render, renderAll } from './display'
12
+
13
+ export function receiver({
14
+ dropClients,
15
+ watch,
16
+ reconcile,
17
+ wakeIfLeading,
18
+ leading,
19
+ post,
20
+ announce,
21
+ dropTab,
22
+ link,
23
+ }: {
24
+ dropClients: (test: (e: Client, id: string) => boolean) => void
25
+ watch: (tab: string) => void
26
+ reconcile: () => void
27
+ wakeIfLeading: () => void
28
+ leading: () => boolean
29
+ post: (msg: Msg) => void
30
+ announce: () => void
31
+ dropTab: (tab: string) => void
32
+ link: (up: boolean) => void
33
+ }) {
34
+ return (m: Msg) => {
35
+ if (m.t === 'tab') {
36
+ tabs.set(m.tab, new Map(m.keys))
37
+ tabAt.set(m.tab, Date.now())
38
+ visible.set(m.tab, m.shown)
39
+ if (m.k && !siblings.has(m.k)) {
40
+ siblings.add(m.k)
41
+ dropClients((_, id) => id === m.k)
42
+ }
43
+ watch(m.tab)
44
+ renderAll()
45
+ reconcile()
46
+ if (m.shown) wakeIfLeading()
47
+ // A tab that arrived late has what the socket already heard.
48
+ if (leading()) {
49
+ const s: Triple[] = []
50
+ for (const [key] of m.keys)
51
+ for (const [id, e] of clients.get(key) ?? []) s.push([id, key, e.bits])
52
+ if (s.length) post({ t: 'net', s })
53
+ }
54
+ } else if (m.t === 'bits') {
55
+ const keys = tabs.get(m.tab) ?? new Map<string, number>()
56
+ tabs.set(m.tab, keys)
57
+ tabAt.set(m.tab, Date.now())
58
+ for (const [key, bits] of m.e) keys.set(key, bits)
59
+ watch(m.tab)
60
+ for (const [key] of m.e) render(key)
61
+ reconcile()
62
+ } else if (m.t === 'shown') {
63
+ visible.set(m.tab, m.shown)
64
+ if (m.shown) wakeIfLeading()
65
+ } else if (m.t === 'roll') announce()
66
+ else if (m.t === 'bye') dropTab(m.tab)
67
+ else if (m.t === 'net') hear(m.s, leading())
68
+ else if (m.t === 'link') link(m.up)
69
+ }
70
+ }
@@ -0,0 +1,30 @@
1
+ import type { Counts } from '@weft-js/core/interaction'
2
+
3
+ export type Pair = [key: string, bits: number]
4
+ export type Triple = [id: string, key: string, bits: number]
5
+ export type Msg =
6
+ | { t: 'tab'; tab: string; keys: Pair[]; shown: boolean; k?: string | undefined }
7
+ | { t: 'bits'; tab: string; e: Pair[] }
8
+ | { t: 'shown'; tab: string; shown: boolean }
9
+ | { t: 'net'; s: Triple[] }
10
+ | { t: 'link'; up: boolean }
11
+ | { t: 'roll' }
12
+ | { t: 'bye'; tab: string }
13
+
14
+ export interface Client {
15
+ bits: number
16
+ at: number
17
+ stale?: boolean
18
+ }
19
+
20
+ export const lines = new Map<string, (c: Counts) => void>()
21
+ export const shown = new Map<string, Counts>()
22
+ /** key → bits, per tab, mine included. */
23
+ export const tabs = new Map<string, Map<string, number>>()
24
+ export const tabAt = new Map<string, number>()
25
+ export const visible = new Map<string, boolean>()
26
+ export const mine = new Map<string, number>()
27
+ export const watched = new Set<string>()
28
+ export const siblings = new Set<string>()
29
+ /** key → other clients. Everything the leader joined; a follower keeps what its own lines want. */
30
+ export const clients = new Map<string, Map<string, Client>>()
@@ -0,0 +1,28 @@
1
+ import type { Msg } from './state'
2
+
3
+ export function tabQueue(
4
+ dirtyTab: Set<string>,
5
+ mine: Map<string, number>,
6
+ post: (msg: Msg) => void,
7
+ tab: string,
8
+ GAP_TAB: number,
9
+ ) {
10
+ let tabLast = 0
11
+ let tabTimer: ReturnType<typeof setTimeout> | undefined
12
+ function flushTab() {
13
+ tabTimer = undefined
14
+ tabLast = performance.now()
15
+ if (dirtyTab.size)
16
+ post({ t: 'bits', tab, e: [...dirtyTab].map((key) => [key, mine.get(key) ?? 0]) })
17
+ dirtyTab.clear()
18
+ }
19
+ function queueTab(key: string) {
20
+ dirtyTab.add(key)
21
+ if (tabTimer !== undefined) return
22
+ const wait = GAP_TAB - (performance.now() - tabLast)
23
+ if (wait <= 0) flushTab()
24
+ else tabTimer = setTimeout(flushTab, wait)
25
+ }
26
+
27
+ return queueTab
28
+ }
@@ -23,25 +23,55 @@ const SHARDS = 64
23
23
  /** One key at a time, or `{ keys }` to answer a whole join at once: a rule that reads a database runs once for it. */
24
24
  export type ShareRule =
25
25
  | ((args: { ctx: Context; key: string }) => boolean | Promise<boolean>)
26
- | { keys: (args: { ctx: Context; keys: string[] }) => Iterable<string> | Promise<Iterable<string>> }
26
+ | {
27
+ keys: (args: { ctx: Context; keys: string[] }) => Iterable<string> | Promise<Iterable<string>>
28
+ }
27
29
 
28
30
  type Issue = { issues: { message: string }[] }
29
31
  type Sent = { add?: unknown; drop?: unknown; e?: unknown } | null | undefined
30
32
  const isIssue = (r: unknown): r is Issue => !!(r as Partial<Issue> | null | undefined)?.issues
31
- const schema = <T,>(validate: (v: Sent) => T | Issue): StandardSchemaV1<T, T> => ({ '~standard': { version: 1, vendor: 'weft', validate: (v: unknown) => { const r = validate(v as Sent); return isIssue(r) ? r : { value: r } } } })
33
+ const schema = <T>(validate: (v: Sent) => T | Issue): StandardSchemaV1<T, T> => ({
34
+ '~standard': {
35
+ version: 1,
36
+ vendor: 'weft',
37
+ validate: (v: unknown) => {
38
+ const r = validate(v as Sent)
39
+ return isIssue(r) ? r : { value: r }
40
+ },
41
+ },
42
+ })
32
43
 
33
44
  /** What a handler of the share channel is given. */
34
- interface Said<M = unknown> { msg: M; id: string; ctx: Context; reply: Client['reply'] }
45
+ interface Said<M = unknown> {
46
+ msg: M
47
+ id: string
48
+ ctx: Context
49
+ reply: Client['reply']
50
+ }
35
51
  const no = (message: string): Issue => ({ issues: [{ message }] })
36
- const isKey = (k: unknown): k is string => typeof k === 'string' && k.length > 0 && k.length <= MAX_KEY
52
+ const isKey = (k: unknown): k is string =>
53
+ typeof k === 'string' && k.length > 0 && k.length <= MAX_KEY
37
54
  const isBits = (b: unknown) => Number.isInteger(b) && (b as number) >= 0 && (b as number) <= 7
38
- const anything = schema(v => v)
55
+ const anything = schema((v) => v)
39
56
 
40
57
  /** Who has `k` in `s`: `[id, key, bits]`. */
41
58
  type Triple = [id: string, key: string, bits: number]
42
- interface Client { id: string; keys: Map<string, number>; reply(name: string, msg: unknown): void }
43
- interface Shard { name: string; refs: number; ch: ReturnType<Hub['channel']>; on: (ev: Ev | null) => void }
44
- interface Sharing { clients: Map<string, Client>; holders: Map<string, Set<Client>>; shards: Map<number, Shard> }
59
+ interface Client {
60
+ id: string
61
+ keys: Map<string, number>
62
+ reply(name: string, msg: unknown): void
63
+ }
64
+ interface Shard {
65
+ name: string
66
+ refs: number
67
+ ch: ReturnType<Hub['channel']>
68
+ on: (ev: Ev | null) => void
69
+ }
70
+ interface Sharing {
71
+ clients: Map<string, Client>
72
+ holders: Map<string, Set<Client>>
73
+ shards: Map<number, Shard>
74
+ }
45
75
 
46
76
  const states = new WeakMap<Hub, Sharing>()
47
77
  const stateOf = (h: Hub) => {
@@ -57,7 +87,7 @@ function shardOf(key: string) {
57
87
  return (x >>> 0) % SHARDS
58
88
  }
59
89
 
60
- const byShard = <T,>(items: T[], key: (item: T) => string) => {
90
+ const byShard = <T>(items: T[], key: (item: T) => string) => {
61
91
  const out = new Map<number, T[]>()
62
92
  for (const item of items) {
63
93
  const s = shardOf(key(item))
@@ -68,29 +98,39 @@ const byShard = <T,>(items: T[], key: (item: T) => string) => {
68
98
  return out
69
99
  }
70
100
 
71
- const publish = (h: Hub, shard: number, body: object) => h.bus.publish(`weft:s:${shard}`, `{"v":1,${JSON.stringify(body).slice(1)}`)
101
+ const publish = (h: Hub, shard: number, body: object) =>
102
+ h.bus.publish(`weft:s:${shard}`, `{"v":1,${JSON.stringify(body).slice(1)}`)
72
103
 
73
104
  function heard(h: Hub, st: Sharing, shard: number, ev: Ev | null) {
74
105
  if (!ev) return // the bus lost something: bits are re-said on a beat, and peers age out
75
- const m = (ev.p ??= JSON.parse(ev.raw)) as { f?: string; e?: [string, number][]; a?: string; k?: string[]; t?: string; s?: Triple[] }
76
- if (m.t) return (m.s && st.clients.get(m.t)?.reply('b', { s: m.s }))
106
+ const m = (ev.p ??= JSON.parse(ev.raw)) as {
107
+ f?: string
108
+ e?: [string, number][]
109
+ a?: string
110
+ k?: string[]
111
+ t?: string
112
+ s?: Triple[]
113
+ }
114
+ if (m.t) return m.s && st.clients.get(m.t)?.reply('b', { s: m.s })
77
115
  if (m.a) {
78
116
  const s: Triple[] = []
79
- for (const key of m.k ?? []) for (const c of st.holders.get(key) ?? []) {
80
- const bits = c.keys.get(key)
81
- if (bits && c.id !== m.a) s.push([c.id, key, bits])
82
- }
117
+ for (const key of m.k ?? [])
118
+ for (const c of st.holders.get(key) ?? []) {
119
+ const bits = c.keys.get(key)
120
+ if (bits && c.id !== m.a) s.push([c.id, key, bits])
121
+ }
83
122
  if (s.length) void publish(h, shard, { t: m.a, s })
84
123
  return
85
124
  }
86
125
  // One frame per client per event, whatever it names.
87
126
  const each = new Map<Client, Triple[]>()
88
- for (const [key, bits] of m.e ?? []) for (const c of st.holders.get(key) ?? []) {
89
- if (c.id === m.f) continue
90
- const list = each.get(c)
91
- if (list) list.push([m.f!, key, bits])
92
- else each.set(c, [[m.f!, key, bits]])
93
- }
127
+ for (const [key, bits] of m.e ?? [])
128
+ for (const c of st.holders.get(key) ?? []) {
129
+ if (c.id === m.f) continue
130
+ const list = each.get(c)
131
+ if (list) list.push([m.f!, key, bits])
132
+ else each.set(c, [[m.f!, key, bits]])
133
+ }
94
134
  for (const [c, s] of each) c.reply('b', { s })
95
135
  }
96
136
 
@@ -103,7 +143,9 @@ function hold(h: Hub, st: Sharing, c: Client, key: string) {
103
143
  const n = shardOf(key)
104
144
  let shard = st.shards.get(n)
105
145
  if (!shard) {
106
- const name = `weft:s:${n}`, ch = h.channel(name), on = (ev: Ev | null) => heard(h, st, n, ev)
146
+ const name = `weft:s:${n}`,
147
+ ch = h.channel(name),
148
+ on = (ev: Ev | null) => heard(h, st, n, ev)
107
149
  ch.listeners.add(on)
108
150
  st.shards.set(n, (shard = { name, refs: 0, ch, on }))
109
151
  }
@@ -116,7 +158,8 @@ function unhold(h: Hub, st: Sharing, c: Client, key: string) {
116
158
  const set = st.holders.get(key)!
117
159
  set.delete(c)
118
160
  if (!set.size) st.holders.delete(key)
119
- const n = shardOf(key), shard = st.shards.get(n)!
161
+ const n = shardOf(key),
162
+ shard = st.shards.get(n)!
120
163
  if (--shard.refs) return
121
164
  st.shards.delete(n)
122
165
  h.unlisten(shard.ch, shard.on)
@@ -124,22 +167,57 @@ function unhold(h: Hub, st: Sharing, c: Client, key: string) {
124
167
 
125
168
  /** Say zero for what these keys held, then let go of them. */
126
169
  function release(h: Hub, st: Sharing, c: Client, keys: string[]) {
127
- const held = keys.filter(k => c.keys.get(k))
170
+ const held = keys.filter((k) => c.keys.get(k))
128
171
  for (const k of keys) unhold(h, st, c, k)
129
- return Promise.all([...byShard(held, k => k)].map(([n, list]) => publish(h, n, { f: c.id, e: list.map(k => [k, 0]) })))
172
+ return Promise.all(
173
+ [...byShard(held, (k) => k)].map(([n, list]) =>
174
+ publish(h, n, { f: c.id, e: list.map((k) => [k, 0]) }),
175
+ ),
176
+ )
130
177
  }
131
178
 
132
179
  async function allowedKeys(rule: ShareRule, ctx: Context, keys: string[]) {
133
180
  if (!keys.length) return keys
134
181
  if (typeof rule !== 'function') {
135
- const yes = await Promise.resolve().then(() => rule.keys({ ctx, keys })).catch(() => [])
182
+ const yes = await Promise.resolve()
183
+ .then(() => rule.keys({ ctx, keys }))
184
+ .catch(() => [])
136
185
  const passed = new Set(yes)
137
- return keys.filter(k => passed.has(k))
186
+ return keys.filter((k) => passed.has(k))
138
187
  }
139
- const answers = await Promise.all(keys.map(key => Promise.resolve().then(() => rule({ ctx, key })).catch(() => false)))
188
+ const answers = await Promise.all(
189
+ keys.map((key) =>
190
+ Promise.resolve()
191
+ .then(() => rule({ ctx, key }))
192
+ .catch(() => false),
193
+ ),
194
+ )
140
195
  return keys.filter((_, i) => answers[i])
141
196
  }
142
197
 
198
+ function clientMessages() {
199
+ return {
200
+ u: schema<{ add: string[]; drop: string[] }>((v) => {
201
+ const add = v?.add ?? [],
202
+ drop = v?.drop ?? []
203
+ return Array.isArray(add) &&
204
+ Array.isArray(drop) &&
205
+ add.length + drop.length <= MAX_BATCH &&
206
+ add.every(isKey) &&
207
+ drop.every(isKey)
208
+ ? { add, drop }
209
+ : no(`\`u\` takes up to ${MAX_BATCH} keys of 1 to ${MAX_KEY} characters`)
210
+ }),
211
+ set: schema<{ e: [string, number][] }>((v) =>
212
+ Array.isArray(v?.e) &&
213
+ v.e.length <= MAX_BATCH &&
214
+ v.e.every((x) => Array.isArray(x) && isKey(x[0]) && isBits(x[1]))
215
+ ? { e: v.e }
216
+ : no('`set` takes `[key, bits]` pairs, bits 0 to 7'),
217
+ ),
218
+ }
219
+ }
220
+
143
221
  /** The `channel` behind `share`, made per hub. socket.ts takes it for `$share`, so every limit a channel has applies. */
144
222
  export function shareChannel(h: Hub): Fn {
145
223
  const rule = h.share
@@ -150,40 +228,47 @@ export function shareChannel(h: Hub): Fn {
150
228
  return c
151
229
  }
152
230
  return {
153
- kind: 'channel', $id: SHARE,
231
+ kind: 'channel',
232
+ $id: SHARE,
154
233
  def: {
155
234
  authorize() {
156
235
  if (rule) return true
157
- throw new StartError('R011', '`interaction({ share })` crossed to the socket, and nothing allows it', 'add `share: ({ ctx, key }) => …` to `realtime()`; `() => true` must be written out')
158
- },
159
- client: {
160
- u: schema<{ add: string[]; drop: string[] }>(v => {
161
- const add = v?.add ?? [], drop = v?.drop ?? []
162
- return Array.isArray(add) && Array.isArray(drop) && add.length + drop.length <= MAX_BATCH && add.every(isKey) && drop.every(isKey) ? { add, drop } : no(`\`u\` takes up to ${MAX_BATCH} keys of 1 to ${MAX_KEY} characters`)
163
- }),
164
- set: schema<{ e: [string, number][] }>(v => (Array.isArray(v?.e) && v.e.length <= MAX_BATCH && v.e.every(x => Array.isArray(x) && isKey(x[0]) && isBits(x[1])) ? { e: v.e } : no('`set` takes `[key, bits]` pairs, bits 0 to 7'))),
236
+ throw new StartError(
237
+ 'R011',
238
+ '`interaction({ share })` crossed to the socket, and nothing allows it',
239
+ 'add `share: ({ ctx, key }) => …` to `realtime()`; `() => true` must be written out',
240
+ )
165
241
  },
242
+ client: clientMessages(),
166
243
  server: { ok: anything, b: anything },
167
244
  on: {
168
245
  async u({ msg, id, ctx, reply }: Said<{ add: string[]; drop: string[] }>) {
169
246
  const c = client(id, reply)
170
247
  await release(h, st, c, msg.drop)
171
248
  const room = Math.max(0, h.limits.shareKeys - c.keys.size)
172
- const fresh = msg.add.filter(k => !c.keys.has(k))
249
+ const fresh = msg.add.filter((k) => !c.keys.has(k))
173
250
  const asked = fresh.slice(0, room)
174
251
  const ok = await allowedKeys(rule!, ctx, asked)
175
252
  if (!st.clients.has(id)) return // it left while the rule ran
176
253
  const passed = new Set(ok)
177
254
  for (const k of ok) hold(h, st, c, k)
178
- await Promise.all(ok.length ? [...byShard<string>(ok, k => k)].map(([n, k]) => st.shards.get(n)!.ch.ready.then(() => publish(h, n, { a: id, k }))) : [])
179
- reply('ok', { id, no: fresh.filter(k => !passed.has(k)) })
255
+ await Promise.all(
256
+ ok.length
257
+ ? [...byShard<string>(ok, (k) => k)].map(([n, k]) =>
258
+ st.shards.get(n)!.ch.ready.then(() => publish(h, n, { a: id, k })),
259
+ )
260
+ : [],
261
+ )
262
+ reply('ok', { id, no: fresh.filter((k) => !passed.has(k)) })
180
263
  },
181
264
  set({ msg, id }: Said<{ e: [string, number][] }>) {
182
265
  const c = st.clients.get(id)
183
266
  if (!c) return
184
267
  const known = msg.e.filter(([k]) => c.keys.has(k))
185
268
  for (const [k, bits] of known) c.keys.set(k, bits)
186
- return Promise.all([...byShard(known, e => e[0])].map(([n, e]) => publish(h, n, { f: id, e })))
269
+ return Promise.all(
270
+ [...byShard(known, (e) => e[0])].map(([n, e]) => publish(h, n, { f: id, e })),
271
+ )
187
272
  },
188
273
  },
189
274
  left({ id }: Pick<Said, 'id'>) {
@@ -198,7 +283,7 @@ export function shareChannel(h: Hub): Fn {
198
283
  if (!c || !rule) return
199
284
  const keys = [...c.keys.keys()]
200
285
  const passed = new Set(await allowedKeys(rule, ctx, keys))
201
- const gone = keys.filter(k => !passed.has(k))
286
+ const gone = keys.filter((k) => !passed.has(k))
202
287
  if (!gone.length) return
203
288
  await release(h, st, c, gone)
204
289
  reply('ok', { id, no: gone })