@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.
@@ -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 {};
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ClientMessage, CallMessageSchema, parseClientMessage, } from '../client-message.js';
3
+ import { ServerMessage, ResultMessageSchema, } from '../server-message.js';
4
+ describe('Mutation Messages', () => {
5
+ describe('ClientMessage.call', () => {
6
+ it('should create a valid call message', () => {
7
+ const msg = ClientMessage.call(1, 'addUser', { name: 'Alice', age: 30 });
8
+ expect(msg).toEqual({
9
+ type: 'call',
10
+ id: 1,
11
+ name: 'addUser',
12
+ args: { name: 'Alice', age: 30 },
13
+ });
14
+ });
15
+ it('should validate against schema', () => {
16
+ const msg = ClientMessage.call(42, 'myMutation', { x: 10 });
17
+ expect(() => CallMessageSchema.parse(msg)).not.toThrow();
18
+ });
19
+ it('should be parseable by parseClientMessage', () => {
20
+ const data = {
21
+ type: 'call',
22
+ id: 5,
23
+ name: 'updateUser',
24
+ args: { id: 123, name: 'Bob' },
25
+ };
26
+ const result = parseClientMessage(data);
27
+ expect(result).toEqual(data);
28
+ expect(result.type).toBe('call');
29
+ });
30
+ it('should reject call without required fields', () => {
31
+ const invalidData = {
32
+ type: 'call',
33
+ id: 1,
34
+ // missing name and args
35
+ };
36
+ expect(() => parseClientMessage(invalidData)).toThrow();
37
+ });
38
+ });
39
+ describe('ServerMessage.resultSuccess', () => {
40
+ it('should create a valid success result message', () => {
41
+ const value = { userId: 42, status: 'created' };
42
+ const msg = ServerMessage.resultSuccess(1, value);
43
+ expect(msg).toEqual({
44
+ type: 'result',
45
+ id: 1,
46
+ success: true,
47
+ value,
48
+ });
49
+ });
50
+ it('should validate against schema', () => {
51
+ const msg = ServerMessage.resultSuccess(1, { data: 'test' });
52
+ expect(() => ResultMessageSchema.parse(msg)).not.toThrow();
53
+ });
54
+ it('should handle null values', () => {
55
+ const msg = ServerMessage.resultSuccess(1, null);
56
+ expect(msg.value).toBe(null);
57
+ expect(() => ResultMessageSchema.parse(msg)).not.toThrow();
58
+ });
59
+ it('should handle primitive values', () => {
60
+ const msgString = ServerMessage.resultSuccess(1, 'hello');
61
+ expect(msgString.value).toBe('hello');
62
+ const msgNumber = ServerMessage.resultSuccess(2, 42);
63
+ expect(msgNumber.value).toBe(42);
64
+ const msgBoolean = ServerMessage.resultSuccess(3, true);
65
+ expect(msgBoolean.value).toBe(true);
66
+ });
67
+ });
68
+ describe('ServerMessage.resultError', () => {
69
+ it('should create a valid error result message', () => {
70
+ const msg = ServerMessage.resultError(1, 'User not found');
71
+ expect(msg).toEqual({
72
+ type: 'result',
73
+ id: 1,
74
+ success: false,
75
+ error: 'User not found',
76
+ });
77
+ });
78
+ it('should validate against schema', () => {
79
+ const msg = ServerMessage.resultError(1, 'Something went wrong');
80
+ expect(() => ResultMessageSchema.parse(msg)).not.toThrow();
81
+ });
82
+ it('should handle empty error messages', () => {
83
+ const msg = ServerMessage.resultError(1, '');
84
+ expect(msg.error).toBe('');
85
+ expect(() => ResultMessageSchema.parse(msg)).not.toThrow();
86
+ });
87
+ });
88
+ });
89
+ describe('MutationResult type', () => {
90
+ it('should represent successful results', () => {
91
+ const result = {
92
+ success: true,
93
+ value: 42,
94
+ };
95
+ expect(result.success).toBe(true);
96
+ if (result.success) {
97
+ expect(result.value).toBe(42);
98
+ }
99
+ });
100
+ it('should represent error results', () => {
101
+ const result = {
102
+ success: false,
103
+ error: 'Failed to compute',
104
+ };
105
+ expect(result.success).toBe(false);
106
+ if (!result.success) {
107
+ expect(result.error).toBe('Failed to compute');
108
+ }
109
+ });
110
+ it('should work with complex types', () => {
111
+ const successResult = {
112
+ success: true,
113
+ value: { id: 1, name: 'Alice' },
114
+ };
115
+ const errorResult = {
116
+ success: false,
117
+ error: 'User creation failed',
118
+ };
119
+ expect(successResult.success).toBe(true);
120
+ expect(errorResult.success).toBe(false);
121
+ });
122
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Queue } from '../queue.js';
3
+ describe('Queue', () => {
4
+ it('should start empty', () => {
5
+ const q = new Queue();
6
+ expect(q.isEmpty()).toBe(true);
7
+ expect(q.length).toBe(0);
8
+ });
9
+ it('should push and pop single item', () => {
10
+ const q = new Queue();
11
+ q.push(1);
12
+ expect(q.length).toBe(1);
13
+ expect(q.pop()).toBe(1);
14
+ expect(q.isEmpty()).toBe(true);
15
+ });
16
+ it('should maintain FIFO order', () => {
17
+ const q = new Queue();
18
+ q.push(1);
19
+ q.push(2);
20
+ q.push(3);
21
+ expect(q.pop()).toBe(1);
22
+ expect(q.pop()).toBe(2);
23
+ expect(q.pop()).toBe(3);
24
+ expect(q.pop()).toBe(undefined);
25
+ });
26
+ it('should handle interleaved push and pop', () => {
27
+ const q = new Queue();
28
+ q.push('a');
29
+ q.push('b');
30
+ expect(q.pop()).toBe('a');
31
+ q.push('c');
32
+ expect(q.pop()).toBe('b');
33
+ expect(q.pop()).toBe('c');
34
+ expect(q.isEmpty()).toBe(true);
35
+ });
36
+ it('should reverse front to back when back is empty', () => {
37
+ const q = new Queue();
38
+ // Fill front
39
+ q.push(1);
40
+ q.push(2);
41
+ q.push(3);
42
+ // Pop empties back, triggers reverse
43
+ expect(q.pop()).toBe(1);
44
+ expect(q.pop()).toBe(2);
45
+ // Add more to front
46
+ q.push(4);
47
+ q.push(5);
48
+ // Continue popping - should get 3 (from back), then 4, 5 (after reverse)
49
+ expect(q.pop()).toBe(3);
50
+ expect(q.pop()).toBe(4);
51
+ expect(q.pop()).toBe(5);
52
+ expect(q.isEmpty()).toBe(true);
53
+ });
54
+ it('should handle large number of operations', () => {
55
+ const q = new Queue();
56
+ const n = 1000;
57
+ // Push n items
58
+ for (let i = 0; i < n; i++) {
59
+ q.push(i);
60
+ }
61
+ expect(q.length).toBe(n);
62
+ // Pop n items in order
63
+ for (let i = 0; i < n; i++) {
64
+ expect(q.pop()).toBe(i);
65
+ }
66
+ expect(q.isEmpty()).toBe(true);
67
+ });
68
+ it('should maintain length correctly through operations', () => {
69
+ const q = new Queue();
70
+ expect(q.length).toBe(0);
71
+ q.push(1);
72
+ expect(q.length).toBe(1);
73
+ q.push(2);
74
+ q.push(3);
75
+ expect(q.length).toBe(3);
76
+ q.pop();
77
+ expect(q.length).toBe(2);
78
+ q.pop();
79
+ q.pop();
80
+ expect(q.length).toBe(0);
81
+ q.pop(); // Pop from empty
82
+ expect(q.length).toBe(0);
83
+ });
84
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Graph } from 'derivation';
3
+ import { ZMap, Reactive, ZMapOperations, ZMapChangeInput } from '@derivation/composable';
4
+ import { ReactiveMapSourceAdapter, ReactiveMapSinkAdapter, sink } from '../reactive-map-adapter.js';
5
+ import * as iso from '../iso.js';
6
+ describe('ReactiveMapSourceAdapter', () => {
7
+ it('should provide snapshot as array of [key, value, weight] tuples', () => {
8
+ const graph = new Graph();
9
+ let zmap = new ZMap();
10
+ zmap = zmap.add('a', 1, 10).add('b', 2, 20);
11
+ const reactiveMap = Reactive.create(graph, new ZMapOperations(), new ZMapChangeInput(graph), zmap);
12
+ const adapter = new ReactiveMapSourceAdapter(reactiveMap, iso.id(), iso.id());
13
+ const snapshot = adapter.Snapshot;
14
+ expect(snapshot).toEqual([['a', 1, 10], ['b', 2, 20]]);
15
+ });
16
+ it('should transform keys and values using isomorphisms', () => {
17
+ const graph = new Graph();
18
+ let zmap = new ZMap();
19
+ zmap = zmap.add(1, 10, 5).add(2, 20, 15);
20
+ const reactiveMap = Reactive.create(graph, new ZMapOperations(), new ZMapChangeInput(graph), zmap);
21
+ const numToString = {
22
+ to: (n) => n.toString(),
23
+ from: (s) => parseInt(s, 10),
24
+ };
25
+ const adapter = new ReactiveMapSourceAdapter(reactiveMap, numToString, numToString);
26
+ const snapshot = adapter.Snapshot;
27
+ expect(snapshot).toEqual([['1', '10', 5], ['2', '20', 15]]);
28
+ });
29
+ it('should provide last change after modifications', () => {
30
+ const graph = new Graph();
31
+ const input = new ZMapChangeInput(graph);
32
+ const reactiveMap = Reactive.create(graph, new ZMapOperations(), input, new ZMap());
33
+ const adapter = new ReactiveMapSourceAdapter(reactiveMap, iso.id(), iso.id());
34
+ // Push a change
35
+ input.add('key', 'value', 1);
36
+ graph.step();
37
+ const lastChange = adapter.LastChange;
38
+ expect(lastChange).toEqual([['key', 'value', 1]]);
39
+ });
40
+ it('should return the underlying reactive map', () => {
41
+ const graph = new Graph();
42
+ const reactiveMap = Reactive.create(graph, new ZMapOperations(), new ZMapChangeInput(graph), new ZMap());
43
+ const adapter = new ReactiveMapSourceAdapter(reactiveMap, iso.id(), iso.id());
44
+ expect(adapter.Stream).toBe(reactiveMap);
45
+ });
46
+ it('should handle empty maps', () => {
47
+ const graph = new Graph();
48
+ const reactiveMap = Reactive.create(graph, new ZMapOperations(), new ZMapChangeInput(graph), new ZMap());
49
+ const adapter = new ReactiveMapSourceAdapter(reactiveMap, iso.id(), iso.id());
50
+ expect(adapter.Snapshot).toEqual([]);
51
+ });
52
+ });
53
+ describe('ReactiveMapSinkAdapter', () => {
54
+ it('should build a reactive map source', () => {
55
+ const graph = new Graph();
56
+ const adapter = new ReactiveMapSinkAdapter(graph, iso.id(), iso.id(), []);
57
+ const { stream: source } = adapter.build();
58
+ expect(source).toBeDefined();
59
+ expect([...source.snapshot.getEntries()]).toEqual([]);
60
+ });
61
+ it('should apply changes to a reactive map', () => {
62
+ const graph = new Graph();
63
+ const adapter = new ReactiveMapSinkAdapter(graph, iso.id(), iso.id(), []);
64
+ const { stream: source, input } = adapter.build();
65
+ // Apply changes
66
+ const change = [['a', 1, 5], ['b', 2, 10]];
67
+ adapter.apply(change, input);
68
+ graph.step();
69
+ // Check snapshot
70
+ const entries = [...source.snapshot.getEntries()];
71
+ expect(entries).toContainEqual(['a', 1, 5]);
72
+ expect(entries).toContainEqual(['b', 2, 10]);
73
+ });
74
+ it('should transform keys and values using isomorphisms', () => {
75
+ const graph = new Graph();
76
+ const strToNum = {
77
+ to: (n) => n.toString(),
78
+ from: (s) => parseInt(s, 10),
79
+ };
80
+ const adapter = new ReactiveMapSinkAdapter(graph, strToNum, strToNum, []);
81
+ const { stream: source, input } = adapter.build();
82
+ // Apply change with string keys and values
83
+ const change = [['5', '10', 1], ['15', '20', 2]];
84
+ adapter.apply(change, input);
85
+ graph.step();
86
+ // Should be converted to numbers
87
+ const entries = [...source.snapshot.getEntries()];
88
+ expect(entries).toContainEqual([5, 10, 1]);
89
+ expect(entries).toContainEqual([15, 20, 2]);
90
+ });
91
+ it('should handle adding entries with different weights', () => {
92
+ const graph = new Graph();
93
+ const adapter = new ReactiveMapSinkAdapter(graph, iso.id(), iso.id(), []);
94
+ const { stream: source, input } = adapter.build();
95
+ // Add same key with different weights
96
+ adapter.apply([['key', 'value1', 5]], input);
97
+ graph.step();
98
+ adapter.apply([['key', 'value2', 3]], input);
99
+ graph.step();
100
+ const entries = [...source.snapshot.getEntries()];
101
+ // Should contain both entries with their weights
102
+ expect(entries.length).toBeGreaterThan(0);
103
+ });
104
+ });
105
+ describe('sink function', () => {
106
+ it('should create a sink initialized with snapshot', () => {
107
+ const graph = new Graph();
108
+ const sinkFn = sink(graph, iso.id(), iso.id());
109
+ const snapshot = [['a', 1, 10], ['b', 2, 20]];
110
+ const sinkAdapter = sinkFn(snapshot);
111
+ const { stream: source } = sinkAdapter.build();
112
+ const entries = [...source.snapshot.getEntries()];
113
+ expect(entries).toContainEqual(['a', 1, 10]);
114
+ expect(entries).toContainEqual(['b', 2, 20]);
115
+ });
116
+ it('should allow applying changes after initialization', () => {
117
+ const graph = new Graph();
118
+ const sinkFn = sink(graph, iso.id(), iso.id());
119
+ const snapshot = [['x', 'y', 1]];
120
+ const sinkAdapter = sinkFn(snapshot);
121
+ const { stream: source, input } = sinkAdapter.build();
122
+ // Initial check
123
+ let entries = [...source.snapshot.getEntries()];
124
+ expect(entries).toContainEqual(['x', 'y', 1]);
125
+ // Apply a change
126
+ const change = [['a', 'b', 2], ['c', 'd', 3]];
127
+ sinkAdapter.apply(change, input);
128
+ graph.step();
129
+ entries = [...source.snapshot.getEntries()];
130
+ expect(entries.length).toBeGreaterThan(1);
131
+ expect(entries).toContainEqual(['a', 'b', 2]);
132
+ expect(entries).toContainEqual(['c', 'd', 3]);
133
+ });
134
+ it('should transform snapshot using isomorphisms', () => {
135
+ const graph = new Graph();
136
+ const strToNum = {
137
+ to: (n) => n.toString(),
138
+ from: (s) => parseInt(s, 10),
139
+ };
140
+ const sinkFn = sink(graph, strToNum, strToNum);
141
+ // Snapshot with string keys and values
142
+ const snapshot = [['10', '20', 5], ['30', '40', 10]];
143
+ const sinkAdapter = sinkFn(snapshot);
144
+ const { stream: source } = sinkAdapter.build();
145
+ // Should be converted to numbers
146
+ const entries = [...source.snapshot.getEntries()];
147
+ expect(entries).toContainEqual([10, 20, 5]);
148
+ expect(entries).toContainEqual([30, 40, 10]);
149
+ });
150
+ it('should handle empty snapshot', () => {
151
+ const graph = new Graph();
152
+ const sinkFn = sink(graph, iso.id(), iso.id());
153
+ const sinkAdapter = sinkFn([]);
154
+ const { stream: source } = sinkAdapter.build();
155
+ expect([...source.snapshot.getEntries()]).toEqual([]);
156
+ });
157
+ it('should create new source instances on each build call', () => {
158
+ const graph = new Graph();
159
+ const sinkFn = sink(graph, iso.id(), iso.id());
160
+ const sinkAdapter = sinkFn([['test', 1, 1]]);
161
+ const { stream: source1 } = sinkAdapter.build();
162
+ const { stream: source2 } = sinkAdapter.build();
163
+ // Each build creates a new instance - use strict equality to avoid vitest's deep comparison
164
+ expect(source1 === source2).toBe(false);
165
+ // But both have the same initial snapshot
166
+ expect([...source1.snapshot.getEntries()]).toEqual([['test', 1, 1]]);
167
+ expect([...source2.snapshot.getEntries()]).toEqual([['test', 1, 1]]);
168
+ });
169
+ it('should work with complex key and value types', () => {
170
+ const graph = new Graph();
171
+ const keyIso = iso.object({
172
+ id: iso.id(),
173
+ });
174
+ const valueIso = iso.object({
175
+ data: iso.id(),
176
+ });
177
+ const sinkFn = sink(graph, keyIso, valueIso);
178
+ const snapshot = [
179
+ [{ id: 1 }, { data: 'first' }, 5],
180
+ [{ id: 2 }, { data: 'second' }, 10],
181
+ ];
182
+ const sinkAdapter = sinkFn(snapshot);
183
+ const { stream: source } = sinkAdapter.build();
184
+ const entries = [...source.snapshot.getEntries()];
185
+ expect(entries.length).toBe(2);
186
+ expect(entries[0][0]).toEqual({ id: 1 });
187
+ expect(entries[0][1]).toEqual({ data: 'first' });
188
+ expect(entries[0][2]).toBe(5);
189
+ });
190
+ });
@@ -0,0 +1 @@
1
+ export {};