@matter/general 0.16.1 → 0.16.2-alpha.0-20260114-b38c6b796
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/cjs/net/Network.d.ts +2 -0
- package/dist/cjs/net/Network.d.ts.map +1 -1
- package/dist/cjs/net/Network.js +3 -0
- package/dist/cjs/net/Network.js.map +1 -1
- package/dist/cjs/net/RetrySchedule.d.ts +1 -1
- package/dist/cjs/net/ServerAddress.d.ts +52 -14
- package/dist/cjs/net/ServerAddress.d.ts.map +1 -1
- package/dist/cjs/net/ServerAddress.js +41 -6
- package/dist/cjs/net/ServerAddress.js.map +2 -2
- package/dist/cjs/net/ServerAddressSet.d.ts +65 -0
- package/dist/cjs/net/ServerAddressSet.d.ts.map +1 -0
- package/dist/cjs/net/ServerAddressSet.js +144 -0
- package/dist/cjs/net/ServerAddressSet.js.map +6 -0
- package/dist/cjs/net/dns-sd/MdnsSocket.d.ts +40 -0
- package/dist/cjs/net/dns-sd/MdnsSocket.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/MdnsSocket.js +164 -0
- package/dist/cjs/net/dns-sd/MdnsSocket.js.map +6 -0
- package/dist/cjs/net/dns-sd/index.d.ts +7 -0
- package/dist/cjs/net/dns-sd/index.d.ts.map +1 -0
- package/dist/cjs/net/dns-sd/index.js +24 -0
- package/dist/cjs/net/dns-sd/index.js.map +6 -0
- package/dist/cjs/net/index.d.ts +2 -0
- package/dist/cjs/net/index.d.ts.map +1 -1
- package/dist/cjs/net/index.js +2 -0
- package/dist/cjs/net/index.js.map +1 -1
- package/dist/cjs/time/TimeUnit.d.ts +4 -0
- package/dist/cjs/time/TimeUnit.d.ts.map +1 -1
- package/dist/cjs/time/TimeUnit.js +2 -0
- package/dist/cjs/time/TimeUnit.js.map +1 -1
- package/dist/cjs/time/Timestamp.d.ts +1 -1
- package/dist/cjs/time/Timestamp.d.ts.map +1 -1
- package/dist/cjs/time/Timestamp.js +1 -1
- package/dist/cjs/time/Timestamp.js.map +1 -1
- package/dist/cjs/util/Abort.d.ts +9 -0
- package/dist/cjs/util/Abort.d.ts.map +1 -1
- package/dist/cjs/util/Abort.js +20 -0
- package/dist/cjs/util/Abort.js.map +1 -1
- package/dist/cjs/util/Heap.d.ts +84 -0
- package/dist/cjs/util/Heap.d.ts.map +1 -0
- package/dist/cjs/util/Heap.js +286 -0
- package/dist/cjs/util/Heap.js.map +6 -0
- package/dist/cjs/util/Observable.d.ts +29 -6
- package/dist/cjs/util/Observable.d.ts.map +1 -1
- package/dist/cjs/util/Observable.js +40 -6
- package/dist/cjs/util/Observable.js.map +1 -1
- package/dist/cjs/util/Promises.d.ts +3 -0
- package/dist/cjs/util/Promises.d.ts.map +1 -1
- package/dist/cjs/util/Promises.js +33 -3
- package/dist/cjs/util/Promises.js.map +2 -2
- package/dist/cjs/util/Scheduler.d.ts +68 -0
- package/dist/cjs/util/Scheduler.d.ts.map +1 -0
- package/dist/cjs/util/Scheduler.js +207 -0
- package/dist/cjs/util/Scheduler.js.map +6 -0
- package/dist/cjs/util/Semaphore.d.ts +0 -4
- package/dist/cjs/util/Semaphore.d.ts.map +1 -1
- package/dist/cjs/util/Semaphore.js.map +1 -1
- package/dist/cjs/util/Set.d.ts.map +1 -1
- package/dist/cjs/util/Set.js +14 -8
- package/dist/cjs/util/Set.js.map +1 -1
- package/dist/cjs/util/index.d.ts +2 -0
- package/dist/cjs/util/index.d.ts.map +1 -1
- package/dist/cjs/util/index.js +2 -0
- package/dist/cjs/util/index.js.map +1 -1
- package/dist/esm/net/Network.d.ts +2 -0
- package/dist/esm/net/Network.d.ts.map +1 -1
- package/dist/esm/net/Network.js +3 -0
- package/dist/esm/net/Network.js.map +1 -1
- package/dist/esm/net/RetrySchedule.d.ts +1 -1
- package/dist/esm/net/ServerAddress.d.ts +52 -14
- package/dist/esm/net/ServerAddress.d.ts.map +1 -1
- package/dist/esm/net/ServerAddress.js +41 -6
- package/dist/esm/net/ServerAddress.js.map +2 -2
- package/dist/esm/net/ServerAddressSet.d.ts +65 -0
- package/dist/esm/net/ServerAddressSet.d.ts.map +1 -0
- package/dist/esm/net/ServerAddressSet.js +124 -0
- package/dist/esm/net/ServerAddressSet.js.map +6 -0
- package/dist/esm/net/dns-sd/MdnsSocket.d.ts +40 -0
- package/dist/esm/net/dns-sd/MdnsSocket.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/MdnsSocket.js +149 -0
- package/dist/esm/net/dns-sd/MdnsSocket.js.map +6 -0
- package/dist/esm/net/dns-sd/index.d.ts +7 -0
- package/dist/esm/net/dns-sd/index.d.ts.map +1 -0
- package/dist/esm/net/dns-sd/index.js +7 -0
- package/dist/esm/net/dns-sd/index.js.map +6 -0
- package/dist/esm/net/index.d.ts +2 -0
- package/dist/esm/net/index.d.ts.map +1 -1
- package/dist/esm/net/index.js +2 -0
- package/dist/esm/net/index.js.map +1 -1
- package/dist/esm/time/TimeUnit.d.ts +4 -0
- package/dist/esm/time/TimeUnit.d.ts.map +1 -1
- package/dist/esm/time/TimeUnit.js +2 -0
- package/dist/esm/time/TimeUnit.js.map +1 -1
- package/dist/esm/time/Timestamp.d.ts +1 -1
- package/dist/esm/time/Timestamp.d.ts.map +1 -1
- package/dist/esm/time/Timestamp.js +1 -1
- package/dist/esm/time/Timestamp.js.map +1 -1
- package/dist/esm/util/Abort.d.ts +9 -0
- package/dist/esm/util/Abort.d.ts.map +1 -1
- package/dist/esm/util/Abort.js +20 -0
- package/dist/esm/util/Abort.js.map +1 -1
- package/dist/esm/util/Heap.d.ts +84 -0
- package/dist/esm/util/Heap.d.ts.map +1 -0
- package/dist/esm/util/Heap.js +266 -0
- package/dist/esm/util/Heap.js.map +6 -0
- package/dist/esm/util/Observable.d.ts +29 -6
- package/dist/esm/util/Observable.d.ts.map +1 -1
- package/dist/esm/util/Observable.js +40 -6
- package/dist/esm/util/Observable.js.map +1 -1
- package/dist/esm/util/Promises.d.ts +3 -0
- package/dist/esm/util/Promises.d.ts.map +1 -1
- package/dist/esm/util/Promises.js +33 -3
- package/dist/esm/util/Promises.js.map +2 -2
- package/dist/esm/util/Scheduler.d.ts +68 -0
- package/dist/esm/util/Scheduler.d.ts.map +1 -0
- package/dist/esm/util/Scheduler.js +187 -0
- package/dist/esm/util/Scheduler.js.map +6 -0
- package/dist/esm/util/Semaphore.d.ts +0 -4
- package/dist/esm/util/Semaphore.d.ts.map +1 -1
- package/dist/esm/util/Semaphore.js.map +1 -1
- package/dist/esm/util/Set.d.ts.map +1 -1
- package/dist/esm/util/Set.js +14 -8
- package/dist/esm/util/Set.js.map +1 -1
- package/dist/esm/util/index.d.ts +2 -0
- package/dist/esm/util/index.d.ts.map +1 -1
- package/dist/esm/util/index.js +2 -0
- package/dist/esm/util/index.js.map +1 -1
- package/package.json +2 -2
- package/src/net/Network.ts +2 -0
- package/src/net/RetrySchedule.ts +1 -1
- package/src/net/ServerAddress.ts +93 -19
- package/src/net/ServerAddressSet.ts +225 -0
- package/src/net/dns-sd/MdnsSocket.ts +203 -0
- package/src/net/dns-sd/index.ts +7 -0
- package/src/net/index.ts +2 -0
- package/src/time/TimeUnit.ts +5 -0
- package/src/time/Timestamp.ts +1 -1
- package/src/util/Abort.ts +25 -0
- package/src/util/Heap.ts +332 -0
- package/src/util/Observable.ts +74 -10
- package/src/util/Promises.ts +61 -4
- package/src/util/Scheduler.ts +185 -0
- package/src/util/Semaphore.ts +0 -5
- package/src/util/Set.ts +15 -8
- package/src/util/index.ts +2 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2026 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DnsCodec,
|
|
9
|
+
DnsMessage,
|
|
10
|
+
DnsMessagePartiallyPreEncoded,
|
|
11
|
+
DnsMessageType,
|
|
12
|
+
DnsMessageTypeFlag,
|
|
13
|
+
MAX_MDNS_MESSAGE_SIZE,
|
|
14
|
+
} from "#codec/DnsCodec.js";
|
|
15
|
+
import { Diagnostic } from "#log/Diagnostic.js";
|
|
16
|
+
import { Logger } from "#log/Logger.js";
|
|
17
|
+
import { MatterAggregateError } from "#MatterError.js";
|
|
18
|
+
import { Network } from "#net/Network.js";
|
|
19
|
+
import { UdpMulticastServer } from "#net/udp/UdpMulticastServer.js";
|
|
20
|
+
import { Bytes } from "#util/Bytes.js";
|
|
21
|
+
import { Lifetime } from "#util/Lifetime.js";
|
|
22
|
+
import { AsyncObservable, BasicObservable } from "#util/Observable.js";
|
|
23
|
+
import { MaybePromise } from "#util/Promises.js";
|
|
24
|
+
|
|
25
|
+
const logger = Logger.get("MdnsListener");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Manages the UDP socket for other components that implement MDNS logic.
|
|
29
|
+
*/
|
|
30
|
+
export class MdnsSocket {
|
|
31
|
+
#socket: UdpMulticastServer;
|
|
32
|
+
#handlers?: Set<PromiseLike<void>>;
|
|
33
|
+
#isClosed = false;
|
|
34
|
+
#receipt: AsyncObservable<[message: MdnsSocket.Message]> = new BasicObservable(
|
|
35
|
+
error => logger.error("Unhandled error in MDNS listener", error),
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
static async create(
|
|
40
|
+
network: Network,
|
|
41
|
+
options?: { enableIpv4?: boolean; netInterface?: string; lifetime?: Lifetime.Owner },
|
|
42
|
+
) {
|
|
43
|
+
const { enableIpv4 = true, netInterface, lifetime } = options ?? {};
|
|
44
|
+
const socket = new MdnsSocket(
|
|
45
|
+
await UdpMulticastServer.create({
|
|
46
|
+
lifetime,
|
|
47
|
+
network,
|
|
48
|
+
netInterface,
|
|
49
|
+
broadcastAddressIpv4: enableIpv4 ? MdnsSocket.BROADCAST_IPV4 : undefined,
|
|
50
|
+
broadcastAddressIpv6: MdnsSocket.BROADCAST_IPV6,
|
|
51
|
+
listeningPort: MdnsSocket.BROADCAST_PORT,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
return socket;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
constructor(socket: UdpMulticastServer) {
|
|
58
|
+
this.#socket = socket;
|
|
59
|
+
socket.onMessage(this.#handleMessage.bind(this));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get network() {
|
|
63
|
+
return this.#socket.network;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get supportsIpv4() {
|
|
67
|
+
return this.#socket.supportsIpv4;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get netInterface() {
|
|
71
|
+
return this.#socket.netInterface;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get receipt() {
|
|
75
|
+
return this.#receipt;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async send(message: Partial<DnsMessage> & { messageType: DnsMessageType }, intf?: string, unicastDest?: string) {
|
|
79
|
+
const { messageType } = message;
|
|
80
|
+
// When we send Queries that are too long they need to have the Truncated flag set
|
|
81
|
+
const truncatedMessageType = DnsMessageType.isQuery(messageType)
|
|
82
|
+
? messageType | DnsMessageTypeFlag.TC
|
|
83
|
+
: messageType;
|
|
84
|
+
|
|
85
|
+
const chunk: DnsMessagePartiallyPreEncoded = {
|
|
86
|
+
transactionId: 0,
|
|
87
|
+
queries: [],
|
|
88
|
+
authorities: [],
|
|
89
|
+
|
|
90
|
+
...message,
|
|
91
|
+
|
|
92
|
+
answers: [],
|
|
93
|
+
additionalRecords: [],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Note - for size calculations we assume queries are relatively small. We only split answers across messages
|
|
97
|
+
let encodedChunkWithoutAnswers = DnsCodec.encode(chunk);
|
|
98
|
+
let chunkSize = encodedChunkWithoutAnswers.byteLength;
|
|
99
|
+
|
|
100
|
+
// Add answers, splitting message as necessary
|
|
101
|
+
for (const answer of message.answers ?? []) {
|
|
102
|
+
const answerEncoded = DnsCodec.encodeRecord(answer);
|
|
103
|
+
|
|
104
|
+
if (chunkSize + answerEncoded.byteLength > MAX_MDNS_MESSAGE_SIZE) {
|
|
105
|
+
if (chunk.answers.length === 0) {
|
|
106
|
+
// The first answer is already too big, log at least a warning
|
|
107
|
+
logger.warn(
|
|
108
|
+
`MDNS message with ${Diagnostic.json(
|
|
109
|
+
chunk.queries,
|
|
110
|
+
)} is too big to fit into a single MDNS message. Send anyway, but please report!`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// New answer does not fit anymore, send out the message
|
|
115
|
+
// When sending a query, we set the Truncated flag to indicate more answers are available
|
|
116
|
+
await this.#send(
|
|
117
|
+
{
|
|
118
|
+
...chunk,
|
|
119
|
+
messageType: truncatedMessageType,
|
|
120
|
+
},
|
|
121
|
+
intf,
|
|
122
|
+
unicastDest,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Reset the message, length counter and included answers to count for next message
|
|
126
|
+
if (chunk.queries.length) {
|
|
127
|
+
chunk.queries.length = 0;
|
|
128
|
+
encodedChunkWithoutAnswers = DnsCodec.encode(chunk);
|
|
129
|
+
}
|
|
130
|
+
chunk.answers.length = 0;
|
|
131
|
+
chunkSize = encodedChunkWithoutAnswers.byteLength + answerEncoded.byteLength;
|
|
132
|
+
} else {
|
|
133
|
+
chunkSize += answerEncoded.byteLength;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
chunk.answers.push(answerEncoded);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add "additional records"... We include these but only if they fit
|
|
140
|
+
const additionalRecords = message.additionalRecords ?? [];
|
|
141
|
+
for (const additionalRecord of additionalRecords) {
|
|
142
|
+
const additionalRecordEncoded = DnsCodec.encodeRecord(additionalRecord);
|
|
143
|
+
chunkSize += additionalRecordEncoded.byteLength;
|
|
144
|
+
if (chunkSize > MAX_MDNS_MESSAGE_SIZE) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
chunk.additionalRecords.push(additionalRecordEncoded);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await this.#send(chunk, intf, unicastDest);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async #send(message: DnsMessagePartiallyPreEncoded, intf?: string, unicastDest?: string) {
|
|
154
|
+
await this.#socket.send(DnsCodec.encode(message), intf, unicastDest);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async close() {
|
|
158
|
+
this.#isClosed = true;
|
|
159
|
+
await this.#socket.close();
|
|
160
|
+
if (this.#handlers) {
|
|
161
|
+
await MatterAggregateError.allSettled(this.#handlers);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#handleMessage(bytes: Bytes, sourceIp: string, sourceIntf: string) {
|
|
166
|
+
// Ignore if closed
|
|
167
|
+
if (this.#isClosed) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse
|
|
172
|
+
const parsed = DnsCodec.decode(bytes);
|
|
173
|
+
|
|
174
|
+
// Skip unparseable
|
|
175
|
+
if (parsed === undefined) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let promise = this.#receipt.emit({
|
|
180
|
+
...parsed,
|
|
181
|
+
sourceIp,
|
|
182
|
+
sourceIntf,
|
|
183
|
+
}) as MaybePromise;
|
|
184
|
+
|
|
185
|
+
if (MaybePromise.is(promise)) {
|
|
186
|
+
if (this.#handlers === undefined) {
|
|
187
|
+
this.#handlers = new Set();
|
|
188
|
+
}
|
|
189
|
+
promise = Promise.resolve(promise).finally(() => this.#handlers?.delete(promise as PromiseLike<void>));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export namespace MdnsSocket {
|
|
195
|
+
export interface Message extends DnsMessage {
|
|
196
|
+
sourceIp: string;
|
|
197
|
+
sourceIntf: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const BROADCAST_IPV4 = "224.0.0.251";
|
|
201
|
+
export const BROADCAST_IPV6 = "ff02::fb";
|
|
202
|
+
export const BROADCAST_PORT = 5353;
|
|
203
|
+
}
|
package/src/net/index.ts
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
export * from "./AppAddress.js";
|
|
8
8
|
export * from "./Channel.js";
|
|
9
9
|
export * from "./ConnectionlessTransport.js";
|
|
10
|
+
export * from "./dns-sd/index.js";
|
|
10
11
|
export * from "./http/index.js";
|
|
11
12
|
export * from "./mock/index.js";
|
|
12
13
|
export * from "./mqtt/index.js";
|
|
13
14
|
export * from "./Network.js";
|
|
14
15
|
export * from "./RetrySchedule.js";
|
|
15
16
|
export * from "./ServerAddress.js";
|
|
17
|
+
export * from "./ServerAddressSet.js";
|
|
16
18
|
export * from "./udp/index.js";
|
package/src/time/TimeUnit.ts
CHANGED
package/src/time/Timestamp.ts
CHANGED
|
@@ -96,7 +96,7 @@ export namespace Timestamp {
|
|
|
96
96
|
/**
|
|
97
97
|
* Compute the duration between two timestamps.
|
|
98
98
|
*/
|
|
99
|
-
export function delta(from: Timestamp, to: Timestamp) {
|
|
99
|
+
export function delta(from: Timestamp, to: Timestamp = Time.nowMs) {
|
|
100
100
|
return (to - from) as Duration;
|
|
101
101
|
}
|
|
102
102
|
}
|
package/src/util/Abort.ts
CHANGED
|
@@ -96,6 +96,13 @@ export class Abort extends Callable<[reason?: Error]> implements AbortController
|
|
|
96
96
|
return Abort.race(this, ...promises);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Race with throw on abort.
|
|
101
|
+
*/
|
|
102
|
+
async attempt<T>(...promises: Array<T | PromiseLike<T>>) {
|
|
103
|
+
return Abort.attempt(this, ...promises);
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
/**
|
|
100
107
|
* Free resources.
|
|
101
108
|
*
|
|
@@ -115,6 +122,12 @@ export class Abort extends Callable<[reason?: Error]> implements AbortController
|
|
|
115
122
|
this.close();
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
if(condition?: unknown, reason?: Error) {
|
|
126
|
+
if (condition) {
|
|
127
|
+
this.abort(reason);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
get aborted() {
|
|
119
132
|
return this.signal.aborted;
|
|
120
133
|
}
|
|
@@ -262,6 +275,18 @@ export namespace Abort {
|
|
|
262
275
|
return SafePromise.race(promises);
|
|
263
276
|
}
|
|
264
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Race with throw on abort.
|
|
280
|
+
*/
|
|
281
|
+
export async function attempt<T>(signal: Signal | undefined, ...promises: Array<T | PromiseLike<T>>) {
|
|
282
|
+
if (signal && "signal" in signal) {
|
|
283
|
+
signal = signal.signal;
|
|
284
|
+
}
|
|
285
|
+
const result = await race(signal, ...promises);
|
|
286
|
+
signal?.throwIfAborted();
|
|
287
|
+
return result as Awaited<T>;
|
|
288
|
+
}
|
|
289
|
+
|
|
265
290
|
/**
|
|
266
291
|
* Perform abortable sleep.
|
|
267
292
|
*/
|
package/src/util/Heap.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2026 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { InternalError } from "#MatterError.js";
|
|
8
|
+
import { Abort } from "./Abort.js";
|
|
9
|
+
import { Observable } from "./Observable.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A heap, useful as a priority queue.
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
*
|
|
16
|
+
* * Configurable as min-heap or max-heap using comparator
|
|
17
|
+
*
|
|
18
|
+
* * O(1) lookup of first item
|
|
19
|
+
*
|
|
20
|
+
* * O(log(n)) enqueue and dequeue
|
|
21
|
+
*
|
|
22
|
+
* * O(1) deletion of arbitrary position, but will add expense of maintaining index of positions
|
|
23
|
+
*
|
|
24
|
+
* * {@link added}, {@link deleted} and {@link firstChanged} events
|
|
25
|
+
*/
|
|
26
|
+
export class Heap<T> {
|
|
27
|
+
// We encode the heap as an array for efficiency. See #leftChildOf and #rightChildOf for how to navigate the tree
|
|
28
|
+
// when encoded this way
|
|
29
|
+
readonly #buffer = Array<T>();
|
|
30
|
+
|
|
31
|
+
readonly #compare: (a: T, b: T) => number;
|
|
32
|
+
readonly #normalize?: (a: T) => T;
|
|
33
|
+
#firstChanged?: Observable<[T | undefined]>;
|
|
34
|
+
#deleted?: Observable<[T]>;
|
|
35
|
+
#added?: Observable<[T]>;
|
|
36
|
+
#positions?: Map<T, number>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create new heap.
|
|
40
|
+
*
|
|
41
|
+
* @param comparator performs ordering of items in the heap
|
|
42
|
+
* @param normalizer optionally converts items to normal form on insert
|
|
43
|
+
*/
|
|
44
|
+
constructor(comparator: (a: T, b: T) => number, normalizer?: (entry: T) => T) {
|
|
45
|
+
this.#compare = comparator;
|
|
46
|
+
this.#normalize = normalizer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Return lowest-ranked item.
|
|
51
|
+
*/
|
|
52
|
+
shift() {
|
|
53
|
+
if (!this.#buffer?.length) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const result = this.first;
|
|
57
|
+
|
|
58
|
+
this.#deleteAt(0);
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The lowest-ranked item.
|
|
65
|
+
*/
|
|
66
|
+
get first(): T | undefined {
|
|
67
|
+
return this.#buffer?.[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The queue length.
|
|
72
|
+
*/
|
|
73
|
+
get size() {
|
|
74
|
+
return this.#buffer.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Is the heap empty?
|
|
79
|
+
*/
|
|
80
|
+
get isEmpty() {
|
|
81
|
+
return !!this.#buffer.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Emits when the head of the queue changes.
|
|
86
|
+
*/
|
|
87
|
+
get firstChanged() {
|
|
88
|
+
if (!this.#firstChanged) {
|
|
89
|
+
this.#firstChanged = new Observable();
|
|
90
|
+
}
|
|
91
|
+
return this.#firstChanged;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Emits when an item is added to the heap.
|
|
96
|
+
*/
|
|
97
|
+
get added() {
|
|
98
|
+
if (!this.#added) {
|
|
99
|
+
this.#added = new Observable();
|
|
100
|
+
}
|
|
101
|
+
return this.#added;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Emits when an item is deleted.
|
|
106
|
+
*/
|
|
107
|
+
get deleted() {
|
|
108
|
+
if (!this.#deleted) {
|
|
109
|
+
this.#deleted = new Observable();
|
|
110
|
+
}
|
|
111
|
+
return this.#deleted;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add an item.
|
|
116
|
+
*/
|
|
117
|
+
add(...items: T[]) {
|
|
118
|
+
for (let item of items) {
|
|
119
|
+
if (this.#normalize) {
|
|
120
|
+
item = this.#normalize(item);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.#buffer.push(item);
|
|
124
|
+
this.#bubbleUp(this.#buffer.length - 1);
|
|
125
|
+
|
|
126
|
+
this.#added?.emit(item);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete an item.
|
|
132
|
+
*
|
|
133
|
+
* The first delete is O(n); subsequent deletes are O(1) but insertions and deletions will have additional cost
|
|
134
|
+
* associated.
|
|
135
|
+
*/
|
|
136
|
+
delete(item: T) {
|
|
137
|
+
if (this.#buffer === undefined) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!this.#positions) {
|
|
142
|
+
this.#positions = new Map();
|
|
143
|
+
for (let i = 0; i < this.#buffer.length; i++) {
|
|
144
|
+
this.#positions.set(this.#buffer[i], i);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.#normalize) {
|
|
149
|
+
item = this.#normalize(item);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const pos = this.#positions.get(item);
|
|
153
|
+
if (pos === undefined) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.#deleteAt(pos);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Remove all entries.
|
|
163
|
+
*/
|
|
164
|
+
clear() {
|
|
165
|
+
if (this.#buffer.length) this.#buffer.length = 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Stream the first value from the heap until aborted.
|
|
170
|
+
*/
|
|
171
|
+
async *stream(abort?: Abort.Signal) {
|
|
172
|
+
if (Abort.is(abort)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while (true) {
|
|
177
|
+
// Yield values currently available
|
|
178
|
+
while (this.size) {
|
|
179
|
+
yield this.shift()!;
|
|
180
|
+
|
|
181
|
+
if (Abort.is(abort)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create promise to resolve when a new value is ready
|
|
187
|
+
let resolve!: () => void;
|
|
188
|
+
const ready = new Promise<void>(r => (resolve = r));
|
|
189
|
+
|
|
190
|
+
// Wait for new value
|
|
191
|
+
try {
|
|
192
|
+
this.added.once(resolve);
|
|
193
|
+
|
|
194
|
+
await Abort.race(abort, ready);
|
|
195
|
+
|
|
196
|
+
if (Abort.is(abort)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
this.added.off(resolve);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Perform internal validation of queue order.
|
|
207
|
+
*/
|
|
208
|
+
validate() {
|
|
209
|
+
for (let i = 0; i < this.#buffer.length; i++) {
|
|
210
|
+
const leftChild = this.#leftChildOf(i);
|
|
211
|
+
if (leftChild < this.#buffer.length) {
|
|
212
|
+
if (this.#compare(this.#buffer[i], this.#buffer[leftChild]) > 0) {
|
|
213
|
+
throw new InternalError(
|
|
214
|
+
`Heap error: buffer #${i} (${this.#buffer[i]}) is greater than left child #${leftChild} (${this.#buffer[leftChild]})`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const rightChild = this.#rightChildOf(i);
|
|
219
|
+
if (rightChild < this.#buffer.length) {
|
|
220
|
+
if (this.#compare(this.#buffer[i], this.#buffer[rightChild]) > 0) {
|
|
221
|
+
throw new InternalError(
|
|
222
|
+
`Heap error: buffer #${i} (${this.#buffer[i]}) is greater than right child #${rightChild} (${this.#buffer[rightChild]})`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (this.#compare(this.#buffer[leftChild], this.#buffer[rightChild]) > 0) {
|
|
227
|
+
throw new InternalError(
|
|
228
|
+
`Heap error: buffer #${leftChild} (${this.#buffer[leftChild]}) is greater than right sibling #${rightChild} (${this.#buffer[rightChild]})`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#deleteAt(index: number) {
|
|
237
|
+
if (index >= this.#buffer.length) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const item = this.#buffer[index];
|
|
242
|
+
if (this.#buffer.length === 1) {
|
|
243
|
+
this.#buffer.length = 0;
|
|
244
|
+
} else {
|
|
245
|
+
const lastIndex = this.#buffer.length - 1;
|
|
246
|
+
this.#buffer[index] = this.#buffer[lastIndex];
|
|
247
|
+
this.#positions?.set(this.#buffer[index], index);
|
|
248
|
+
this.#buffer.length = lastIndex;
|
|
249
|
+
if (index < this.#buffer.length) {
|
|
250
|
+
if (index) {
|
|
251
|
+
index = this.#bubbleUp(index);
|
|
252
|
+
}
|
|
253
|
+
this.#sinkDown(index);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.#positions?.delete(item);
|
|
258
|
+
|
|
259
|
+
this.#deleted?.emit(item);
|
|
260
|
+
if (!index) {
|
|
261
|
+
this.#firstChanged?.emit(this.first);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#sinkDown(index: number) {
|
|
266
|
+
while (true) {
|
|
267
|
+
let moveTo: number | undefined;
|
|
268
|
+
|
|
269
|
+
const leftChild = this.#leftChildOf(index);
|
|
270
|
+
|
|
271
|
+
if (leftChild < this.#buffer.length) {
|
|
272
|
+
if (this.#compare(this.#buffer[index], this.#buffer[leftChild]) > 0) {
|
|
273
|
+
moveTo = leftChild;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const rightChild = this.#rightChildOf(index);
|
|
277
|
+
|
|
278
|
+
if (rightChild < this.#buffer.length) {
|
|
279
|
+
if (this.#compare(this.#buffer[moveTo ?? index], this.#buffer[rightChild]) > 0) {
|
|
280
|
+
moveTo = rightChild;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (moveTo === undefined) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.#swap(index, moveTo);
|
|
290
|
+
index = moveTo;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#bubbleUp(index: number) {
|
|
295
|
+
while (index) {
|
|
296
|
+
const parent = this.#parentOf(index);
|
|
297
|
+
|
|
298
|
+
if (this.#compare(this.#buffer[parent], this.#buffer[index]) <= 0) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.#swap(index, parent);
|
|
303
|
+
index = parent;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!index) {
|
|
307
|
+
this.#firstChanged?.emit(this.first);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return index;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#swap(index1: number, index2: number) {
|
|
314
|
+
[this.#buffer[index1], this.#buffer[index2]] = [this.#buffer[index2], this.#buffer[index1]];
|
|
315
|
+
if (this.#positions) {
|
|
316
|
+
this.#positions.set(this.#buffer[index1], index1);
|
|
317
|
+
this.#positions.set(this.#buffer[index2], index2);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#leftChildOf(index: number) {
|
|
322
|
+
return 2 * index + 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#rightChildOf(index: number) {
|
|
326
|
+
return 2 * index + 2;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#parentOf(index: number) {
|
|
330
|
+
return Math.floor((index - 1) / 2);
|
|
331
|
+
}
|
|
332
|
+
}
|