@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.
- package/README.md +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +51 -10
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +97 -168
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +113 -35
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -174
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -964
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
package/src/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 = {
|
|
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 {
|
|
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 ?? {}))
|
|
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))
|
|
64
|
-
|
|
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
|
+
}
|
package/src/share-server.ts
CHANGED
|
@@ -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
|
-
| {
|
|
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
|
|
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> {
|
|
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 =>
|
|
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 {
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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) =>
|
|
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 {
|
|
76
|
-
|
|
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 ?? [])
|
|
80
|
-
const
|
|
81
|
-
|
|
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 ?? [])
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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}`,
|
|
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),
|
|
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(
|
|
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()
|
|
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(
|
|
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',
|
|
231
|
+
kind: 'channel',
|
|
232
|
+
$id: SHARE,
|
|
154
233
|
def: {
|
|
155
234
|
authorize() {
|
|
156
235
|
if (rule) return true
|
|
157
|
-
throw new StartError(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
179
|
-
|
|
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(
|
|
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 })
|