@elefunc/send 0.1.0
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/LICENSE +21 -0
- package/README.md +57 -0
- package/package.json +52 -0
- package/patches/@rezi-ui%2Fcore@0.1.0-alpha.60.patch +227 -0
- package/patches/werift@0.22.9.patch +31 -0
- package/src/core/files.ts +79 -0
- package/src/core/paths.ts +19 -0
- package/src/core/protocol.ts +241 -0
- package/src/core/session.ts +1435 -0
- package/src/core/targeting.ts +39 -0
- package/src/index.ts +283 -0
- package/src/tui/app.ts +1442 -0
- package/src/tui/file-search-protocol.ts +48 -0
- package/src/tui/file-search.ts +282 -0
- package/src/tui/file-search.worker.ts +127 -0
- package/src/tui/rezi-checkbox-click.ts +63 -0
- package/src/types/bun-runtime.d.ts +5 -0
- package/src/types/bun-test.d.ts +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elefunc, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @elefunc/send
|
|
2
|
+
|
|
3
|
+
Browser-compatible file transfer CLI and TUI built with Bun, WebRTC, and Rezi.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Bun `>= 1.3.11`
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add -g @elefunc/send
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
send peers
|
|
19
|
+
send offer ./file.txt
|
|
20
|
+
send accept
|
|
21
|
+
send tui --events
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Rooms
|
|
25
|
+
|
|
26
|
+
`--room` is optional on all commands. If you omit it, `send` creates a random room and prints or shows it.
|
|
27
|
+
|
|
28
|
+
## Self Identity
|
|
29
|
+
|
|
30
|
+
`--self` accepts three forms:
|
|
31
|
+
|
|
32
|
+
- `name`
|
|
33
|
+
- `name-ID`
|
|
34
|
+
- `-ID` using the attached CLI form `--self=-ab12cd34`
|
|
35
|
+
|
|
36
|
+
`SEND_SELF` supports the same raw values, including `SEND_SELF=-ab12cd34`.
|
|
37
|
+
|
|
38
|
+
The ID suffix must be exactly 8 lowercase alphanumeric characters.
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
send peers --self alice
|
|
44
|
+
send offer ./demo.txt --self alice-ab12cd34
|
|
45
|
+
send accept --self=-ab12cd34
|
|
46
|
+
SEND_SELF=-ab12cd34 send tui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bun install
|
|
53
|
+
bun run typecheck
|
|
54
|
+
bun test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The package is Bun-native and keeps its runtime patches in `patches/`.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elefunc/send",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"packageManager": "bun@1.3.11",
|
|
8
|
+
"engines": {
|
|
9
|
+
"bun": ">=1.3.11"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"send": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"patches",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"package.json"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "bun run ./src/index.ts",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"fix:rezi": "bun run ./dia/rezi-fix.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/elefunc/send.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/elefunc/send#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/elefunc/send/issues"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@rezi-ui/core": "0.1.0-alpha.60",
|
|
40
|
+
"@rezi-ui/node": "0.1.0-alpha.60",
|
|
41
|
+
"cac": "^7.0.0",
|
|
42
|
+
"werift": "^0.22.9"
|
|
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
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^24.5.2",
|
|
50
|
+
"typescript": "^5.8.3"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { access, mkdir, stat } from "node:fs/promises"
|
|
2
|
+
import { basename, extname, join, resolve } from "node:path"
|
|
3
|
+
import { resolveUserPath } from "./paths"
|
|
4
|
+
|
|
5
|
+
export interface LocalFile {
|
|
6
|
+
path: string
|
|
7
|
+
name: string
|
|
8
|
+
size: number
|
|
9
|
+
type: string
|
|
10
|
+
lastModified: number
|
|
11
|
+
blob: Blob
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LocalFileInfo = Omit<LocalFile, "blob">
|
|
15
|
+
export interface LocalPathIssue {
|
|
16
|
+
path: string
|
|
17
|
+
error: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const exists = async (path: string) => access(path).then(() => true, () => false)
|
|
21
|
+
|
|
22
|
+
export const inspectLocalFile = async (path: string): Promise<LocalFileInfo> => {
|
|
23
|
+
const absolute = resolveUserPath(path)
|
|
24
|
+
const info = await stat(absolute)
|
|
25
|
+
if (!info.isFile()) throw new Error(`not a file: ${absolute}`)
|
|
26
|
+
const blob = Bun.file(absolute)
|
|
27
|
+
return {
|
|
28
|
+
path: absolute,
|
|
29
|
+
name: basename(absolute),
|
|
30
|
+
size: info.size,
|
|
31
|
+
type: blob.type || "application/octet-stream",
|
|
32
|
+
lastModified: Math.round(info.mtimeMs || Date.now()),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const inspectLocalPaths = async (paths: string[]) => {
|
|
37
|
+
const results = await Promise.allSettled(paths.map(inspectLocalFile))
|
|
38
|
+
const files: LocalFileInfo[] = []
|
|
39
|
+
const errors: LocalPathIssue[] = []
|
|
40
|
+
results.forEach((result, index) => {
|
|
41
|
+
if (result.status === "fulfilled") {
|
|
42
|
+
files.push(result.value)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
errors.push({
|
|
46
|
+
path: resolveUserPath(paths[index] || ""),
|
|
47
|
+
error: result.reason instanceof Error ? result.reason.message : `${result.reason}`,
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
return { files, errors }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const loadLocalFile = async (path: string): Promise<LocalFile> => {
|
|
54
|
+
const info = await inspectLocalFile(path)
|
|
55
|
+
return {
|
|
56
|
+
...info,
|
|
57
|
+
blob: Bun.file(info.path),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const loadLocalFiles = (paths: string[]) => Promise.all(paths.map(loadLocalFile))
|
|
62
|
+
|
|
63
|
+
export const readFileChunk = async (file: LocalFile, offset: number, size: number) => Buffer.from(await file.blob.slice(offset, offset + size).arrayBuffer())
|
|
64
|
+
|
|
65
|
+
export const uniqueOutputPath = async (directory: string, fileName: string) => {
|
|
66
|
+
await mkdir(directory, { recursive: true })
|
|
67
|
+
const extension = extname(fileName)
|
|
68
|
+
const stem = extension ? fileName.slice(0, -extension.length) : fileName
|
|
69
|
+
for (let index = 0; ; index += 1) {
|
|
70
|
+
const candidate = join(directory, index ? `${stem} (${index})${extension}` : fileName)
|
|
71
|
+
if (!await exists(candidate)) return candidate
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const saveIncomingFile = async (directory: string, fileName: string, data: Buffer) => {
|
|
76
|
+
const path = await uniqueOutputPath(directory, fileName)
|
|
77
|
+
await Bun.write(path, data)
|
|
78
|
+
return path
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
|
|
4
|
+
const normalizePathInput = (value: string) => value.replace(/\\/gu, "/")
|
|
5
|
+
|
|
6
|
+
export const isHomeDirectoryPath = (value: string) => {
|
|
7
|
+
const normalized = normalizePathInput(value)
|
|
8
|
+
return normalized === "~" || normalized.startsWith("~/")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const expandHomePath = (value: string, home = homedir()) => {
|
|
12
|
+
const normalized = normalizePathInput(value)
|
|
13
|
+
if (normalized === "~") return home
|
|
14
|
+
if (normalized.startsWith("~/")) return resolve(home, normalized.slice(2))
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const resolveUserPath = (value: string, cwd = process.cwd(), home = homedir()) =>
|
|
19
|
+
expandHomePath(value, home) ?? resolve(cwd, value)
|