@derivation/rpc 0.3.3 → 0.3.5
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/tests/context.test.d.ts +1 -0
- package/dist/tests/context.test.js +252 -0
- package/dist/tests/iso.test.d.ts +1 -0
- package/dist/tests/iso.test.js +186 -0
- package/dist/tests/messages.test.d.ts +1 -0
- package/dist/tests/messages.test.js +152 -0
- package/dist/tests/mutations.test.d.ts +1 -0
- package/dist/tests/mutations.test.js +122 -0
- package/dist/tests/queue.test.d.ts +1 -0
- package/dist/tests/queue.test.js +84 -0
- package/dist/tests/reactive-map-adapter.test.d.ts +1 -0
- package/dist/tests/reactive-map-adapter.test.js +190 -0
- package/dist/tests/reactive-set-adapter.test.d.ts +1 -0
- package/dist/tests/reactive-set-adapter.test.js +157 -0
- package/dist/tests/stream-adapter.test.d.ts +1 -0
- package/dist/tests/stream-adapter.test.js +119 -0
- package/dist/tests/weak-list.test.d.ts +1 -0
- package/dist/tests/weak-list.test.js +100 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
import { Graph, inputValue } from 'derivation';
|
|
5
|
+
import { StreamSourceAdapter } from '../index.js';
|
|
6
|
+
import { setupWebSocketServer } from '../web-socket-server.js';
|
|
7
|
+
import { id } from '../iso.js';
|
|
8
|
+
describe('Context', () => {
|
|
9
|
+
let server;
|
|
10
|
+
let graph;
|
|
11
|
+
let port;
|
|
12
|
+
let receivedContexts = [];
|
|
13
|
+
beforeEach((ctx) => {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
graph = new Graph();
|
|
16
|
+
server = createServer();
|
|
17
|
+
receivedContexts = [];
|
|
18
|
+
// Create test endpoints that capture context
|
|
19
|
+
const streamEndpoints = {
|
|
20
|
+
testStream: async (args, ctx) => {
|
|
21
|
+
receivedContexts.push(ctx);
|
|
22
|
+
const input = inputValue(graph, { value: `Hello from ${ctx.userId}` });
|
|
23
|
+
return new StreamSourceAdapter(input, id());
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const mutationEndpoints = {
|
|
27
|
+
testMutation: async (args, ctx) => {
|
|
28
|
+
receivedContexts.push(ctx);
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
value: {
|
|
32
|
+
output: `Processed: ${args.input}`,
|
|
33
|
+
userId: ctx.userId,
|
|
34
|
+
role: ctx.role,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
// Setup server with createContext
|
|
40
|
+
setupWebSocketServer(graph, server, streamEndpoints, mutationEndpoints, {
|
|
41
|
+
createContext: (ws, req) => {
|
|
42
|
+
// Parse userId from query params
|
|
43
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
44
|
+
const userId = url.searchParams.get('userId') || 'anonymous';
|
|
45
|
+
const role = url.searchParams.get('role') || 'user';
|
|
46
|
+
return { userId, role };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
server.listen(0, () => {
|
|
50
|
+
const addr = server.address();
|
|
51
|
+
if (addr && typeof addr === 'object') {
|
|
52
|
+
port = addr.port;
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('should pass context to stream endpoints', async () => {
|
|
59
|
+
const ws = new WebSocket(`ws://localhost:${port}/api/ws?userId=test-user&role=admin`);
|
|
60
|
+
await new Promise((resolve) => {
|
|
61
|
+
ws.on('open', () => {
|
|
62
|
+
ws.send(JSON.stringify({
|
|
63
|
+
type: 'subscribe',
|
|
64
|
+
id: 1,
|
|
65
|
+
name: 'testStream',
|
|
66
|
+
args: {},
|
|
67
|
+
}));
|
|
68
|
+
});
|
|
69
|
+
ws.on('message', (data) => {
|
|
70
|
+
const msg = JSON.parse(data.toString());
|
|
71
|
+
if (msg.type === 'snapshot') {
|
|
72
|
+
expect(receivedContexts).toHaveLength(1);
|
|
73
|
+
expect(receivedContexts[0]).toEqual({
|
|
74
|
+
userId: 'test-user',
|
|
75
|
+
role: 'admin',
|
|
76
|
+
});
|
|
77
|
+
ws.close();
|
|
78
|
+
resolve();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
server.close();
|
|
83
|
+
});
|
|
84
|
+
it('should pass context to mutation endpoints', async () => {
|
|
85
|
+
const ws = new WebSocket(`ws://localhost:${port}/api/ws?userId=alice&role=user`);
|
|
86
|
+
await new Promise((resolve) => {
|
|
87
|
+
ws.on('open', () => {
|
|
88
|
+
ws.send(JSON.stringify({
|
|
89
|
+
type: 'call',
|
|
90
|
+
id: 1,
|
|
91
|
+
name: 'testMutation',
|
|
92
|
+
args: { input: 'test data' },
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
ws.on('message', (data) => {
|
|
96
|
+
const msg = JSON.parse(data.toString());
|
|
97
|
+
if (msg.type === 'result') {
|
|
98
|
+
expect(msg.success).toBe(true);
|
|
99
|
+
expect(msg.value).toEqual({
|
|
100
|
+
output: 'Processed: test data',
|
|
101
|
+
userId: 'alice',
|
|
102
|
+
role: 'user',
|
|
103
|
+
});
|
|
104
|
+
expect(receivedContexts).toHaveLength(1);
|
|
105
|
+
expect(receivedContexts[0]).toEqual({
|
|
106
|
+
userId: 'alice',
|
|
107
|
+
role: 'user',
|
|
108
|
+
});
|
|
109
|
+
ws.close();
|
|
110
|
+
resolve();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
server.close();
|
|
115
|
+
});
|
|
116
|
+
it('should use default context when no query params', async () => {
|
|
117
|
+
const ws = new WebSocket(`ws://localhost:${port}/api/ws`);
|
|
118
|
+
await new Promise((resolve) => {
|
|
119
|
+
ws.on('open', () => {
|
|
120
|
+
ws.send(JSON.stringify({
|
|
121
|
+
type: 'subscribe',
|
|
122
|
+
id: 1,
|
|
123
|
+
name: 'testStream',
|
|
124
|
+
args: {},
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
127
|
+
ws.on('message', (data) => {
|
|
128
|
+
const msg = JSON.parse(data.toString());
|
|
129
|
+
if (msg.type === 'snapshot') {
|
|
130
|
+
expect(receivedContexts).toHaveLength(1);
|
|
131
|
+
expect(receivedContexts[0]).toEqual({
|
|
132
|
+
userId: 'anonymous',
|
|
133
|
+
role: 'user',
|
|
134
|
+
});
|
|
135
|
+
ws.close();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
server.close();
|
|
141
|
+
});
|
|
142
|
+
it('should create separate context for each connection', async () => {
|
|
143
|
+
const ws1 = new WebSocket(`ws://localhost:${port}/api/ws?userId=user1`);
|
|
144
|
+
const ws2 = new WebSocket(`ws://localhost:${port}/api/ws?userId=user2`);
|
|
145
|
+
const contexts = await Promise.all([
|
|
146
|
+
new Promise((resolve) => {
|
|
147
|
+
ws1.on('open', () => {
|
|
148
|
+
ws1.send(JSON.stringify({
|
|
149
|
+
type: 'call',
|
|
150
|
+
id: 1,
|
|
151
|
+
name: 'testMutation',
|
|
152
|
+
args: { input: 'from user1' },
|
|
153
|
+
}));
|
|
154
|
+
});
|
|
155
|
+
ws1.on('message', (data) => {
|
|
156
|
+
const msg = JSON.parse(data.toString());
|
|
157
|
+
if (msg.type === 'result') {
|
|
158
|
+
ws1.close();
|
|
159
|
+
resolve({ userId: msg.value.userId, role: msg.value.role });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}),
|
|
163
|
+
new Promise((resolve) => {
|
|
164
|
+
ws2.on('open', () => {
|
|
165
|
+
ws2.send(JSON.stringify({
|
|
166
|
+
type: 'call',
|
|
167
|
+
id: 1,
|
|
168
|
+
name: 'testMutation',
|
|
169
|
+
args: { input: 'from user2' },
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
ws2.on('message', (data) => {
|
|
173
|
+
const msg = JSON.parse(data.toString());
|
|
174
|
+
if (msg.type === 'result') {
|
|
175
|
+
ws2.close();
|
|
176
|
+
resolve({ userId: msg.value.userId, role: msg.value.role });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}),
|
|
180
|
+
]);
|
|
181
|
+
expect(contexts[0]).toEqual({ userId: 'user1', role: 'user' });
|
|
182
|
+
expect(contexts[1]).toEqual({ userId: 'user2', role: 'user' });
|
|
183
|
+
expect(receivedContexts).toHaveLength(2);
|
|
184
|
+
server.close();
|
|
185
|
+
});
|
|
186
|
+
it('should handle async createContext and process messages', async () => {
|
|
187
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
188
|
+
// Create new server with async context creation
|
|
189
|
+
const newServer = createServer();
|
|
190
|
+
const newGraph = new Graph();
|
|
191
|
+
let capturedContext = null;
|
|
192
|
+
const streamEndpoints = {
|
|
193
|
+
testStream: async (args, ctx) => {
|
|
194
|
+
capturedContext = ctx;
|
|
195
|
+
const input = inputValue(newGraph, { value: 'test' });
|
|
196
|
+
return new StreamSourceAdapter(input, id());
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const mutationEndpoints = {
|
|
200
|
+
testMutation: async (args, ctx) => ({
|
|
201
|
+
success: true,
|
|
202
|
+
value: { output: args.input, userId: ctx.userId, role: ctx.role },
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
setupWebSocketServer(newGraph, newServer, streamEndpoints, mutationEndpoints, {
|
|
206
|
+
createContext: async (ws, req) => {
|
|
207
|
+
// Simulate async auth validation (e.g., database lookup)
|
|
208
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
209
|
+
return { userId: 'async-user', role: 'admin' };
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const newPort = await new Promise((resolve) => {
|
|
213
|
+
newServer.listen(0, () => {
|
|
214
|
+
const addr = newServer.address();
|
|
215
|
+
if (addr && typeof addr === 'object') {
|
|
216
|
+
resolve(addr.port);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
const ws = new WebSocket(`ws://localhost:${newPort}/api/ws`);
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
const timeout = setTimeout(() => {
|
|
223
|
+
reject(new Error('Test timed out - async context creation may have race condition'));
|
|
224
|
+
}, 2000);
|
|
225
|
+
ws.on('open', () => {
|
|
226
|
+
// Send subscribe message immediately - this tests if async context works
|
|
227
|
+
ws.send(JSON.stringify({
|
|
228
|
+
type: 'subscribe',
|
|
229
|
+
id: 1,
|
|
230
|
+
name: 'testStream',
|
|
231
|
+
args: {},
|
|
232
|
+
}));
|
|
233
|
+
});
|
|
234
|
+
ws.on('message', (data) => {
|
|
235
|
+
const msg = JSON.parse(data.toString());
|
|
236
|
+
if (msg.type === 'snapshot') {
|
|
237
|
+
clearTimeout(timeout);
|
|
238
|
+
// Verify the async context was actually used
|
|
239
|
+
expect(capturedContext).not.toBeNull();
|
|
240
|
+
expect(capturedContext === null || capturedContext === void 0 ? void 0 : capturedContext.userId).toBe('async-user');
|
|
241
|
+
expect(capturedContext === null || capturedContext === void 0 ? void 0 : capturedContext.role).toBe('admin');
|
|
242
|
+
ws.close();
|
|
243
|
+
newServer.close(() => resolve());
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
ws.on('error', (err) => {
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
reject(err);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ZSet, ZMap } from '@derivation/composable';
|
|
3
|
+
import * as iso from '../iso.js';
|
|
4
|
+
describe('iso', () => {
|
|
5
|
+
describe('id', () => {
|
|
6
|
+
it('should return the same value for to and from', () => {
|
|
7
|
+
const identity = iso.id();
|
|
8
|
+
expect(identity.to(42)).toBe(42);
|
|
9
|
+
expect(identity.from(42)).toBe(42);
|
|
10
|
+
});
|
|
11
|
+
it('should work with objects', () => {
|
|
12
|
+
const identity = iso.id();
|
|
13
|
+
const obj = { x: 10 };
|
|
14
|
+
expect(identity.to(obj)).toBe(obj);
|
|
15
|
+
expect(identity.from(obj)).toBe(obj);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('unknown', () => {
|
|
19
|
+
it('should convert to unknown and back', () => {
|
|
20
|
+
const unknownIso = iso.unknown();
|
|
21
|
+
expect(unknownIso.to('hello')).toBe('hello');
|
|
22
|
+
expect(unknownIso.from('world')).toBe('world');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('flip', () => {
|
|
26
|
+
it('should reverse the direction of an isomorphism', () => {
|
|
27
|
+
const original = {
|
|
28
|
+
to: (n) => n.toString(),
|
|
29
|
+
from: (s) => parseInt(s, 10),
|
|
30
|
+
};
|
|
31
|
+
const flipped = iso.flip(original);
|
|
32
|
+
expect(flipped.to('42')).toBe(42);
|
|
33
|
+
expect(flipped.from(42)).toBe('42');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('compose', () => {
|
|
37
|
+
it('should compose two isomorphisms', () => {
|
|
38
|
+
const numToString = {
|
|
39
|
+
to: (n) => n.toString(),
|
|
40
|
+
from: (s) => parseInt(s, 10),
|
|
41
|
+
};
|
|
42
|
+
const stringToUpper = {
|
|
43
|
+
to: (s) => s.toUpperCase(),
|
|
44
|
+
from: (s) => s.toLowerCase(),
|
|
45
|
+
};
|
|
46
|
+
const composed = iso.compose(numToString, stringToUpper);
|
|
47
|
+
expect(composed.to(42)).toBe('42');
|
|
48
|
+
expect(composed.from('42')).toBe(42);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('array', () => {
|
|
52
|
+
it('should map over array elements', () => {
|
|
53
|
+
const numToString = {
|
|
54
|
+
to: (n) => n.toString(),
|
|
55
|
+
from: (s) => parseInt(s, 10),
|
|
56
|
+
};
|
|
57
|
+
const arrayIso = iso.array(numToString);
|
|
58
|
+
expect(arrayIso.to([1, 2, 3])).toEqual(['1', '2', '3']);
|
|
59
|
+
expect(arrayIso.from(['1', '2', '3'])).toEqual([1, 2, 3]);
|
|
60
|
+
});
|
|
61
|
+
it('should work with empty arrays', () => {
|
|
62
|
+
const arrayIso = iso.array(iso.id());
|
|
63
|
+
expect(arrayIso.to([])).toEqual([]);
|
|
64
|
+
expect(arrayIso.from([])).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('zset', () => {
|
|
68
|
+
it('should convert ZSet elements', () => {
|
|
69
|
+
const numToString = {
|
|
70
|
+
to: (n) => n.toString(),
|
|
71
|
+
from: (s) => parseInt(s, 10),
|
|
72
|
+
};
|
|
73
|
+
const zsetIso = iso.zset(numToString);
|
|
74
|
+
let inputZSet = new ZSet();
|
|
75
|
+
inputZSet = inputZSet.add(1, 2).add(2, 3);
|
|
76
|
+
const outputZSet = zsetIso.to(inputZSet);
|
|
77
|
+
expect([...outputZSet.getEntries()]).toEqual([['1', 2], ['2', 3]]);
|
|
78
|
+
const roundTrip = zsetIso.from(outputZSet);
|
|
79
|
+
expect([...roundTrip.getEntries()]).toEqual([...inputZSet.getEntries()]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('zsetToArray', () => {
|
|
83
|
+
it('should convert ZSet to array of [item, weight] tuples', () => {
|
|
84
|
+
const zsetToArrayIso = iso.zsetToArray();
|
|
85
|
+
let zset = new ZSet();
|
|
86
|
+
zset = zset.add('a', 1).add('b', 2);
|
|
87
|
+
const array = zsetToArrayIso.to(zset);
|
|
88
|
+
expect(array).toEqual([['a', 1], ['b', 2]]);
|
|
89
|
+
const backToZSet = zsetToArrayIso.from(array);
|
|
90
|
+
expect([...backToZSet.getEntries()]).toEqual([...zset.getEntries()]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('object', () => {
|
|
94
|
+
it('should convert object properties', () => {
|
|
95
|
+
const objIso = iso.object({
|
|
96
|
+
x: iso.id(),
|
|
97
|
+
y: iso.id(),
|
|
98
|
+
});
|
|
99
|
+
const result = objIso.to({ x: 42, y: 'hello' });
|
|
100
|
+
expect(result).toEqual({ x: 42, y: 'hello' });
|
|
101
|
+
const reverse = objIso.from({ x: 42, y: 'hello' });
|
|
102
|
+
expect(reverse).toEqual({ x: 42, y: 'hello' });
|
|
103
|
+
});
|
|
104
|
+
it('should transform object properties', () => {
|
|
105
|
+
const numToString = {
|
|
106
|
+
to: (n) => n.toString(),
|
|
107
|
+
from: (s) => parseInt(s, 10),
|
|
108
|
+
};
|
|
109
|
+
const objIso = iso.object({
|
|
110
|
+
age: numToString,
|
|
111
|
+
name: iso.id(),
|
|
112
|
+
});
|
|
113
|
+
expect(objIso.to({ age: 30, name: 'Alice' })).toEqual({ age: '30', name: 'Alice' });
|
|
114
|
+
expect(objIso.from({ age: '30', name: 'Alice' })).toEqual({ age: 30, name: 'Alice' });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('shallowRecord', () => {
|
|
118
|
+
it('should convert between Immutable Record and plain object', () => {
|
|
119
|
+
const recordIso = iso.shallowRecord();
|
|
120
|
+
const plain = { name: 'Bob', age: 25 };
|
|
121
|
+
const record = recordIso.from(plain);
|
|
122
|
+
expect(record.get('name')).toBe('Bob');
|
|
123
|
+
expect(record.get('age')).toBe(25);
|
|
124
|
+
const backToPlain = recordIso.to(record);
|
|
125
|
+
expect(backToPlain).toEqual(plain);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('record', () => {
|
|
129
|
+
it('should convert Immutable Record with property transformations', () => {
|
|
130
|
+
const numToString = {
|
|
131
|
+
to: (n) => n.toString(),
|
|
132
|
+
from: (s) => parseInt(s, 10),
|
|
133
|
+
};
|
|
134
|
+
const recordIso = iso.record({
|
|
135
|
+
name: iso.id(),
|
|
136
|
+
age: numToString,
|
|
137
|
+
});
|
|
138
|
+
const serialized = { name: 'Charlie', age: '35' };
|
|
139
|
+
const record = recordIso.from(serialized);
|
|
140
|
+
expect(record.get('name')).toBe('Charlie');
|
|
141
|
+
expect(record.get('age')).toBe(35);
|
|
142
|
+
const backToSerialized = recordIso.to(record);
|
|
143
|
+
expect(backToSerialized).toEqual({ name: 'Charlie', age: '35' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('tuple', () => {
|
|
147
|
+
it('should convert tuple elements', () => {
|
|
148
|
+
const numToString = {
|
|
149
|
+
to: (n) => n.toString(),
|
|
150
|
+
from: (s) => parseInt(s, 10),
|
|
151
|
+
};
|
|
152
|
+
const tupleIso = iso.tuple(numToString, iso.id(), iso.id());
|
|
153
|
+
expect(tupleIso.to([42, true, 'test'])).toEqual(['42', true, 'test']);
|
|
154
|
+
expect(tupleIso.from(['42', true, 'test'])).toEqual([42, true, 'test']);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('map', () => {
|
|
158
|
+
it('should convert Map to array of entries with transformed keys and values', () => {
|
|
159
|
+
const numToString = {
|
|
160
|
+
to: (n) => n.toString(),
|
|
161
|
+
from: (s) => parseInt(s, 10),
|
|
162
|
+
};
|
|
163
|
+
const mapIso = iso.map(numToString, iso.id());
|
|
164
|
+
const inputMap = new Map([[1, 'one'], [2, 'two']]);
|
|
165
|
+
const array = mapIso.to(inputMap);
|
|
166
|
+
expect(array).toEqual([['1', 'one'], ['2', 'two']]);
|
|
167
|
+
const backToMap = mapIso.from(array);
|
|
168
|
+
expect([...backToMap.entries()]).toEqual([...inputMap.entries()]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('zmap', () => {
|
|
172
|
+
it('should convert ZMap to array with weights', () => {
|
|
173
|
+
const numToString = {
|
|
174
|
+
to: (n) => n.toString(),
|
|
175
|
+
from: (s) => parseInt(s, 10),
|
|
176
|
+
};
|
|
177
|
+
const zmapIso = iso.zmap(numToString, iso.id());
|
|
178
|
+
let zmap = new ZMap();
|
|
179
|
+
zmap = zmap.add(1, 'one', 5).add(2, 'two', 3);
|
|
180
|
+
const array = zmapIso.to(zmap);
|
|
181
|
+
expect(array).toEqual([['1', 'one', 5], ['2', 'two', 3]]);
|
|
182
|
+
const backToZMap = zmapIso.from(array);
|
|
183
|
+
expect([...backToZMap.getEntries()]).toEqual([...zmap.getEntries()]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ClientMessage, parseClientMessage, SubscribeMessageSchema, UnsubscribeMessageSchema, HeartbeatMessageSchema as ClientHeartbeatMessageSchema, } from '../client-message.js';
|
|
3
|
+
import { ServerMessage, HeartbeatMessageSchema as ServerHeartbeatMessageSchema, SubscribedSchema, DeltaMessageSchema, } from '../server-message.js';
|
|
4
|
+
describe('ClientMessage', () => {
|
|
5
|
+
describe('subscribe', () => {
|
|
6
|
+
it('should create a valid subscribe message', () => {
|
|
7
|
+
const msg = ClientMessage.subscribe(1, 'testStream', { foo: 'bar' });
|
|
8
|
+
expect(msg).toEqual({
|
|
9
|
+
type: 'subscribe',
|
|
10
|
+
id: 1,
|
|
11
|
+
name: 'testStream',
|
|
12
|
+
args: { foo: 'bar' },
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it('should validate against schema', () => {
|
|
16
|
+
const msg = ClientMessage.subscribe(42, 'myStream', { x: 10 });
|
|
17
|
+
expect(() => SubscribeMessageSchema.parse(msg)).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('unsubscribe', () => {
|
|
21
|
+
it('should create a valid unsubscribe message', () => {
|
|
22
|
+
const msg = ClientMessage.unsubscribe(5);
|
|
23
|
+
expect(msg).toEqual({
|
|
24
|
+
type: 'unsubscribe',
|
|
25
|
+
id: 5,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('should validate against schema', () => {
|
|
29
|
+
const msg = ClientMessage.unsubscribe(99);
|
|
30
|
+
expect(() => UnsubscribeMessageSchema.parse(msg)).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('heartbeat', () => {
|
|
34
|
+
it('should create a valid heartbeat message', () => {
|
|
35
|
+
const msg = ClientMessage.heartbeat();
|
|
36
|
+
expect(msg).toEqual({
|
|
37
|
+
type: 'heartbeat',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('should validate against schema', () => {
|
|
41
|
+
const msg = ClientMessage.heartbeat();
|
|
42
|
+
expect(() => ClientHeartbeatMessageSchema.parse(msg)).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('parseClientMessage', () => {
|
|
46
|
+
it('should parse valid subscribe messages', () => {
|
|
47
|
+
const data = {
|
|
48
|
+
type: 'subscribe',
|
|
49
|
+
id: 1,
|
|
50
|
+
name: 'stream1',
|
|
51
|
+
args: { key: 'value' },
|
|
52
|
+
};
|
|
53
|
+
const result = parseClientMessage(data);
|
|
54
|
+
expect(result).toEqual(data);
|
|
55
|
+
expect(result.type).toBe('subscribe');
|
|
56
|
+
});
|
|
57
|
+
it('should parse valid unsubscribe messages', () => {
|
|
58
|
+
const data = {
|
|
59
|
+
type: 'unsubscribe',
|
|
60
|
+
id: 10,
|
|
61
|
+
};
|
|
62
|
+
const result = parseClientMessage(data);
|
|
63
|
+
expect(result).toEqual(data);
|
|
64
|
+
expect(result.type).toBe('unsubscribe');
|
|
65
|
+
});
|
|
66
|
+
it('should parse valid heartbeat messages', () => {
|
|
67
|
+
const data = {
|
|
68
|
+
type: 'heartbeat',
|
|
69
|
+
};
|
|
70
|
+
const result = parseClientMessage(data);
|
|
71
|
+
expect(result).toEqual(data);
|
|
72
|
+
expect(result.type).toBe('heartbeat');
|
|
73
|
+
});
|
|
74
|
+
it('should reject invalid messages', () => {
|
|
75
|
+
const invalidData = {
|
|
76
|
+
type: 'invalid',
|
|
77
|
+
foo: 'bar',
|
|
78
|
+
};
|
|
79
|
+
expect(() => parseClientMessage(invalidData)).toThrow();
|
|
80
|
+
});
|
|
81
|
+
it('should reject subscribe without required fields', () => {
|
|
82
|
+
const invalidData = {
|
|
83
|
+
type: 'subscribe',
|
|
84
|
+
id: 1,
|
|
85
|
+
// missing name and args
|
|
86
|
+
};
|
|
87
|
+
expect(() => parseClientMessage(invalidData)).toThrow();
|
|
88
|
+
});
|
|
89
|
+
it('should reject unsubscribe without id', () => {
|
|
90
|
+
const invalidData = {
|
|
91
|
+
type: 'unsubscribe',
|
|
92
|
+
};
|
|
93
|
+
expect(() => parseClientMessage(invalidData)).toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('ServerMessage', () => {
|
|
98
|
+
describe('heartbeat', () => {
|
|
99
|
+
it('should create a valid heartbeat message', () => {
|
|
100
|
+
const msg = ServerMessage.heartbeat();
|
|
101
|
+
expect(msg).toEqual({
|
|
102
|
+
type: 'heartbeat',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('should validate against schema', () => {
|
|
106
|
+
const msg = ServerMessage.heartbeat();
|
|
107
|
+
expect(() => ServerHeartbeatMessageSchema.parse(msg)).not.toThrow();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('subscribed', () => {
|
|
111
|
+
it('should create a valid subscribed message with snapshot', () => {
|
|
112
|
+
const snapshot = { data: [1, 2, 3] };
|
|
113
|
+
const msg = ServerMessage.subscribed(7, snapshot);
|
|
114
|
+
expect(msg).toEqual({
|
|
115
|
+
type: 'snapshot',
|
|
116
|
+
id: 7,
|
|
117
|
+
snapshot,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it('should validate against schema', () => {
|
|
121
|
+
const msg = ServerMessage.subscribed(1, { foo: 'bar' });
|
|
122
|
+
expect(() => SubscribedSchema.parse(msg)).not.toThrow();
|
|
123
|
+
});
|
|
124
|
+
it('should handle null snapshot', () => {
|
|
125
|
+
const msg = ServerMessage.subscribed(1, null);
|
|
126
|
+
expect(msg.snapshot).toBe(null);
|
|
127
|
+
expect(() => SubscribedSchema.parse(msg)).not.toThrow();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('delta', () => {
|
|
131
|
+
it('should create a valid delta message', () => {
|
|
132
|
+
const changes = {
|
|
133
|
+
1: { add: ['a', 'b'] },
|
|
134
|
+
2: { remove: ['c'] },
|
|
135
|
+
};
|
|
136
|
+
const msg = ServerMessage.delta(changes);
|
|
137
|
+
expect(msg).toEqual({
|
|
138
|
+
type: 'delta',
|
|
139
|
+
changes,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
it('should validate against schema', () => {
|
|
143
|
+
const msg = ServerMessage.delta({ 1: { x: 10 }, 2: { y: 20 } });
|
|
144
|
+
expect(() => DeltaMessageSchema.parse(msg)).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
it('should handle empty changes', () => {
|
|
147
|
+
const msg = ServerMessage.delta({});
|
|
148
|
+
expect(msg.changes).toEqual({});
|
|
149
|
+
expect(() => DeltaMessageSchema.parse(msg)).not.toThrow();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|