@statewalker/httpeers-join 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 +94 -59
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @statewalker/httpeers-join
|
|
2
2
|
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
3
5
|
The **join-the-mesh widget**: one small piece of plain DOM that every page joining
|
|
4
6
|
an httpeers hub shows the same way.
|
|
5
7
|
|
|
@@ -15,27 +17,30 @@ an httpeers hub shows the same way.
|
|
|
15
17
|
(`session.resetIdentity()`, which forgets the identity), behind a confirmation.
|
|
16
18
|
- **Invite** (mesh admins only): make an invitation as a member or an admin, valid for 1 hour,
|
|
17
19
|
1 day or 7 days, and hand it over as a link, a QR code, the share sheet or the clipboard. See
|
|
18
|
-
[Invite](#invite).
|
|
20
|
+
[Invite](#invite-is-shown-only-to-an-admin-the-hub-confirms).
|
|
19
21
|
|
|
20
22
|
Every button is shown only when `SessionState.controls` allows it. There is one addition: "Leave"
|
|
21
23
|
is hidden while there is no identity (a first run has nothing to forget). The widget decides
|
|
22
24
|
nothing else about the session: `PeerSession` already decides it, and the widget shows the
|
|
23
25
|
result.
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
import { mountJoinWidget } from "@statewalker/httpeers-join";
|
|
27
|
-
import { createSession } from "@statewalker/httpeers-member/browser";
|
|
27
|
+
## Why it exists
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
Several pages join a hub: llm-chat's `mesh.html` (React and Tailwind) and the demos' `app`,
|
|
30
|
+
`images` and `proxy` pages (plain DOM). Each needs the same join form, QR scanning, Disconnect,
|
|
31
|
+
Reconnect, Leave and, for an admin, Invite, with the same wording. Apps do not import from other
|
|
32
|
+
apps, so a package is the one place they can all share it from, and plain DOM is the one shape both
|
|
33
|
+
a React page and a framework-free page can use.
|
|
34
|
+
|
|
35
|
+
## How to use
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pnpm add @statewalker/httpeers-join
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
One entry point (`.`). It needs a DOM at mount time, not at import. Dependencies:
|
|
42
|
+
`@statewalker/httpeers-member` (types and `invitationFromQrText`) and `@statewalker/httpeers-qr`
|
|
43
|
+
(the scanner and `qrSvg`, both loaded lazily); `html5-qrcode` comes with the QR scanner.
|
|
39
44
|
|
|
40
45
|
| Export | What it is |
|
|
41
46
|
|---|---|
|
|
@@ -69,7 +74,52 @@ await session.start();
|
|
|
69
74
|
The state passed to `update()` may carry `handle` (a `SessionState` does). The widget reads only
|
|
70
75
|
its `hubPeerId`, `baseUrl` and `meshView()`, and only for the Invite panel.
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
## Examples
|
|
78
|
+
|
|
79
|
+
A plain page:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { type JoinWidget, mountJoinWidget } from "@statewalker/httpeers-join";
|
|
83
|
+
import { createSession } from "@statewalker/httpeers-member/browser";
|
|
84
|
+
|
|
85
|
+
let widget: JoinWidget | undefined;
|
|
86
|
+
const session = createSession({ key: "peers", mounts, rules, onChange: (state) => widget?.update(state) });
|
|
87
|
+
widget = mountJoinWidget(document.querySelector("#mesh-join")!, {
|
|
88
|
+
session,
|
|
89
|
+
state: session.state(),
|
|
90
|
+
});
|
|
91
|
+
await session.start();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
In React. The widget is not a React component, and React pages wrap it in a few lines. The session and the
|
|
95
|
+
state stay in React. The widget is mounted once and fed each state:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useEffect, useRef } from "react";
|
|
99
|
+
import { type JoinWidget, mountJoinWidget } from "@statewalker/httpeers-join";
|
|
100
|
+
|
|
101
|
+
function JoinWidgetView({ session, state, compact }: Props) {
|
|
102
|
+
const host = useRef<HTMLDivElement>(null);
|
|
103
|
+
const widget = useRef<JoinWidget | null>(null);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (host.current == null) return;
|
|
106
|
+
const w = mountJoinWidget(host.current, { session, compact });
|
|
107
|
+
widget.current = w;
|
|
108
|
+
return () => { w.destroy(); widget.current = null; };
|
|
109
|
+
}, [session, compact]);
|
|
110
|
+
useEffect(() => widget.current?.update(state), [state]);
|
|
111
|
+
return <div ref={host} />;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`apps/llm-chat/src/mesh/join-widget.tsx` is this wrapper. It lives in `mesh/` because the
|
|
116
|
+
boundary test keeps httpeers out of everything the standalone page reaches.
|
|
117
|
+
|
|
118
|
+
## Internals
|
|
119
|
+
|
|
120
|
+
### The QR scanner loads on the first scan
|
|
121
|
+
|
|
122
|
+
`defaultQrScanner()` loads `@statewalker/httpeers-qr/browser` (and with it
|
|
73
123
|
`html5-qrcode`) with a dynamic `import()` on the **first scan**, so a page that only resumes
|
|
74
124
|
never downloads the camera library, and a bundler gives it a chunk of its own. The camera button
|
|
75
125
|
is hidden when `navigator.mediaDevices.getUserMedia` is missing, which covers an insecure origin
|
|
@@ -78,7 +128,9 @@ is reported when the button is pressed, with a pointer to the picture button. A
|
|
|
78
128
|
QR code that is not an invitation keeps scanning (camera) or says so (picture). A bare invitation
|
|
79
129
|
id is not accepted from a QR code: nothing tells it apart from any other short string.
|
|
80
130
|
|
|
81
|
-
|
|
131
|
+
### A picture gets two decoders, because one misses mid-size screenshots
|
|
132
|
+
|
|
133
|
+
html5-qrcode's `scanFile` could not read the demos hub's own
|
|
82
134
|
invitation QR (a 310-character join blob) from a clean 684 px screenshot, nor at 600 or 480 px.
|
|
83
135
|
It read the same picture at 400 px and below. Phone screenshots and photos are larger than that.
|
|
84
136
|
When html5-qrcode finds nothing, `defaultQrScanner` therefore draws the picture to a canvas at
|
|
@@ -88,19 +140,23 @@ built demos app, every size from 300 to 3000 px decodes, as does a blurred, tilt
|
|
|
88
140
|
3000 px copy. The same run, scanning the hub page's real QR code, joined its mesh live. The
|
|
89
141
|
camera path is html5-qrcode alone. It gets many frames and the person aims it.
|
|
90
142
|
|
|
91
|
-
|
|
143
|
+
### Status and errors are announced
|
|
144
|
+
|
|
145
|
+
The status line is `role="status"`. The phase message is `role="alert"` when it
|
|
92
146
|
reports trouble (a hub that does not know this peer, a refused invitation, `blocked`, `failed`) and
|
|
93
147
|
`role="status"` when it gives instructions (a first run, `disconnected`, a live note). A rejected
|
|
94
148
|
call and a failed scan go to a separate `role="alert"` line. The field has a `<label>`, every
|
|
95
149
|
control is a `<button>` (or a labelled file input), and ids are unique per instance.
|
|
96
150
|
|
|
97
|
-
|
|
151
|
+
### Styles beat Tailwind's preflight without leaking
|
|
152
|
+
|
|
153
|
+
The widget injects one `<style id="hp-join-style">` per document on its first mount.
|
|
98
154
|
Every rule is on an `hp-join-` or `hp-invite-` class, and none is in a cascade layer, so the rules beat
|
|
99
155
|
Tailwind's preflight (`@layer base`) without leaking into the page. Colours are custom properties
|
|
100
156
|
on `.hp-join` (`--hp-join-accent`, `--hp-join-danger`, `--hp-join-bg`, …). The widget inherits
|
|
101
157
|
the page's font.
|
|
102
158
|
|
|
103
|
-
### Invite
|
|
159
|
+
### Invite is shown only to an admin the hub confirms
|
|
104
160
|
|
|
105
161
|
A mesh admin gets an **Invite someone** section: below the controls in the full widget, and in
|
|
106
162
|
the "Mesh" menu in the compact one. It holds:
|
|
@@ -151,40 +207,10 @@ scanner. It encodes the hub's `link`, not the bare blob, so a phone's own camera
|
|
|
151
207
|
join page directly. The widget's scanner accepts the same link.
|
|
152
208
|
|
|
153
209
|
**On a phone**, below 30rem, the compact widget's menu becomes a sheet along the bottom of the
|
|
154
|
-
screen (at most 70% of its height, scrolling), because anchored to its button
|
|
155
|
-
left edge. The header, and the toggle that closes the menu, stay in view.
|
|
156
|
-
|
|
157
|
-
### In React
|
|
158
|
-
|
|
159
|
-
The widget is not a React component, and React pages wrap it in a few lines. The session and the
|
|
160
|
-
state stay in React. The widget is mounted once and fed each state:
|
|
161
|
-
|
|
162
|
-
```tsx
|
|
163
|
-
function JoinWidgetView({ session, state, compact }: Props) {
|
|
164
|
-
const host = useRef<HTMLDivElement>(null);
|
|
165
|
-
const widget = useRef<JoinWidget | null>(null);
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (host.current == null) return;
|
|
168
|
-
const w = mountJoinWidget(host.current, { session, compact });
|
|
169
|
-
widget.current = w;
|
|
170
|
-
return () => { w.destroy(); widget.current = null; };
|
|
171
|
-
}, [session, compact]);
|
|
172
|
-
useEffect(() => widget.current?.update(state), [state]);
|
|
173
|
-
return <div ref={host} />;
|
|
174
|
-
}
|
|
175
|
-
```
|
|
210
|
+
screen (at most 70% of its height, scrolling), because a menu anchored to its button would run off
|
|
211
|
+
the left edge. The header, and the toggle that closes the menu, stay in view.
|
|
176
212
|
|
|
177
|
-
|
|
178
|
-
boundary test keeps httpeers out of everything the standalone page reaches.
|
|
179
|
-
|
|
180
|
-
## Design
|
|
181
|
-
|
|
182
|
-
**What had to be shared.** Four pages join a hub: llm-chat's `mesh.html` (React and Tailwind)
|
|
183
|
-
and the demos' `app`, `images` and `proxy` pages (plain DOM). Each had its own copy of the join
|
|
184
|
-
form and the Disconnect/Reconnect buttons. None of them offered QR scanning or a way to leave, and
|
|
185
|
-
the copies had already drifted in wording.
|
|
186
|
-
|
|
187
|
-
**Three shapes were considered.**
|
|
213
|
+
### A mount function, not a component or a custom element
|
|
188
214
|
|
|
189
215
|
1. **A React component, with a DOM copy for the demos.** Rejected. That keeps two copies, which is
|
|
190
216
|
the problem this package solves, and the demos would take on a framework to show one form.
|
|
@@ -193,18 +219,21 @@ the copies had already drifted in wording.
|
|
|
193
219
|
ship a copy. Its state goes in through a property that React 19 sets but earlier versions do
|
|
194
220
|
not. Its lifecycle (`connectedCallback`, upgrade order) is harder to test than a function call.
|
|
195
221
|
None of this buys anything a function does not.
|
|
196
|
-
3. **A function that mounts into a container and returns `{ update, destroy }`.**
|
|
222
|
+
3. **A function that mounts into a container and returns `{ update, destroy }`.** This one. It is
|
|
197
223
|
plain TypeScript and DOM, has no globals and no registration, and is straightforward to test
|
|
198
224
|
under happy-dom with a fake session. React wraps it in about ten lines, and a plain page calls
|
|
199
225
|
it directly.
|
|
200
226
|
|
|
201
|
-
|
|
227
|
+
### The widget holds no session state
|
|
228
|
+
|
|
229
|
+
The session reports changes through one `onChange` that
|
|
202
230
|
the page wires up when it creates it. The page therefore owns the session and passes each
|
|
203
231
|
state to `update()`. The widget keeps only what exists only on screen: the typed text, a running
|
|
204
232
|
camera, a button whose call is in flight, and the last rejection.
|
|
205
233
|
|
|
206
|
-
|
|
207
|
-
|
|
234
|
+
### It is a package of its own, not an entry of `httpeers-member`
|
|
235
|
+
|
|
236
|
+
Two reasons:
|
|
208
237
|
|
|
209
238
|
- `httpeers-member` compiles without the DOM lib on purpose (see its `tsconfig.json`), and its
|
|
210
239
|
boundary test keeps DOM out of everything the root reaches. A widget is DOM from top to bottom.
|
|
@@ -217,16 +246,18 @@ camera, a button whose call is in flight, and the last rejection.
|
|
|
217
246
|
`httpeers-qr`, and nothing in `packages/` depends on it except the private conformance leaf. Apps
|
|
218
247
|
never import from other apps, so a package is the only place both apps can share it from.
|
|
219
248
|
|
|
220
|
-
|
|
249
|
+
### Nothing happens at import
|
|
250
|
+
|
|
251
|
+
The widget touches no `document`, `window` or stylesheet until
|
|
221
252
|
`mountJoinWidget` runs, and the camera library is loaded by the first scan.
|
|
222
253
|
`tests/import.test.ts` imports the built `dist/` under plain Node to check both.
|
|
223
254
|
`httpeers-conformance`'s `runtime-import.test.ts` imports the entry again, together with every
|
|
224
255
|
other published entry.
|
|
225
256
|
|
|
226
|
-
|
|
257
|
+
### Pages bundle `dist/`, so rebuild after changing `src/`
|
|
227
258
|
|
|
228
|
-
The
|
|
229
|
-
`
|
|
259
|
+
The `exports` map has a `source` condition, but the apps' Vite configs do not enable it (no
|
|
260
|
+
`resolve.conditions`), so **pages bundle `dist/`**, which is gitignored and holds whatever was last
|
|
230
261
|
built. After changing `src/`:
|
|
231
262
|
|
|
232
263
|
```sh
|
|
@@ -237,7 +268,7 @@ pnpm --filter @statewalker/httpeers-llm-chat build # and/or the demos
|
|
|
237
268
|
Then check that the page's bundle hash changed. If it did not, the change did not reach the
|
|
238
269
|
bundle. `turbo build` builds this package before the apps because they depend on it.
|
|
239
270
|
|
|
240
|
-
|
|
271
|
+
### What the tests cover
|
|
241
272
|
|
|
242
273
|
```sh
|
|
243
274
|
pnpm --filter @statewalker/httpeers-join test # build, typecheck, vitest
|
|
@@ -263,3 +294,7 @@ pnpm --filter @statewalker/httpeers-join test # build, typecheck, vitest
|
|
|
263
294
|
Nothing here drives a real camera or a real hub. That is covered by the apps' Playwright smokes
|
|
264
295
|
(`apps/demos/scripts/join-smoke.mjs`, `apps/llm-chat/scripts/mesh-smoke.mjs`), which join through
|
|
265
296
|
this widget.
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/httpeers-join",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The join-the-mesh widget: paste or scan an invitation, see the link to the hub, disconnect, reconnect or leave. Plain DOM, no framework.",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/statewalker/httpeers/issues"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/statewalker/httpeers/tree/main/packages/httpeers-join#readme",
|
|
6
10
|
"license": "MIT",
|
|
7
11
|
"repository": {
|
|
8
12
|
"type": "git",
|
|
9
|
-
"url": "git+
|
|
13
|
+
"url": "git+https://github.com/statewalker/httpeers.git",
|
|
10
14
|
"directory": "packages/httpeers-join"
|
|
11
15
|
},
|
|
12
16
|
"main": "./dist/index.js",
|
|
@@ -25,8 +29,8 @@
|
|
|
25
29
|
"sideEffects": false,
|
|
26
30
|
"dependencies": {
|
|
27
31
|
"html5-qrcode": "^2.3.8",
|
|
28
|
-
"@statewalker/httpeers-member": "^0.1.
|
|
29
|
-
"@statewalker/httpeers-qr": "^0.1.
|
|
32
|
+
"@statewalker/httpeers-member": "^0.1.6",
|
|
33
|
+
"@statewalker/httpeers-qr": "^0.1.3"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@biomejs/biome": "^2.5.15",
|