@agentstage/bridge 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.
Files changed (87) hide show
  1. package/counter/store.json +8 -0
  2. package/dist/browser/createBridgeStore.d.ts +5 -0
  3. package/dist/browser/createBridgeStore.d.ts.map +1 -0
  4. package/dist/browser/createBridgeStore.js +232 -0
  5. package/dist/browser/createBridgeStore.js.map +1 -0
  6. package/dist/browser/index.d.ts +4 -0
  7. package/dist/browser/index.d.ts.map +1 -0
  8. package/dist/browser/index.js +2 -0
  9. package/dist/browser/index.js.map +1 -0
  10. package/dist/browser/types.d.ts +36 -0
  11. package/dist/browser/types.d.ts.map +1 -0
  12. package/dist/browser/types.js +2 -0
  13. package/dist/browser/types.js.map +1 -0
  14. package/dist/gateway/apiHandler.d.ts +10 -0
  15. package/dist/gateway/apiHandler.d.ts.map +1 -0
  16. package/dist/gateway/apiHandler.js +91 -0
  17. package/dist/gateway/apiHandler.js.map +1 -0
  18. package/dist/gateway/createBridgeGateway.d.ts +3 -0
  19. package/dist/gateway/createBridgeGateway.d.ts.map +1 -0
  20. package/dist/gateway/createBridgeGateway.js +689 -0
  21. package/dist/gateway/createBridgeGateway.js.map +1 -0
  22. package/dist/gateway/fileStore.d.ts +39 -0
  23. package/dist/gateway/fileStore.d.ts.map +1 -0
  24. package/dist/gateway/fileStore.js +189 -0
  25. package/dist/gateway/fileStore.js.map +1 -0
  26. package/dist/gateway/index.d.ts +6 -0
  27. package/dist/gateway/index.d.ts.map +1 -0
  28. package/dist/gateway/index.js +5 -0
  29. package/dist/gateway/index.js.map +1 -0
  30. package/dist/gateway/registry.d.ts +21 -0
  31. package/dist/gateway/registry.d.ts.map +1 -0
  32. package/dist/gateway/registry.js +136 -0
  33. package/dist/gateway/registry.js.map +1 -0
  34. package/dist/gateway/types.d.ts +50 -0
  35. package/dist/gateway/types.d.ts.map +1 -0
  36. package/dist/gateway/types.js +2 -0
  37. package/dist/gateway/types.js.map +1 -0
  38. package/dist/index.d.ts +26 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +24 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/sdk/BridgeClient.d.ts +38 -0
  43. package/dist/sdk/BridgeClient.d.ts.map +1 -0
  44. package/dist/sdk/BridgeClient.js +163 -0
  45. package/dist/sdk/BridgeClient.js.map +1 -0
  46. package/dist/sdk/index.d.ts +3 -0
  47. package/dist/sdk/index.d.ts.map +1 -0
  48. package/dist/sdk/index.js +2 -0
  49. package/dist/sdk/index.js.map +1 -0
  50. package/dist/shared/types.d.ts +154 -0
  51. package/dist/shared/types.d.ts.map +1 -0
  52. package/dist/shared/types.js +5 -0
  53. package/dist/shared/types.js.map +1 -0
  54. package/dist/utils/logger.d.ts +33 -0
  55. package/dist/utils/logger.d.ts.map +1 -0
  56. package/dist/utils/logger.js +206 -0
  57. package/dist/utils/logger.js.map +1 -0
  58. package/dist/vite/index.d.ts +6 -0
  59. package/dist/vite/index.d.ts.map +1 -0
  60. package/dist/vite/index.js +23 -0
  61. package/dist/vite/index.js.map +1 -0
  62. package/package.json +60 -0
  63. package/src/browser/createBridgeStore.ts +276 -0
  64. package/src/browser/index.ts +6 -0
  65. package/src/browser/types.ts +36 -0
  66. package/src/gateway/apiHandler.ts +107 -0
  67. package/src/gateway/createBridgeGateway.ts +854 -0
  68. package/src/gateway/fileStore.ts +244 -0
  69. package/src/gateway/index.ts +12 -0
  70. package/src/gateway/registry.ts +166 -0
  71. package/src/gateway/types.ts +65 -0
  72. package/src/index.ts +33 -0
  73. package/src/sdk/BridgeClient.ts +203 -0
  74. package/src/sdk/index.ts +2 -0
  75. package/src/shared/types.ts +117 -0
  76. package/src/utils/logger.ts +262 -0
  77. package/src/vite/index.ts +31 -0
  78. package/test/e2e/bridge.test.ts +386 -0
  79. package/test/integration/gateway.test.ts +485 -0
  80. package/test/mocks/mockWebSocket.ts +49 -0
  81. package/test/unit/browser.test.ts +267 -0
  82. package/test/unit/fileStore.test.ts +98 -0
  83. package/test/unit/registry.test.ts +345 -0
  84. package/test-page/store.json +8 -0
  85. package/tsconfig.json +20 -0
  86. package/tsconfig.tsbuildinfo +1 -0
  87. package/vitest.config.ts +17 -0
