@derivation/rpc 0.3.0 → 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/iso.d.ts +1 -1
- package/dist/iso.js +1 -1
- package/dist/reactive-map-adapter.d.ts +7 -7
- package/dist/reactive-map-adapter.js +4 -3
- package/dist/reactive-set-adapter.d.ts +7 -7
- package/dist/reactive-set-adapter.js +4 -3
- 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 +10 -5
- package/dist/websocket-server.d.ts +0 -11
- package/dist/websocket-server.js +0 -59
- package/dist/websocket-transport.d.ts +0 -28
- package/dist/websocket-transport.js +0 -58
package/dist/iso.d.ts
CHANGED
package/dist/iso.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { Graph } from "derivation";
|
|
2
|
-
import {
|
|
2
|
+
import { ZMap, Reactive, ZMapChangeInput } from "@derivation/composable";
|
|
3
3
|
import { Source, Sink } from "./stream-types.js";
|
|
4
4
|
import { Iso } from "./iso.js";
|
|
5
|
-
export declare class ReactiveMapSourceAdapter<K, V> implements Source<
|
|
5
|
+
export declare class ReactiveMapSourceAdapter<K, V> implements Source<Reactive<ZMap<K, V>>> {
|
|
6
6
|
private readonly map;
|
|
7
7
|
private readonly iso;
|
|
8
|
-
constructor(map:
|
|
8
|
+
constructor(map: Reactive<ZMap<K, V>>, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>);
|
|
9
9
|
get Snapshot(): object;
|
|
10
10
|
get LastChange(): object | null;
|
|
11
|
-
get Stream():
|
|
11
|
+
get Stream(): Reactive<ZMap<K, V>>;
|
|
12
12
|
}
|
|
13
|
-
export declare class ReactiveMapSinkAdapter<K, V> implements Sink<
|
|
13
|
+
export declare class ReactiveMapSinkAdapter<K, V> implements Sink<Reactive<ZMap<K, V>>, ZMapChangeInput<K, V>> {
|
|
14
14
|
private readonly graph;
|
|
15
15
|
private readonly iso;
|
|
16
16
|
private readonly initialMap;
|
|
17
17
|
constructor(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>, snapshot: object);
|
|
18
18
|
apply(change: object, input: ZMapChangeInput<K, V>): void;
|
|
19
19
|
build(): {
|
|
20
|
-
stream:
|
|
20
|
+
stream: Reactive<ZMap<K, V>>;
|
|
21
21
|
input: ZMapChangeInput<K, V>;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
export declare function sink<K, V>(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>): (snapshot: object) => Sink<
|
|
24
|
+
export declare function sink<K, V>(graph: Graph, keyIso: Iso<K, unknown>, valueIso: Iso<V, unknown>): (snapshot: object) => Sink<Reactive<ZMap<K, V>>, ZMapChangeInput<K, V>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Reactive, ZMapOperations, ZMapChangeInput } from "@derivation/composable";
|
|
2
2
|
import { zmap } from "./iso.js";
|
|
3
3
|
export class ReactiveMapSourceAdapter {
|
|
4
4
|
constructor(map, keyIso, valueIso) {
|
|
@@ -31,8 +31,9 @@ export class ReactiveMapSinkAdapter {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
build() {
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const input = new ZMapChangeInput(this.graph);
|
|
35
|
+
const stream = Reactive.create(this.graph, new ZMapOperations(), input, this.initialMap);
|
|
36
|
+
return { stream, input };
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
export function sink(graph, keyIso, valueIso) {
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { Graph } from "derivation";
|
|
2
|
-
import { ZSet,
|
|
2
|
+
import { ZSet, Reactive, ZSetChangeInput } from "@derivation/composable";
|
|
3
3
|
import { Source, Sink } from "./stream-types.js";
|
|
4
4
|
import { Iso } from "./iso.js";
|
|
5
|
-
export declare class ReactiveSetSourceAdapter<T> implements Source<
|
|
5
|
+
export declare class ReactiveSetSourceAdapter<T> implements Source<Reactive<ZSet<T>>> {
|
|
6
6
|
private readonly set;
|
|
7
7
|
private readonly iso;
|
|
8
|
-
constructor(set:
|
|
8
|
+
constructor(set: Reactive<ZSet<T>>, iso: Iso<T, unknown>);
|
|
9
9
|
get Snapshot(): object;
|
|
10
10
|
get LastChange(): object | null;
|
|
11
|
-
get Stream():
|
|
11
|
+
get Stream(): Reactive<ZSet<T>>;
|
|
12
12
|
}
|
|
13
|
-
export declare class ReactiveSetSinkAdapter<T> implements Sink<
|
|
13
|
+
export declare class ReactiveSetSinkAdapter<T> implements Sink<Reactive<ZSet<T>>, ZSetChangeInput<T>> {
|
|
14
14
|
private readonly graph;
|
|
15
15
|
private readonly iso;
|
|
16
16
|
private readonly initialSet;
|
|
17
17
|
constructor(graph: Graph, iso: Iso<ZSet<T>, unknown>, snapshot: object);
|
|
18
18
|
apply(change: object, input: ZSetChangeInput<T>): void;
|
|
19
19
|
build(): {
|
|
20
|
-
stream:
|
|
20
|
+
stream: Reactive<ZSet<T>>;
|
|
21
21
|
input: ZSetChangeInput<T>;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
export declare function sink<T>(graph: Graph, iso: Iso<T, unknown>): (snapshot: object) => Sink<
|
|
24
|
+
export declare function sink<T>(graph: Graph, iso: Iso<T, unknown>): (snapshot: object) => Sink<Reactive<ZSet<T>>, ZSetChangeInput<T>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Reactive, ZSetOperations, ZSetChangeInput } from "@derivation/composable";
|
|
2
2
|
import { zset, zsetToArray, compose } from "./iso.js";
|
|
3
3
|
export class ReactiveSetSourceAdapter {
|
|
4
4
|
constructor(set, iso) {
|
|
@@ -28,8 +28,9 @@ export class ReactiveSetSinkAdapter {
|
|
|
28
28
|
input.push(this.iso.from(change));
|
|
29
29
|
}
|
|
30
30
|
build() {
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
const input = new ZSetChangeInput(this.graph);
|
|
32
|
+
const stream = Reactive.create(this.graph, new ZSetOperations(), input, this.initialSet);
|
|
33
|
+
return { stream, input };
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
export function sink(graph, iso) {
|
|
@@ -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 {};
|