@lat-murmeldjur/weeb_3 0.0.321001 → 0.0.323001

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  `weeb-3` is a browser-side Swarm client built in Rust and compiled to WebAssembly.
4
4
  The main `weeb-3` project is the full released browser client, published at [lat-murmeldjur.github.io/weeb-3](https://lat-murmeldjur.github.io/weeb-3), where the client is used together with its own interface.
5
5
 
6
- This npm package is the library edition of that same client. Projects can use the API directly or mount the bundled browser interface with `renderInterface(container)`.
6
+ This npm package is the library edition of that same client. Projects can use the API directly, attach Swarm HLS playback to their own media element, or mount the bundled browser interface with `renderInterface(container)`.
7
7
 
8
8
  Project repository: [github.com/lat-murmeldjur/weeb-3](https://github.com/lat-murmeldjur/weeb-3)
9
9
 
@@ -22,43 +22,29 @@ This package contains the browser-targeted WebAssembly build of the `weeb-3` cli
22
22
  The main exports are:
23
23
 
24
24
  - `Weeb3No103` as the higher-level client interface
25
- - `BootstrapNode` for defining bootstrap peers
26
- - `Weeb3` for lower-level direct access to the underlying client
27
25
 
28
26
  The higher-level `Weeb3No103` interface provides the main methods used by the embedding example:
29
27
 
30
28
  - `start(options?)`
31
- - `connect()`
32
29
  - `networkState()`
33
- - `switchMainnet()` / `switch_mainnet()`
34
- - `switchTestnet()` / `switch_testnet()`
35
- - `switchNetwork(mode)` / `switch_network(mode)`
36
- - `connectProfile(mode)` / `connect_profile(mode)`
30
+ - `switchNetwork(mode)`
37
31
  - `retrieve(address)`
38
32
  - `upload(file, encryption, index_string, add_to_feed, feed_topic)`
39
33
  - `uploadWithRedundancy(file, encryption, redundancy_level, index_string, add_to_feed, feed_topic)`
40
34
  - `postUploadBytesWithRedundancy(bytes, mime, filename, encryption, redundancy_level, add_to_feed, feed_topic)`
41
- - `openStreamFeed(owner, topic)`
42
- - `playHlsStream(owner, topic, media_type, index?)`
43
- - `attachHlsStream(media, owner, topic, options)`
44
- - `detachHlsStream()`
45
- - `configureStreamingRoutes(service_worker_url, route_base)`
35
+ - `attachStream(media, owner, topic, start)`
46
36
  - `renderInterface(container)`
47
37
  - `resetStamp()`
48
38
  - `postPushChunk(data, soc, chunk_address, stamp)`
49
39
 
50
40
  Sequence-feed indexes use Bee's fixed-width eight-byte big-endian encoding.
51
41
 
52
- ## HLS streaming
53
-
54
- HLS playback is an optional dapp integration, not a Bee/Swarm standard. `playHlsStream(...)` displays a stream in the bundled interface after `renderInterface(container)`. For an application-owned `<video>` or `<audio>` element, call `attachHlsStream(media, owner, topic, { start: "beginning" })`; use `"current-window"` for a rolling/live presentation. Call `detachHlsStream()` before removing or replacing that element. The Service Worker must be copied from the package to a same-origin URL whose scope contains the page, then configured with `configureStreamingRoutes(...)`. Canonical mainnet links use `/stream/{owner}/{topic}[/{index}]`; testnet inserts `/testnet` before `/stream`.
55
-
56
42
  ## Basic usage
57
43
 
58
44
  Call `init()` once before creating a client instance so the WebAssembly module is loaded.
59
45
 
60
46
  ```js
61
- import init, { Weeb3No103, BootstrapNode } from "@lat-murmeldjur/weeb_3";
47
+ import init, { Weeb3No103 } from "@lat-murmeldjur/weeb_3";
62
48
 
63
49
  await init();
64
50
 
@@ -69,17 +55,12 @@ weeb3node.start();
69
55
  console.log(await weeb3node.networkState());
70
56
 
71
57
  // Switch explicitly between built-in profiles.
72
- await weeb3node.switchTestnet();
73
- await weeb3node.switchMainnet();
74
-
75
- // Or use the generic form. Accepted values include:
76
- // "mainnet", "gnosis", "1", "testnet", "sepolia", and "10".
77
58
  await weeb3node.switchNetwork("testnet");
78
59
  await weeb3node.switchNetwork("mainnet");
79
60
 
80
61
  // You can still start with explicit browser-dialable bootnodes and network id.
81
62
  const BOOTSTRAP_NODES = [
82
- new BootstrapNode("/ip4/example/tcp/443/wss/p2p/examplePeerId", true),
63
+ { multiaddr: "/ip4/example/tcp/443/wss/p2p/examplePeerId", usable: true },
83
64
  ];
84
65
 
85
66
  weeb3node.start({
@@ -91,41 +72,59 @@ weeb3node.start({
91
72
  weeb3node.start({ testnet: true });
92
73
  ```
93
74
 
75
+ ## Stream playback
76
+
77
+ The application creates and owns the media element. `"beginning"` reconstructs the earliest available feed timeline, while `"live"` opens the current authenticated feed head and starts at the player's safe live-sync position. Calling `attachStream` again switches the same element between them.
78
+
79
+ ```js
80
+ import init, { Weeb3No103 } from "@lat-murmeldjur/weeb_3";
81
+
82
+ await init();
83
+
84
+ const node = new Weeb3No103();
85
+ const video = document.querySelector("video");
86
+ const owner = "6F2728386F8a47ef5EBe323721188e630Ff0FdE9";
87
+ const topic = "b347b89b-933c-424f-a3d1-403bdd270b25";
88
+
89
+ node.start();
90
+ await node.attachStream(video, owner, topic, "beginning");
91
+
92
+ // Later, for example from the application's own Live button:
93
+ await node.attachStream(video, owner, topic, "live");
94
+ ```
95
+
96
+ Public HLS feeds use mainnet. The embedding page must be below the `/weeb-3/` scope and serve the packaged worker at `/weeb-3/service.js`, or already be controlled by a worker implementing the same forwarding protocol.
97
+
94
98
  ## Example corresponding to `example.html`
95
99
 
96
100
  This is a compact npm-import form of the same usage pattern shown in the project's `example.html`:
97
101
 
98
102
  ```js
99
- import init, { Weeb3No103, BootstrapNode } from "@lat-murmeldjur/weeb_3";
103
+ import init, { Weeb3No103 } from "@lat-murmeldjur/weeb_3";
100
104
 
101
105
  await init();
102
106
 
103
107
  const weeb3node = new Weeb3No103();
104
108
 
105
- await weeb3node.switchMainnet();
109
+ await weeb3node.switchNetwork("mainnet");
106
110
 
107
111
  const entries = await weeb3node.retrieve(
108
112
  "695fceb3a8c212cd123e2e40d86ec08b52fe4fe6ca46687ce9ea69b8f05471f6aa25b5d4d41bf78b1db3479c048fd5fd8137ba844604821b71786196306b68e7"
109
113
  );
110
114
  ```
111
115
 
112
- ## Erasure-coding selector
116
+ ## Upload API
113
117
 
114
118
  Legacy upload methods use Bee's Medium level. Explicit upload levels are `0` None, `1` Medium, `2` Strong, `3` Insane, and `4` Paranoid. The generated TypeScript declaration exposes this as `UploadRedundancyLevel = 0 | 1 | 2 | 3 | 4`.
115
119
 
116
- `renderInterface(container)` includes a Medium-default erasure-coding dropdown. A custom interface can populate its own dropdown from the same canonical metadata:
120
+ `renderInterface(container)` includes a Medium-default erasure-coding dropdown. A custom interface can pass the selected numeric level directly:
117
121
 
118
122
  ```js
119
- import init, {
120
- Weeb3No103,
121
- defaultUploadRedundancyLevel,
122
- uploadRedundancyOptions,
123
- } from "@lat-murmeldjur/weeb_3";
123
+ import init, { Weeb3No103 } from "@lat-murmeldjur/weeb_3";
124
124
 
125
125
  await init();
126
126
  const node = new Weeb3No103();
127
- const choices = uploadRedundancyOptions();
128
- const level = defaultUploadRedundancyLevel();
127
+ const level = 1;
129
128
 
130
129
  const result = await node.uploadWithRedundancy(
131
130
  file,
@@ -142,6 +141,6 @@ Retrieval reads the level encoded in the Swarm tree and uses parity when data sh
142
141
  ## Notes
143
142
 
144
143
  - This package is meant for browser applications, not a plain Node.js runtime.
145
- - Use one active `Weeb3No103` node and HLS session per loaded Wasm module.
144
+ - `attachStream` uses the same runtime, connection buildup, retrieval, prefetch, accounting, hls.js or native-HLS player, and recovery logic as the built-in interface.
146
145
  - The package does not publish the standalone site HTML, but `renderInterface(container)` embeds the same interface shell—including its erasure-coding selector—from the Wasm bundle.
147
146
  - The full released browser client remains available in the main project repository and on the project site.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@lat-murmeldjur/weeb_3",
3
3
  "type": "module",
4
4
  "description": "A Swarm client for browsers",
5
- "version": "0.0.321001",
5
+ "version": "0.0.323001",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -42,9 +42,6 @@
42
42
  "types": "./weeb_3.d.ts",
43
43
  "import": "./weeb_3.js",
44
44
  "default": "./weeb_3.js"
45
- },
46
- "./service.js": {
47
- "default": "./service.js"
48
45
  }
49
46
  },
50
47
  "dependencies": {
package/service.js CHANGED
@@ -6,13 +6,11 @@ const NETWORK_ROUTE_PREFIXES = ["", "mainnet/", "testnet/"];
6
6
  const RAW_ROUTE_KINDS = [
7
7
  ["hls/bytes", "hls-bytes"],
8
8
  ["bytes", "bytes"],
9
- ["chunks", "chunk"],
10
- ["chunk", "chunk"]
9
+ ["chunks", "chunk"]
11
10
  ];
12
11
  const FETCH_TIMEOUT_MS = 240000;
13
12
  const SERVICE_WORKER_MARKER = "forwarder-default20";
14
13
  const SERVICE_WORKER_PROTOCOL = 5;
15
- const DEBUG_SERVICE_WORKER = false;
16
14
  const MIB_BYTES = 1024 * 1024;
17
15
  const STREAM_STORAGE_WINDOW_BYTES = MIB_BYTES / 2;
18
16
  const STREAM_LOOKAHEAD_CHUNKS = 8;
@@ -22,12 +20,6 @@ const CLIENT_RUNTIME_PROBE_TIMEOUT_MS = 1_500;
22
20
 
23
21
  console.log(`weeb-3 service worker start ${SERVICE_WORKER_MARKER}`);
24
22
 
25
- function debugLog(...args) {
26
- if (DEBUG_SERVICE_WORKER) {
27
- console.log(...args);
28
- }
29
- }
30
-
31
23
  function logServiceWorkerVersion(reason) {
32
24
  console.log(`weeb-3 service worker ${reason} ${SERVICE_WORKER_MARKER}`);
33
25
  }
@@ -79,32 +71,17 @@ function isCanonicalStreamTopic(value) {
79
71
  }
80
72
  }
81
73
 
82
- function isCanonicalStreamIndex(value) {
83
- if (value === undefined) {
84
- return true;
85
- }
86
- return /^[0-9]+$/.test(value) && BigInt(value) <= 18446744073709551615n;
87
- }
88
-
89
74
  function isDirectShareShellPath(pathname) {
90
75
  if (!pathname.startsWith(SCOPE_PATH)) {
91
76
  return false;
92
77
  }
93
78
 
94
79
  const parts = pathname.substring(SCOPE_PATH.length).split("/");
95
- if (parts[0] === "testnet") {
96
- parts.shift();
97
- }
98
-
99
- const kind = parts.shift();
100
- if (kind !== "stream") {
101
- return false;
102
- }
103
-
104
- return (parts.length === 2 || parts.length === 3) &&
105
- /^(?:0[xX])?[a-fA-F0-9]{40}$/.test(parts[0] || "") &&
106
- isCanonicalStreamTopic(parts[1]) &&
107
- isCanonicalStreamIndex(parts[2]);
80
+ const streamOffset = parts[0] === "live" ? 1 : 0;
81
+ return parts.length === streamOffset + 3 &&
82
+ parts[streamOffset] === "stream" &&
83
+ /^[a-fA-F0-9]{40}$/.test(parts[streamOffset + 1]) &&
84
+ isCanonicalStreamTopic(parts[streamOffset + 2]);
108
85
  }
109
86
 
110
87
  function isBzzUploadPath(pathname) {
@@ -138,21 +115,26 @@ function canonicalBzzResource(url) {
138
115
  }
139
116
 
140
117
  function canonicalRawResource(url) {
141
- for (const [marker] of rawRouteMarkers()) {
118
+ for (const [marker, rawType] of rawRouteMarkers()) {
142
119
  if (!url.pathname.startsWith(marker)) {
143
120
  continue;
144
121
  }
145
122
 
146
- const resource = url.pathname.substring(marker.length);
147
- if (!resource) {
123
+ const encodedResource = url.pathname.substring(marker.length);
124
+ if (!encodedResource) {
148
125
  return null;
149
126
  }
150
127
 
128
+ let resource;
151
129
  try {
152
- return decodeURIComponent(resource);
130
+ resource = decodeURIComponent(encodedResource);
153
131
  } catch (_) {
154
- return resource;
132
+ resource = encodedResource;
155
133
  }
134
+ if (rawType === "hls-bytes" && !isSwarmReference(resource)) {
135
+ return null;
136
+ }
137
+ return resource;
156
138
  }
157
139
 
158
140
  return null;
@@ -180,15 +162,6 @@ function canonicalFeedResource(url) {
180
162
  return null;
181
163
  }
182
164
 
183
- function isCanonicalRequest(request) {
184
- try {
185
- const url = new URL(request.url);
186
- return canonicalBzzResource(url) !== null || canonicalRawResource(url) !== null;
187
- } catch (_) {
188
- return false;
189
- }
190
- }
191
-
192
165
  function isAppShellNavigation(request) {
193
166
  const headerDestination = request.headers.get("Sec-Fetch-Dest") || "";
194
167
  return request.method === "GET" &&
@@ -295,9 +268,6 @@ self.addEventListener("fetch", (event) => {
295
268
  return;
296
269
  }
297
270
 
298
- // This worker is safe to mount in an npm application: requests outside the
299
- // explicit weeb-3 routes remain entirely under the host app/browser's
300
- // normal fetch and caching policy.
301
271
  });
302
272
 
303
273
  function clientInScope(client) {
@@ -382,10 +352,7 @@ async function firstReadyClient(candidates, requiredNetworkId) {
382
352
  return [];
383
353
  }
384
354
 
385
- // Start the cheap liveness/network probes together so stale tabs add at
386
- // most one probe timeout. Preserve the originating client's fast path and
387
- // candidate priority; the accounting-sensitive operation itself is still
388
- // sent exactly once by messageFirstClient.
355
+ // Probe candidates concurrently without redispatching work.
389
356
  const probes = candidates.map((candidate) => clientWeeb3NetworkId(candidate));
390
357
  if (await probes[0] === requiredNetworkId) {
391
358
  return [candidates[0]];
@@ -413,12 +380,7 @@ async function requestClients(event, requestUrl, requiredNetworkId) {
413
380
  )
414
381
  );
415
382
 
416
- // HLS fetches originate in the top-level page that attached the shared Rust
417
- // player, whether that page is the bundled shell or an arbitrary npm host.
418
- // Dispatch those directly: the Rust bridge validates networkId before
419
- // retrieval, while a redundant liveness round-trip here can false-timeout
420
- // when a large WASM retrieval wave briefly occupies the browser thread.
421
- // Nested clients retain the probed fallback below.
383
+ // Direct top-level HLS requests skip the redundant liveness probe.
422
384
  if (
423
385
  directHlsRequest &&
424
386
  eventClient &&
@@ -436,9 +398,6 @@ async function requestClients(event, requestUrl, requiredNetworkId) {
436
398
  const seen = new Set();
437
399
  const requestReference = requestUrlObject ? bzzReferenceFromUrl(requestUrlObject) : "";
438
400
 
439
- // Prefer the top-level context that originated the request. A nested BZZ
440
- // site frame does not host the Rust runtime and must fall through to its
441
- // active app-shell tab below.
442
401
  if (eventClient && isTopLevelClient(eventClient) && clientInScope(eventClient)) {
443
402
  pushUniqueClient(candidates, seen, eventClient);
444
403
  }
@@ -532,10 +491,7 @@ function messageFirstClient(clients, message, timeoutMs = FETCH_TIMEOUT_MS) {
532
491
  });
533
492
  }
534
493
 
535
- // Uploads and canonical retrievals can enter postage/accounting state. Send
536
- // each request exactly once. A timeout merely detaches this response port;
537
- // already-dispatched work is deliberately left to drain and is never replayed
538
- // through another tab.
494
+ // Timeouts detach the port; dispatched accounting work is never replayed.
539
495
  return messageClient(clients[0], message, timeoutMs);
540
496
  }
541
497
 
@@ -638,16 +594,6 @@ function toUint8Array(body) {
638
594
  return new Uint8Array();
639
595
  }
640
596
 
641
- function oneChunkResponseBody(body) {
642
- const bytes = toUint8Array(body);
643
- return new ReadableStream({
644
- start(controller) {
645
- controller.enqueue(bytes);
646
- controller.close();
647
- }
648
- });
649
- }
650
-
651
597
  function responseHeaders(headerRows) {
652
598
  const headers = new Headers();
653
599
  for (const row of headerRows || []) {
@@ -774,7 +720,7 @@ async function forwardRequestToRust(request, event) {
774
720
  return new Response(
775
721
  request.method === "HEAD" || status === 304
776
722
  ? null
777
- : oneChunkResponseBody(response.body),
723
+ : toUint8Array(response.body),
778
724
  {
779
725
  status,
780
726
  headers
@@ -788,12 +734,9 @@ async function forwardRequestToRust(request, event) {
788
734
  }
789
735
 
790
736
  function parseUploadRedundancyHeader(value) {
791
- // Bee marks this header `omitempty`, so a present empty value has the same
792
- // Medium default as an omitted header.
793
737
  if (value === null || value === "") {
794
738
  return 1;
795
739
  }
796
- // Bee parses an unsigned base-10 header, so reject Number()-only forms such as hex or exponents.
797
740
  if (!/^[0-9]+$/.test(value)) {
798
741
  return null;
799
742
  }
@@ -1,5 +1,3 @@
1
- // Dynamic `import()` has no CSP-safe Web API callable directly from Wasm.
2
- // Keep this irreducible bridge tiny; player policy and lifecycle live in Rust.
3
1
  export async function loadHls() {
4
2
  const module = await import("hls.js");
5
3
  return module.default ?? module.Hls;