@seriousme/opifex 1.1.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.
Files changed (120) hide show
  1. package/.github/workflows/closeStale.yml +15 -0
  2. package/.github/workflows/codeql-analysis.yml +71 -0
  3. package/.github/workflows/deno-ci.yml +16 -0
  4. package/.github/workflows/nodejs-ci.yml +23 -0
  5. package/.github/workflows/publish.yml +16 -0
  6. package/.vscode/extensions.json +10 -0
  7. package/.vscode/settings.json +12 -0
  8. package/LICENSE.txt +21 -0
  9. package/README.md +34 -0
  10. package/bin/conformanceServer.ts +70 -0
  11. package/bin/demoServer.ts +64 -0
  12. package/bin/mqtt.ts +211 -0
  13. package/client/client.ts +181 -0
  14. package/client/context.ts +197 -0
  15. package/client/deps.ts +36 -0
  16. package/client/handlers/handleConnack.ts +21 -0
  17. package/client/handlers/handlePacket.ts +70 -0
  18. package/client/handlers/handlePuback.ts +10 -0
  19. package/client/handlers/handlePubcomp.ts +13 -0
  20. package/client/handlers/handlePublish.ts +32 -0
  21. package/client/handlers/handlePubrec.ts +24 -0
  22. package/client/handlers/handlePubrel.ts +26 -0
  23. package/client/handlers/handleSuback.ts +14 -0
  24. package/client/handlers/handleUnsuback.ts +11 -0
  25. package/client/mod.ts +11 -0
  26. package/client/store/deps.ts +22 -0
  27. package/client/store/memoryStore.ts +56 -0
  28. package/client/store/store.ts +29 -0
  29. package/deno/README.md +64 -0
  30. package/deno/client.ts +48 -0
  31. package/deno/server.test.ts +14 -0
  32. package/deno/server.ts +31 -0
  33. package/deno/wrapDenoConn.ts +12 -0
  34. package/deno.json +8 -0
  35. package/deno.lock +47 -0
  36. package/dev_utils/dummyConn.ts +95 -0
  37. package/dev_utils/mod.ts +1 -0
  38. package/mqttConn/mqttConn.test.ts +110 -0
  39. package/mqttConn/mqttConn.ts +134 -0
  40. package/mqttPacket/AuthenticationResult.ts +16 -0
  41. package/mqttPacket/BitMask.ts +10 -0
  42. package/mqttPacket/PacketType.ts +21 -0
  43. package/mqttPacket/connack.test.ts +129 -0
  44. package/mqttPacket/connack.ts +37 -0
  45. package/mqttPacket/connect.test.ts +297 -0
  46. package/mqttPacket/connect.ts +159 -0
  47. package/mqttPacket/decoder.test.ts +123 -0
  48. package/mqttPacket/decoder.ts +109 -0
  49. package/mqttPacket/disconnect.test.ts +50 -0
  50. package/mqttPacket/disconnect.ts +21 -0
  51. package/mqttPacket/encoder.test.ts +39 -0
  52. package/mqttPacket/encoder.ts +72 -0
  53. package/mqttPacket/length.test.ts +75 -0
  54. package/mqttPacket/length.ts +57 -0
  55. package/mqttPacket/mod.ts +151 -0
  56. package/mqttPacket/pingreq.test.ts +50 -0
  57. package/mqttPacket/pingreq.ts +22 -0
  58. package/mqttPacket/pingres.test.ts +50 -0
  59. package/mqttPacket/pingres.ts +21 -0
  60. package/mqttPacket/puback.test.ts +55 -0
  61. package/mqttPacket/puback.ts +28 -0
  62. package/mqttPacket/pubcomp.test.ts +55 -0
  63. package/mqttPacket/pubcomp.ts +28 -0
  64. package/mqttPacket/publish.test.ts +198 -0
  65. package/mqttPacket/publish.ts +73 -0
  66. package/mqttPacket/pubrec.test.ts +55 -0
  67. package/mqttPacket/pubrec.ts +28 -0
  68. package/mqttPacket/pubrel.test.ts +55 -0
  69. package/mqttPacket/pubrel.ts +28 -0
  70. package/mqttPacket/suback.test.ts +49 -0
  71. package/mqttPacket/suback.ts +41 -0
  72. package/mqttPacket/subscribe.test.ts +233 -0
  73. package/mqttPacket/subscribe.ts +69 -0
  74. package/mqttPacket/types.ts +27 -0
  75. package/mqttPacket/unsuback.test.ts +55 -0
  76. package/mqttPacket/unsuback.ts +28 -0
  77. package/mqttPacket/unsubscribe.test.ts +136 -0
  78. package/mqttPacket/unsubscribe.ts +48 -0
  79. package/mqttPacket/validators.ts +20 -0
  80. package/node/README.md +46 -0
  81. package/node/client.ts +58 -0
  82. package/node/server.test.ts +14 -0
  83. package/node/server.ts +34 -0
  84. package/node/wrapNodeSocket.ts +54 -0
  85. package/package.json +35 -0
  86. package/persistence/deps.ts +12 -0
  87. package/persistence/memory/memoryPersistence.test.ts +151 -0
  88. package/persistence/memory/memoryPersistence.ts +156 -0
  89. package/persistence/persistence.ts +24 -0
  90. package/persistence/store.ts +14 -0
  91. package/server/context.ts +157 -0
  92. package/server/deps.ts +37 -0
  93. package/server/handlers/handleConnect.ts +73 -0
  94. package/server/handlers/handleDisconnect.ts +6 -0
  95. package/server/handlers/handlePacket.ts +78 -0
  96. package/server/handlers/handlePingreq.ts +8 -0
  97. package/server/handlers/handlePuback.ts +12 -0
  98. package/server/handlers/handlePubcomp.ts +12 -0
  99. package/server/handlers/handlePublish.ts +47 -0
  100. package/server/handlers/handlePubrec.ts +20 -0
  101. package/server/handlers/handlePubrel.ts +24 -0
  102. package/server/handlers/handleSubscribe.ts +47 -0
  103. package/server/handlers/handleUnsubscribe.ts +17 -0
  104. package/server/mod.ts +16 -0
  105. package/server/server.ts +74 -0
  106. package/server/test/handleConnect.test.ts +126 -0
  107. package/server/test/test-handlers.ts +52 -0
  108. package/socket/socket.ts +59 -0
  109. package/timer/timer.test.ts +48 -0
  110. package/timer/timer.ts +45 -0
  111. package/trie/trie.test.ts +147 -0
  112. package/trie/trie.ts +106 -0
  113. package/utils/args.ts +86 -0
  114. package/utils/assert.ts +31 -0
  115. package/utils/asyncQueue.ts +63 -0
  116. package/utils/deferred.ts +12 -0
  117. package/utils/logger.ts +35 -0
  118. package/utils/mod.ts +7 -0
  119. package/utils/nextTick.ts +3 -0
  120. package/utils/platform.ts +4 -0
