@simplysm/service-common 13.0.69 → 13.0.70

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.
@@ -0,0 +1,336 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { createServiceProtocol, type ServiceProtocol } from "../../src/protocol/service-protocol";
3
+ import type { ServiceMessage } from "../../src/protocol/protocol.types";
4
+ import { Uuid } from "@simplysm/core-common";
5
+
6
+ describe("ServiceProtocol", () => {
7
+ let protocol: ServiceProtocol;
8
+
9
+ beforeEach(() => {
10
+ protocol = createServiceProtocol();
11
+ });
12
+
13
+ afterEach(() => {
14
+ protocol.dispose();
15
+ });
16
+
17
+ describe("Encoding", () => {
18
+ it("encode single message", () => {
19
+ const uuid = Uuid.new().toString();
20
+ const message: ServiceMessage = { name: "test.method", body: [{ test: "data" }] };
21
+
22
+ const result = protocol.encode(uuid, message);
23
+
24
+ expect(result.chunks.length).toBe(1);
25
+ expect(result.totalSize).toBeGreaterThan(0);
26
+ });
27
+
28
+ it("encode message without body", () => {
29
+ const uuid = Uuid.new().toString();
30
+ const message: ServiceMessage = {
31
+ name: "reload",
32
+ body: { clientName: undefined, changedFileSet: new Set() },
33
+ };
34
+
35
+ const result = protocol.encode(uuid, message);
36
+
37
+ expect(result.chunks.length).toBe(1);
38
+ });
39
+
40
+ it("throw error when message exceeds 100MB", () => {
41
+ const uuid = Uuid.new().toString();
42
+ // Generate data larger than 100MB
43
+ const largeData = "x".repeat(101 * 1024 * 1024);
44
+ const message: ServiceMessage = { name: "test.method", body: [largeData] };
45
+
46
+ expect(() => protocol.encode(uuid, message)).toThrow("Message size exceeds the limit.");
47
+ });
48
+ });
49
+
50
+ describe("Decoding", () => {
51
+ it("decode single message", () => {
52
+ const uuid = Uuid.new().toString();
53
+ const message: ServiceMessage = { name: "test.method", body: [{ value: 123 }] };
54
+
55
+ const encoded = protocol.encode(uuid, message);
56
+ const result = protocol.decode(encoded.chunks[0]);
57
+
58
+ expect(result.type).toBe("complete");
59
+ if (result.type === "complete") {
60
+ expect(result.message.name).toBe("test.method");
61
+ expect(result.message.body).toEqual([{ value: 123 }]);
62
+ }
63
+ });
64
+
65
+ it("throw error when buffer size is smaller than header size", () => {
66
+ const smallBytes = new Uint8Array(20);
67
+
68
+ expect(() => protocol.decode(smallBytes)).toThrow("Buffer size is smaller than header size.");
69
+ });
70
+
71
+ it("throw error when decoded message exceeds 100MB", () => {
72
+ // Manually create header with totalSize exceeding 100MB
73
+ const headerBytes = new Uint8Array(28);
74
+ const uuidBytes = new Uuid(Uuid.new().toString()).toBytes();
75
+ headerBytes.set(uuidBytes, 0);
76
+
77
+ const headerView = new DataView(
78
+ headerBytes.buffer,
79
+ headerBytes.byteOffset,
80
+ headerBytes.byteLength,
81
+ );
82
+ headerView.setBigUint64(16, BigInt(101 * 1024 * 1024), false); // 101MB
83
+ headerView.setUint32(24, 0, false);
84
+
85
+ expect(() => protocol.decode(headerBytes)).toThrow("Message size exceeds the limit.");
86
+ });
87
+ });
88
+
89
+ describe("Chunking", () => {
90
+ it("chunk message larger than 3MB", () => {
91
+ const uuid = Uuid.new().toString();
92
+ // Create 4MB data
93
+ const largeData = "x".repeat(4 * 1024 * 1024);
94
+ const message: ServiceMessage = { name: "test.method", body: [largeData] };
95
+
96
+ const result = protocol.encode(uuid, message);
97
+
98
+ expect(result.chunks.length).toBeGreaterThan(1);
99
+ });
100
+
101
+ it("assemble chunked message in order", () => {
102
+ const uuid = Uuid.new().toString();
103
+ // 4MB data
104
+ const largeData = "x".repeat(4 * 1024 * 1024);
105
+ const message: ServiceMessage = { name: "test.method", body: [largeData] };
106
+
107
+ const encoded = protocol.encode(uuid, message);
108
+ expect(encoded.chunks.length).toBeGreaterThan(1);
109
+
110
+ // Decode chunks in order
111
+ let result!: ReturnType<typeof protocol.decode>;
112
+ for (let i = 0; i < encoded.chunks.length; i++) {
113
+ result = protocol.decode(encoded.chunks[i]);
114
+ if (i < encoded.chunks.length - 1) {
115
+ expect(result.type).toBe("progress");
116
+ }
117
+ }
118
+
119
+ expect(result.type).toBe("complete");
120
+ if (result.type === "complete") {
121
+ expect(result.message.body).toEqual([largeData]);
122
+ }
123
+ });
124
+
125
+ it("assemble chunked message in reverse order", () => {
126
+ const uuid = Uuid.new().toString();
127
+ // 4MB data
128
+ const largeData = "x".repeat(4 * 1024 * 1024);
129
+ const message: ServiceMessage = { name: "test.method", body: [largeData] };
130
+
131
+ const encoded = protocol.encode(uuid, message);
132
+ const reversedChunks = [...encoded.chunks].reverse();
133
+
134
+ // Decode in reverse order
135
+ let result!: ReturnType<typeof protocol.decode>;
136
+ for (let i = 0; i < reversedChunks.length; i++) {
137
+ result = protocol.decode(reversedChunks[i]);
138
+ }
139
+
140
+ // Should complete at the end
141
+ expect(result.type).toBe("complete");
142
+ if (result.type === "complete") {
143
+ expect(result.message.body).toEqual([largeData]);
144
+ }
145
+ });
146
+
147
+ it("prevent duplicate packets", () => {
148
+ const uuid = Uuid.new().toString();
149
+ // 4MB data
150
+ const largeData = "x".repeat(4 * 1024 * 1024);
151
+ const message: ServiceMessage = { name: "test.method", body: [largeData] };
152
+
153
+ const encoded = protocol.encode(uuid, message);
154
+
155
+ // Send first chunk twice
156
+ protocol.decode(encoded.chunks[0]);
157
+ const result1 = protocol.decode(encoded.chunks[0]); // Duplicate
158
+
159
+ // Should be in progress state, and completedSize should not increase from duplicate
160
+ expect(result1.type).toBe("progress");
161
+
162
+ // Send remaining chunks
163
+ let result!: ReturnType<typeof protocol.decode>;
164
+ for (let i = 1; i < encoded.chunks.length; i++) {
165
+ result = protocol.decode(encoded.chunks[i]);
166
+ }
167
+
168
+ expect(result.type).toBe("complete");
169
+ if (result.type === "complete") {
170
+ expect(result.message.body).toEqual([largeData]);
171
+ }
172
+ });
173
+ });
174
+
175
+ describe("UUID interleaving", () => {
176
+ it("receive chunks from multiple UUIDs in interleaved order", () => {
177
+ const uuid1 = Uuid.new().toString();
178
+ const uuid2 = Uuid.new().toString();
179
+
180
+ // Each with 4MB data to trigger chunking
181
+ const largeData1 = "A".repeat(4 * 1024 * 1024);
182
+ const largeData2 = "B".repeat(4 * 1024 * 1024);
183
+ const message1: ServiceMessage = { name: "test.method1", body: [largeData1] };
184
+ const message2: ServiceMessage = { name: "test.method2", body: [largeData2] };
185
+
186
+ const encoded1 = protocol.encode(uuid1, message1);
187
+ const encoded2 = protocol.encode(uuid2, message2);
188
+
189
+ expect(encoded1.chunks.length).toBeGreaterThan(1);
190
+ expect(encoded2.chunks.length).toBeGreaterThan(1);
191
+
192
+ // Decode chunks in interleaved order (uuid1[0], uuid2[0], uuid1[1], uuid2[1], ...)
193
+ const maxChunks = Math.max(encoded1.chunks.length, encoded2.chunks.length);
194
+ let result1!: ReturnType<typeof protocol.decode>;
195
+ let result2!: ReturnType<typeof protocol.decode>;
196
+
197
+ for (let i = 0; i < maxChunks; i++) {
198
+ if (i < encoded1.chunks.length) {
199
+ result1 = protocol.decode(encoded1.chunks[i]);
200
+ }
201
+ if (i < encoded2.chunks.length) {
202
+ result2 = protocol.decode(encoded2.chunks[i]);
203
+ }
204
+ }
205
+
206
+ // Both messages should complete
207
+ expect(result1.type).toBe("complete");
208
+ expect(result2.type).toBe("complete");
209
+
210
+ if (result1.type === "complete" && result2.type === "complete") {
211
+ expect(result1.message.name).toBe("test.method1");
212
+ expect(result1.message.body).toEqual([largeData1]);
213
+ expect(result2.message.name).toBe("test.method2");
214
+ expect(result2.message.body).toEqual([largeData2]);
215
+ }
216
+ });
217
+
218
+ it("receive 3 UUIDs in random order", () => {
219
+ const uuids = [Uuid.new().toString(), Uuid.new().toString(), Uuid.new().toString()];
220
+ const data = [
221
+ "X".repeat(4 * 1024 * 1024),
222
+ "Y".repeat(4 * 1024 * 1024),
223
+ "Z".repeat(4 * 1024 * 1024),
224
+ ];
225
+ const messages: ServiceMessage[] = data.map((d, i) => ({
226
+ name: `test.method${i}`,
227
+ body: [d],
228
+ }));
229
+
230
+ const encodedList = uuids.map((uuid, i) => protocol.encode(uuid, messages[i]));
231
+
232
+ // Combine all chunks into one array
233
+ const allChunks: { uuid: string; chunk: Uint8Array; originalIndex: number }[] = [];
234
+ encodedList.forEach((encoded, msgIdx) => {
235
+ encoded.chunks.forEach((chunk, chunkIdx) => {
236
+ allChunks.push({ uuid: uuids[msgIdx], chunk, originalIndex: chunkIdx });
237
+ });
238
+ });
239
+
240
+ // Randomize order (use reverse instead of seed-based shuffle)
241
+ allChunks.reverse();
242
+
243
+ // Decode all chunks
244
+ const results: Map<string, ReturnType<typeof protocol.decode>> = new Map();
245
+ for (const { uuid, chunk } of allChunks) {
246
+ results.set(uuid, protocol.decode(chunk));
247
+ }
248
+
249
+ // Verify all messages completed
250
+ for (let i = 0; i < 3; i++) {
251
+ const result = results.get(uuids[i]);
252
+ expect(result?.type).toBe("complete");
253
+ if (result?.type === "complete") {
254
+ expect(result.message.name).toBe(`test.method${i}`);
255
+ expect(result.message.body).toEqual([data[i]]);
256
+ }
257
+ }
258
+ });
259
+ });
260
+
261
+ describe("Edge cases", () => {
262
+ it("handle empty body", () => {
263
+ const uuid = Uuid.new().toString();
264
+ const message: ServiceMessage = { name: "test.method", body: [""] };
265
+
266
+ const encoded = protocol.encode(uuid, message);
267
+ const decoded = protocol.decode(encoded.chunks[0]);
268
+
269
+ expect(decoded.type).toBe("complete");
270
+ if (decoded.type === "complete") {
271
+ expect(decoded.message.body).toEqual([""]);
272
+ }
273
+ });
274
+
275
+ it("handle null body", () => {
276
+ const uuid = Uuid.new().toString();
277
+ const message: ServiceMessage = { name: "test.method", body: [null] };
278
+
279
+ const encoded = protocol.encode(uuid, message);
280
+ const decoded = protocol.decode(encoded.chunks[0]);
281
+
282
+ expect(decoded.type).toBe("complete");
283
+ if (decoded.type === "complete") {
284
+ // JsonConvert.stringify/parse converts null to undefined
285
+ expect(decoded.message.body).toEqual([undefined]);
286
+ }
287
+ });
288
+
289
+ it("serialize complex object", () => {
290
+ const uuid = Uuid.new().toString();
291
+ const complexData = {
292
+ array: [1, 2, 3],
293
+ nested: { deep: { value: "test" } },
294
+ date: new Date().toISOString(),
295
+ unicode: "Korean test 🚀",
296
+ };
297
+ const message: ServiceMessage = { name: "test.method", body: [complexData] };
298
+
299
+ const encoded = protocol.encode(uuid, message);
300
+ const decoded = protocol.decode(encoded.chunks[0]);
301
+
302
+ expect(decoded.type).toBe("complete");
303
+ if (decoded.type === "complete") {
304
+ expect(decoded.message.body).toEqual([complexData]);
305
+ }
306
+ });
307
+
308
+ it("handle message at exactly 3MB boundary", () => {
309
+ const uuid = Uuid.new().toString();
310
+ // Exactly 3MB
311
+ const data = "x".repeat(3 * 1024 * 1024 - 50); // Account for some JSON overhead
312
+ const message: ServiceMessage = { name: "test.method", body: [data] };
313
+
314
+ const encoded = protocol.encode(uuid, message);
315
+ // Messages up to 3MB should not be chunked
316
+ expect(encoded.chunks.length).toBe(1);
317
+ });
318
+
319
+ it("include correct information in progress response", () => {
320
+ const uuid = Uuid.new().toString();
321
+ const largeData = "x".repeat(4 * 1024 * 1024);
322
+ const message: ServiceMessage = { name: "test.method", body: [largeData] };
323
+
324
+ const encoded = protocol.encode(uuid, message);
325
+ const result = protocol.decode(encoded.chunks[0]);
326
+
327
+ expect(result.type).toBe("progress");
328
+ if (result.type === "progress") {
329
+ expect(result.uuid).toBe(uuid);
330
+ expect(result.totalSize).toBe(encoded.totalSize);
331
+ expect(result.completedSize).toBeGreaterThan(0);
332
+ expect(result.completedSize).toBeLessThan(result.totalSize);
333
+ }
334
+ });
335
+ });
336
+ });