doover-js 0.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.
- package/LICENSE +201 -0
- package/README.md +35 -0
- package/dist/apis/agents-api.d.ts +22 -0
- package/dist/apis/agents-api.js +20 -0
- package/dist/apis/aggregates-api.d.ts +15 -0
- package/dist/apis/aggregates-api.js +21 -0
- package/dist/apis/alarms-api.d.ts +12 -0
- package/dist/apis/alarms-api.js +27 -0
- package/dist/apis/channels-api.d.ts +27 -0
- package/dist/apis/channels-api.js +24 -0
- package/dist/apis/connections-api.d.ts +23 -0
- package/dist/apis/connections-api.js +24 -0
- package/dist/apis/messages-api.d.ts +32 -0
- package/dist/apis/messages-api.js +45 -0
- package/dist/apis/notifications-api.d.ts +23 -0
- package/dist/apis/notifications-api.js +54 -0
- package/dist/apis/permissions-api.d.ts +9 -0
- package/dist/apis/permissions-api.js +18 -0
- package/dist/apis/processors-api.d.ts +21 -0
- package/dist/apis/processors-api.js +45 -0
- package/dist/apis/turn-api.d.ts +7 -0
- package/dist/apis/turn-api.js +12 -0
- package/dist/client/doover-client.d.ts +29 -0
- package/dist/client/doover-client.js +34 -0
- package/dist/gateway/gateway-client.d.ts +37 -0
- package/dist/gateway/gateway-client.js +258 -0
- package/dist/gateway/types.d.ts +135 -0
- package/dist/gateway/types.js +2 -0
- package/dist/http/errors.d.ts +19 -0
- package/dist/http/errors.js +28 -0
- package/dist/http/rest-client.d.ts +35 -0
- package/dist/http/rest-client.js +129 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +41 -0
- package/dist/test/apis.test.d.ts +1 -0
- package/dist/test/apis.test.js +248 -0
- package/dist/test/doover-client.test.d.ts +1 -0
- package/dist/test/doover-client.test.js +29 -0
- package/dist/test/doover-data-provider.test.d.ts +1 -0
- package/dist/test/doover-data-provider.test.js +228 -0
- package/dist/test/gateway-client.test.d.ts +1 -0
- package/dist/test/gateway-client.test.js +235 -0
- package/dist/test/helpers.d.ts +32 -0
- package/dist/test/helpers.js +115 -0
- package/dist/test/rest-client.test.d.ts +1 -0
- package/dist/test/rest-client.test.js +115 -0
- package/dist/test/utils.test.d.ts +1 -0
- package/dist/test/utils.test.js +54 -0
- package/dist/types/common.d.ts +130 -0
- package/dist/types/common.js +2 -0
- package/dist/types/openapi.d.ts +144 -0
- package/dist/types/openapi.js +2 -0
- package/dist/types/viewer.d.ts +70 -0
- package/dist/types/viewer.js +2 -0
- package/dist/utils/snowflake.d.ts +13 -0
- package/dist/utils/snowflake.js +26 -0
- package/dist/viewer/doover-data-provider.d.ts +55 -0
- package/dist/viewer/doover-data-provider.js +191 -0
- package/dist/viewer/path-parsing.d.ts +5 -0
- package/dist/viewer/path-parsing.js +38 -0
- package/package.json +46 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const chai_as_promised_1 = __importDefault(require("chai-as-promised"));
|
|
8
|
+
const mocha_1 = require("mocha");
|
|
9
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
10
|
+
const errors_1 = require("../http/errors");
|
|
11
|
+
const doover_data_provider_1 = require("../viewer/doover-data-provider");
|
|
12
|
+
const snowflake_1 = require("../utils/snowflake");
|
|
13
|
+
const helpers_1 = require("./helpers");
|
|
14
|
+
(0, chai_1.use)(chai_as_promised_1.default);
|
|
15
|
+
(0, mocha_1.describe)("DooverDataProvider", () => {
|
|
16
|
+
(0, mocha_1.beforeEach)(() => {
|
|
17
|
+
(0, helpers_1.installSessionStorageMock)();
|
|
18
|
+
helpers_1.MockWebSocket.reset();
|
|
19
|
+
Object.defineProperty(globalThis, "WebSocket", {
|
|
20
|
+
value: helpers_1.MockWebSocket,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
(0, mocha_1.it)("calls control endpoints for getMe and getAgents", async () => {
|
|
26
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
27
|
+
if (url.endsWith("/users/me")) {
|
|
28
|
+
return (0, helpers_1.createJsonResponse)({ id: "u1" });
|
|
29
|
+
}
|
|
30
|
+
return (0, helpers_1.createJsonResponse)({ agents: [] });
|
|
31
|
+
});
|
|
32
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
33
|
+
dataRestUrl: "https://api.example.com",
|
|
34
|
+
controlApiUrl: "https://control.example.com",
|
|
35
|
+
dataWssUrl: "wss://ws.example.com",
|
|
36
|
+
fetchImpl: fetchMock,
|
|
37
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
38
|
+
});
|
|
39
|
+
await (0, chai_1.expect)(provider.getMe()).to.eventually.deep.equal({ id: "u1" });
|
|
40
|
+
await (0, chai_1.expect)(provider.getAgents()).to.eventually.deep.equal({ agents: [] });
|
|
41
|
+
(0, chai_1.expect)(fetchMock.getCall(0).args[0]).to.equal("https://control.example.com/users/me");
|
|
42
|
+
(0, chai_1.expect)(fetchMock.getCall(1).args[0]).to.equal("https://control.example.com/agents");
|
|
43
|
+
});
|
|
44
|
+
(0, mocha_1.it)("returns undefined for missing identifiers where expected", async () => {
|
|
45
|
+
const fetchMock = (0, helpers_1.createFetchMock)();
|
|
46
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
47
|
+
dataRestUrl: "https://api.example.com",
|
|
48
|
+
controlApiUrl: "https://control.example.com",
|
|
49
|
+
dataWssUrl: "wss://ws.example.com",
|
|
50
|
+
fetchImpl: fetchMock,
|
|
51
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
52
|
+
});
|
|
53
|
+
await (0, chai_1.expect)(provider.getChannels({})).to.eventually.equal(undefined);
|
|
54
|
+
await (0, chai_1.expect)(provider.getChannel({ agentId: "a1" })).to.eventually.equal(undefined);
|
|
55
|
+
await (0, chai_1.expect)(provider.getAggregate({ channelName: "c1" })).to.eventually.equal(undefined);
|
|
56
|
+
await (0, chai_1.expect)(provider.getMessages({})).to.eventually.equal(undefined);
|
|
57
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(0);
|
|
58
|
+
});
|
|
59
|
+
(0, mocha_1.it)("creates channels with public and private options", async () => {
|
|
60
|
+
const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({ name: "c1", owner_id: "a1", is_private: false }));
|
|
61
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
62
|
+
dataRestUrl: "https://api.example.com",
|
|
63
|
+
controlApiUrl: "https://control.example.com",
|
|
64
|
+
dataWssUrl: "wss://ws.example.com",
|
|
65
|
+
fetchImpl: fetchMock,
|
|
66
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
67
|
+
});
|
|
68
|
+
await provider.createChannel({ agentId: "a1", channelName: "ignored" }, "c1");
|
|
69
|
+
await provider.createChannel({ agentId: "a1" }, "c2", { is_private: true });
|
|
70
|
+
const firstBody = JSON.parse(fetchMock.getCall(0).args[1]?.body);
|
|
71
|
+
const secondBody = JSON.parse(fetchMock.getCall(1).args[1]?.body);
|
|
72
|
+
(0, chai_1.expect)(firstBody).to.deep.equal({ is_private: false });
|
|
73
|
+
(0, chai_1.expect)(secondBody).to.deep.equal({ is_private: true });
|
|
74
|
+
});
|
|
75
|
+
(0, mocha_1.it)("fetches messages with the compatibility limit and reverses them", async () => {
|
|
76
|
+
const oldId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|
|
77
|
+
const newId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:10.000Z"));
|
|
78
|
+
const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)([
|
|
79
|
+
{
|
|
80
|
+
id: newId,
|
|
81
|
+
author_id: "u1",
|
|
82
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
83
|
+
data: { v: 2 },
|
|
84
|
+
attachments: [],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: oldId,
|
|
88
|
+
author_id: "u1",
|
|
89
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
90
|
+
data: { v: 1 },
|
|
91
|
+
attachments: [],
|
|
92
|
+
},
|
|
93
|
+
]));
|
|
94
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
95
|
+
dataRestUrl: "https://api.example.com",
|
|
96
|
+
controlApiUrl: "https://control.example.com",
|
|
97
|
+
dataWssUrl: "wss://ws.example.com",
|
|
98
|
+
fetchImpl: fetchMock,
|
|
99
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
100
|
+
});
|
|
101
|
+
const messages = await provider.getMessages({ agentId: "a1", channelName: "c1" }, "cursor");
|
|
102
|
+
(0, chai_1.expect)(messages?.map((message) => message.id)).to.deep.equal([oldId, newId]);
|
|
103
|
+
(0, chai_1.expect)(fetchMock.getCall(0).args[0]).to.equal("https://api.example.com/agents/a1/channels/c1/messages?before=cursor&limit=10");
|
|
104
|
+
});
|
|
105
|
+
(0, mocha_1.it)("prefers channel aggregate data and falls back to the aggregate endpoint", async () => {
|
|
106
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
107
|
+
if (url.endsWith("/channels/c1")) {
|
|
108
|
+
return (0, helpers_1.createJsonResponse)({ name: "c1", owner_id: "a1", is_private: false, aggregate: { data: { x: 1 }, attachments: [] } });
|
|
109
|
+
}
|
|
110
|
+
return (0, helpers_1.createJsonResponse)({ data: { y: 2 }, attachments: [] });
|
|
111
|
+
});
|
|
112
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
113
|
+
dataRestUrl: "https://api.example.com",
|
|
114
|
+
controlApiUrl: "https://control.example.com",
|
|
115
|
+
dataWssUrl: "wss://ws.example.com",
|
|
116
|
+
fetchImpl: fetchMock,
|
|
117
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
118
|
+
});
|
|
119
|
+
await (0, chai_1.expect)(provider.getAggregate({ agentId: "a1", channelName: "c1" })).to.eventually.deep.equal({ data: { x: 1 }, attachments: [] });
|
|
120
|
+
const fallbackFetch = (0, helpers_1.createFetchMock)((url) => {
|
|
121
|
+
if (url.endsWith("/channels/c1")) {
|
|
122
|
+
return (0, helpers_1.createJsonResponse)({ name: "c1", owner_id: "a1", is_private: false });
|
|
123
|
+
}
|
|
124
|
+
return (0, helpers_1.createJsonResponse)({ data: { y: 2 }, attachments: [] });
|
|
125
|
+
});
|
|
126
|
+
const fallbackProvider = new doover_data_provider_1.DooverDataProvider({
|
|
127
|
+
dataRestUrl: "https://api.example.com",
|
|
128
|
+
controlApiUrl: "https://control.example.com",
|
|
129
|
+
dataWssUrl: "wss://ws.example.com",
|
|
130
|
+
fetchImpl: fallbackFetch,
|
|
131
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
132
|
+
});
|
|
133
|
+
await (0, chai_1.expect)(fallbackProvider.getAggregate({ agentId: "a1", channelName: "c1" })).to.eventually.deep.equal({ data: { y: 2 }, attachments: [] });
|
|
134
|
+
});
|
|
135
|
+
(0, mocha_1.it)("maps websocket events to subscription callbacks and unsubscribes when the last listener is removed", async () => {
|
|
136
|
+
const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({}));
|
|
137
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
138
|
+
dataRestUrl: "https://api.example.com",
|
|
139
|
+
controlApiUrl: "https://control.example.com",
|
|
140
|
+
dataWssUrl: "wss://ws.example.com",
|
|
141
|
+
fetchImpl: fetchMock,
|
|
142
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
143
|
+
});
|
|
144
|
+
const onMessage = sinon_1.default.spy();
|
|
145
|
+
const onAggregate = sinon_1.default.spy();
|
|
146
|
+
await provider.subscribeToChannel({ agentId: "a1", channelName: "c1" }, onMessage, onAggregate);
|
|
147
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
148
|
+
ws.open();
|
|
149
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
150
|
+
ws.receive({
|
|
151
|
+
op: 0,
|
|
152
|
+
t: "Ready",
|
|
153
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
154
|
+
});
|
|
155
|
+
const id = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|
|
156
|
+
ws.receive({
|
|
157
|
+
op: 0,
|
|
158
|
+
t: "MessageCreate",
|
|
159
|
+
d: {
|
|
160
|
+
id,
|
|
161
|
+
author_id: "u1",
|
|
162
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
163
|
+
data: {},
|
|
164
|
+
attachments: [],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
ws.receive({
|
|
168
|
+
op: 0,
|
|
169
|
+
t: "ChannelSync",
|
|
170
|
+
d: {
|
|
171
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
172
|
+
aggregate: { data: { x: 1 }, attachments: [] },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
ws.receive({
|
|
176
|
+
op: 0,
|
|
177
|
+
t: "AggregateUpdate",
|
|
178
|
+
d: {
|
|
179
|
+
author_id: "u1",
|
|
180
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
181
|
+
aggregate: { data: { x: 2 }, attachments: [] },
|
|
182
|
+
request_data: { data: {}, attachments: [] },
|
|
183
|
+
organisation_id: "org-1",
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
(0, chai_1.expect)(onMessage.calledWithMatch({ agentId: "a1", channelName: "c1" }, sinon_1.default.match({ id }))).to.equal(true);
|
|
187
|
+
(0, chai_1.expect)(onAggregate.callCount).to.equal(2);
|
|
188
|
+
await provider.unsubscribeFromChannel({ agentId: "a1", channelName: "c1" }, onMessage);
|
|
189
|
+
(0, chai_1.expect)(JSON.parse(ws.sent[ws.sent.length - 1])).to.deep.include({ op: 13 });
|
|
190
|
+
});
|
|
191
|
+
(0, mocha_1.it)("throws validation errors for required channel operations", async () => {
|
|
192
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
193
|
+
dataRestUrl: "https://api.example.com",
|
|
194
|
+
controlApiUrl: "https://control.example.com",
|
|
195
|
+
dataWssUrl: "wss://ws.example.com",
|
|
196
|
+
fetchImpl: (0, helpers_1.createFetchMock)(),
|
|
197
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
198
|
+
});
|
|
199
|
+
(0, chai_1.expect)(() => provider.createChannel({}, "c1")).to.throw(errors_1.DooverValidationError);
|
|
200
|
+
(0, chai_1.expect)(() => provider.archiveChannel({ agentId: "a1" })).to.throw(errors_1.DooverValidationError);
|
|
201
|
+
await (0, chai_1.expect)(provider.unsubscribeFromChannel({ agentId: "a1", channelName: "c1" }, sinon_1.default.spy())).to.be.rejectedWith(errors_1.DooverValidationError);
|
|
202
|
+
});
|
|
203
|
+
(0, mocha_1.it)("returns the compatibility agent info shim and path parsing result", async () => {
|
|
204
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
205
|
+
dataRestUrl: "https://api.example.com",
|
|
206
|
+
controlApiUrl: "https://control.example.com",
|
|
207
|
+
dataWssUrl: "wss://ws.example.com",
|
|
208
|
+
fetchImpl: (0, helpers_1.createFetchMock)(),
|
|
209
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
210
|
+
});
|
|
211
|
+
await (0, chai_1.expect)(provider.getAgentInfo("a1")).to.eventually.deep.equal({
|
|
212
|
+
id: "a1",
|
|
213
|
+
name: "a1",
|
|
214
|
+
display_name: "a1",
|
|
215
|
+
type: "device",
|
|
216
|
+
fa_icon: "fa-solid fa-robot",
|
|
217
|
+
organisation: "Organisation",
|
|
218
|
+
group: "group",
|
|
219
|
+
archived: false,
|
|
220
|
+
fixed_location: { latitude: 0, longitude: 0 },
|
|
221
|
+
extra_config: {},
|
|
222
|
+
});
|
|
223
|
+
(0, chai_1.expect)(provider.getIdentifierFromPath("/a1/c1/path/to/value", new URLSearchParams())).to.deep.equal({
|
|
224
|
+
identifier: { agentId: "a1", channelName: "c1" },
|
|
225
|
+
aggregatePath: "path/to/value",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const mocha_1 = require("mocha");
|
|
8
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
9
|
+
const gateway_client_1 = require("../gateway/gateway-client");
|
|
10
|
+
const snowflake_1 = require("../utils/snowflake");
|
|
11
|
+
const helpers_1 = require("./helpers");
|
|
12
|
+
(0, mocha_1.describe)("GatewayClient", () => {
|
|
13
|
+
(0, mocha_1.beforeEach)(() => {
|
|
14
|
+
helpers_1.MockWebSocket.reset();
|
|
15
|
+
sinon_1.default.useFakeTimers(new Date("2026-01-01T00:00:00.000Z"));
|
|
16
|
+
Object.defineProperty(globalThis, "WebSocket", {
|
|
17
|
+
value: helpers_1.MockWebSocket,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
(0, mocha_1.afterEach)(() => {
|
|
23
|
+
sinon_1.default.restore();
|
|
24
|
+
});
|
|
25
|
+
(0, mocha_1.it)("identifies on hello and resumes after a stored session", () => {
|
|
26
|
+
const client = new gateway_client_1.GatewayClient({
|
|
27
|
+
dataRestUrl: "https://api.example.com",
|
|
28
|
+
controlApiUrl: "https://control.example.com",
|
|
29
|
+
dataWssUrl: "wss://ws.example.com",
|
|
30
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
31
|
+
});
|
|
32
|
+
client.connect();
|
|
33
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
34
|
+
ws.open();
|
|
35
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
36
|
+
(0, chai_1.expect)(JSON.parse(ws.sent[0])).to.deep.include({ op: 10 });
|
|
37
|
+
ws.receive({
|
|
38
|
+
op: 0,
|
|
39
|
+
t: "Ready",
|
|
40
|
+
d: { session_id: "s1", session_token: "token", subscriptions: [] },
|
|
41
|
+
});
|
|
42
|
+
client.disconnect();
|
|
43
|
+
client.connect();
|
|
44
|
+
const ws2 = helpers_1.MockWebSocket.instances[1];
|
|
45
|
+
ws2.open();
|
|
46
|
+
ws2.receive({ op: 0, t: "Hello", d: {} });
|
|
47
|
+
const resumePayload = JSON.parse(ws2.sent[0]);
|
|
48
|
+
(0, chai_1.expect)(resumePayload.op).to.equal(11);
|
|
49
|
+
(0, chai_1.expect)(resumePayload.d).to.deep.include({
|
|
50
|
+
session_id: "s1",
|
|
51
|
+
session_token: "token",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
(0, mocha_1.it)("dispatches websocket events and supports channel operations", () => {
|
|
55
|
+
const client = new gateway_client_1.GatewayClient({
|
|
56
|
+
dataRestUrl: "https://api.example.com",
|
|
57
|
+
controlApiUrl: "https://control.example.com",
|
|
58
|
+
dataWssUrl: "wss://ws.example.com",
|
|
59
|
+
organisationId: "org-1",
|
|
60
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
61
|
+
});
|
|
62
|
+
const channelSync = sinon_1.default.spy();
|
|
63
|
+
const messageCreate = sinon_1.default.spy();
|
|
64
|
+
const messageUpdate = sinon_1.default.spy();
|
|
65
|
+
const aggregateUpdate = sinon_1.default.spy();
|
|
66
|
+
const alarmTrigger = sinon_1.default.spy();
|
|
67
|
+
const oneShot = sinon_1.default.spy();
|
|
68
|
+
const subscribed = sinon_1.default.spy();
|
|
69
|
+
const unsubscribed = sinon_1.default.spy();
|
|
70
|
+
const wssError = sinon_1.default.spy();
|
|
71
|
+
client.on("channelSync", channelSync);
|
|
72
|
+
client.on("messageCreate", messageCreate);
|
|
73
|
+
client.on("messageUpdate", messageUpdate);
|
|
74
|
+
client.on("aggregateUpdate", aggregateUpdate);
|
|
75
|
+
client.on("alarmTrigger", alarmTrigger);
|
|
76
|
+
client.on("oneShotMessage", oneShot);
|
|
77
|
+
client.on("channelSubscription", subscribed);
|
|
78
|
+
client.on("channelUnsubscription", unsubscribed);
|
|
79
|
+
client.on("wssError", wssError);
|
|
80
|
+
client.connect();
|
|
81
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
82
|
+
ws.open();
|
|
83
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
84
|
+
ws.receive({
|
|
85
|
+
op: 0,
|
|
86
|
+
t: "Ready",
|
|
87
|
+
d: { session_id: "s1", session_token: "token", subscriptions: [] },
|
|
88
|
+
});
|
|
89
|
+
client.subscribe({ agent_id: "a1", name: "c1" });
|
|
90
|
+
client.syncChannel({ agent_id: "a1", name: "c1" });
|
|
91
|
+
client.sendOneShotMessage({ agent_id: "a1", name: "c1" }, { hello: "world" });
|
|
92
|
+
client.unsubscribe({ agent_id: "a1", name: "c1" });
|
|
93
|
+
(0, chai_1.expect)(ws.sent.map((message) => JSON.parse(message).op)).to.deep.equal([10, 12, 14, 15, 13]);
|
|
94
|
+
const id = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|
|
95
|
+
ws.receive({
|
|
96
|
+
op: 0,
|
|
97
|
+
t: "ChannelSync",
|
|
98
|
+
d: {
|
|
99
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
100
|
+
aggregate: { data: {}, attachments: [] },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
ws.receive({
|
|
104
|
+
op: 0,
|
|
105
|
+
t: "MessageCreate",
|
|
106
|
+
d: {
|
|
107
|
+
id,
|
|
108
|
+
author_id: "u1",
|
|
109
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
110
|
+
data: {},
|
|
111
|
+
attachments: [],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
ws.receive({
|
|
115
|
+
op: 0,
|
|
116
|
+
t: "MessageUpdate",
|
|
117
|
+
d: {
|
|
118
|
+
id,
|
|
119
|
+
author_id: "u1",
|
|
120
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
121
|
+
data: {},
|
|
122
|
+
attachments: [],
|
|
123
|
+
request_data: {},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
ws.receive({
|
|
127
|
+
op: 0,
|
|
128
|
+
t: "AggregateUpdate",
|
|
129
|
+
d: {
|
|
130
|
+
author_id: "u1",
|
|
131
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
132
|
+
aggregate: { data: {}, attachments: [] },
|
|
133
|
+
request_data: { data: {}, attachments: [] },
|
|
134
|
+
organisation_id: "org-1",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
ws.receive({
|
|
138
|
+
op: 0,
|
|
139
|
+
t: "AlarmTrigger",
|
|
140
|
+
d: {
|
|
141
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
142
|
+
alarm: {
|
|
143
|
+
id: "al1",
|
|
144
|
+
name: "alarm",
|
|
145
|
+
description: "",
|
|
146
|
+
enabled: true,
|
|
147
|
+
key: "temp",
|
|
148
|
+
operator: "gt",
|
|
149
|
+
value: 1,
|
|
150
|
+
state: "Alarm",
|
|
151
|
+
expiry_mins: null,
|
|
152
|
+
entered_state_ts: 1,
|
|
153
|
+
},
|
|
154
|
+
old_state: "OK",
|
|
155
|
+
new_state: "Alarm",
|
|
156
|
+
aggregate: { data: {}, attachments: [] },
|
|
157
|
+
request_data: { data: {}, attachments: [] },
|
|
158
|
+
organisation_id: "org-1",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
ws.receive({
|
|
162
|
+
op: 0,
|
|
163
|
+
t: "OneShotMessage",
|
|
164
|
+
d: {
|
|
165
|
+
id: null,
|
|
166
|
+
author_id: "u1",
|
|
167
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
168
|
+
data: { hello: "world" },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
ws.receive({
|
|
172
|
+
op: 0,
|
|
173
|
+
t: "ChannelSubscription",
|
|
174
|
+
d: {
|
|
175
|
+
agent_id: "u1",
|
|
176
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
177
|
+
session_id: "s2",
|
|
178
|
+
default_session: true,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
ws.receive({
|
|
182
|
+
op: 0,
|
|
183
|
+
t: "ChannelUnsubscription",
|
|
184
|
+
d: {
|
|
185
|
+
agent_id: "u1",
|
|
186
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
187
|
+
session_id: "s2",
|
|
188
|
+
default_session: true,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
ws.receive({
|
|
192
|
+
op: 0,
|
|
193
|
+
t: "WSSErrorEvent",
|
|
194
|
+
d: { message: "boom" },
|
|
195
|
+
});
|
|
196
|
+
(0, chai_1.expect)(channelSync.calledOnce).to.equal(true);
|
|
197
|
+
(0, chai_1.expect)(messageCreate.calledWithMatch(sinon_1.default.match({ id, timestamp: sinon_1.default.match.number }))).to.equal(true);
|
|
198
|
+
(0, chai_1.expect)(messageUpdate.calledWithMatch(sinon_1.default.match({ id, timestamp: sinon_1.default.match.number }))).to.equal(true);
|
|
199
|
+
(0, chai_1.expect)(aggregateUpdate.calledOnce).to.equal(true);
|
|
200
|
+
(0, chai_1.expect)(alarmTrigger.calledOnce).to.equal(true);
|
|
201
|
+
(0, chai_1.expect)(oneShot.calledOnce).to.equal(true);
|
|
202
|
+
(0, chai_1.expect)(subscribed.calledOnce).to.equal(true);
|
|
203
|
+
(0, chai_1.expect)(unsubscribed.calledOnce).to.equal(true);
|
|
204
|
+
(0, chai_1.expect)(wssError.calledWith({ message: "boom" })).to.equal(true);
|
|
205
|
+
});
|
|
206
|
+
(0, mocha_1.it)("sends heartbeats, measures latency, and handles session cancellation", () => {
|
|
207
|
+
const client = new gateway_client_1.GatewayClient({
|
|
208
|
+
dataRestUrl: "https://api.example.com",
|
|
209
|
+
controlApiUrl: "https://control.example.com",
|
|
210
|
+
dataWssUrl: "wss://ws.example.com",
|
|
211
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
212
|
+
});
|
|
213
|
+
const ack = sinon_1.default.spy();
|
|
214
|
+
const cancelled = sinon_1.default.spy();
|
|
215
|
+
client.on("heartbeatAck", ack);
|
|
216
|
+
client.on("sessionCancelled", cancelled);
|
|
217
|
+
client.connect();
|
|
218
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
219
|
+
ws.open();
|
|
220
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
221
|
+
ws.receive({
|
|
222
|
+
op: 0,
|
|
223
|
+
t: "Ready",
|
|
224
|
+
d: { session_id: "s1", session_token: "token", subscriptions: [] },
|
|
225
|
+
});
|
|
226
|
+
sinon_1.default.clock.tick(20000);
|
|
227
|
+
(0, chai_1.expect)(JSON.parse(ws.sent[ws.sent.length - 1])).to.deep.equal({ op: 1, d: {} });
|
|
228
|
+
sinon_1.default.clock.tick(10);
|
|
229
|
+
ws.receive({ op: 2, d: {} });
|
|
230
|
+
(0, chai_1.expect)(ack.calledWith(10)).to.equal(true);
|
|
231
|
+
ws.receive({ op: 3, d: {} });
|
|
232
|
+
(0, chai_1.expect)(cancelled.calledOnce).to.equal(true);
|
|
233
|
+
(0, chai_1.expect)(client.getSession()).to.equal(null);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sinon from "sinon";
|
|
2
|
+
export declare function createJsonResponse(body: unknown, init?: ResponseInit): Response;
|
|
3
|
+
export declare function createTextResponse(body: string, init?: ResponseInit): Response;
|
|
4
|
+
export declare function createBlobResponse(body: string, init?: ResponseInit): Response;
|
|
5
|
+
export declare function createFetchMock(responseFactory?: (url: string, init?: RequestInit) => Response | Promise<Response>): sinon.SinonStub<any[], any>;
|
|
6
|
+
export declare function installSessionStorageMock(): void;
|
|
7
|
+
type MessageHandler = ((event: MessageEvent<string>) => void) | null;
|
|
8
|
+
type CloseHandler = ((event: CloseEvent) => void) | null;
|
|
9
|
+
type OpenHandler = (() => void) | null;
|
|
10
|
+
type ErrorHandler = ((event: Event) => void) | null;
|
|
11
|
+
export declare class MockWebSocket {
|
|
12
|
+
static instances: MockWebSocket[];
|
|
13
|
+
static CONNECTING: number;
|
|
14
|
+
static OPEN: number;
|
|
15
|
+
static CLOSING: number;
|
|
16
|
+
static CLOSED: number;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readyState: number;
|
|
19
|
+
sent: string[];
|
|
20
|
+
onmessage: MessageHandler;
|
|
21
|
+
onclose: CloseHandler;
|
|
22
|
+
onopen: OpenHandler;
|
|
23
|
+
onerror: ErrorHandler;
|
|
24
|
+
constructor(url: string);
|
|
25
|
+
send(data: string): void;
|
|
26
|
+
close(code?: number, reason?: string): void;
|
|
27
|
+
open(): void;
|
|
28
|
+
receive(payload: unknown): void;
|
|
29
|
+
error(): void;
|
|
30
|
+
static reset(): void;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MockWebSocket = void 0;
|
|
7
|
+
exports.createJsonResponse = createJsonResponse;
|
|
8
|
+
exports.createTextResponse = createTextResponse;
|
|
9
|
+
exports.createBlobResponse = createBlobResponse;
|
|
10
|
+
exports.createFetchMock = createFetchMock;
|
|
11
|
+
exports.installSessionStorageMock = installSessionStorageMock;
|
|
12
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
13
|
+
function createJsonResponse(body, init) {
|
|
14
|
+
return new Response(JSON.stringify(body), {
|
|
15
|
+
status: init?.status ?? 200,
|
|
16
|
+
headers: {
|
|
17
|
+
"content-type": "application/json",
|
|
18
|
+
...(init?.headers ?? {}),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function createTextResponse(body, init) {
|
|
23
|
+
return new Response(body, {
|
|
24
|
+
status: init?.status ?? 200,
|
|
25
|
+
headers: init?.headers,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function createBlobResponse(body, init) {
|
|
29
|
+
return new Response(new Blob([body]), {
|
|
30
|
+
status: init?.status ?? 200,
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "application/octet-stream",
|
|
33
|
+
...(init?.headers ?? {}),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function createFetchMock(responseFactory) {
|
|
38
|
+
return sinon_1.default.stub().callsFake(async (input, init) => {
|
|
39
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
40
|
+
if (responseFactory) {
|
|
41
|
+
return responseFactory(url, init);
|
|
42
|
+
}
|
|
43
|
+
return createJsonResponse({ ok: true });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
class StorageMock {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.store = new Map();
|
|
49
|
+
}
|
|
50
|
+
get length() {
|
|
51
|
+
return this.store.size;
|
|
52
|
+
}
|
|
53
|
+
clear() {
|
|
54
|
+
this.store.clear();
|
|
55
|
+
}
|
|
56
|
+
getItem(key) {
|
|
57
|
+
return this.store.get(key) ?? null;
|
|
58
|
+
}
|
|
59
|
+
key(index) {
|
|
60
|
+
return Array.from(this.store.keys())[index] ?? null;
|
|
61
|
+
}
|
|
62
|
+
removeItem(key) {
|
|
63
|
+
this.store.delete(key);
|
|
64
|
+
}
|
|
65
|
+
setItem(key, value) {
|
|
66
|
+
this.store.set(key, value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function installSessionStorageMock() {
|
|
70
|
+
Object.defineProperty(globalThis, "sessionStorage", {
|
|
71
|
+
value: new StorageMock(),
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
class MockWebSocket {
|
|
77
|
+
constructor(url) {
|
|
78
|
+
this.readyState = MockWebSocket.CONNECTING;
|
|
79
|
+
this.sent = [];
|
|
80
|
+
this.onmessage = null;
|
|
81
|
+
this.onclose = null;
|
|
82
|
+
this.onopen = null;
|
|
83
|
+
this.onerror = null;
|
|
84
|
+
this.url = url;
|
|
85
|
+
MockWebSocket.instances.push(this);
|
|
86
|
+
}
|
|
87
|
+
send(data) {
|
|
88
|
+
this.sent.push(data);
|
|
89
|
+
}
|
|
90
|
+
close(code, reason) {
|
|
91
|
+
this.readyState = MockWebSocket.CLOSED;
|
|
92
|
+
this.onclose?.({ code: code ?? 1000, reason: reason ?? "" });
|
|
93
|
+
}
|
|
94
|
+
open() {
|
|
95
|
+
this.readyState = MockWebSocket.OPEN;
|
|
96
|
+
this.onopen?.();
|
|
97
|
+
}
|
|
98
|
+
receive(payload) {
|
|
99
|
+
this.onmessage?.(new MessageEvent("message", {
|
|
100
|
+
data: JSON.stringify(payload),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
error() {
|
|
104
|
+
this.onerror?.(new Event("error"));
|
|
105
|
+
}
|
|
106
|
+
static reset() {
|
|
107
|
+
MockWebSocket.instances = [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.MockWebSocket = MockWebSocket;
|
|
111
|
+
MockWebSocket.instances = [];
|
|
112
|
+
MockWebSocket.CONNECTING = 0;
|
|
113
|
+
MockWebSocket.OPEN = 1;
|
|
114
|
+
MockWebSocket.CLOSING = 2;
|
|
115
|
+
MockWebSocket.CLOSED = 3;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|