@muhkoo/connect 0.10.4-alpha.0 → 0.10.6-alpha.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.
@@ -5057,7 +5057,7 @@ function base64UrlDecode(s) {
5057
5057
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
5058
5058
  * `package.json` `version`.
5059
5059
  */
5060
- const VERSION = "0.10.4-alpha.0";
5060
+ const VERSION = "0.10.6-alpha.0";
5061
5061
 
5062
5062
  /**
5063
5063
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -7599,12 +7599,46 @@ class Space {
7599
7599
  }
7600
7600
  // -- room-scoped files (no websocket needed) -------------------------------
7601
7601
  async putFile(file, metadata, opts = {}) {
7602
- return this.fileStorage(opts.backgroundUpload)
7602
+ const result = await this.fileStorage(opts.backgroundUpload)
7603
7603
  .writeFileToShards({ data: file, metadata, onProgress: opts.onProgress });
7604
+ // Declare this manifest's shards to the accelerator: attributes the stored
7605
+ // bytes to the owning app (so storage is never unmetered) and reference-
7606
+ // counts the shards for GC. Public shard hashes only — the per-chunk keys
7607
+ // stay inside the manifest, which the caller keeps in a vault/Space.
7608
+ await this.commitShards(result.manifest, "commit");
7609
+ return result;
7604
7610
  }
7605
7611
  async getFile(manifest) {
7606
7612
  return this.fileStorage().readFileFromShards(manifest);
7607
7613
  }
7614
+ /** Release a file's shards (ref--) when the owner deletes or unshares it. At
7615
+ * zero references a shard enters a retention window, then is GC'd. Idempotent-
7616
+ * ish: safe to call once per manifest the owner drops. */
7617
+ async releaseFile(manifest) {
7618
+ await this.commitShards(manifest, "release");
7619
+ }
7620
+ /** Commit/release a manifest's shards with the accelerator (metering + refcount). */
7621
+ async commitShards(manifest, action) {
7622
+ const hashes = manifest.chunks.flatMap((c) => c.shardHashes);
7623
+ if (hashes.length === 0)
7624
+ return;
7625
+ const body = JSON.stringify({ hashes, bytes: manifest.size });
7626
+ for (let attempt = 0; attempt < 3; attempt++) {
7627
+ try {
7628
+ const res = await this.deps.fetch(`${this.deps.httpBaseUrl}/api/storage/${action}`, {
7629
+ method: "POST", headers: { "Content-Type": "application/json" }, body,
7630
+ });
7631
+ if (res.ok)
7632
+ return;
7633
+ }
7634
+ catch { /* retry */ }
7635
+ await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
7636
+ }
7637
+ // Best-effort: a missed commit leaves the shards to orphan-GC (still not
7638
+ // durable-unmetered long-term); a missed release just delays GC. Reconciled
7639
+ // by the platform sweep. Don't fail the caller's write on a metering hiccup.
7640
+ console.warn(`[muhkoo] storage ${action} did not confirm for ${hashes.length} shard(s)`);
7641
+ }
7608
7642
  fileStorage(backgroundUpload = false) {
7609
7643
  // Global, content-addressed shard store — same store `client.storage`
7610
7644
  // uses, so a file shared into a channel resolves from anywhere by
@@ -8739,10 +8773,13 @@ class PeerExchange {
8739
8773
  * to Space membership. The sender (`from`) is server-stamped and can't be
8740
8774
  * spoofed; the payload is opaque to the server.
8741
8775
  *
8742
- * Envelopes carry a `to` (a member id, or `"*"` for discovery) so the
8743
- * accelerator can optionally unicast peer-pair signaling instead of broadcasting
8744
- * (a small, backward-compatible `pub` enhancement); the client filters by `to`
8745
- * regardless.
8776
+ * Peers are identified by a unique **peer id** (per device/connection), carried
8777
+ * in the envelope's `from`/`to` — NOT by the server-stamped member id. This is
8778
+ * essential: one user's own devices share a member id, so the server's `from`
8779
+ * can't tell them apart (and would make each treat the others' signals as its
8780
+ * own echo). We therefore route entirely on the envelope peer ids and broadcast
8781
+ * to the whole Space, filtering client-side. The server `from` still guarantees
8782
+ * the sender is an authenticated Space member.
8746
8783
  */
8747
8784
  const P2P_SUBJECT = "__p2p__";
8748
8785
  class SpaceSignaler {
@@ -8754,11 +8791,11 @@ class SpaceSignaler {
8754
8791
  }
8755
8792
  /** Send a signal to one peer (or `"*"`). */
8756
8793
  send(to, kind, data) {
8757
- const env = { to, kind, data };
8758
- // Ask the server to unicast for a directed target; broadcast for "*".
8759
- // `to` also stays in the envelope so a broadcast-only server still works
8760
- // (the client filters by it).
8761
- this.space.sendEphemeral(P2P_SUBJECT, env, to !== "*" ? to : undefined);
8794
+ const env = { from: this.myId, to, kind, data };
8795
+ // Always broadcast to the Space: the server can't unicast to a specific
8796
+ // DEVICE (a user's devices share a member id), so we route client-side on
8797
+ // the envelope peer id. Signaling frames are tiny.
8798
+ this.space.sendEphemeral(P2P_SUBJECT, env);
8762
8799
  }
8763
8800
  /** Announce/withdraw P2P availability to all Space members. */
8764
8801
  broadcast(kind, data) {
@@ -8766,18 +8803,22 @@ class SpaceSignaler {
8766
8803
  }
8767
8804
  /**
8768
8805
  * Subscribe to inbound signals addressed to us (or broadcast). Skips our own
8769
- * echoes. Returns an unsubscribe function.
8806
+ * echoes. Returns an unsubscribe function. Routing is entirely by the envelope
8807
+ * peer id (`env.from`/`env.to`), so a user's own devices — which share the
8808
+ * server-stamped `e.from` — are correctly distinguished.
8770
8809
  */
8771
8810
  onSignal(cb) {
8772
8811
  return this.space.onEphemeral((e) => {
8773
- if (e.subject !== P2P_SUBJECT || e.from === this.myId)
8812
+ if (e.subject !== P2P_SUBJECT)
8774
8813
  return;
8775
8814
  const env = e.data;
8776
- if (!env || typeof env.kind !== "string")
8815
+ if (!env || typeof env.kind !== "string" || typeof env.from !== "string")
8777
8816
  return;
8817
+ if (env.from === this.myId)
8818
+ return; // our own device's echo
8778
8819
  if (env.to !== this.myId && env.to !== "*")
8779
8820
  return;
8780
- cb(e.from, env.kind, env.data);
8821
+ cb(env.from, env.kind, env.data);
8781
8822
  });
8782
8823
  }
8783
8824
  }
@@ -9554,6 +9595,10 @@ class SpaceNamespace {
9554
9595
  spaceCache;
9555
9596
  /** Spaces built this session, by id — used to route offline send replays. */
9556
9597
  openSpaces = new Map();
9598
+ /** Unique id for THIS device/session — the P2P peer identity. A user's own
9599
+ * devices share a member id, so the mesh MUST distinguish them by this or a
9600
+ * device treats its siblings' signals as its own echo and never connects. */
9601
+ deviceId = `peer-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)}`;
9557
9602
  constructor(deps) {
9558
9603
  this.deps = deps;
9559
9604
  if (deps.offline?.enabled) {
@@ -9752,7 +9797,7 @@ class SpaceNamespace {
9752
9797
  if (this.deps.p2p?.enabled && isP2pCapable()) {
9753
9798
  space.attachPeerNetwork(new PeerNetwork({
9754
9799
  space,
9755
- myId: memberId,
9800
+ myId: this.deviceId, // per-device peer id, NOT the shared member id
9756
9801
  workerFactory: this.deps.p2p.workerFactory,
9757
9802
  iceServers: this.deps.p2p.iceServers,
9758
9803
  maxPeers: this.deps.p2p.maxPeers,