@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.
- package/counter/store.json +8 -0
- package/dist/browser/createBridgeStore.d.ts +5 -0
- package/dist/browser/createBridgeStore.d.ts.map +1 -0
- package/dist/browser/createBridgeStore.js +232 -0
- package/dist/browser/createBridgeStore.js.map +1 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/types.d.ts +36 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/gateway/apiHandler.d.ts +10 -0
- package/dist/gateway/apiHandler.d.ts.map +1 -0
- package/dist/gateway/apiHandler.js +91 -0
- package/dist/gateway/apiHandler.js.map +1 -0
- package/dist/gateway/createBridgeGateway.d.ts +3 -0
- package/dist/gateway/createBridgeGateway.d.ts.map +1 -0
- package/dist/gateway/createBridgeGateway.js +689 -0
- package/dist/gateway/createBridgeGateway.js.map +1 -0
- package/dist/gateway/fileStore.d.ts +39 -0
- package/dist/gateway/fileStore.d.ts.map +1 -0
- package/dist/gateway/fileStore.js +189 -0
- package/dist/gateway/fileStore.js.map +1 -0
- package/dist/gateway/index.d.ts +6 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +5 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/registry.d.ts +21 -0
- package/dist/gateway/registry.d.ts.map +1 -0
- package/dist/gateway/registry.js +136 -0
- package/dist/gateway/registry.js.map +1 -0
- package/dist/gateway/types.d.ts +50 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +2 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk/BridgeClient.d.ts +38 -0
- package/dist/sdk/BridgeClient.d.ts.map +1 -0
- package/dist/sdk/BridgeClient.js +163 -0
- package/dist/sdk/BridgeClient.js.map +1 -0
- package/dist/sdk/index.d.ts +3 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/shared/types.d.ts +154 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +206 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +23 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +60 -0
- package/src/browser/createBridgeStore.ts +276 -0
- package/src/browser/index.ts +6 -0
- package/src/browser/types.ts +36 -0
- package/src/gateway/apiHandler.ts +107 -0
- package/src/gateway/createBridgeGateway.ts +854 -0
- package/src/gateway/fileStore.ts +244 -0
- package/src/gateway/index.ts +12 -0
- package/src/gateway/registry.ts +166 -0
- package/src/gateway/types.ts +65 -0
- package/src/index.ts +33 -0
- package/src/sdk/BridgeClient.ts +203 -0
- package/src/sdk/index.ts +2 -0
- package/src/shared/types.ts +117 -0
- package/src/utils/logger.ts +262 -0
- package/src/vite/index.ts +31 -0
- package/test/e2e/bridge.test.ts +386 -0
- package/test/integration/gateway.test.ts +485 -0
- package/test/mocks/mockWebSocket.ts +49 -0
- package/test/unit/browser.test.ts +267 -0
- package/test/unit/fileStore.test.ts +98 -0
- package/test/unit/registry.test.ts +345 -0
- package/test-page/store.json +8 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
});
|