@@ -0,0 +1,386 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { createServer } from 'http';
3
+ import { WebSocket, WebSocketServer } from 'ws';
4
+ import getPort from 'get-port';
5
+ import { createBridgeGateway } from '../../src/gateway/createBridgeGateway.js';
6
+ import { BridgeClient } from '../../src/sdk/BridgeClient.js';
7
+
8
+ import { mkdtempSync, rmSync } from 'fs';
9
+ import { tmpdir } from 'os';
10
+ import { join } from 'path';
11
+
12
+ describe('Bridge E2E Tests', () => {
13
+ let gateway: ReturnType<typeof createBridgeGateway> & {
14
+ attach: (server: any) => WebSocketServer;
15
+ destroy: () => void;
16
+ findStore: (pageId: string, storeKey: string) => { id: string } | undefined;
17
+ };
18
+ let server: ReturnType<typeof createServer>;
19
+ let wss: WebSocketServer;
20
+ let port: number;
21
+ let gatewayUrl: string;
22
+ let tempDir: string;
23
+
24
+ beforeEach(async () => {
25
+ // Create a temp directory for each test to isolate file storage
26
+ tempDir = mkdtempSync(join(tmpdir(), 'bridge-e2e-'));
27
+ gateway = createBridgeGateway({ pagesDir: tempDir }) as any;
28
+ server = createServer();
29
+ wss = gateway.attach(server);
30
+ port = await getPort();
31
+ gatewayUrl = `ws://localhost:${port}/_bridge`;
32
+ await new Promise<void>((resolve) => server.listen(port, resolve));
33
+ });
34
+
35
+ afterEach(() => {
36
+ gateway.destroy();
37
+ wss.close();
38
+ server.close();
39
+ // Clean up temp directory
40
+ try {
41
+ rmSync(tempDir, { recursive: true, force: true });
42
+ } catch {}
43
+ });
44
+
45
+ describe('Browser store <-> Gateway <-> SDK Client', () => {
46
+ it('should sync state from browser to SDK client', async () => {
47
+ // 1. Browser connects and registers
48
+ const browserWs = new WebSocket(`${gatewayUrl}?type=browser`);
49
+ await new Promise<void>((resolve, reject) => {
50
+ browserWs.on('open', resolve);
51
+ browserWs.on('error', reject);
52
+ });
53
+
54
+ browserWs.send(JSON.stringify({
55
+ type: 'store.register',
56
+ payload: {
57
+ storeId: 'counter#test1',
58
+ pageId: 'counter',
59
+ storeKey: 'main',
60
+ description: {
61
+ pageId: 'counter',
62
+ storeKey: 'main',
63
+ schema: { type: 'object' },
64
+ actions: {},
65
+ },
66
+ initialState: { count: 0 },
67
+ },
68
+ }));
69
+
70
+ await new Promise(r => setTimeout(r, 50));
71
+
72
+ // 2. SDK Client connects and subscribes
73
+ const client = new BridgeClient(gatewayUrl);
74
+ const events: unknown[] = [];
75
+
76
+ client.onEvent((event) => {
77
+ events.push(event);
78
+ });
79
+
80
+ await client.connect();
81
+ client.subscribe('counter#test1');
82
+
83
+ await new Promise(r => setTimeout(r, 100));
84
+
85
+ // Should have received snapshot
86
+ const snapshot = events.find(e => (e as any).type === 'stateChanged');
87
+ expect(snapshot).toBeDefined();
88
+ expect((snapshot as any).state).toEqual({ count: 0 });
89
+
90
+ // 3. Browser updates state
91
+ browserWs.send(JSON.stringify({
92
+ type: 'store.stateChanged',
93
+ payload: {
94
+ storeId: 'counter#test1',
95
+ state: { count: 5 },
96
+ version: 1,
97
+ source: 'browser',
98
+ },
99
+ }));
100
+
101
+ await new Promise(r => setTimeout(r, 100));
102
+
103
+ // 4. Client should receive the update
104
+ const update = events.find(
105
+ e => (e as any).type === 'stateChanged' && (e as any).source === 'browser'
106
+ );
107
+ expect(update).toBeDefined();
108
+ expect((update as any).state).toEqual({ count: 5 });
109
+
110
+ browserWs.close();
111
+ client.disconnect();
112
+ });
113
+
114
+ it('should dispatch action from SDK to browser', async () => {
115
+ // 1. Browser connects
116
+ const browserWs = new WebSocket(`${gatewayUrl}?type=browser`);
117
+ const receivedMessages: unknown[] = [];
118
+
119
+ browserWs.on('message', (data) => {
120
+ receivedMessages.push(JSON.parse(data.toString()));
121
+ });
122
+
123
+ await new Promise<void>((resolve, reject) => {
124
+ browserWs.on('open', resolve);
125
+ browserWs.on('error', reject);
126
+ });
127
+
128
+ browserWs.send(JSON.stringify({
129
+ type: 'store.register',
130
+ payload: {
131
+ storeId: 'counter#test2',
132
+ pageId: 'counter',
133
+ storeKey: 'main',
134
+ description: {
135
+ pageId: 'counter',
136
+ storeKey: 'main',
137
+ schema: { type: 'object' },
138
+ actions: {},
139
+ },
140
+ initialState: { count: 0 },
141
+ },
142
+ }));
143
+
144
+ await new Promise(r => setTimeout(r, 50));
145
+
146
+ // 2. SDK Client connects and dispatches action
147
+ const client = new BridgeClient(gatewayUrl);
148
+ await client.connect();
149
+
150
+ // Note: SDK's dispatch is not fully implemented in the current BridgeClient
151
+ // but we can test via gateway API directly
152
+ await gateway.dispatch('counter#test2', { type: 'increment', payload: { by: 10 } });
153
+
154
+ await new Promise(r => setTimeout(r, 100));
155
+
156
+ // 3. Browser should receive the dispatch
157
+ const dispatchMsg = receivedMessages.find(
158
+ m => (m as any).type === 'client.dispatch'
159
+ );
160
+ expect(dispatchMsg).toBeDefined();
161
+ expect((dispatchMsg as any).payload.action).toEqual({ type: 'increment', payload: { by: 10 } });
162
+
163
+ browserWs.close();
164
+ client.disconnect();
165
+ });
166
+
167
+ it('should handle page refresh scenario', async () => {
168
+ const client = new BridgeClient(gatewayUrl);
169
+ const events: unknown[] = [];
170
+
171
+ client.onEvent((event) => {
172
+ events.push(event);
173
+ });
174
+
175
+ await client.connect();
176
+
177
+ // 1. First browser tab connects
178
+ const browser1 = new WebSocket(`${gatewayUrl}?type=browser`);
179
+ await new Promise<void>((resolve) => browser1.on('open', resolve));
180
+
181
+ browser1.send(JSON.stringify({
182
+ type: 'store.register',
183
+ payload: {
184
+ storeId: 'page#old',
185
+ pageId: 'test-page',
186
+ storeKey: 'main',
187
+ description: {
188
+ pageId: 'test-page',
189
+ storeKey: 'main',
190
+ schema: { type: 'object' },
191
+ actions: {},
192
+ },
193
+ initialState: { version: 1 },
194
+ },
195
+ }));
196
+
197
+ await new Promise(r => setTimeout(r, 50));
198
+
199
+ // Client subscribes
200
+ client.subscribe('page#old');
201
+ await new Promise(r => setTimeout(r, 50));
202
+
203
+ // Clear events
204
+ events.length = 0;
205
+
206
+ // 2. Page refreshes (new tab)
207
+ const browser2 = new WebSocket(`${gatewayUrl}?type=browser`);
208
+ await new Promise<void>((resolve) => browser2.on('open', resolve));
209
+
210
+ browser2.send(JSON.stringify({
211
+ type: 'store.register',
212
+ payload: {
213
+ storeId: 'page#new',
214
+ pageId: 'test-page',
215
+ storeKey: 'main',
216
+ description: {
217
+ pageId: 'test-page',
218
+ storeKey: 'main',
219
+ schema: { type: 'object' },
220
+ actions: {},
221
+ },
222
+ initialState: { version: 2 },
223
+ },
224
+ }));
225
+
226
+ await new Promise(r => setTimeout(r, 100));
227
+
228
+ // 3. Client should receive disconnected for old store
229
+ const disconnected = events.find(e => (e as any).type === 'disconnected');
230
+ expect(disconnected).toBeDefined();
231
+ expect((disconnected as any).storeId).toBe('page#old');
232
+
233
+ browser1.close();
234
+ browser2.close();
235
+ client.disconnect();
236
+ });
237
+
238
+ it('should handle multiple browsers on same page', async () => {
239
+ const client = new BridgeClient(gatewayUrl);
240
+ const events: unknown[] = [];
241
+
242
+ client.onEvent((event) => {
243
+ events.push(event);
244
+ });
245
+
246
+ await client.connect();
247
+
248
+ // 1. Browser 1 connects
249
+ const browser1 = new WebSocket(`${gatewayUrl}?type=browser`);
250
+ await new Promise<void>((resolve) => browser1.on('open', resolve));
251
+
252
+ browser1.send(JSON.stringify({
253
+ type: 'store.register',
254
+ payload: {
255
+ storeId: 'page#tab1',
256
+ pageId: 'test-page',
257
+ storeKey: 'main',
258
+ description: {
259
+ pageId: 'test-page',
260
+ storeKey: 'main',
261
+ schema: { type: 'object' },
262
+ actions: {},
263
+ },
264
+ initialState: { tab: 1 },
265
+ },
266
+ }));
267
+
268
+ await new Promise(r => setTimeout(r, 50));
269
+
270
+ // Client subscribes to first tab
271
+ client.subscribe('page#tab1');
272
+ await new Promise(r => setTimeout(r, 50));
273
+
274
+ // 2. Browser 2 (second tab) connects with same pageId+storeKey
275
+ const browser2 = new WebSocket(`${gatewayUrl}?type=browser`);
276
+ await new Promise<void>((resolve) => browser2.on('open', resolve));
277
+
278
+ browser2.send(JSON.stringify({
279
+ type: 'store.register',
280
+ payload: {
281
+ storeId: 'page#tab2',
282
+ pageId: 'test-page',
283
+ storeKey: 'main',
284
+ description: {
285
+ pageId: 'test-page',
286
+ storeKey: 'main',
287
+ schema: { type: 'object' },
288
+ actions: {},
289
+ },
290
+ initialState: { tab: 2 },
291
+ },
292
+ }));
293
+
294
+ await new Promise(r => setTimeout(r, 100));
295
+
296
+ // First tab's store should be replaced (disconnected)
297
+ // Client should have received disconnected for page#tab1
298
+ const disconnected = events.find(
299
+ e => (e as any).type === 'disconnected' && (e as any).storeId === 'page#tab1'
300
+ );
301
+ expect(disconnected).toBeDefined();
302
+
303
+ // Gateway.findStore should return the new store
304
+ const currentStore = gateway.findStore('test-page', 'main');
305
+ expect(currentStore?.id).toBe('page#tab2');
306
+
307
+ browser1.close();
308
+ browser2.close();
309
+ client.disconnect();
310
+ });
311
+ });
312
+
313
+ describe('HTTP API', () => {
314
+ it('should list stores via API', async () => {
315
+ // 1. Register a store via WebSocket
316
+ const browserWs = new WebSocket(`${gatewayUrl}?type=browser`);
317
+ await new Promise<void>((resolve, reject) => {
318
+ browserWs.on('open', resolve);
319
+ browserWs.on('error', reject);
320
+ });
321
+
322
+ browserWs.send(JSON.stringify({
323
+ type: 'store.register',
324
+ payload: {
325
+ storeId: 'page#api-test',
326
+ pageId: 'api-page',
327
+ storeKey: 'main',
328
+ description: {
329
+ pageId: 'api-page',
330
+ storeKey: 'main',
331
+ schema: { type: 'object' },
332
+ actions: {},
333
+ },
334
+ initialState: { data: 'test' },
335
+ },
336
+ }));
337
+
338
+ await new Promise(r => setTimeout(r, 50));
339
+
340
+ // 2. Call API to list stores
341
+ const stores = gateway.listStores();
342
+
343
+ const found = stores.find(s => s.id === 'page#api-test');
344
+ expect(found).toBeDefined();
345
+ expect(found?.pageId).toBe('api-page');
346
+
347
+ browserWs.close();
348
+ });
349
+
350
+ it('should get store state via API', async () => {
351
+ // 1. Register a store
352
+ const browserWs = new WebSocket(`${gatewayUrl}?type=browser`);
353
+ await new Promise<void>((resolve, reject) => {
354
+ browserWs.on('open', resolve);
355
+ browserWs.on('error', reject);
356
+ });
357
+
358
+ browserWs.send(JSON.stringify({
359
+ type: 'store.register',
360
+ payload: {
361
+ storeId: 'page#state-test',
362
+ pageId: 'state-page',
363
+ storeKey: 'main',
364
+ description: {
365
+ pageId: 'state-page',
366
+ storeKey: 'main',
367
+ schema: { type: 'object' },
368
+ actions: {},
369
+ },
370
+ initialState: { count: 42 },
371
+ },
372
+ }));
373
+
374
+ await new Promise(r => setTimeout(r, 50));
375
+
376
+ // 2. Get state via gateway API
377
+ const state = gateway.getState('page#state-test');
378
+
379
+ expect(state).toBeDefined();
380
+ expect(state?.state).toEqual({ count: 42 });
381
+ expect(state?.version).toBe(0);
382
+
383
+ browserWs.close();
384
+ });
385
+ });
386
+ });