@ziplayer/net 0.0.0-stage → 0.0.1

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.
Files changed (56) hide show
  1. package/README.md +136 -2
  2. package/dist/NetNode.d.ts +124 -0
  3. package/dist/NetNode.js +593 -0
  4. package/dist/NetNode.js.map +1 -0
  5. package/dist/ZiNet.d.ts +165 -0
  6. package/dist/ZiNet.js +599 -0
  7. package/dist/ZiNet.js.map +1 -0
  8. package/dist/crypto/handshake.d.ts +84 -0
  9. package/dist/crypto/handshake.js +204 -0
  10. package/dist/crypto/handshake.js.map +1 -0
  11. package/dist/crypto/identity.d.ts +44 -0
  12. package/dist/crypto/identity.js +92 -0
  13. package/dist/crypto/identity.js.map +1 -0
  14. package/dist/crypto/session.d.ts +56 -0
  15. package/dist/crypto/session.js +148 -0
  16. package/dist/crypto/session.js.map +1 -0
  17. package/dist/debug.d.ts +56 -0
  18. package/dist/debug.js +51 -0
  19. package/dist/debug.js.map +1 -0
  20. package/dist/ext/NetExtension.d.ts +15 -0
  21. package/dist/ext/NetExtension.js +39 -0
  22. package/dist/ext/NetExtension.js.map +1 -0
  23. package/dist/index.d.ts +28 -0
  24. package/dist/index.js +52 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/protocol/frames.d.ts +22 -0
  27. package/dist/protocol/frames.js +45 -0
  28. package/dist/protocol/frames.js.map +1 -0
  29. package/dist/router.d.ts +38 -0
  30. package/dist/router.js +51 -0
  31. package/dist/router.js.map +1 -0
  32. package/dist/search.d.ts +72 -0
  33. package/dist/search.js +94 -0
  34. package/dist/search.js.map +1 -0
  35. package/dist/stream/StreamImporter.d.ts +37 -0
  36. package/dist/stream/StreamImporter.js +138 -0
  37. package/dist/stream/StreamImporter.js.map +1 -0
  38. package/dist/stream/StreamProvider.d.ts +49 -0
  39. package/dist/stream/StreamProvider.js +157 -0
  40. package/dist/stream/StreamProvider.js.map +1 -0
  41. package/dist/transport/MemoryTransport.d.ts +36 -0
  42. package/dist/transport/MemoryTransport.js +84 -0
  43. package/dist/transport/MemoryTransport.js.map +1 -0
  44. package/dist/transport/MqttTransport.d.ts +26 -0
  45. package/dist/transport/MqttTransport.js +104 -0
  46. package/dist/transport/MqttTransport.js.map +1 -0
  47. package/dist/transport/Transport.d.ts +21 -0
  48. package/dist/transport/Transport.js +18 -0
  49. package/dist/transport/Transport.js.map +1 -0
  50. package/dist/voice/Gateway.d.ts +16 -0
  51. package/dist/voice/Gateway.js +35 -0
  52. package/dist/voice/Gateway.js.map +1 -0
  53. package/dist/voice/RemoteVoice.d.ts +29 -0
  54. package/dist/voice/RemoteVoice.js +80 -0
  55. package/dist/voice/RemoteVoice.js.map +1 -0
  56. package/package.json +53 -5