@@ -0,0 +1,47 @@
1
+ import type { Context } from "../context.ts";
2
+ import {
3
+ PacketType,
4
+ type SubscribePacket,
5
+ type Subscription,
6
+ type Topic,
7
+ } from "../deps.ts";
8
+
9
+ const SubscriptionFailure = 0x80;
10
+ function authorizedToSubscribe(ctx: Context, topicFilter: Topic) {
11
+ if (ctx.handlers.isAuthorizedToSubscribe) {
12
+ return ctx.handlers.isAuthorizedToSubscribe(ctx, topicFilter);
13
+ }
14
+ return true;
15
+ }
16
+
17
+ export async function handleSubscribe(
18
+ ctx: Context,
19
+ packet: SubscribePacket,
20
+ ): Promise<void> {
21
+ // The order of return codes in the SUBACK Packet MUST match the order of
22
+ // Topic Filters in the SUBSCRIBE Packet [MQTT-3.9.3-1].
23
+
24
+ const validSubscriptions: Subscription[] = [];
25
+ const returnCodes = packet.subscriptions.map((sub) => {
26
+ if (ctx.store) {
27
+ if (!authorizedToSubscribe(ctx, sub.topicFilter)) {
28
+ return SubscriptionFailure;
29
+ }
30
+ ctx.persistence.subscribe(ctx.store, sub.topicFilter, sub.qos);
31
+ validSubscriptions.push(sub);
32
+ return sub.qos;
33
+ }
34
+ return SubscriptionFailure; // failure
35
+ });
36
+
37
+ await ctx.send({
38
+ type: PacketType.suback,
39
+ id: packet.id,
40
+ returnCodes: returnCodes,
41
+ });
42
+
43
+ // send any retained messages that match these subscriptions
44
+ if (ctx.store) {
45
+ ctx.persistence.handleRetained(ctx.store.clientId);
46
+ }
47
+ }
@@ -0,0 +1,17 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType, type UnsubscribePacket } from "../deps.ts";
3
+
4
+ export async function handleUnsubscribe(
5
+ ctx: Context,
6
+ packet: UnsubscribePacket,
7
+ ): Promise<void> {
8
+ for (const topic of packet.topicFilters) {
9
+ if (ctx.store) {
10
+ ctx.persistence.unsubscribe(ctx.store, topic);
11
+ }
12
+ }
13
+ await ctx.send({
14
+ type: PacketType.unsuback,
15
+ id: packet.id,
16
+ });
17
+ }
package/server/mod.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * This module provides an MQTT Server interface
3
+ * see the /bin folder for examples
4
+ * @module
5
+ */
6
+ export { AuthenticationResult } from "./deps.ts";
7
+ export type {
8
+ IPersistence,
9
+ SockConn,
10
+ TAuthenticationResult,
11
+ Topic,
12
+ } from "./deps.ts";
13
+ export { MqttServer } from "./server.ts";
14
+ export type { MqttServerOptions } from "./server.ts";
15
+ export { Context } from "./context.ts";
16
+ export type { Handlers } from "./context.ts";
@@ -0,0 +1,74 @@
1
+ import { Context, type Handlers } from "./context.ts";
2
+ import {
3
+ AuthenticationResult,
4
+ type IPersistence,
5
+ logger,
6
+ MemoryPersistence,
7
+ type SockConn,
8
+ type Topic,
9
+ } from "./deps.ts";
10
+
11
+ import { handlePacket } from "./handlers/handlePacket.ts";
12
+
13
+ const defaultIsAuthenticated = (
14
+ _ctx: Context,
15
+ _clientId: string,
16
+ _username: string,
17
+ _password: Uint8Array,
18
+ ) => AuthenticationResult.ok;
19
+
20
+ const defaultIsAuthorized = (_ctx: Context, _topic: Topic) => true;
21
+
22
+ /**
23
+ * The options to configure the MqttServer
24
+ */
25
+ export type MqttServerOptions = {
26
+ persistence?: IPersistence;
27
+ handlers?: Handlers;
28
+ };
29
+
30
+ /** The MqttServer class provides a MQTT server with configurable persistence and
31
+ * authentication/authorization handlers.
32
+ *
33
+ * The default handlers are:
34
+ * - isAuthenticated: always returns ok
35
+ * - isAuthorizedToPublish: always returns true
36
+ * - isAuthorizedToSubscribe: always returns true
37
+ *
38
+ * To customize the handlers, pass in a Handlers object.
39
+ * To customize the persistence, pass in a Persistence object.
40
+ */
41
+
42
+ export class MqttServer {
43
+ handlers: Handlers;
44
+ persistence: IPersistence;
45
+ constructor({
46
+ persistence,
47
+ handlers,
48
+ }: MqttServerOptions) {
49
+ this.persistence = persistence || new MemoryPersistence();
50
+ this.handlers = {
51
+ isAuthenticated: handlers?.isAuthenticated || defaultIsAuthenticated,
52
+ isAuthorizedToPublish: handlers?.isAuthorizedToPublish ||
53
+ defaultIsAuthorized,
54
+ isAuthorizedToSubscribe: handlers?.isAuthorizedToSubscribe ||
55
+ defaultIsAuthorized,
56
+ };
57
+ }
58
+
59
+ async serve(conn: SockConn): Promise<void> {
60
+ const ctx = new Context(this.persistence, conn, this.handlers);
61
+ if (conn.remoteAddr?.transport === "tcp") {
62
+ logger.debug(`socket connected from ${conn.remoteAddr.hostname}`);
63
+ }
64
+ try {
65
+ for await (const packet of ctx.mqttConn) {
66
+ handlePacket(ctx, packet);
67
+ }
68
+ } catch (err) {
69
+ logger.debug(`Error while serving:${err}`);
70
+ } finally {
71
+ ctx.close();
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,126 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { makeDummyQueueSockConn } from "../../dev_utils/mod.ts";
4
+ import {
5
+ type AnyPacket,
6
+ AuthenticationResult,
7
+ PacketType,
8
+ } from "../../mqttPacket/mod.ts";
9
+ import { AsyncQueue, nextTick } from "../../utils/mod.ts";
10
+ import { MqttConn } from "../deps.ts";
11
+ import { MqttServer } from "../mod.ts";
12
+ import { handlers } from "./test-handlers.ts";
13
+
14
+ const txtEncoder = new TextEncoder();
15
+ // logger.level(LogLevel.debug);
16
+
17
+ const connectPacket: AnyPacket = {
18
+ type: PacketType.connect,
19
+ protocolName: "MQTT",
20
+ protocolLevel: 4,
21
+ clientId: "testClient",
22
+ clean: true,
23
+ keepAlive: 0,
24
+ username: "IoTester_1",
25
+ password: txtEncoder.encode("strong_password"),
26
+ will: undefined,
27
+ };
28
+
29
+ const disconnectPacket: AnyPacket = {
30
+ type: PacketType.disconnect,
31
+ };
32
+
33
+ const mqttServer = new MqttServer({ handlers });
34
+
35
+ function startServer(): {
36
+ reader: AsyncIterableIterator<AnyPacket>;
37
+ mqttConn: MqttConn;
38
+ } {
39
+ const reader = new AsyncQueue<Uint8Array>();
40
+ const writer = new AsyncQueue<Uint8Array>();
41
+
42
+ const outputConn = makeDummyQueueSockConn(writer, reader);
43
+ const mqttConn = new MqttConn({ conn: outputConn });
44
+ const inputConn = makeDummyQueueSockConn(reader, writer, () => {
45
+ mqttConn.close();
46
+ });
47
+ mqttServer.serve(inputConn);
48
+ return { reader: mqttConn[Symbol.asyncIterator](), mqttConn };
49
+ }
50
+
51
+ test("Authentication with valid username and password works", async () => {
52
+ const { reader, mqttConn } = startServer();
53
+ mqttConn.send(connectPacket);
54
+ const { value: connack } = await reader.next();
55
+ assert.deepStrictEqual(
56
+ connack.type,
57
+ PacketType.connack,
58
+ "Expect Connack packet",
59
+ );
60
+ if (connack.type === PacketType.connack) {
61
+ assert.deepStrictEqual(
62
+ connack.returnCode,
63
+ AuthenticationResult.ok,
64
+ "Expected OK",
65
+ );
66
+ }
67
+ mqttConn.send(disconnectPacket);
68
+ await nextTick();
69
+ assert.deepStrictEqual(
70
+ mqttConn.isClosed,
71
+ true,
72
+ "Expected connection to be closed",
73
+ );
74
+ });
75
+
76
+ test("Authentication with invalid username fails", async () => {
77
+ const newPacket = Object.assign({}, connectPacket);
78
+ newPacket.username = "wrong";
79
+ const { reader, mqttConn } = startServer();
80
+ mqttConn.send(newPacket);
81
+ const { value: connack } = await reader.next();
82
+ assert.deepStrictEqual(
83
+ connack.type,
84
+ PacketType.connack,
85
+ "Expected Connack packet",
86
+ );
87
+ if (connack.type === PacketType.connack) {
88
+ assert.deepStrictEqual(
89
+ connack.returnCode,
90
+ AuthenticationResult.badUsernameOrPassword,
91
+ "Expected badUsernameOrPassword",
92
+ );
93
+ }
94
+ await nextTick();
95
+ assert.deepStrictEqual(
96
+ mqttConn.isClosed,
97
+ true,
98
+ "Expected connection to be closed",
99
+ );
100
+ });
101
+
102
+ test("Authentication with invalid password fails", async () => {
103
+ const newPacket = Object.assign({}, connectPacket);
104
+ newPacket.password = undefined;
105
+ const { reader, mqttConn } = startServer();
106
+ mqttConn.send(newPacket);
107
+ const { value: connack } = await reader.next();
108
+ assert.deepStrictEqual(
109
+ connack.type,
110
+ PacketType.connack,
111
+ "Expected Connack packet",
112
+ );
113
+ if (connack.type === PacketType.connack) {
114
+ assert.deepStrictEqual(
115
+ connack.returnCode,
116
+ AuthenticationResult.badUsernameOrPassword,
117
+ "Expected badUsernameOrPassword",
118
+ );
119
+ }
120
+ await nextTick();
121
+ assert.deepStrictEqual(
122
+ mqttConn.isClosed,
123
+ true,
124
+ "Expected connection to be closed",
125
+ );
126
+ });
@@ -0,0 +1,52 @@
1
+ import { AuthenticationResult, type Context, type Topic } from "../mod.ts";
2
+ import { logger } from "../../utils/mod.ts";
3
+ import type { TAuthenticationResult } from "../deps.ts";
4
+
5
+ const utf8Decoder = new TextDecoder();
6
+ const userTable = new Map();
7
+ userTable.set("IoTester_1", "strong_password");
8
+ userTable.set("IoTester_2", "strong_password");
9
+ const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
10
+
11
+ function isAuthenticated(
12
+ _ctx: Context,
13
+ clientId: string,
14
+ username: string,
15
+ password: Uint8Array,
16
+ ): TAuthenticationResult {
17
+ const pwd = utf8Decoder.decode(password);
18
+ logger.debug(
19
+ `Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`,
20
+ );
21
+
22
+ if (!userTable.has(username)) {
23
+ if (!strictUsername.test(username)) {
24
+ return AuthenticationResult.badUsernameOrPassword;
25
+ }
26
+ }
27
+
28
+ const pass = userTable.get(username);
29
+ if (pwd === pass) {
30
+ return AuthenticationResult.ok;
31
+ }
32
+ return AuthenticationResult.badUsernameOrPassword;
33
+ }
34
+
35
+ function isAuthorizedToPublish(ctx: Context, topic: Topic): boolean {
36
+ logger.debug(
37
+ `Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`,
38
+ );
39
+ return true;
40
+ }
41
+ function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
42
+ logger.debug(
43
+ `Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`,
44
+ );
45
+ return true;
46
+ }
47
+
48
+ export const handlers = {
49
+ isAuthenticated,
50
+ isAuthorizedToPublish,
51
+ isAuthorizedToSubscribe,
52
+ };
@@ -0,0 +1,59 @@
1
+ export type SockAddr = NetAddr | UnixAddr | undefined;
2
+ export interface NetAddr {
3
+ transport?: "tcp" | "udp";
4
+ hostname: string;
5
+ port: number;
6
+ }
7
+
8
+ export interface UnixAddr {
9
+ transport: "unix" | "unixpacket";
10
+ path: string;
11
+ }
12
+
13
+ export type SockConn = {
14
+ readable: ReadableStream<Uint8Array>;
15
+ writable: WritableStream<Uint8Array>;
16
+ closer: () => void;
17
+ localAddr?: SockAddr;
18
+ remoteAddr?: SockAddr;
19
+ };
20
+
21
+ export class Conn {
22
+ reader: ReadableStreamBYOBReader;
23
+ writer: WritableStreamDefaultWriter<Uint8Array>;
24
+ closed: boolean;
25
+ remoteAddr: SockAddr | undefined;
26
+ closer: () => void;
27
+
28
+ constructor(sockConn: SockConn) {
29
+ this.closed = false;
30
+ this.reader = sockConn.readable.getReader({ mode: "byob" });
31
+ this.writer = sockConn.writable.getWriter();
32
+ this.closer = sockConn.closer;
33
+ this.remoteAddr = sockConn.remoteAddr;
34
+ }
35
+
36
+ async read(buff: Uint8Array): Promise<number | null> {
37
+ const buff2 = new Uint8Array(buff.length);
38
+ const result = await this.reader.read(buff2);
39
+ if (!result.done) {
40
+ buff.set(result.value);
41
+ }
42
+ return result.value?.byteLength || null;
43
+ }
44
+
45
+ write(data: Uint8Array): Promise<number> {
46
+ if (this.closed) {
47
+ return Promise.reject();
48
+ }
49
+ this.writer.write(data);
50
+ return new Promise((resolve) => resolve(data.length));
51
+ }
52
+
53
+ close() {
54
+ if (!this.closed) {
55
+ this.closed = true;
56
+ this.closer();
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,48 @@
1
+ import { Timer } from "./timer.ts";
2
+ import assert from "node:assert/strict";
3
+ import { test } from "node:test";
4
+
5
+ function delay(ms: number) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ test("timer should ring", async () => {
10
+ const wait = 50;
11
+ const start = Date.now();
12
+ let end = Date.now();
13
+ const _timer = new Timer(() => {
14
+ end = Date.now();
15
+ }, wait);
16
+ await delay(wait);
17
+ const timeDiff = end - start - wait;
18
+ assert.deepStrictEqual(timeDiff < 10, true);
19
+ });
20
+
21
+ test("snooze should work", async () => {
22
+ const wait = 50;
23
+ const quarterWait = Math.floor(wait / 4);
24
+ const halfWait = quarterWait * 2;
25
+ const start = Date.now();
26
+ let end = 0;
27
+ const timer = new Timer(() => {
28
+ end = Date.now();
29
+ }, wait);
30
+ await delay(halfWait);
31
+ assert.deepStrictEqual(end, 0);
32
+ timer.reset();
33
+ await delay(halfWait + quarterWait);
34
+ assert.deepStrictEqual(end, 0);
35
+ await delay(wait + halfWait);
36
+ const timeDiff = end - start - (wait + halfWait);
37
+ assert.deepStrictEqual(timeDiff < 15, true, `TimeDiff of ${timeDiff} < 15`);
38
+ });
39
+
40
+ test("clear should work", () => {
41
+ const wait = 50;
42
+ let end = 0;
43
+ const timer = new Timer(() => {
44
+ end = Date.now();
45
+ }, wait);
46
+ timer.clear();
47
+ assert.deepStrictEqual(end, 0);
48
+ });
package/timer/timer.ts ADDED
@@ -0,0 +1,45 @@
1
+ export class Timer {
2
+ readonly delay: number; // delay in microseconds
3
+ // deno-lint-ignore ban-types
4
+ private action: Function; // function to perform when timer expires
5
+ private timer = 0;
6
+ private end = 0;
7
+ private running = false;
8
+
9
+ // deno-lint-ignore ban-types
10
+ constructor(action: Function, delay: number, wait = false) {
11
+ this.delay = delay;
12
+ this.action = action;
13
+ if (!wait) {
14
+ this.reset();
15
+ }
16
+ }
17
+
18
+ private startTimer(delay: number): void {
19
+ this.running = true;
20
+ this.timer = setTimeout(() => this.ring(), delay);
21
+ }
22
+
23
+ private ring(): void {
24
+ const timeLeft = this.end - Date.now();
25
+ // check if delay has passed, else execute action
26
+ if (timeLeft > 0) {
27
+ this.startTimer(timeLeft);
28
+ return;
29
+ }
30
+ this.running = false;
31
+ this.action();
32
+ }
33
+
34
+ reset(): void {
35
+ this.end = Date.now() + this.delay;
36
+ if (!this.running) {
37
+ this.startTimer(this.delay);
38
+ }
39
+ }
40
+
41
+ clear(): void {
42
+ clearTimeout(this.timer);
43
+ this.running = false;
44
+ }
45
+ }
@@ -0,0 +1,147 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { Trie } from "./trie.ts";
4
+
5
+ type Data = Array<[string, number]>;
6
+ type Matches = Array<[string, number[]]>;
7
+ type MatchResult = Array<number | object>;
8
+
9
+ const includesAll = (arr: MatchResult, values: MatchResult) =>
10
+ values.every((v) => arr.includes(v));
11
+
12
+ function doTest(data: Data, matches: Matches) {
13
+ const root = new Trie<number>();
14
+ for (const [key, value] of data) {
15
+ root.add(key, value);
16
+ }
17
+
18
+ for (const [match, result] of matches) {
19
+ assert.ok(includesAll(root.match(match), result), `${match} found`);
20
+ }
21
+ }
22
+
23
+ test("new should create new Trie object", () => {
24
+ const root = new Trie<number>();
25
+ assert.deepStrictEqual(typeof root, "object");
26
+ assert.deepStrictEqual(root instanceof Trie, true);
27
+ });
28
+
29
+ test("match() should find the correct nodes in the trie structure", () => {
30
+ const data: Data = [
31
+ ["foo", 1],
32
+ ["foo/bar", 2],
33
+ ["foo/bar/buzz", 3],
34
+ ["foo/bar/buzz", 4],
35
+ ];
36
+
37
+ const matches: Matches = [
38
+ ["", []],
39
+ ["foo", [1]],
40
+ ["foo/bar", [2]],
41
+ ["foo/bar/buzz", [3, 4]],
42
+ ["foo/bar/buzz/", []],
43
+ ["/foo/bar/buzz/", []],
44
+ ["$SYS/foo/bar/buzz/", []],
45
+ ];
46
+
47
+ doTest(data, matches);
48
+ });
49
+
50
+ test("wildCardOne works", () => {
51
+ const data: Data = [
52
+ ["foo", 1],
53
+ ["foo/+/buzz", 2],
54
+ ["foo/bar/buzz", 3],
55
+ ["foo/bar/buzz", 4],
56
+ ["+/+", 5],
57
+ ["/+", 6],
58
+ ["+", 7],
59
+ ];
60
+
61
+ const matches: Matches = [
62
+ ["", [7]],
63
+ ["foo", [1, 7]],
64
+ ["foo/bar", [5]],
65
+ ["foo/bar/buzz", [2, 3, 4]],
66
+ ["/foo/bar/buzz", []],
67
+ ["/finance", [5, 6]],
68
+ ];
69
+
70
+ doTest(data, matches);
71
+ });
72
+
73
+ test("wildCardSubtree works", () => {
74
+ const data: Data = [
75
+ ["foo", 1],
76
+ ["foo/#", 2],
77
+ ["foo/bar/buzz", 3],
78
+ ["foo/bar/#", 4],
79
+ ["#", 5],
80
+ ["/#", 6],
81
+ ];
82
+
83
+ const matches: Matches = [
84
+ ["", [5]],
85
+ ["foo", [1, 5]],
86
+ ["foo/bar", [2, 5]],
87
+ ["foo/bar/buzz", [2, 3, 4, 5]],
88
+ ["/foo/bar/buzz", [5, 6]],
89
+ ["/finance", [5, 6]],
90
+ ];
91
+
92
+ doTest(data, matches);
93
+ });
94
+
95
+ test("Overlapping wildcards work", () => {
96
+ const data: Data = [
97
+ ["foo/+/buzz", 1],
98
+ ["+/bar/buzz", 2],
99
+ ];
100
+
101
+ const matches: Matches = [["foo/bar/buzz", [1, 2]]];
102
+
103
+ doTest(data, matches);
104
+ });
105
+
106
+ test("Reserved prefixes are excluded from toplevel wildcards", () => {
107
+ const data: Data = [
108
+ ["+/bar/buzz", 1],
109
+ ["#", 2],
110
+ ["$SYS/#", 3],
111
+ ["$SYS/+/bar/buzz", 4],
112
+ ];
113
+
114
+ const matches: Matches = [
115
+ ["foo/bar/buzz", [1, 2]],
116
+ ["$SYS/foo/bar/buzz", [3, 4]],
117
+ ];
118
+
119
+ doTest(data, matches);
120
+ });
121
+
122
+ test("Removal works", () => {
123
+ const root = new Trie<number>();
124
+ root.add("foo/bar", 1);
125
+ root.add("foo/bar", 2);
126
+ assert.ok(includesAll(root.match("foo/bar"), [1, 2]), "two items found");
127
+ root.remove("foo/bar", 2);
128
+ assert.ok(includesAll(root.match("foo/bar"), [1]), "one item found");
129
+ root.remove("foo/bar", 1);
130
+ assert.deepStrictEqual(root.match("foo/bar").length, 0, "no items left");
131
+ });
132
+
133
+ test("Removal of object values works", () => {
134
+ type ComplexValue = { a: number; b: number; c: number };
135
+
136
+ const root = new Trie<ComplexValue>(true);
137
+ const c1: ComplexValue = { a: 1, b: 2, c: 3 };
138
+ const c2: ComplexValue = { a: 2, b: 4, c: 6 };
139
+ root.add("foo/bar", c1);
140
+ root.add("foo/bar", c2);
141
+ assert.ok(includesAll(root.match("foo/bar"), [c1, c2]), "two items found");
142
+ root.remove("foo/bar", c2);
143
+ assert.ok(includesAll(root.match("foo/bar"), [c1]), "one item found");
144
+ root.remove("foo/bar", { a: 1, b: 2, c: 3 });
145
+ assert.ok(includesAll(root.match("foo/bar"), []), "empty array");
146
+ assert.deepStrictEqual(root.match("foo/bar").length, 0, "matches left");
147
+ });