@elefunc/send 0.1.3 → 0.1.4
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 +3 -3
- package/package.json +2 -6
- package/runtime/install.ts +22 -0
- package/runtime/rezi-files.ts +90 -0
- package/runtime/rezi-input-caret.ts +61 -0
- package/src/core/session.ts +51 -69
- package/src/index.ts +27 -4
- package/src/tui/app.ts +1 -1
- package/patches/@rezi-ui%2Fcore@0.1.0-alpha.60.patch +0 -227
- package/patches/werift@0.22.9.patch +0 -31
- package/src/tui/rezi-input-caret.ts +0 -46
- /package/{src/tui → runtime}/rezi-checkbox-click.ts +0 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ send
|
|
|
19
19
|
send peers
|
|
20
20
|
send offer ./file.txt
|
|
21
21
|
send accept
|
|
22
|
-
send tui
|
|
22
|
+
send tui
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
When no subcommand is provided, `send` launches the TUI by default.
|
|
@@ -28,7 +28,7 @@ When no subcommand is provided, `send` launches the TUI by default.
|
|
|
28
28
|
|
|
29
29
|
`--room` is optional on all commands. If you omit it, `send` creates a random room and prints or shows it.
|
|
30
30
|
|
|
31
|
-
In the TUI, the room row includes a `📋` invite link that opens the equivalent web app URL for the current committed room and toggle state.
|
|
31
|
+
In the TUI, the room row includes a `📋` invite link that opens the equivalent web app URL for the current committed room and toggle state.
|
|
32
32
|
|
|
33
33
|
## Self Identity
|
|
34
34
|
|
|
@@ -59,4 +59,4 @@ bun run typecheck
|
|
|
59
59
|
bun test
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The package is Bun-native and keeps its runtime patches in `
|
|
62
|
+
The package is Bun-native and keeps its runtime patches in `runtime/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elefunc/send",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
|
-
"
|
|
16
|
+
"runtime",
|
|
17
17
|
"README.md",
|
|
18
18
|
"LICENSE",
|
|
19
19
|
"package.json"
|
|
@@ -41,10 +41,6 @@
|
|
|
41
41
|
"cac": "^7.0.0",
|
|
42
42
|
"werift": "^0.22.9"
|
|
43
43
|
},
|
|
44
|
-
"patchedDependencies": {
|
|
45
|
-
"@rezi-ui/core@0.1.0-alpha.60": "patches/@rezi-ui%2Fcore@0.1.0-alpha.60.patch",
|
|
46
|
-
"werift@0.22.9": "patches/werift@0.22.9.patch"
|
|
47
|
-
},
|
|
48
44
|
"devDependencies": {
|
|
49
45
|
"@types/node": "^24.5.2",
|
|
50
46
|
"typescript": "^5.8.3"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ensureReziFilePatches } from "./rezi-files"
|
|
2
|
+
import { ensureReziInputCaretPatch } from "./rezi-input-caret"
|
|
3
|
+
|
|
4
|
+
let sessionInstallPromise: Promise<void> | null = null
|
|
5
|
+
let tuiInstallPromise: Promise<void> | null = null
|
|
6
|
+
|
|
7
|
+
export const ensureSessionRuntimePatches = () => {
|
|
8
|
+
if (sessionInstallPromise) return sessionInstallPromise
|
|
9
|
+
sessionInstallPromise = Promise.resolve()
|
|
10
|
+
return sessionInstallPromise
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ensureTuiRuntimePatches = () => {
|
|
14
|
+
if (tuiInstallPromise) return tuiInstallPromise
|
|
15
|
+
tuiInstallPromise = (async () => {
|
|
16
|
+
await ensureReziFilePatches()
|
|
17
|
+
await ensureReziInputCaretPatch()
|
|
18
|
+
})()
|
|
19
|
+
return tuiInstallPromise
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ensureRuntimeFilePatches = ensureTuiRuntimePatches
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
|
|
3
|
+
type Replacement = readonly [before: string, after: string]
|
|
4
|
+
type FilePatch = { relativeUrl: string; replacements: readonly Replacement[] }
|
|
5
|
+
|
|
6
|
+
const REZI_FILE_PATCHES: readonly FilePatch[] = [
|
|
7
|
+
{
|
|
8
|
+
relativeUrl: "./layout/kinds/box.js",
|
|
9
|
+
replacements: [
|
|
10
|
+
[
|
|
11
|
+
'import { validateBoxProps } from "../validateProps.js";',
|
|
12
|
+
'import { validateBoxProps } from "../validateProps.js";\nconst OVERFLOW_CONTENT_LIMIT = 2147483647;',
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
'const ch = clampNonNegative(outerHLimit - bt - bb - spacing.top - spacing.bottom);\n // Children are laid out as a Column inside the content rect.',
|
|
16
|
+
'const ch = clampNonNegative(outerHLimit - bt - bb - spacing.top - spacing.bottom);\n const flowMeasureH = propsRes.value.overflow === "scroll" ? OVERFLOW_CONTENT_LIMIT : ch;\n // Children are laid out as a Column inside the content rect.',
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
'const innerRes = measureNode(columnNode, cw, ch, "column");',
|
|
20
|
+
'const innerRes = measureNode(columnNode, cw, flowMeasureH, "column");',
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
'const columnNode = getSyntheticColumn(vnode, propsRes.value.gap);\n // The synthetic column wrapper must fill the box content rect so that\n // percentage constraints resolve against the actual available space.\n const innerRes = layoutNode(columnNode, cx, cy, cw, ch, "column", cw, ch);',
|
|
24
|
+
'const columnNode = getSyntheticColumn(vnode, propsRes.value.gap);\n const flowMeasureH = propsRes.value.overflow === "scroll" ? OVERFLOW_CONTENT_LIMIT : ch;\n const flowMeasureRes = measureNode(columnNode, cw, flowMeasureH, "column");\n if (!flowMeasureRes.ok)\n return flowMeasureRes;\n const flowLayoutH = propsRes.value.overflow === "scroll"\n ? Math.max(ch, flowMeasureRes.value.h)\n : ch;\n // The synthetic column wrapper must fill the box content rect so that\n // percentage constraints resolve against the actual available space.\n const innerRes = layoutNode(columnNode, cx, cy, cw, flowLayoutH, "column", cw, flowLayoutH, flowMeasureRes.value);',
|
|
25
|
+
],
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
relativeUrl: "./app/widgetRenderer.js",
|
|
30
|
+
replacements: [
|
|
31
|
+
[
|
|
32
|
+
" scrollOverrides = new Map();",
|
|
33
|
+
" scrollOverrides = new Map();\n hasPendingScrollOverride = false;",
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
" scrollOverrides: this.scrollOverrides,\n findScrollableAncestors: (targetId) => this.findScrollableAncestors(targetId),",
|
|
37
|
+
' scrollOverrides: this.scrollOverrides,\n markScrollOverrideDirty: () => {\n this.hasPendingScrollOverride = true;\n },\n findScrollableAncestors: (targetId) => this.findScrollableAncestors(targetId),',
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
" return Object.freeze([]);\n }\n applyScrollOverridesToVNode(vnode, overrides = this",
|
|
41
|
+
' return Object.freeze([]);\n }\n syncScrollOverridesFromLayoutTree() {\n if (!this.committedRoot || !this.layoutTree) {\n this.scrollOverrides.clear();\n return;\n }\n const nextOverrides = new Map();\n const stack = [\n {\n runtimeNode: this.committedRoot,\n layoutNode: this.layoutTree,\n },\n ];\n while (stack.length > 0) {\n const frame = stack.pop();\n if (!frame)\n continue;\n const runtimeNode = frame.runtimeNode;\n const layoutNode = frame.layoutNode;\n const props = runtimeNode.vnode.props;\n const nodeId = typeof props.id === "string" && props.id.length > 0 ? props.id : null;\n if (nodeId !== null && props.overflow === "scroll" && layoutNode.meta) {\n const meta = layoutNode.meta;\n const hasScrollableAxis = meta.contentWidth > meta.viewportWidth || meta.contentHeight > meta.viewportHeight;\n if (hasScrollableAxis) {\n nextOverrides.set(nodeId, Object.freeze({\n scrollX: meta.scrollX,\n scrollY: meta.scrollY,\n }));\n }\n }\n const childCount = Math.min(runtimeNode.children.length, layoutNode.children.length);\n for (let i = childCount - 1; i >= 0; i--) {\n const runtimeChild = runtimeNode.children[i];\n const layoutChild = layoutNode.children[i];\n if (!runtimeChild || !layoutChild)\n continue;\n stack.push({\n runtimeNode: runtimeChild,\n layoutNode: layoutChild,\n });\n }\n }\n this.scrollOverrides.clear();\n for (const [nodeId, override] of nextOverrides) {\n this.scrollOverrides.set(nodeId, override);\n }\n }\n applyScrollOverridesToVNode(vnode, overrides = this',
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
" if (override) {",
|
|
45
|
+
' if (override && propsForRead.overflow === "scroll") {',
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
" if (this.scrollOverrides.size > 0)",
|
|
49
|
+
" if (this.hasPendingScrollOverride)",
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
" this.scrollOverrides.clear();",
|
|
53
|
+
" this.hasPendingScrollOverride = false;",
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
" this.layoutTree = nextLayoutTree;",
|
|
57
|
+
" this.layoutTree = nextLayoutTree;\n this.syncScrollOverridesFromLayoutTree();",
|
|
58
|
+
],
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
relativeUrl: "./app/widgetRenderer/mouseRouting.js",
|
|
63
|
+
replacements: [[
|
|
64
|
+
" ctx.scrollOverrides.set(nodeId, {\n scrollX: r.nextScrollX ?? meta.scrollX,\n scrollY: r.nextScrollY ?? meta.scrollY,\n });\n return ROUTE_RENDER;",
|
|
65
|
+
' ctx.scrollOverrides.set(nodeId, {\n scrollX: r.nextScrollX ?? meta.scrollX,\n scrollY: r.nextScrollY ?? meta.scrollY,\n });\n ctx.markScrollOverrideDirty?.();\n return ROUTE_RENDER;',
|
|
66
|
+
]],
|
|
67
|
+
},
|
|
68
|
+
] as const
|
|
69
|
+
|
|
70
|
+
let patchedRoots: Set<string> | null = null
|
|
71
|
+
|
|
72
|
+
const applyFilePatch = async (baseUrl: string, patch: FilePatch) => {
|
|
73
|
+
const fileUrl = new URL(patch.relativeUrl, baseUrl)
|
|
74
|
+
const source = await readFile(fileUrl, "utf8")
|
|
75
|
+
let next = source
|
|
76
|
+
for (const [before, after] of patch.replacements) {
|
|
77
|
+
if (next.includes(after)) continue
|
|
78
|
+
if (!next.includes(before)) throw new Error(`Unsupported @rezi-ui/core runtime patch target at ${fileUrl.href}`)
|
|
79
|
+
next = next.replace(before, after)
|
|
80
|
+
}
|
|
81
|
+
if (next !== source) await writeFile(fileUrl, next)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const ensureReziFilePatches = async () => {
|
|
85
|
+
const baseUrl = await import.meta.resolve("@rezi-ui/core")
|
|
86
|
+
patchedRoots ??= new Set<string>()
|
|
87
|
+
if (patchedRoots.has(baseUrl)) return
|
|
88
|
+
for (const patch of REZI_FILE_PATCHES) await applyFilePatch(baseUrl, patch)
|
|
89
|
+
patchedRoots.add(baseUrl)
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
|
|
3
|
+
type Replacement = readonly [before: string, after: string]
|
|
4
|
+
type FilePatch = { relativeUrl: string; replacements: readonly Replacement[] }
|
|
5
|
+
|
|
6
|
+
const CARET_SAMPLE = "user"
|
|
7
|
+
const CARET_EXPECTED_WIDTH = CARET_SAMPLE.length + 3
|
|
8
|
+
const CARET_RULE_TEXT = "single-line ui.input width must equal value.length + 3"
|
|
9
|
+
|
|
10
|
+
const REZI_INPUT_CARET_PATCHES: readonly FilePatch[] = [
|
|
11
|
+
{
|
|
12
|
+
relativeUrl: "./layout/engine/intrinsic.js",
|
|
13
|
+
replacements: [[
|
|
14
|
+
"return ok(clampSize({ w: textW + 2, h: 1 }));",
|
|
15
|
+
"return ok(clampSize({ w: textW + 3, h: 1 }));",
|
|
16
|
+
]],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
relativeUrl: "./layout/kinds/leaf.js",
|
|
20
|
+
replacements: [[
|
|
21
|
+
"const w = Math.min(maxW, textW + 2);",
|
|
22
|
+
"const w = Math.min(maxW, textW + 3);",
|
|
23
|
+
]],
|
|
24
|
+
},
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
let verifiedRoots: Set<string> | null = null
|
|
28
|
+
|
|
29
|
+
const applyFilePatch = async (baseUrl: string, patch: FilePatch) => {
|
|
30
|
+
const fileUrl = new URL(patch.relativeUrl, baseUrl)
|
|
31
|
+
const source = await readFile(fileUrl, "utf8")
|
|
32
|
+
let next = source
|
|
33
|
+
for (const [before, after] of patch.replacements) {
|
|
34
|
+
if (next.includes(after)) continue
|
|
35
|
+
if (!next.includes(before)) throw new Error(`Unsupported @rezi-ui/core caret patch target at ${fileUrl.href}`)
|
|
36
|
+
next = next.replace(before, after)
|
|
37
|
+
}
|
|
38
|
+
if (next !== source) await writeFile(fileUrl, next)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const verifyInputCaretWidth = async () => {
|
|
42
|
+
const { createTestRenderer, ui } = await import("@rezi-ui/core")
|
|
43
|
+
const renderer = createTestRenderer({ viewport: { cols: 40, rows: 8 } })
|
|
44
|
+
const view = renderer.render(ui.input({ id: "caret-probe", value: CARET_SAMPLE, onInput: () => {} }))
|
|
45
|
+
const field = view.findById("caret-probe")
|
|
46
|
+
if (!field) throw new Error("Rezi caret probe could not find the rendered input node")
|
|
47
|
+
return field.rect.w
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ensureReziInputCaretPatch = async () => {
|
|
51
|
+
const baseUrl = await import.meta.resolve("@rezi-ui/core")
|
|
52
|
+
verifiedRoots ??= new Set<string>()
|
|
53
|
+
if (verifiedRoots.has(baseUrl)) return
|
|
54
|
+
for (const patch of REZI_INPUT_CARET_PATCHES) await applyFilePatch(baseUrl, patch)
|
|
55
|
+
const width = await verifyInputCaretWidth()
|
|
56
|
+
if (width !== CARET_EXPECTED_WIDTH) {
|
|
57
|
+
const installRoot = new URL("../", baseUrl).href
|
|
58
|
+
throw new Error(`Rezi input caret verification failed for ${installRoot}: ${CARET_RULE_TEXT}; expected ${CARET_EXPECTED_WIDTH} for "${CARET_SAMPLE}", got ${width}. This usually means @rezi-ui/core was imported before runtime patches were applied or the upstream Rezi input layout changed.`)
|
|
59
|
+
}
|
|
60
|
+
verifiedRoots.add(baseUrl)
|
|
61
|
+
}
|
package/src/core/session.ts
CHANGED
|
@@ -170,8 +170,6 @@ const STATS_POLL_MS = 1000
|
|
|
170
170
|
const PROFILE_URL = "https://ip.rt.ht/"
|
|
171
171
|
const PULSE_URL = "https://sig.efn.kr/pulse"
|
|
172
172
|
|
|
173
|
-
type StatsEntry = { id?: string; type?: string; [key: string]: unknown }
|
|
174
|
-
|
|
175
173
|
export interface PeerConnectivitySnapshot {
|
|
176
174
|
rttMs: number
|
|
177
175
|
localCandidateType: string
|
|
@@ -191,7 +189,6 @@ export interface PulseSnapshot {
|
|
|
191
189
|
|
|
192
190
|
const progressOf = (transfer: TransferState) => transfer.size ? Math.max(0, Math.min(100, transfer.bytes / transfer.size * 100)) : FINAL_STATUSES.has(transfer.status as never) ? 100 : 0
|
|
193
191
|
const isFinal = (transfer: TransferState) => FINAL_STATUSES.has(transfer.status as never)
|
|
194
|
-
const candidateTypeRank = (type: string) => ({ host: 0, srflx: 1, prflx: 1, relay: 2 }[type] ?? Number.NaN)
|
|
195
192
|
export const candidateTypeLabel = (type: string) => ({ host: "Direct", srflx: "NAT", prflx: "NAT", relay: "TURN" }[type] || "—")
|
|
196
193
|
const emptyConnectivitySnapshot = (): PeerConnectivitySnapshot => ({ rttMs: Number.NaN, localCandidateType: "", remoteCandidateType: "", pathLabel: "—" })
|
|
197
194
|
const emptyPulseSnapshot = (): PulseSnapshot => ({ state: "idle", at: 0, ms: 0, error: "" })
|
|
@@ -262,74 +259,54 @@ export const turnUsageState = (
|
|
|
262
259
|
|
|
263
260
|
const timeoutSignal = (ms: number) => typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(ms) : undefined
|
|
264
261
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return []
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const candidatePairEntries = (entries: StatsEntry[], transportId?: string) => entries.filter(entry =>
|
|
284
|
-
entry.type === "candidate-pair"
|
|
285
|
-
&& (!transportId || entry.transportId === transportId))
|
|
286
|
-
|
|
287
|
-
const preferredCandidatePair = (entries: StatsEntry[]) => {
|
|
288
|
-
let selectedFallback: StatsEntry | null = null
|
|
289
|
-
let succeededFallback: StatsEntry | null = null
|
|
290
|
-
for (const entry of entries) {
|
|
291
|
-
const selected = entry.selected === true || entry.nominated === true
|
|
292
|
-
const succeeded = entry.state === "succeeded"
|
|
293
|
-
if (selected && succeeded) return entry
|
|
294
|
-
if (!selectedFallback && selected) selectedFallback = entry
|
|
295
|
-
if (!succeededFallback && succeeded) succeededFallback = entry
|
|
296
|
-
}
|
|
297
|
-
return selectedFallback || succeededFallback
|
|
262
|
+
const normalizeCandidateType = (value: unknown) => typeof value === "string" ? value.toLowerCase() : ""
|
|
263
|
+
const validCandidateType = (value: string) => ["host", "srflx", "prflx", "relay"].includes(value)
|
|
264
|
+
|
|
265
|
+
export const activeIcePairFromPeerConnection = (pc: { iceTransports?: unknown[] } | null | undefined) => {
|
|
266
|
+
const transports = Array.isArray(pc?.iceTransports) ? pc.iceTransports as Array<Record<string, any>> : []
|
|
267
|
+
let fallback: { transport: Record<string, any>; connection: Record<string, any>; pair: Record<string, any> } | null = null
|
|
268
|
+
for (const transport of transports) {
|
|
269
|
+
const connection = transport?.connection
|
|
270
|
+
const pair = connection?.nominated
|
|
271
|
+
if (!pair) continue
|
|
272
|
+
fallback ||= { transport, connection, pair }
|
|
273
|
+
const state = `${transport?.state ?? ""}`.toLowerCase()
|
|
274
|
+
if (!state || state === "connected" || state === "completed") return { transport, connection, pair }
|
|
275
|
+
}
|
|
276
|
+
return fallback
|
|
298
277
|
}
|
|
299
278
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export const connectivitySnapshotFromReport = (report: unknown, previous: PeerConnectivitySnapshot = emptyConnectivitySnapshot()): PeerConnectivitySnapshot => {
|
|
311
|
-
const entries = statsEntriesFromReport(report)
|
|
312
|
-
const byId = new Map(entries.flatMap(entry => typeof entry.id === "string" && entry.id ? [[entry.id, entry] as const] : []))
|
|
313
|
-
const pair = selectedCandidatePair(entries, byId)
|
|
314
|
-
const rttMs = typeof pair?.currentRoundTripTime === "number" ? pair.currentRoundTripTime * 1000 : previous.rttMs
|
|
315
|
-
const localCandidate = typeof pair?.localCandidateId === "string" ? byId.get(pair.localCandidateId) : undefined
|
|
316
|
-
const remoteCandidate = typeof pair?.remoteCandidateId === "string" ? byId.get(pair.remoteCandidateId) : undefined
|
|
317
|
-
const localCandidateType = typeof localCandidate?.candidateType === "string" ? localCandidate.candidateType : ""
|
|
318
|
-
const remoteCandidateType = typeof remoteCandidate?.candidateType === "string" ? remoteCandidate.candidateType : ""
|
|
319
|
-
if (!pair || !Number.isFinite(candidateTypeRank(localCandidateType)) || !Number.isFinite(candidateTypeRank(remoteCandidateType))) {
|
|
320
|
-
return { ...previous, rttMs }
|
|
321
|
-
}
|
|
279
|
+
export const connectivitySnapshotFromPeerConnection = (
|
|
280
|
+
pc: { iceTransports?: unknown[] } | null | undefined,
|
|
281
|
+
previous: PeerConnectivitySnapshot = emptyConnectivitySnapshot(),
|
|
282
|
+
): PeerConnectivitySnapshot => {
|
|
283
|
+
const pair = activeIcePairFromPeerConnection(pc)?.pair
|
|
284
|
+
const localCandidateType = normalizeCandidateType(pair?.localCandidate?.type ?? pair?.localCandidate?.candidateType)
|
|
285
|
+
const remoteCandidateType = normalizeCandidateType(pair?.remoteCandidate?.type ?? pair?.remoteCandidate?.candidateType)
|
|
286
|
+
if (!validCandidateType(localCandidateType) || !validCandidateType(remoteCandidateType)) return previous
|
|
322
287
|
return {
|
|
323
288
|
...previous,
|
|
324
|
-
rttMs,
|
|
325
289
|
localCandidateType,
|
|
326
290
|
remoteCandidateType,
|
|
327
291
|
pathLabel: `${candidateTypeLabel(localCandidateType)} ↔ ${candidateTypeLabel(remoteCandidateType)}`,
|
|
328
292
|
}
|
|
329
293
|
}
|
|
330
294
|
|
|
295
|
+
export const probeIcePairConsentRtt = async (connection: Record<string, any> | null | undefined, pair: Record<string, any> | null | undefined) => {
|
|
296
|
+
if (!connection || !pair?.protocol?.request || typeof connection.buildRequest !== "function" || typeof connection.remotePassword !== "string") return Number.NaN
|
|
297
|
+
const request = connection.buildRequest({
|
|
298
|
+
nominate: false,
|
|
299
|
+
localUsername: connection.localUsername,
|
|
300
|
+
remoteUsername: connection.remoteUsername,
|
|
301
|
+
iceControlling: connection.iceControlling,
|
|
302
|
+
})
|
|
303
|
+
const startedAt = performance.now()
|
|
304
|
+
await pair.protocol.request(request, pair.remoteAddr, Buffer.from(connection.remotePassword, "utf8"), 0)
|
|
305
|
+
return performance.now() - startedAt
|
|
306
|
+
}
|
|
307
|
+
|
|
331
308
|
const sameConnectivity = (left: PeerConnectivitySnapshot, right: PeerConnectivitySnapshot) =>
|
|
332
|
-
left.rttMs === right.rttMs
|
|
309
|
+
(left.rttMs === right.rttMs || Number.isNaN(left.rttMs) && Number.isNaN(right.rttMs))
|
|
333
310
|
&& left.localCandidateType === right.localCandidateType
|
|
334
311
|
&& left.remoteCandidateType === right.remoteCandidateType
|
|
335
312
|
&& left.pathLabel === right.pathLabel
|
|
@@ -710,18 +687,23 @@ export class SendSession {
|
|
|
710
687
|
if (this.stopped) return
|
|
711
688
|
let dirty = false
|
|
712
689
|
for (const peer of this.peers.values()) {
|
|
713
|
-
if (peer.presence !== "active"
|
|
714
|
-
|
|
715
|
-
const next = connectivitySnapshotFromReport(await peer.pc.getStats(), peer.connectivity)
|
|
716
|
-
if (sameConnectivity(peer.connectivity, next)) continue
|
|
717
|
-
peer.connectivity = next
|
|
718
|
-
dirty = true
|
|
719
|
-
this.emit({ type: "peer", peer: this.peerSnapshot(peer) })
|
|
720
|
-
} catch {}
|
|
690
|
+
if (peer.presence !== "active") continue
|
|
691
|
+
dirty = await this.refreshPeerConnectivity(peer) || dirty
|
|
721
692
|
}
|
|
722
693
|
if (dirty) this.notify()
|
|
723
694
|
}
|
|
724
695
|
|
|
696
|
+
private async refreshPeerConnectivity(peer: PeerState) {
|
|
697
|
+
const activePair = activeIcePairFromPeerConnection(peer.pc as { iceTransports?: unknown[] } | null | undefined)
|
|
698
|
+
const next = connectivitySnapshotFromPeerConnection(peer.pc as { iceTransports?: unknown[] } | null | undefined, peer.connectivity)
|
|
699
|
+
const rttMs = await probeIcePairConsentRtt(activePair?.connection, activePair?.pair).catch(() => Number.NaN)
|
|
700
|
+
if (Number.isFinite(rttMs)) next.rttMs = rttMs
|
|
701
|
+
if (sameConnectivity(peer.connectivity, next)) return false
|
|
702
|
+
peer.connectivity = next
|
|
703
|
+
this.emit({ type: "peer", peer: this.peerSnapshot(peer) })
|
|
704
|
+
return true
|
|
705
|
+
}
|
|
706
|
+
|
|
725
707
|
private pushLog(kind: string, payload: unknown, level: "info" | "error" = "info") {
|
|
726
708
|
const log = { id: uid(6), at: Date.now(), kind, level, payload }
|
|
727
709
|
this.logs.unshift(log)
|
|
@@ -1319,8 +1301,8 @@ export class SendSession {
|
|
|
1319
1301
|
}
|
|
1320
1302
|
|
|
1321
1303
|
private async handleTransferControl(peer: PeerState, message: DataMessage) {
|
|
1322
|
-
this.pushLog("data:in", message)
|
|
1323
1304
|
if (message.to && message.to !== this.localId && message.to !== "*") return
|
|
1305
|
+
this.pushLog("data:in", message)
|
|
1324
1306
|
switch (message.kind) {
|
|
1325
1307
|
case "file-offer": {
|
|
1326
1308
|
if (!this.transfers.has(message.transferId)) {
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
3
|
import { cac, type CAC } from "cac"
|
|
4
4
|
import { cleanRoom } from "./core/protocol"
|
|
5
|
-
import { SendSession,
|
|
5
|
+
import type { SendSession, SessionConfig, SessionEvent } from "./core/session"
|
|
6
6
|
import { resolvePeerTargets } from "./core/targeting"
|
|
7
|
-
import {
|
|
7
|
+
import { ensureSessionRuntimePatches, ensureTuiRuntimePatches } from "../runtime/install"
|
|
8
8
|
|
|
9
9
|
export class ExitError extends Error {
|
|
10
10
|
constructor(message: string, readonly code = 1) {
|
|
@@ -122,6 +122,27 @@ const waitForFinalTransfers = async (session: SendSession, transferIds: string[]
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
let sessionRuntimePromise: Promise<typeof import("./core/session")> | null = null
|
|
126
|
+
let tuiRuntimePromise: Promise<typeof import("./tui/app")> | null = null
|
|
127
|
+
|
|
128
|
+
const loadSessionRuntime = () => {
|
|
129
|
+
if (sessionRuntimePromise) return sessionRuntimePromise
|
|
130
|
+
sessionRuntimePromise = (async () => {
|
|
131
|
+
await ensureSessionRuntimePatches()
|
|
132
|
+
return import("./core/session")
|
|
133
|
+
})()
|
|
134
|
+
return sessionRuntimePromise
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const loadTuiRuntime = () => {
|
|
138
|
+
if (tuiRuntimePromise) return tuiRuntimePromise
|
|
139
|
+
tuiRuntimePromise = (async () => {
|
|
140
|
+
await ensureTuiRuntimePatches()
|
|
141
|
+
return import("./tui/app")
|
|
142
|
+
})()
|
|
143
|
+
return tuiRuntimePromise
|
|
144
|
+
}
|
|
145
|
+
|
|
125
146
|
const handleSignals = (session: SendSession) => {
|
|
126
147
|
const onSignal = async () => {
|
|
127
148
|
await session.close()
|
|
@@ -132,6 +153,7 @@ const handleSignals = (session: SendSession) => {
|
|
|
132
153
|
}
|
|
133
154
|
|
|
134
155
|
const peersCommand = async (options: Record<string, unknown>) => {
|
|
156
|
+
const { SendSession } = await loadSessionRuntime()
|
|
135
157
|
const session = new SendSession(sessionConfigFrom(options, {}))
|
|
136
158
|
handleSignals(session)
|
|
137
159
|
printRoomAnnouncement(session.room, !!options.json)
|
|
@@ -158,6 +180,7 @@ const offerCommand = async (files: string[], options: Record<string, unknown>) =
|
|
|
158
180
|
if (!files.length) throw new ExitError("offer requires at least one file path", 1)
|
|
159
181
|
const selectors = offerSelectors(options.to)
|
|
160
182
|
const timeoutMs = waitPeerTimeout(options.waitPeer)
|
|
183
|
+
const { SendSession } = await loadSessionRuntime()
|
|
161
184
|
const session = new SendSession(sessionConfigFrom(options, {}))
|
|
162
185
|
handleSignals(session)
|
|
163
186
|
printRoomAnnouncement(session.room, !!options.json)
|
|
@@ -173,6 +196,7 @@ const offerCommand = async (files: string[], options: Record<string, unknown>) =
|
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
const acceptCommand = async (options: Record<string, unknown>) => {
|
|
199
|
+
const { SendSession } = await loadSessionRuntime()
|
|
176
200
|
const session = new SendSession(sessionConfigFrom(options, { autoAcceptIncoming: true, autoSaveIncoming: true }))
|
|
177
201
|
handleSignals(session)
|
|
178
202
|
printRoomAnnouncement(session.room, !!options.json)
|
|
@@ -194,8 +218,7 @@ const acceptCommand = async (options: Record<string, unknown>) => {
|
|
|
194
218
|
|
|
195
219
|
const tuiCommand = async (options: Record<string, unknown>) => {
|
|
196
220
|
const initialConfig = sessionConfigFrom(options, { autoAcceptIncoming: true, autoSaveIncoming: true })
|
|
197
|
-
await
|
|
198
|
-
const { startTui } = await import("./tui/app")
|
|
221
|
+
const { startTui } = await loadTuiRuntime()
|
|
199
222
|
await startTui(initialConfig, !!options.events)
|
|
200
223
|
}
|
|
201
224
|
|
package/src/tui/app.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { SendSession, type PeerSnapshot, type SessionConfig, type SessionSnapsho
|
|
|
5
5
|
import { cleanLocalId, cleanName, cleanRoom, displayPeerName, fallbackName, formatBytes, type LogEntry, peerDefaultsToken, type PeerProfile, uid } from "../core/protocol"
|
|
6
6
|
import { FILE_SEARCH_VISIBLE_ROWS, type FileSearchEvent, type FileSearchMatch, type FileSearchRequest } from "./file-search-protocol"
|
|
7
7
|
import { deriveFileSearchScope, formatFileSearchDisplayPath, normalizeSearchQuery, offsetFileSearchMatchIndices } from "./file-search"
|
|
8
|
-
import { installCheckboxClickPatch } from "
|
|
8
|
+
import { installCheckboxClickPatch } from "../../runtime/rezi-checkbox-click"
|
|
9
9
|
|
|
10
10
|
type Notice = { text: string; variant: "info" | "success" | "warning" | "error" }
|
|
11
11
|
type DraftItem = { id: string; path: string; name: string; size: number; createdAt: number }
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
diff --git a/dist/layout/engine/intrinsic.js b/dist/layout/engine/intrinsic.js
|
|
2
|
-
index 8df3d41..d29496d 100644
|
|
3
|
-
--- a/dist/layout/engine/intrinsic.js
|
|
4
|
-
+++ b/dist/layout/engine/intrinsic.js
|
|
5
|
-
@@ -168,7 +168,7 @@ function measureLeafMaxContent(vnode, axis, measureNode) {
|
|
6
|
-
const placeholder = typeof placeholderRaw === "string" ? placeholderRaw : "";
|
|
7
|
-
const content = propsRes.value.value.length > 0 ? propsRes.value.value : placeholder;
|
|
8
|
-
const textW = measureTextCells(content);
|
|
9
|
-
- return ok(clampSize({ w: textW + 2, h: 1 }));
|
|
10
|
-
+ return ok(clampSize({ w: textW + 3, h: 1 }));
|
|
11
|
-
}
|
|
12
|
-
case "progress": {
|
|
13
|
-
const props = vnode.props;
|
|
14
|
-
@@ -388,4 +388,4 @@ export function measureMaxContent(vnode, axis, measureNode) {
|
|
15
|
-
return measureLeafMaxContent(vnode, axis, measureNode);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
-//# sourceMappingURL=intrinsic.js.map
|
|
19
|
-
|
|
20
|
-
+//# sourceMappingURL=intrinsic.js.map
|
|
21
|
-
diff --git a/dist/layout/kinds/leaf.js b/dist/layout/kinds/leaf.js
|
|
22
|
-
index 255750a..4daf5da 100644
|
|
23
|
-
--- a/dist/layout/kinds/leaf.js
|
|
24
|
-
+++ b/dist/layout/kinds/leaf.js
|
|
25
|
-
@@ -71,7 +71,7 @@ export function measureLeaf(vnode, maxW, maxH, axis) {
|
|
26
|
-
const placeholder = typeof placeholderRaw === "string" ? placeholderRaw : "";
|
|
27
|
-
const content = propsRes.value.value.length > 0 ? propsRes.value.value : placeholder;
|
|
28
|
-
const textW = measureTextCells(content);
|
|
29
|
-
- const w = Math.min(maxW, textW + 2);
|
|
30
|
-
+ const w = Math.min(maxW, textW + 3);
|
|
31
|
-
const h = Math.min(maxH, 1);
|
|
32
|
-
return ok({ w, h });
|
|
33
|
-
}
|
|
34
|
-
@@ -533,4 +533,4 @@ export function layoutLeafKind(vnode, x, y, rectW, rectH) {
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
-//# sourceMappingURL=leaf.js.map
|
|
39
|
-
|
|
40
|
-
+//# sourceMappingURL=leaf.js.map
|
|
41
|
-
diff --git a/dist/layout/kinds/box.js b/dist/layout/kinds/box.js
|
|
42
|
-
index dca103c..14f2973 100644
|
|
43
|
-
--- a/dist/layout/kinds/box.js
|
|
44
|
-
+++ b/dist/layout/kinds/box.js
|
|
45
|
-
@@ -4,6 +4,7 @@ import { childHasAbsolutePosition } from "../engine/guards.js";
|
|
46
|
-
import { ok } from "../engine/result.js";
|
|
47
|
-
import { resolveMargin as resolveMarginProps, resolveSpacing as resolveSpacingProps, } from "../spacing.js";
|
|
48
|
-
import { validateBoxProps } from "../validateProps.js";
|
|
49
|
-
+const OVERFLOW_CONTENT_LIMIT = 2147483647;
|
|
50
|
-
const syntheticColumnCache = new WeakMap();
|
|
51
|
-
function computeFlowSignature(children) {
|
|
52
|
-
let signature = `${children.length}:`;
|
|
53
|
-
@@ -88,12 +89,13 @@ export function measureBoxKinds(vnode, maxW, maxH, axis, measureNode) {
|
|
54
|
-
const outerHLimit = forcedH ?? maxHCap;
|
|
55
|
-
const cw = clampNonNegative(outerWLimit - bl - br - spacing.left - spacing.right);
|
|
56
|
-
const ch = clampNonNegative(outerHLimit - bt - bb - spacing.top - spacing.bottom);
|
|
57
|
-
+ const flowMeasureH = propsRes.value.overflow === "scroll" ? OVERFLOW_CONTENT_LIMIT : ch;
|
|
58
|
-
// Children are laid out as a Column inside the content rect.
|
|
59
|
-
let contentUsedW = 0;
|
|
60
|
-
let contentUsedH = 0;
|
|
61
|
-
if (vnode.children.length > 0) {
|
|
62
|
-
const columnNode = getSyntheticColumn(vnode, propsRes.value.gap);
|
|
63
|
-
- const innerRes = measureNode(columnNode, cw, ch, "column");
|
|
64
|
-
+ const innerRes = measureNode(columnNode, cw, flowMeasureH, "column");
|
|
65
|
-
if (!innerRes.ok)
|
|
66
|
-
return innerRes;
|
|
67
|
-
contentUsedW = innerRes.value.w;
|
|
68
|
-
@@ -154,9 +156,16 @@ export function layoutBoxKinds(vnode, x, y, rectW, rectH, axis, measureNode, lay
|
|
69
|
-
const children = [];
|
|
70
|
-
if (vnode.children.length > 0) {
|
|
71
|
-
const columnNode = getSyntheticColumn(vnode, propsRes.value.gap);
|
|
72
|
-
+ const flowMeasureH = propsRes.value.overflow === "scroll" ? OVERFLOW_CONTENT_LIMIT : ch;
|
|
73
|
-
+ const flowMeasureRes = measureNode(columnNode, cw, flowMeasureH, "column");
|
|
74
|
-
+ if (!flowMeasureRes.ok)
|
|
75
|
-
+ return flowMeasureRes;
|
|
76
|
-
+ const flowLayoutH = propsRes.value.overflow === "scroll"
|
|
77
|
-
+ ? Math.max(ch, flowMeasureRes.value.h)
|
|
78
|
-
+ : ch;
|
|
79
|
-
// The synthetic column wrapper must fill the box content rect so that
|
|
80
|
-
// percentage constraints resolve against the actual available space.
|
|
81
|
-
- const innerRes = layoutNode(columnNode, cx, cy, cw, ch, "column", cw, ch);
|
|
82
|
-
+ const innerRes = layoutNode(columnNode, cx, cy, cw, flowLayoutH, "column", cw, flowLayoutH, flowMeasureRes.value);
|
|
83
|
-
if (!innerRes.ok)
|
|
84
|
-
return innerRes;
|
|
85
|
-
// Attach the box's children (not the synthetic column wrapper).
|
|
86
|
-
@@ -234,4 +243,4 @@ export function layoutBoxKinds(vnode, x, y, rectW, rectH, axis, measureNode, lay
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
-//# sourceMappingURL=box.js.map
|
|
91
|
-
|
|
92
|
-
+//# sourceMappingURL=box.js.map
|
|
93
|
-
diff --git a/dist/app/widgetRenderer.js b/dist/app/widgetRenderer.js
|
|
94
|
-
index 38d4a5f..69ee099 100644
|
|
95
|
-
--- a/dist/app/widgetRenderer.js
|
|
96
|
-
+++ b/dist/app/widgetRenderer.js
|
|
97
|
-
@@ -614,6 +614,7 @@ export class WidgetRenderer {
|
|
98
|
-
tableStore = createTableStateStore();
|
|
99
|
-
treeStore = createTreeStateStore();
|
|
100
|
-
scrollOverrides = new Map();
|
|
101
|
-
+ hasPendingScrollOverride = false;
|
|
102
|
-
/* --- Tree Lazy-Loading Cache (per tree id, per node key) --- */
|
|
103
|
-
loadedTreeChildrenByTreeId = new Map();
|
|
104
|
-
treeLoadTokenByTreeAndKey = new Map();
|
|
105
|
-
@@ -1472,6 +1473,9 @@ export class WidgetRenderer {
|
|
106
|
-
diffViewerById: this.diffViewerById,
|
|
107
|
-
rectById: this.rectById,
|
|
108
|
-
scrollOverrides: this.scrollOverrides,
|
|
109
|
-
+ markScrollOverrideDirty: () => {
|
|
110
|
-
+ this.hasPendingScrollOverride = true;
|
|
111
|
-
+ },
|
|
112
|
-
findScrollableAncestors: (targetId) => this.findScrollableAncestors(targetId),
|
|
113
|
-
});
|
|
114
|
-
if (wheelRoute)
|
|
115
|
-
@@ -1750,6 +1754,53 @@ export class WidgetRenderer {
|
|
116
|
-
}
|
|
117
|
-
return Object.freeze([]);
|
|
118
|
-
}
|
|
119
|
-
+ syncScrollOverridesFromLayoutTree() {
|
|
120
|
-
+ if (!this.committedRoot || !this.layoutTree) {
|
|
121
|
-
+ this.scrollOverrides.clear();
|
|
122
|
-
+ return;
|
|
123
|
-
+ }
|
|
124
|
-
+ const nextOverrides = new Map();
|
|
125
|
-
+ const stack = [
|
|
126
|
-
+ {
|
|
127
|
-
+ runtimeNode: this.committedRoot,
|
|
128
|
-
+ layoutNode: this.layoutTree,
|
|
129
|
-
+ },
|
|
130
|
-
+ ];
|
|
131
|
-
+ while (stack.length > 0) {
|
|
132
|
-
+ const frame = stack.pop();
|
|
133
|
-
+ if (!frame)
|
|
134
|
-
+ continue;
|
|
135
|
-
+ const runtimeNode = frame.runtimeNode;
|
|
136
|
-
+ const layoutNode = frame.layoutNode;
|
|
137
|
-
+ const props = runtimeNode.vnode.props;
|
|
138
|
-
+ const nodeId = typeof props.id === "string" && props.id.length > 0 ? props.id : null;
|
|
139
|
-
+ if (nodeId !== null && props.overflow === "scroll" && layoutNode.meta) {
|
|
140
|
-
+ const meta = layoutNode.meta;
|
|
141
|
-
+ const hasScrollableAxis = meta.contentWidth > meta.viewportWidth || meta.contentHeight > meta.viewportHeight;
|
|
142
|
-
+ if (hasScrollableAxis) {
|
|
143
|
-
+ nextOverrides.set(nodeId, Object.freeze({
|
|
144
|
-
+ scrollX: meta.scrollX,
|
|
145
|
-
+ scrollY: meta.scrollY,
|
|
146
|
-
+ }));
|
|
147
|
-
+ }
|
|
148
|
-
+ }
|
|
149
|
-
+ const childCount = Math.min(runtimeNode.children.length, layoutNode.children.length);
|
|
150
|
-
+ for (let i = childCount - 1; i >= 0; i--) {
|
|
151
|
-
+ const runtimeChild = runtimeNode.children[i];
|
|
152
|
-
+ const layoutChild = layoutNode.children[i];
|
|
153
|
-
+ if (!runtimeChild || !layoutChild)
|
|
154
|
-
+ continue;
|
|
155
|
-
+ stack.push({
|
|
156
|
-
+ runtimeNode: runtimeChild,
|
|
157
|
-
+ layoutNode: layoutChild,
|
|
158
|
-
+ });
|
|
159
|
-
+ }
|
|
160
|
-
+ }
|
|
161
|
-
+ this.scrollOverrides.clear();
|
|
162
|
-
+ for (const [nodeId, override] of nextOverrides) {
|
|
163
|
-
+ this.scrollOverrides.set(nodeId, override);
|
|
164
|
-
+ }
|
|
165
|
-
+ }
|
|
166
|
-
applyScrollOverridesToVNode(vnode, overrides = this
|
|
167
|
-
.scrollOverrides) {
|
|
168
|
-
const propsRecord = (vnode.props ?? {});
|
|
169
|
-
@@ -1763,7 +1814,7 @@ export class WidgetRenderer {
|
|
170
|
-
nextPropsMutable = { ...propsRecord };
|
|
171
|
-
return nextPropsMutable;
|
|
172
|
-
};
|
|
173
|
-
- if (override) {
|
|
174
|
-
+ if (override && propsForRead.overflow === "scroll") {
|
|
175
|
-
if (propsForRead.scrollX !== override.scrollX || propsForRead.scrollY !== override.scrollY) {
|
|
176
|
-
const mutable = ensureMutableProps();
|
|
177
|
-
mutable.scrollX = override.scrollX;
|
|
178
|
-
@@ -2643,7 +2694,7 @@ export class WidgetRenderer {
|
|
179
|
-
// layout when explicitly requested (resize/layout dirty), bootstrap lacks
|
|
180
|
-
// a tree, or committed layout signatures changed.
|
|
181
|
-
let doLayout = plan.layout || this.layoutTree === null;
|
|
182
|
-
- if (this.scrollOverrides.size > 0)
|
|
183
|
-
+ if (this.hasPendingScrollOverride)
|
|
184
|
-
doLayout = true;
|
|
185
|
-
const frameNowMs = typeof plan.nowMs === "number" && Number.isFinite(plan.nowMs)
|
|
186
|
-
? plan.nowMs
|
|
187
|
-
@@ -2916,7 +2967,7 @@ export class WidgetRenderer {
|
|
188
|
-
const layoutRootVNode = pendingScrollOverrides !== null
|
|
189
|
-
? this.applyScrollOverridesToVNode(constrainedLayoutRootVNode, pendingScrollOverrides)
|
|
190
|
-
: constrainedLayoutRootVNode;
|
|
191
|
-
- this.scrollOverrides.clear();
|
|
192
|
-
+ this.hasPendingScrollOverride = false;
|
|
193
|
-
const initialLayoutRes = this.layoutWithShapeFallback(layoutRootVNode, constrainedLayoutRootVNode, rootPad, rootW, rootH, true);
|
|
194
|
-
if (!initialLayoutRes.ok) {
|
|
195
|
-
perfMarkEnd("layout", layoutToken);
|
|
196
|
-
@@ -3001,6 +3052,7 @@ export class WidgetRenderer {
|
|
197
|
-
}
|
|
198
|
-
perfMarkEnd("layout", layoutToken);
|
|
199
|
-
this.layoutTree = nextLayoutTree;
|
|
200
|
-
+ this.syncScrollOverridesFromLayoutTree();
|
|
201
|
-
if (doCommit) {
|
|
202
|
-
// Seed/refresh per-instance layout stability signatures after a real
|
|
203
|
-
// layout pass so subsequent commits can take the signature fast path.
|
|
204
|
-
@@ -4076,4 +4128,4 @@ export class WidgetRenderer {
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
-//# sourceMappingURL=widgetRenderer.js.map
|
|
209
|
-
|
|
210
|
-
+//# sourceMappingURL=widgetRenderer.js.map
|
|
211
|
-
diff --git a/dist/app/widgetRenderer/mouseRouting.js b/dist/app/widgetRenderer/mouseRouting.js
|
|
212
|
-
index d3b08cf..00b77a3 100644
|
|
213
|
-
--- a/dist/app/widgetRenderer/mouseRouting.js
|
|
214
|
-
+++ b/dist/app/widgetRenderer/mouseRouting.js
|
|
215
|
-
@@ -1205,9 +1205,10 @@ export function routeMouseWheel(event, ctx) {
|
|
216
|
-
scrollX: r.nextScrollX ?? meta.scrollX,
|
|
217
|
-
scrollY: r.nextScrollY ?? meta.scrollY,
|
|
218
|
-
});
|
|
219
|
-
+ ctx.markScrollOverrideDirty?.();
|
|
220
|
-
return ROUTE_RENDER;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
-//# sourceMappingURL=mouseRouting.js.map
|
|
226
|
-
|
|
227
|
-
+//# sourceMappingURL=mouseRouting.js.map
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
diff --git a/lib/webrtc/src/transport/ice.js b/lib/webrtc/src/transport/ice.js
|
|
2
|
-
index 25c8489..cd06dbf 100644
|
|
3
|
-
--- a/lib/webrtc/src/transport/ice.js
|
|
4
|
-
+++ b/lib/webrtc/src/transport/ice.js
|
|
5
|
-
@@ -230,25 +230,25 @@ class RTCIceTransport {
|
|
6
|
-
stats.push(candidateStats);
|
|
7
|
-
}
|
|
8
|
-
// Candidate pairs
|
|
9
|
-
const pairs = this.connection?.candidatePairs
|
|
10
|
-
? [
|
|
11
|
-
...this.connection.candidatePairs.filter((p) => p.nominated),
|
|
12
|
-
...this.connection.candidatePairs.filter((p) => !p.nominated),
|
|
13
|
-
]
|
|
14
|
-
: [];
|
|
15
|
-
for (const pair of pairs) {
|
|
16
|
-
const pairStats = {
|
|
17
|
-
type: "candidate-pair",
|
|
18
|
-
- id: (0, stats_1.generateStatsId)("candidate-pair", pair.foundation),
|
|
19
|
-
+ id: (0, stats_1.generateStatsId)("candidate-pair", pair.localCandidate.foundation, pair.remoteCandidate.foundation),
|
|
20
|
-
timestamp,
|
|
21
|
-
transportId: (0, stats_1.generateStatsId)("transport", this.id),
|
|
22
|
-
localCandidateId: (0, stats_1.generateStatsId)("local-candidate", pair.localCandidate.foundation),
|
|
23
|
-
remoteCandidateId: (0, stats_1.generateStatsId)("remote-candidate", pair.remoteCandidate.foundation),
|
|
24
|
-
state: pair.state,
|
|
25
|
-
nominated: pair.nominated,
|
|
26
|
-
packetsSent: pair.packetsSent,
|
|
27
|
-
packetsReceived: pair.packetsReceived,
|
|
28
|
-
bytesSent: pair.bytesSent,
|
|
29
|
-
bytesReceived: pair.bytesReceived,
|
|
30
|
-
currentRoundTripTime: pair.rtt,
|
|
31
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises"
|
|
2
|
-
import { fileURLToPath } from "node:url"
|
|
3
|
-
|
|
4
|
-
const PATCH_FLAG = Symbol.for("send.rezi.inputCaretPatchInstalled")
|
|
5
|
-
|
|
6
|
-
type PatchedRuntime = {
|
|
7
|
-
[PATCH_FLAG]?: Set<string>
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type FilePatchSpec = {
|
|
11
|
-
relativeUrl: string
|
|
12
|
-
before: string
|
|
13
|
-
after: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const INPUT_PATCHES: readonly FilePatchSpec[] = [
|
|
17
|
-
{
|
|
18
|
-
relativeUrl: "./layout/engine/intrinsic.js",
|
|
19
|
-
before: "return ok(clampSize({ w: textW + 2, h: 1 }));",
|
|
20
|
-
after: "return ok(clampSize({ w: textW + 3, h: 1 }));",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
relativeUrl: "./layout/kinds/leaf.js",
|
|
24
|
-
before: "const w = Math.min(maxW, textW + 2);",
|
|
25
|
-
after: "const w = Math.min(maxW, textW + 3);",
|
|
26
|
-
},
|
|
27
|
-
] as const
|
|
28
|
-
|
|
29
|
-
const patchRuntime = globalThis as PatchedRuntime
|
|
30
|
-
|
|
31
|
-
const patchFile = async (spec: FilePatchSpec, coreIndexUrl: string) => {
|
|
32
|
-
const path = fileURLToPath(new URL(spec.relativeUrl, coreIndexUrl))
|
|
33
|
-
const source = await readFile(path, "utf8")
|
|
34
|
-
if (source.includes(spec.after)) return
|
|
35
|
-
if (!source.includes(spec.before)) throw new Error(`Unsupported @rezi-ui/core input layout at ${path}`)
|
|
36
|
-
await writeFile(path, source.replace(spec.before, spec.after))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const ensureReziInputCaretPatch = async () => {
|
|
40
|
-
const coreIndexUrl = await import.meta.resolve("@rezi-ui/core")
|
|
41
|
-
const patchedRoots = patchRuntime[PATCH_FLAG] ?? new Set<string>()
|
|
42
|
-
if (patchedRoots.has(coreIndexUrl)) return
|
|
43
|
-
for (const spec of INPUT_PATCHES) await patchFile(spec, coreIndexUrl)
|
|
44
|
-
patchedRoots.add(coreIndexUrl)
|
|
45
|
-
patchRuntime[PATCH_FLAG] = patchedRoots
|
|
46
|
-
}
|
|
File without changes
|