@@ -0,0 +1,37 @@
1
+ import { Readable } from "node:stream";
2
+ import type { NetNode } from "../NetNode";
3
+ import { type AudioFrame } from "../protocol/frames";
4
+ export interface RemoteStream {
5
+ stream: Readable;
6
+ streamType: string;
7
+ seekable: boolean;
8
+ /** Re-open the stream at `positionMs` from the same owner (seek / URL refresh). */
9
+ recreate: (positionMs: number) => Promise<Readable>;
10
+ }
11
+ export interface StreamImporterOptions {
12
+ /** Receive buffer in bytes; also the maximum credit outstanding. Default 512 KiB. */
13
+ bufferBytes?: number;
14
+ /** Max out-of-order chunks held while waiting for a gap. Default 256. */
15
+ maxReorder?: number;
16
+ openTimeoutMs?: number;
17
+ /** Accepted stream types from peers. Default: compressed audio (Opus containers and `arbitrary`), never raw PCM. */
18
+ allowedStreamTypes?: string[];
19
+ }
20
+ /** Consumer side: asks the owner streamInfo node for audio and exposes it as a normal Readable. */
21
+ export declare class StreamImporter {
22
+ private readonly node;
23
+ private readonly options;
24
+ private readonly streams;
25
+ private nextStreamId;
26
+ private readonly bufferBytes;
27
+ private readonly maxReorder;
28
+ constructor(node: NetNode, options?: StreamImporterOptions);
29
+ /** Called by the owner of the audio handler for every frame received. */
30
+ onFrame(peerId: string, frame: AudioFrame): void;
31
+ open(peerId: string, track: unknown, options?: {
32
+ position?: number;
33
+ fresh?: boolean;
34
+ signal?: AbortSignal;
35
+ }): Promise<RemoteStream>;
36
+ closeAll(): void;
37
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StreamImporter = void 0;
4
+ const node_stream_1 = require("node:stream");
5
+ const StreamProvider_1 = require("./StreamProvider");
6
+ const frames_1 = require("../protocol/frames");
7
+ class IncomingStream extends node_stream_1.Readable {
8
+ streamId;
9
+ peerId;
10
+ bufferBytes;
11
+ grant;
12
+ onDestroy;
13
+ expected = 0;
14
+ received = 0;
15
+ granted;
16
+ pendingChunks = new Map();
17
+ constructor(streamId, peerId, bufferBytes, grant, onDestroy) {
18
+ super({ highWaterMark: bufferBytes });
19
+ this.streamId = streamId;
20
+ this.peerId = peerId;
21
+ this.bufferBytes = bufferBytes;
22
+ this.grant = grant;
23
+ this.onDestroy = onDestroy;
24
+ this.granted = bufferBytes;
25
+ }
26
+ _read() {
27
+ // Node may call _read() before the consumer has drained its read, so re-check once it has.
28
+ queueMicrotask(() => this.topUp());
29
+ }
30
+ /** Every consumption path (flowing mode, async iteration, pipe) calls read(); re-grant credit right after. */
31
+ read(size) {
32
+ const chunk = super.read(size);
33
+ this.topUp();
34
+ return chunk;
35
+ }
36
+ topUp() {
37
+ if (this.destroyed)
38
+ return;
39
+ const outstanding = this.granted - this.received;
40
+ const want = this.bufferBytes - this.readableLength - outstanding;
41
+ if (want >= this.bufferBytes / 4) {
42
+ this.granted += want;
43
+ this.grant(want);
44
+ }
45
+ }
46
+ _destroy(error, callback) {
47
+ this.onDestroy();
48
+ callback(error);
49
+ }
50
+ }
51
+ /** Consumer side: asks the owner streamInfo node for audio and exposes it as a normal Readable. */
52
+ class StreamImporter {
53
+ node;
54
+ options;
55
+ streams = new Map();
56
+ nextStreamId = 1;
57
+ bufferBytes;
58
+ maxReorder;
59
+ constructor(node, options = {}) {
60
+ this.node = node;
61
+ this.options = options;
62
+ this.bufferBytes = options.bufferBytes ?? 512 * 1024;
63
+ this.maxReorder = options.maxReorder ?? 256;
64
+ node.on("sessionClose", (peerId) => {
65
+ for (const s of [...this.streams.values()])
66
+ if (s.peerId === peerId)
67
+ s.destroy(new Error(`Session with ${peerId} closed`));
68
+ });
69
+ node.handle("stream.error", (body, ctx) => {
70
+ this.streams
71
+ .get(`${ctx.from}#${Number(body?.streamId)}`)
72
+ ?.destroy(new Error(String(body?.message ?? "Remote stream error")));
73
+ });
74
+ }
75
+ /** Called by the owner of the audio handler for every frame received. */
76
+ onFrame(peerId, frame) {
77
+ const stream = this.streams.get(`${peerId}#${frame.streamId}`);
78
+ if (!stream || stream.destroyed)
79
+ return;
80
+ if (frame.seq < stream.expected)
81
+ return; // duplicate
82
+ if (frame.seq > stream.expected) {
83
+ if (stream.pendingChunks.size >= this.maxReorder)
84
+ return void stream.destroy(new Error("Too many out-of-order audio frames"));
85
+ stream.pendingChunks.set(frame.seq, frame);
86
+ return;
87
+ }
88
+ let next = frame;
89
+ while (next) {
90
+ stream.expected++;
91
+ stream.pendingChunks.delete(next.seq);
92
+ if (next.payload.length) {
93
+ stream.received += next.payload.length;
94
+ stream.push(next.payload);
95
+ }
96
+ if (next.flags & frames_1.AUDIO_FLAG_EOS) {
97
+ stream.push(null);
98
+ return;
99
+ }
100
+ next = stream.pendingChunks.get(stream.expected);
101
+ }
102
+ }
103
+ async open(peerId, track, options) {
104
+ const streamId = this.nextStreamId++ >>> 0;
105
+ const key = `${peerId}#${streamId}`;
106
+ const incoming = new IncomingStream(streamId, peerId, this.bufferBytes, (bytes) => void this.node.notify(peerId, "stream.credit", { streamId, bytes }).catch(() => { }), () => {
107
+ if (this.streams.delete(key))
108
+ void this.node.notify(peerId, "stream.close", { streamId }).catch(() => { });
109
+ });
110
+ this.streams.set(key, incoming);
111
+ try {
112
+ const reply = await this.node.request(peerId, "stream.open", { streamId, track, position: options?.position ?? 0, fresh: options?.fresh === true, credit: this.bufferBytes }, { timeoutMs: this.options.openTimeoutMs ?? 20_000, signal: options?.signal });
113
+ const allowed = (this.options.allowedStreamTypes ?? StreamProvider_1.DEFAULT_ALLOWED_STREAM_TYPES).map((t) => t.toLowerCase());
114
+ if (!allowed.includes(String(reply.streamType).toLowerCase())) {
115
+ throw new Error(`Peer offered disallowed stream type "${reply.streamType}"`);
116
+ }
117
+ if ((options?.position ?? 0) > 0 && !reply.positionApplied) {
118
+ throw new Error("Stream owner cannot seek this stream");
119
+ }
120
+ return {
121
+ stream: incoming,
122
+ streamType: reply.streamType,
123
+ seekable: reply.seekable,
124
+ recreate: async (positionMs) => (await this.open(peerId, track, { position: positionMs, fresh: true })).stream,
125
+ };
126
+ }
127
+ catch (error) {
128
+ incoming.destroy();
129
+ throw error;
130
+ }
131
+ }
132
+ closeAll() {
133
+ for (const s of [...this.streams.values()])
134
+ s.destroy();
135
+ }
136
+ }
137
+ exports.StreamImporter = StreamImporter;
138
+ //# sourceMappingURL=StreamImporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StreamImporter.js","sourceRoot":"","sources":["../../src/stream/StreamImporter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { Readable } from "node:stream";
2
+ import type { NetNode } from "../NetNode";
3
+ /** The subset of ziplayer's `StreamInfo` the provider needs (keeps this module free of a runtime ziplayer dependency). */
4
+ /**
5
+ * Compressed audio only. Official plugins (YouTube, SoundCloud, TTS...) return `arbitrary` (a compressed container the
6
+ * receiver demuxes exactly as it would locally), so it is allowed; raw PCM is not. Pass `["webm/opus", "ogg/opus", "opus"]`
7
+ * to accept Opus containers only.
8
+ */
9
+ export declare const DEFAULT_ALLOWED_STREAM_TYPES: string[];
10
+ export interface ProvidedStreamInfo {
11
+ stream?: Readable;
12
+ url?: string;
13
+ streamType?: string;
14
+ type?: string;
15
+ recreate?: (position: number) => Promise<Readable>;
16
+ remote?: boolean;
17
+ }
18
+ export interface StreamProviderOptions {
19
+ /** Resolve a stream for a track, typically `(track) => manager.resolveStream(track)`. */
20
+ resolveStream: (track: any, options: {
21
+ fresh?: boolean;
22
+ }) => Promise<ProvidedStreamInfo | null>;
23
+ /** Return false to refuse a track (allowlist of sources, etc.). */
24
+ allow?: (track: any, peerId: string) => boolean | Promise<boolean>;
25
+ /** Stream types that may leave this node. Default: compressed audio (Opus containers and `arbitrary`), never raw PCM. */
26
+ allowedStreamTypes?: string[];
27
+ chunkBytes?: number;
28
+ maxStreamsPerPeer?: number;
29
+ debug?: (message: string, ...args: unknown[]) => void;
30
+ }
31
+ /**
32
+ * Owner-streamInfo side. The node whose plugins resolve the stream keeps the source and pushes encoded
33
+ * audio (webm/ogg/opus, never PCM) to the requesting node, paced by credit the receiver grants.
34
+ */
35
+ export declare class StreamProvider {
36
+ private readonly node;
37
+ private readonly options;
38
+ private readonly open;
39
+ private readonly chunkBytes;
40
+ private readonly allowedTypes;
41
+ constructor(node: NetNode, options: StreamProviderOptions);
42
+ private key;
43
+ private onOpen;
44
+ private onCredit;
45
+ private pump;
46
+ close(peerId: string, streamId: number): void;
47
+ closePeer(peerId: string): void;
48
+ closeAll(): void;
49
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StreamProvider = exports.DEFAULT_ALLOWED_STREAM_TYPES = void 0;
4
+ const node_stream_1 = require("node:stream");
5
+ const frames_1 = require("../protocol/frames");
6
+ /** The subset of ziplayer's `StreamInfo` the provider needs (keeps this module free of a runtime ziplayer dependency). */
7
+ /**
8
+ * Compressed audio only. Official plugins (YouTube, SoundCloud, TTS...) return `arbitrary` (a compressed container the
9
+ * receiver demuxes exactly as it would locally), so it is allowed; raw PCM is not. Pass `["webm/opus", "ogg/opus", "opus"]`
10
+ * to accept Opus containers only.
11
+ */
12
+ exports.DEFAULT_ALLOWED_STREAM_TYPES = ["webm/opus", "ogg/opus", "opus", "arbitrary"];
13
+ /**
14
+ * Owner-streamInfo side. The node whose plugins resolve the stream keeps the source and pushes encoded
15
+ * audio (webm/ogg/opus, never PCM) to the requesting node, paced by credit the receiver grants.
16
+ */
17
+ class StreamProvider {
18
+ node;
19
+ options;
20
+ open = new Map();
21
+ chunkBytes;
22
+ allowedTypes;
23
+ constructor(node, options) {
24
+ this.node = node;
25
+ this.options = options;
26
+ this.allowedTypes = new Set((options.allowedStreamTypes ?? exports.DEFAULT_ALLOWED_STREAM_TYPES).map((t) => t.toLowerCase()));
27
+ this.chunkBytes = Math.max(1024, Math.min(options.chunkBytes ?? 16 * 1024, 256 * 1024));
28
+ node.handle("stream.open", (body, ctx) => this.onOpen(body, ctx.from), { role: "provide-stream" });
29
+ node.handle("stream.credit", (body, ctx) => this.onCredit(body, ctx.from), { role: "provide-stream" });
30
+ node.handle("stream.close", (body, ctx) => this.close(ctx.from, body?.streamId), { role: "provide-stream" });
31
+ node.on("sessionClose", (peerId) => this.closePeer(peerId));
32
+ }
33
+ key(peerId, streamId) {
34
+ return `${peerId}#${streamId}`;
35
+ }
36
+ async onOpen(body, from) {
37
+ const streamId = Number(body?.streamId);
38
+ if (!Number.isInteger(streamId) || streamId < 0 || streamId > 0xffffffff)
39
+ throw new Error("Invalid streamId");
40
+ const track = body?.track;
41
+ if (!track || typeof track !== "object")
42
+ throw new Error("Missing track");
43
+ if (this.options.allow && !(await this.options.allow(track, from)))
44
+ throw new Error("Track refused by provider");
45
+ const perPeer = [...this.open.values()].filter((s) => s.peerId === from).length;
46
+ if (perPeer >= (this.options.maxStreamsPerPeer ?? 8))
47
+ throw new Error("Too many open streams");
48
+ if (this.open.has(this.key(from, streamId)))
49
+ this.close(from, streamId);
50
+ const info = await this.options.resolveStream(track, { fresh: body?.fresh === true });
51
+ if (!info)
52
+ throw new Error("No stream available");
53
+ const streamType = String(info.streamType ?? info.type ?? "arbitrary");
54
+ if (!this.allowedTypes.has(streamType.toLowerCase())) {
55
+ info.stream?.destroy();
56
+ throw new Error(`Stream type "${streamType}" is not transferable (allowed: ${[...this.allowedTypes].join(", ")})`);
57
+ }
58
+ const position = Number(body?.position) > 0 ? Number(body.position) : 0;
59
+ let source = null;
60
+ if (position > 0 && info.recreate) {
61
+ info.stream?.destroy();
62
+ source = await info.recreate(position);
63
+ }
64
+ else if (info.stream) {
65
+ source = info.stream;
66
+ }
67
+ else if (info.url) {
68
+ source = await fetchUrl(info.url);
69
+ }
70
+ if (!source)
71
+ throw new Error("Stream has no readable source");
72
+ const state = {
73
+ peerId: from,
74
+ streamId,
75
+ source,
76
+ credit: Math.max(0, Number(body?.credit) || 0),
77
+ waiter: null,
78
+ closed: false,
79
+ };
80
+ this.open.set(this.key(from, streamId), state);
81
+ // Start pumping after the reply has been queued.
82
+ setImmediate(() => void this.pump(state));
83
+ return { streamId, streamType, seekable: Boolean(info.recreate), positionApplied: position > 0 && Boolean(info.recreate) };
84
+ }
85
+ onCredit(body, from) {
86
+ const state = this.open.get(this.key(from, Number(body?.streamId)));
87
+ if (!state)
88
+ return;
89
+ state.credit += Math.max(0, Number(body?.bytes) || 0);
90
+ state.waiter?.();
91
+ }
92
+ async pump(state) {
93
+ let seq = 0;
94
+ try {
95
+ for await (const chunk of state.source) {
96
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
97
+ for (let offset = 0; offset < buf.length; offset += this.chunkBytes) {
98
+ const part = buf.subarray(offset, offset + this.chunkBytes);
99
+ while (state.credit < part.length && !state.closed) {
100
+ await new Promise((resolve) => (state.waiter = resolve));
101
+ state.waiter = null;
102
+ }
103
+ if (state.closed)
104
+ return;
105
+ state.credit -= part.length;
106
+ await this.node.sendAudio(state.peerId, { streamId: state.streamId, seq: seq++, flags: 0, payload: part });
107
+ }
108
+ }
109
+ if (!state.closed) {
110
+ await this.node.sendAudio(state.peerId, {
111
+ streamId: state.streamId,
112
+ seq: seq++,
113
+ flags: frames_1.AUDIO_FLAG_EOS,
114
+ payload: Buffer.alloc(0),
115
+ });
116
+ }
117
+ }
118
+ catch (error) {
119
+ if (!state.closed) {
120
+ this.options.debug?.(`stream ${state.streamId} failed: ${error.message}`);
121
+ this.node
122
+ .notify(state.peerId, "stream.error", { streamId: state.streamId, message: error.message })
123
+ .catch(() => { });
124
+ }
125
+ }
126
+ finally {
127
+ this.close(state.peerId, state.streamId);
128
+ }
129
+ }
130
+ close(peerId, streamId) {
131
+ const k = this.key(peerId, Number(streamId));
132
+ const state = this.open.get(k);
133
+ if (!state)
134
+ return;
135
+ state.closed = true;
136
+ this.open.delete(k);
137
+ state.waiter?.();
138
+ state.source.destroy();
139
+ }
140
+ closePeer(peerId) {
141
+ for (const s of [...this.open.values()])
142
+ if (s.peerId === peerId)
143
+ this.close(peerId, s.streamId);
144
+ }
145
+ closeAll() {
146
+ for (const s of [...this.open.values()])
147
+ this.close(s.peerId, s.streamId);
148
+ }
149
+ }
150
+ exports.StreamProvider = StreamProvider;
151
+ async function fetchUrl(url) {
152
+ const res = await fetch(url);
153
+ if (!res.ok || !res.body)
154
+ throw new Error(`Upstream returned ${res.status}`);
155
+ return node_stream_1.Readable.fromWeb(res.body);
156
+ }
157
+ //# sourceMappingURL=StreamProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StreamProvider.js","sourceRoot":"","sources":["../../src/stream/StreamProvider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { type MessageHandler, type PublishOptions, type Transport } from "./Transport";
2
+ interface Subscription {
3
+ client: MemoryTransport;
4
+ filter: string;
5
+ handler: MessageHandler;
6
+ }
7
+ /** In-process broker for tests and single-process setups. Mimics retained messages and last-will. */
8
+ export declare class MemoryBroker {
9
+ readonly subscriptions: Set<Subscription>;
10
+ readonly retained: Map<string, Uint8Array<ArrayBufferLike>>;
11
+ /** Messages delivered, so tests can assert what an untrusted broker would see. */
12
+ readonly log: Array<{
13
+ topic: string;
14
+ payload: Uint8Array;
15
+ }>;
16
+ /** Optional tamper hook: return null to drop, or a replacement payload. */
17
+ intercept?: (topic: string, payload: Uint8Array) => Uint8Array | null;
18
+ deliver(topic: string, payload: Uint8Array, options?: PublishOptions): void;
19
+ transport(): MemoryTransport;
20
+ }
21
+ export declare class MemoryTransport implements Transport {
22
+ private readonly broker;
23
+ private will?;
24
+ private connected;
25
+ private readonly owned;
26
+ constructor(broker: MemoryBroker);
27
+ setLastWill(topic: string, payload: Uint8Array, options?: PublishOptions): void;
28
+ connect(): Promise<void>;
29
+ /** Graceful close: the last will is NOT published. */
30
+ close(): Promise<void>;
31
+ /** Simulates a crash: drops subscriptions and publishes the last will. */
32
+ kill(): Promise<void>;
33
+ publish(topic: string, payload: Uint8Array, options?: PublishOptions): Promise<void>;
34
+ subscribe(filter: string, handler: MessageHandler): Promise<() => void>;
35
+ }
36
+ export {};
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryTransport = exports.MemoryBroker = void 0;
4
+ const Transport_1 = require("./Transport");
5
+ /** In-process broker for tests and single-process setups. Mimics retained messages and last-will. */
6
+ class MemoryBroker {
7
+ subscriptions = new Set();
8
+ retained = new Map();
9
+ /** Messages delivered, so tests can assert what an untrusted broker would see. */
10
+ log = [];
11
+ /** Optional tamper hook: return null to drop, or a replacement payload. */
12
+ intercept;
13
+ deliver(topic, payload, options) {
14
+ const out = this.intercept ? this.intercept(topic, payload) : payload;
15
+ if (!out)
16
+ return;
17
+ this.log.push({ topic, payload: out });
18
+ if (options?.retain) {
19
+ if (out.length === 0)
20
+ this.retained.delete(topic);
21
+ else
22
+ this.retained.set(topic, out);
23
+ }
24
+ for (const sub of [...this.subscriptions]) {
25
+ if ((0, Transport_1.topicMatches)(sub.filter, topic))
26
+ queueMicrotask(() => sub.handler(topic, out));
27
+ }
28
+ }
29
+ transport() {
30
+ return new MemoryTransport(this);
31
+ }
32
+ }
33
+ exports.MemoryBroker = MemoryBroker;
34
+ class MemoryTransport {
35
+ broker;
36
+ will;
37
+ connected = false;
38
+ owned = new Set();
39
+ constructor(broker) {
40
+ this.broker = broker;
41
+ }
42
+ setLastWill(topic, payload, options) {
43
+ this.will = { topic, payload, options };
44
+ }
45
+ async connect() {
46
+ this.connected = true;
47
+ }
48
+ /** Graceful close: the last will is NOT published. */
49
+ async close() {
50
+ this.connected = false;
51
+ for (const sub of this.owned)
52
+ this.broker.subscriptions.delete(sub);
53
+ this.owned.clear();
54
+ }
55
+ /** Simulates a crash: drops subscriptions and publishes the last will. */
56
+ async kill() {
57
+ const will = this.will;
58
+ await this.close();
59
+ if (will)
60
+ this.broker.deliver(will.topic, will.payload, will.options);
61
+ }
62
+ async publish(topic, payload, options) {
63
+ if (!this.connected)
64
+ throw new Error("MemoryTransport is not connected");
65
+ this.broker.deliver(topic, payload, options);
66
+ }
67
+ async subscribe(filter, handler) {
68
+ if (!this.connected)
69
+ throw new Error("MemoryTransport is not connected");
70
+ const sub = { client: this, filter, handler };
71
+ this.broker.subscriptions.add(sub);
72
+ this.owned.add(sub);
73
+ for (const [topic, payload] of this.broker.retained) {
74
+ if ((0, Transport_1.topicMatches)(filter, topic))
75
+ queueMicrotask(() => handler(topic, payload));
76
+ }
77
+ return () => {
78
+ this.broker.subscriptions.delete(sub);
79
+ this.owned.delete(sub);
80
+ };
81
+ }
82
+ }
83
+ exports.MemoryTransport = MemoryTransport;
84
+ //# sourceMappingURL=MemoryTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemoryTransport.js","sourceRoot":"","sources":["../../src/transport/MemoryTransport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type MessageHandler, type PublishOptions, type Transport } from "./Transport";
2
+ export interface MqttTransportOptions {
3
+ /** e.g. `mqtts://broker.example.com:8883`. Use TLS: node ids and message sizes are visible otherwise. */
4
+ url: string;
5
+ username?: string;
6
+ password?: string;
7
+ clientId?: string;
8
+ /** Extra options passed straight to `mqtt.connect` (TLS certs, keepalive...). */
9
+ connectOptions?: Record<string, unknown>;
10
+ connectTimeoutMs?: number;
11
+ /** MQTT protocol level. Default 5; use 4 (3.1.1) for brokers without v5 support. Nothing here depends on v5-only features. */
12
+ protocolVersion?: 3 | 4 | 5;
13
+ }
14
+ /** MQTT v5 transport. Requires the optional peer dependency `mqtt`. */
15
+ export declare class MqttTransport implements Transport {
16
+ private readonly options;
17
+ private client;
18
+ private will?;
19
+ private readonly handlers;
20
+ constructor(options: MqttTransportOptions);
21
+ setLastWill(topic: string, payload: Uint8Array, options?: PublishOptions): void;
22
+ connect(): Promise<void>;
23
+ close(): Promise<void>;
24
+ publish(topic: string, payload: Uint8Array, options?: PublishOptions): Promise<void>;
25
+ subscribe(filter: string, handler: MessageHandler): Promise<() => void>;
26
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MqttTransport = void 0;
4
+ const Transport_1 = require("./Transport");
5
+ /** MQTT v5 transport. Requires the optional peer dependency `mqtt`. */
6
+ class MqttTransport {
7
+ options;
8
+ client = null;
9
+ will;
10
+ handlers = new Map();
11
+ constructor(options) {
12
+ this.options = options;
13
+ }
14
+ setLastWill(topic, payload, options) {
15
+ this.will = { topic, payload, options };
16
+ }
17
+ async connect() {
18
+ let mqtt;
19
+ try {
20
+ mqtt = require("mqtt");
21
+ }
22
+ catch {
23
+ throw new Error('MqttTransport requires the "mqtt" package: npm install mqtt');
24
+ }
25
+ const opts = {
26
+ protocolVersion: this.options.protocolVersion ?? 5,
27
+ clean: true,
28
+ reconnectPeriod: 2000,
29
+ username: this.options.username,
30
+ password: this.options.password,
31
+ clientId: this.options.clientId,
32
+ ...this.options.connectOptions,
33
+ };
34
+ if (this.will) {
35
+ opts.will = {
36
+ topic: this.will.topic,
37
+ payload: Buffer.from(this.will.payload),
38
+ qos: this.will.options?.qos ?? 1,
39
+ retain: this.will.options?.retain ?? false,
40
+ };
41
+ }
42
+ const client = mqtt.connect(this.options.url, opts);
43
+ this.client = client;
44
+ client.on("message", (topic, payload) => {
45
+ for (const [filter, set] of this.handlers) {
46
+ if (!(0, Transport_1.topicMatches)(filter, topic))
47
+ continue;
48
+ for (const handler of set)
49
+ handler(topic, payload);
50
+ }
51
+ });
52
+ await new Promise((resolve, reject) => {
53
+ const timer = setTimeout(() => reject(new Error("MQTT connect timed out")), this.options.connectTimeoutMs ?? 10_000);
54
+ client.once("connect", () => {
55
+ clearTimeout(timer);
56
+ resolve();
57
+ });
58
+ client.once("error", (error) => {
59
+ clearTimeout(timer);
60
+ reject(error);
61
+ });
62
+ });
63
+ // Clean sessions drop subscriptions on reconnect, so subscribe again.
64
+ client.on("connect", () => {
65
+ for (const filter of this.handlers.keys())
66
+ client.subscribe(filter, { qos: 1 });
67
+ });
68
+ }
69
+ async close() {
70
+ const client = this.client;
71
+ this.client = null;
72
+ this.handlers.clear();
73
+ if (client)
74
+ await new Promise((resolve) => client.end(false, {}, () => resolve()));
75
+ }
76
+ publish(topic, payload, options) {
77
+ const client = this.client;
78
+ if (!client)
79
+ return Promise.reject(new Error("MqttTransport is not connected"));
80
+ return new Promise((resolve, reject) => client.publish(topic, Buffer.from(payload), { qos: options?.qos ?? 1, retain: options?.retain ?? false }, (error) => (error ? reject(error) : resolve())));
81
+ }
82
+ async subscribe(filter, handler) {
83
+ const client = this.client;
84
+ if (!client)
85
+ throw new Error("MqttTransport is not connected");
86
+ let set = this.handlers.get(filter);
87
+ const first = !set;
88
+ if (!set)
89
+ this.handlers.set(filter, (set = new Set()));
90
+ set.add(handler);
91
+ if (first) {
92
+ await new Promise((resolve, reject) => client.subscribe(filter, { qos: 1 }, (error) => (error ? reject(error) : resolve())));
93
+ }
94
+ return () => {
95
+ set.delete(handler);
96
+ if (set.size === 0) {
97
+ this.handlers.delete(filter);
98
+ this.client?.unsubscribe(filter);
99
+ }
100
+ };
101
+ }
102
+ }
103
+ exports.MqttTransport = MqttTransport;
104
+ //# sourceMappingURL=MqttTransport.js.map