@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.
package/dist/connect.d.ts CHANGED
@@ -4146,6 +4146,12 @@ declare class Space {
4146
4146
  data: Uint8Array;
4147
4147
  stat: FileStat;
4148
4148
  }>;
4149
+ /** Release a file's shards (ref--) when the owner deletes or unshares it. At
4150
+ * zero references a shard enters a retention window, then is GC'd. Idempotent-
4151
+ * ish: safe to call once per manifest the owner drops. */
4152
+ releaseFile(manifest: FileManifest): Promise<void>;
4153
+ /** Commit/release a manifest's shards with the accelerator (metering + refcount). */
4154
+ private commitShards;
4149
4155
  private fileStorage;
4150
4156
  /** Wire the RAW_FRAME handler that drives fan-out events. */
4151
4157
  private wireFanout;
@@ -4379,6 +4385,10 @@ declare class SpaceNamespace {
4379
4385
  private spaceCache?;
4380
4386
  /** Spaces built this session, by id — used to route offline send replays. */
4381
4387
  private readonly openSpaces;
4388
+ /** Unique id for THIS device/session — the P2P peer identity. A user's own
4389
+ * devices share a member id, so the mesh MUST distinguish them by this or a
4390
+ * device treats its siblings' signals as its own echo and never connects. */
4391
+ private readonly deviceId;
4382
4392
  constructor(deps: SpaceNamespaceDeps);
4383
4393
  /**
4384
4394
  * Create a new space: generate its keypair (public key = id), register
@@ -5392,15 +5402,20 @@ declare class PeerExchange implements PeerBlockSource {
5392
5402
  * to Space membership. The sender (`from`) is server-stamped and can't be
5393
5403
  * spoofed; the payload is opaque to the server.
5394
5404
  *
5395
- * Envelopes carry a `to` (a member id, or `"*"` for discovery) so the
5396
- * accelerator can optionally unicast peer-pair signaling instead of broadcasting
5397
- * (a small, backward-compatible `pub` enhancement); the client filters by `to`
5398
- * regardless.
5405
+ * Peers are identified by a unique **peer id** (per device/connection), carried
5406
+ * in the envelope's `from`/`to` — NOT by the server-stamped member id. This is
5407
+ * essential: one user's own devices share a member id, so the server's `from`
5408
+ * can't tell them apart (and would make each treat the others' signals as its
5409
+ * own echo). We therefore route entirely on the envelope peer ids and broadcast
5410
+ * to the whole Space, filtering client-side. The server `from` still guarantees
5411
+ * the sender is an authenticated Space member.
5399
5412
  */
5400
5413
  declare const P2P_SUBJECT = "__p2p__";
5401
5414
  type SignalKind = "hello" | "bye" | "offer" | "answer" | "ice";
5402
5415
  interface SignalEnvelope {
5403
- /** Target member id, or `"*"` to broadcast (discovery / departure). */
5416
+ /** Sender's unique PEER id (per device/connection). */
5417
+ from: string;
5418
+ /** Target peer id, or `"*"` to broadcast (discovery / departure). */
5404
5419
  to: string;
5405
5420
  kind: SignalKind;
5406
5421
  data?: unknown;
@@ -5424,7 +5439,9 @@ declare class SpaceSignaler {
5424
5439
  broadcast(kind: SignalKind, data?: unknown): void;
5425
5440
  /**
5426
5441
  * Subscribe to inbound signals addressed to us (or broadcast). Skips our own
5427
- * echoes. Returns an unsubscribe function.
5442
+ * echoes. Returns an unsubscribe function. Routing is entirely by the envelope
5443
+ * peer id (`env.from`/`env.to`), so a user's own devices — which share the
5444
+ * server-stamped `e.from` — are correctly distinguished.
5428
5445
  */
5429
5446
  onSignal(cb: (from: string, kind: SignalKind, data: unknown) => void): () => void;
5430
5447
  }
@@ -2976,7 +2976,7 @@ function base64UrlDecode(s) {
2976
2976
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
2977
2977
  * `package.json` `version`.
2978
2978
  */
2979
- const VERSION = "0.10.4-alpha.0";
2979
+ const VERSION = "0.10.6-alpha.0";
2980
2980
 
2981
2981
  /**
2982
2982
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -7330,12 +7330,46 @@ class Space {
7330
7330
  }
7331
7331
  // -- room-scoped files (no websocket needed) -------------------------------
7332
7332
  async putFile(file, metadata, opts = {}) {
7333
- return this.fileStorage(opts.backgroundUpload)
7333
+ const result = await this.fileStorage(opts.backgroundUpload)
7334
7334
  .writeFileToShards({ data: file, metadata, onProgress: opts.onProgress });
7335
+ // Declare this manifest's shards to the accelerator: attributes the stored
7336
+ // bytes to the owning app (so storage is never unmetered) and reference-
7337
+ // counts the shards for GC. Public shard hashes only — the per-chunk keys
7338
+ // stay inside the manifest, which the caller keeps in a vault/Space.
7339
+ await this.commitShards(result.manifest, "commit");
7340
+ return result;
7335
7341
  }
7336
7342
  async getFile(manifest) {
7337
7343
  return this.fileStorage().readFileFromShards(manifest);
7338
7344
  }
7345
+ /** Release a file's shards (ref--) when the owner deletes or unshares it. At
7346
+ * zero references a shard enters a retention window, then is GC'd. Idempotent-
7347
+ * ish: safe to call once per manifest the owner drops. */
7348
+ async releaseFile(manifest) {
7349
+ await this.commitShards(manifest, "release");
7350
+ }
7351
+ /** Commit/release a manifest's shards with the accelerator (metering + refcount). */
7352
+ async commitShards(manifest, action) {
7353
+ const hashes = manifest.chunks.flatMap((c) => c.shardHashes);
7354
+ if (hashes.length === 0)
7355
+ return;
7356
+ const body = JSON.stringify({ hashes, bytes: manifest.size });
7357
+ for (let attempt = 0; attempt < 3; attempt++) {
7358
+ try {
7359
+ const res = await this.deps.fetch(`${this.deps.httpBaseUrl}/api/storage/${action}`, {
7360
+ method: "POST", headers: { "Content-Type": "application/json" }, body,
7361
+ });
7362
+ if (res.ok)
7363
+ return;
7364
+ }
7365
+ catch { /* retry */ }
7366
+ await new Promise((r) => setTimeout(r, 400 * (attempt + 1)));
7367
+ }
7368
+ // Best-effort: a missed commit leaves the shards to orphan-GC (still not
7369
+ // durable-unmetered long-term); a missed release just delays GC. Reconciled
7370
+ // by the platform sweep. Don't fail the caller's write on a metering hiccup.
7371
+ console.warn(`[muhkoo] storage ${action} did not confirm for ${hashes.length} shard(s)`);
7372
+ }
7339
7373
  fileStorage(backgroundUpload = false) {
7340
7374
  // Global, content-addressed shard store — same store `client.storage`
7341
7375
  // uses, so a file shared into a channel resolves from anywhere by
@@ -8470,10 +8504,13 @@ class PeerExchange {
8470
8504
  * to Space membership. The sender (`from`) is server-stamped and can't be
8471
8505
  * spoofed; the payload is opaque to the server.
8472
8506
  *
8473
- * Envelopes carry a `to` (a member id, or `"*"` for discovery) so the
8474
- * accelerator can optionally unicast peer-pair signaling instead of broadcasting
8475
- * (a small, backward-compatible `pub` enhancement); the client filters by `to`
8476
- * regardless.
8507
+ * Peers are identified by a unique **peer id** (per device/connection), carried
8508
+ * in the envelope's `from`/`to` — NOT by the server-stamped member id. This is
8509
+ * essential: one user's own devices share a member id, so the server's `from`
8510
+ * can't tell them apart (and would make each treat the others' signals as its
8511
+ * own echo). We therefore route entirely on the envelope peer ids and broadcast
8512
+ * to the whole Space, filtering client-side. The server `from` still guarantees
8513
+ * the sender is an authenticated Space member.
8477
8514
  */
8478
8515
  const P2P_SUBJECT = "__p2p__";
8479
8516
  class SpaceSignaler {
@@ -8485,11 +8522,11 @@ class SpaceSignaler {
8485
8522
  }
8486
8523
  /** Send a signal to one peer (or `"*"`). */
8487
8524
  send(to, kind, data) {
8488
- const env = { to, kind, data };
8489
- // Ask the server to unicast for a directed target; broadcast for "*".
8490
- // `to` also stays in the envelope so a broadcast-only server still works
8491
- // (the client filters by it).
8492
- this.space.sendEphemeral(P2P_SUBJECT, env, to !== "*" ? to : undefined);
8525
+ const env = { from: this.myId, to, kind, data };
8526
+ // Always broadcast to the Space: the server can't unicast to a specific
8527
+ // DEVICE (a user's devices share a member id), so we route client-side on
8528
+ // the envelope peer id. Signaling frames are tiny.
8529
+ this.space.sendEphemeral(P2P_SUBJECT, env);
8493
8530
  }
8494
8531
  /** Announce/withdraw P2P availability to all Space members. */
8495
8532
  broadcast(kind, data) {
@@ -8497,18 +8534,22 @@ class SpaceSignaler {
8497
8534
  }
8498
8535
  /**
8499
8536
  * Subscribe to inbound signals addressed to us (or broadcast). Skips our own
8500
- * echoes. Returns an unsubscribe function.
8537
+ * echoes. Returns an unsubscribe function. Routing is entirely by the envelope
8538
+ * peer id (`env.from`/`env.to`), so a user's own devices — which share the
8539
+ * server-stamped `e.from` — are correctly distinguished.
8501
8540
  */
8502
8541
  onSignal(cb) {
8503
8542
  return this.space.onEphemeral((e) => {
8504
- if (e.subject !== P2P_SUBJECT || e.from === this.myId)
8543
+ if (e.subject !== P2P_SUBJECT)
8505
8544
  return;
8506
8545
  const env = e.data;
8507
- if (!env || typeof env.kind !== "string")
8546
+ if (!env || typeof env.kind !== "string" || typeof env.from !== "string")
8508
8547
  return;
8548
+ if (env.from === this.myId)
8549
+ return; // our own device's echo
8509
8550
  if (env.to !== this.myId && env.to !== "*")
8510
8551
  return;
8511
- cb(e.from, env.kind, env.data);
8552
+ cb(env.from, env.kind, env.data);
8512
8553
  });
8513
8554
  }
8514
8555
  }
@@ -9285,6 +9326,10 @@ class SpaceNamespace {
9285
9326
  spaceCache;
9286
9327
  /** Spaces built this session, by id — used to route offline send replays. */
9287
9328
  openSpaces = new Map();
9329
+ /** Unique id for THIS device/session — the P2P peer identity. A user's own
9330
+ * devices share a member id, so the mesh MUST distinguish them by this or a
9331
+ * device treats its siblings' signals as its own echo and never connects. */
9332
+ deviceId = `peer-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)}`;
9288
9333
  constructor(deps) {
9289
9334
  this.deps = deps;
9290
9335
  if (deps.offline?.enabled) {
@@ -9483,7 +9528,7 @@ class SpaceNamespace {
9483
9528
  if (this.deps.p2p?.enabled && isP2pCapable()) {
9484
9529
  space.attachPeerNetwork(new PeerNetwork({
9485
9530
  space,
9486
- myId: memberId,
9531
+ myId: this.deviceId, // per-device peer id, NOT the shared member id
9487
9532
  workerFactory: this.deps.p2p.workerFactory,
9488
9533
  iceServers: this.deps.p2p.iceServers,
9489
9534
  maxPeers: this.deps.p2p.maxPeers,