@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/browser/index.js +61 -16
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +23 -6
- package/dist/server/index.js +61 -16
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
5396
|
-
*
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
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
|
-
/**
|
|
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
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
*
|
|
8474
|
-
*
|
|
8475
|
-
*
|
|
8476
|
-
*
|
|
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
|
-
//
|
|
8490
|
-
//
|
|
8491
|
-
//
|
|
8492
|
-
this.space.sendEphemeral(P2P_SUBJECT, env
|
|
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
|
|
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(
|
|
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:
|
|
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,
|