@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 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAMlD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAoBtE"}
@@ -0,0 +1,23 @@
1
+ import { join } from 'path';
2
+ import { createBridgeGateway } from '../gateway/createBridgeGateway.js';
3
+ export function bridgePlugin(options = {}) {
4
+ return {
5
+ name: 'agentstage-bridge',
6
+ configureServer(server) {
7
+ const pagesDir = options.pagesDir || join(process.cwd(), 'src', 'pages');
8
+ const gateway = createBridgeGateway({ pagesDir });
9
+ // 保存 gateway 引用供后续使用
10
+ server.bridgeGateway = gateway;
11
+ // 同步检查 httpServer 是否可用
12
+ if (server.httpServer) {
13
+ gateway.attach(server.httpServer);
14
+ console.log('[Bridge] WebSocket mounted at /_bridge');
15
+ console.log('[Bridge] Pages directory:', pagesDir);
16
+ }
17
+ else {
18
+ console.warn('[Bridge] httpServer not available during configureServer');
19
+ }
20
+ },
21
+ };
22
+ }
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAMxE,MAAM,UAAU,YAAY,CAAC,UAA+B,EAAE;IAC5D,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,eAAe,CAAC,MAAqB;YACnC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACzE,MAAM,OAAO,GAAG,mBAAmB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;YAElD,qBAAqB;YACpB,MAAc,CAAC,aAAa,GAAG,OAAO,CAAC;YAExC,uBAAuB;YACvB,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;gBACtB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,UAAwC,CAAC,CAAC;gBAChE,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;gBACtD,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,QAAQ,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@agentstage/bridge",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/gateway/index.d.ts",
8
+ "default": "./dist/gateway/index.js"
9
+ },
10
+ "./browser": {
11
+ "types": "./dist/browser/index.d.ts",
12
+ "default": "./dist/browser/index.js"
13
+ },
14
+ "./sdk": {
15
+ "types": "./dist/sdk/index.d.ts",
16
+ "default": "./dist/sdk/index.js"
17
+ },
18
+ "./vite": {
19
+ "types": "./dist/vite/index.d.ts",
20
+ "default": "./dist/vite/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:unit": "vitest run test/unit",
30
+ "test:integration": "vitest run test/integration",
31
+ "test:e2e": "vitest run test/e2e",
32
+ "test:coverage": "vitest run --coverage"
33
+ },
34
+ "dependencies": {
35
+ "crossws": "^0.4.4",
36
+ "ws": "^8.14.0",
37
+ "zod": "^3.22.0",
38
+ "zod-to-json-schema": "^3.22.0",
39
+ "zustand": "^4.5.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@tanstack/react-router": "^1.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@tanstack/react-router": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.19.33",
51
+ "@types/ws": "^8.5.0",
52
+ "get-port": "^7.0.0",
53
+ "typescript": "^5.3.0",
54
+ "vite": "^5.4.21",
55
+ "vitest": "^2.0.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
@@ -0,0 +1,276 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla';
2
+ import { zodToJsonSchema } from 'zod-to-json-schema';
3
+ import type { CreateBridgeStoreOptions, BridgeStore } from './types.js';
4
+ import type { StoreDescription, GatewayMessage, ServerMessage } from '../shared/types.js';
5
+
6
+ const WS_PATH = '/_bridge';
7
+
8
+ function generateStoreId(pageId: string): string {
9
+ const random = Math.random().toString(36).substring(2, 10);
10
+ return `${pageId}#${random}`;
11
+ }
12
+
13
+ function getGatewayUrl(): string {
14
+ if (typeof window === 'undefined') {
15
+ return '';
16
+ }
17
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
18
+ return `${protocol}//${window.location.host}${WS_PATH}?type=browser`;
19
+ }
20
+
21
+ export function createBridgeStore<
22
+ TState,
23
+ TActions extends Record<string, { payload?: unknown }> = Record<string, never>
24
+ >(
25
+ options: CreateBridgeStoreOptions<TState, TActions>
26
+ ): BridgeStore<TState> {
27
+ const gatewayUrl = options.gatewayUrl;
28
+ const storeKey = options.storeKey || 'main';
29
+ const storeId = generateStoreId(options.pageId);
30
+
31
+ const store = createStore<TState>((set, get) =>
32
+ options.createState(
33
+ (fn) => set(fn(get())),
34
+ get
35
+ )
36
+ );
37
+
38
+ const description: StoreDescription = {
39
+ pageId: options.pageId,
40
+ storeKey,
41
+ schema: zodToJsonSchema(options.description.schema, { name: 'State' }),
42
+ actions: Object.fromEntries(
43
+ Object.entries(options.description.actions).map(([key, def]) => [
44
+ key,
45
+ {
46
+ description: def.description,
47
+ payload: def.payload
48
+ ? zodToJsonSchema(def.payload, { name: `${key}Payload` })
49
+ : undefined,
50
+ },
51
+ ])
52
+ ),
53
+ events: options.description.events
54
+ ? Object.fromEntries(
55
+ Object.entries(options.description.events).map(([key, def]) => [
56
+ key,
57
+ {
58
+ description: def.description,
59
+ payload: def.payload
60
+ ? zodToJsonSchema(def.payload, { name: `${key}Payload` })
61
+ : undefined,
62
+ },
63
+ ])
64
+ )
65
+ : undefined,
66
+ };
67
+
68
+ let ws: WebSocket | null = null;
69
+ let version = 0;
70
+ let isConnected = false;
71
+ let isHydrated = false;
72
+ let resolveHydration: (() => void) | null = null;
73
+ let hydrationPromise: Promise<void> | null = null;
74
+ let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
75
+ let suppressNextPublish = false;
76
+
77
+ function send(message: GatewayMessage) {
78
+ if (ws?.readyState === WebSocket.OPEN) {
79
+ ws.send(JSON.stringify(message));
80
+ }
81
+ }
82
+
83
+ function startHeartbeat() {
84
+ heartbeatInterval = setInterval(() => {
85
+ send({ type: 'store.heartbeat' });
86
+ }, 30000);
87
+ }
88
+
89
+ function stopHeartbeat() {
90
+ if (heartbeatInterval) {
91
+ clearInterval(heartbeatInterval);
92
+ heartbeatInterval = null;
93
+ }
94
+ }
95
+
96
+ function handleGatewayMessage(data: string) {
97
+ try {
98
+ const msg = JSON.parse(data) as ServerMessage;
99
+
100
+ switch (msg.type) {
101
+ case 'client.setState': {
102
+ const { state, expectedVersion, requestId, version: targetVersion } = msg.payload;
103
+ const canAck = typeof requestId === 'string' && requestId.length > 0;
104
+ if (expectedVersion !== undefined && expectedVersion !== version) {
105
+ if (canAck) {
106
+ send({
107
+ type: 'store.stateApplied',
108
+ payload: {
109
+ storeId,
110
+ requestId,
111
+ status: 'version_mismatch',
112
+ version,
113
+ },
114
+ });
115
+ }
116
+ return;
117
+ }
118
+ try {
119
+ suppressNextPublish = true;
120
+ store.setState(state as TState);
121
+ if (typeof targetVersion === 'number') {
122
+ version = targetVersion;
123
+ }
124
+ if (canAck) {
125
+ send({
126
+ type: 'store.stateApplied',
127
+ payload: {
128
+ storeId,
129
+ requestId,
130
+ status: 'applied',
131
+ version,
132
+ },
133
+ });
134
+ }
135
+ } catch (error) {
136
+ if (canAck) {
137
+ send({
138
+ type: 'store.stateApplied',
139
+ payload: {
140
+ storeId,
141
+ requestId,
142
+ status: 'failed',
143
+ version,
144
+ error: error instanceof Error ? error.message : String(error),
145
+ },
146
+ });
147
+ }
148
+ }
149
+
150
+ // Mark as hydrated on first setState
151
+ if (!isHydrated) {
152
+ isHydrated = true;
153
+ resolveHydration?.();
154
+ }
155
+ break;
156
+ }
157
+
158
+ case 'client.dispatch': {
159
+ const { action } = msg.payload;
160
+ const current = store.getState();
161
+ if (typeof (current as any).dispatch === 'function') {
162
+ (current as any).dispatch(action);
163
+ }
164
+ break;
165
+ }
166
+
167
+ case 'client.ping':
168
+ break;
169
+ }
170
+ } catch (err) {
171
+ console.error('[BridgeStore] Failed to handle message:', err);
172
+ }
173
+ }
174
+
175
+ const unsubscribe = store.subscribe((state) => {
176
+ if (suppressNextPublish) {
177
+ suppressNextPublish = false;
178
+ return;
179
+ }
180
+ version += 1;
181
+ send({
182
+ type: 'store.stateChanged',
183
+ payload: {
184
+ storeId,
185
+ state,
186
+ version,
187
+ source: 'browser',
188
+ },
189
+ });
190
+ });
191
+
192
+ return {
193
+ store,
194
+
195
+ describes() {
196
+ return description;
197
+ },
198
+
199
+ get isConnected() {
200
+ return isConnected;
201
+ },
202
+
203
+ get isHydrated() {
204
+ return isHydrated;
205
+ },
206
+
207
+ connect(): Promise<{ storeId: string; disconnect: () => void }> {
208
+ return new Promise((resolve, reject) => {
209
+ if (ws?.readyState === WebSocket.OPEN) {
210
+ resolve({ storeId, disconnect: () => ws?.close() });
211
+ return;
212
+ }
213
+
214
+ const url = gatewayUrl || getGatewayUrl();
215
+ if (!url) {
216
+ reject(new Error('Cannot connect: gatewayUrl not provided and window is not available'));
217
+ return;
218
+ }
219
+
220
+ ws = new WebSocket(url);
221
+
222
+ ws.onopen = () => {
223
+ isConnected = true;
224
+
225
+ // Create hydration promise - resolves when first client.setState arrives
226
+ hydrationPromise = new Promise((resolveHydrationFn) => {
227
+ resolveHydration = resolveHydrationFn;
228
+ });
229
+
230
+ send({
231
+ type: 'store.register',
232
+ payload: {
233
+ storeId,
234
+ pageId: options.pageId,
235
+ storeKey,
236
+ description,
237
+ initialState: store.getState(),
238
+ },
239
+ });
240
+
241
+ startHeartbeat();
242
+ };
243
+
244
+ // Wait for hydration before resolving connect()
245
+ const checkHydration = async () => {
246
+ if (hydrationPromise) {
247
+ await hydrationPromise;
248
+ }
249
+ resolve({
250
+ storeId,
251
+ disconnect: () => {
252
+ send({ type: 'store.disconnect' });
253
+ stopHeartbeat();
254
+ unsubscribe();
255
+ ws?.close();
256
+ },
257
+ });
258
+ };
259
+ checkHydration();
260
+
261
+ ws.onmessage = (event) => {
262
+ handleGatewayMessage(event.data);
263
+ };
264
+
265
+ ws.onclose = () => {
266
+ isConnected = false;
267
+ stopHeartbeat();
268
+ };
269
+
270
+ ws.onerror = (err) => {
271
+ reject(err);
272
+ };
273
+ });
274
+ },
275
+ };
276
+ }
@@ -0,0 +1,6 @@
1
+ export { createBridgeStore } from './createBridgeStore.js';
2
+ export type {
3
+ CreateBridgeStoreOptions,
4
+ BridgeStore,
5
+ } from './types.js';
6
+ export type { StoreDescription, StoreState } from '../shared/types.js';
@@ -0,0 +1,36 @@
1
+ import type { StoreApi } from 'zustand/vanilla';
2
+ import type { ZodSchema } from 'zod';
3
+ import type { StoreDescription, StoreState } from '../shared/types.js';
4
+
5
+ export interface CreateBridgeStoreOptions<
6
+ TState,
7
+ TActions extends Record<string, { payload?: unknown }>
8
+ > {
9
+ gatewayUrl?: string;
10
+ pageId: string;
11
+ storeKey?: string;
12
+ description: {
13
+ schema: ZodSchema<TState>;
14
+ actions: {
15
+ [K in keyof TActions]: {
16
+ description: string;
17
+ payload?: ZodSchema<TActions[K]['payload']>;
18
+ }
19
+ };
20
+ events?: Record<string, { description: string; payload?: ZodSchema<unknown> }>;
21
+ };
22
+ createState: (
23
+ set: (fn: (state: TState) => Partial<TState>) => void,
24
+ get: () => TState
25
+ ) => TState;
26
+ }
27
+
28
+ export interface BridgeStore<TState> {
29
+ readonly store: StoreApi<TState>;
30
+ describes(): StoreDescription;
31
+ connect(): Promise<{ storeId: string; disconnect: () => void }>;
32
+ readonly isConnected: boolean;
33
+ readonly isHydrated: boolean;
34
+ }
35
+
36
+ export type { StoreDescription, StoreState };
@@ -0,0 +1,107 @@
1
+ import type { Gateway } from './types.js';
2
+
3
+ export function createBridgeApiHandler(gateway: Gateway) {
4
+ return {
5
+ GET: async ({ request }: { request: Request }) => {
6
+ const url = new URL(request.url);
7
+ const path = url.pathname.replace('/api/bridge', '').split('/').filter(Boolean);
8
+
9
+ if (path.length === 1 && path[0] === 'stores') {
10
+ return Response.json({ stores: gateway.listStores() });
11
+ }
12
+
13
+ if (path.length === 2 && path[0] === 'stores') {
14
+ const storeId = path[1];
15
+ const store = gateway.getStore(storeId);
16
+ if (!store) {
17
+ return new Response(JSON.stringify({ error: 'Store not found' }), {
18
+ status: 404,
19
+ headers: { 'Content-Type': 'application/json' }
20
+ });
21
+ }
22
+ return Response.json({
23
+ id: store.id,
24
+ pageId: store.pageId,
25
+ storeKey: store.storeKey,
26
+ description: store.description,
27
+ version: store.version,
28
+ connectedAt: store.connectedAt,
29
+ lastActivity: store.lastActivity,
30
+ });
31
+ }
32
+
33
+ if (path.length === 3 && path[0] === 'stores' && path[2] === 'state') {
34
+ const storeId = path[1];
35
+ const state = gateway.getState(storeId);
36
+ if (!state) {
37
+ return new Response(JSON.stringify({ error: 'Store not found' }), {
38
+ status: 404,
39
+ headers: { 'Content-Type': 'application/json' }
40
+ });
41
+ }
42
+ return Response.json(state);
43
+ }
44
+
45
+ if (path.length === 3 && path[0] === 'pages' && path[2] === 'stores') {
46
+ const pageId = path[1];
47
+ const stores = gateway.stores;
48
+ const pageStores = Array.from(stores.values())
49
+ .filter(s => s.pageId === pageId)
50
+ .map(s => ({
51
+ id: s.id,
52
+ storeKey: s.storeKey,
53
+ version: s.version,
54
+ connectedAt: s.connectedAt,
55
+ }));
56
+ return Response.json({ stores: pageStores });
57
+ }
58
+
59
+ return new Response(JSON.stringify({ error: 'Not found' }), {
60
+ status: 404,
61
+ headers: { 'Content-Type': 'application/json' }
62
+ });
63
+ },
64
+
65
+ POST: async ({ request }: { request: Request }) => {
66
+ const url = new URL(request.url);
67
+ const path = url.pathname.replace('/api/bridge', '').split('/').filter(Boolean);
68
+
69
+ if (path.length === 3 && path[0] === 'stores' && path[2] === 'state') {
70
+ const storeId = path[1];
71
+ const body = await request.json();
72
+
73
+ try {
74
+ await gateway.setState(storeId, body.state, {
75
+ expectedVersion: body.expectedVersion
76
+ });
77
+ return Response.json({ ok: true });
78
+ } catch (err) {
79
+ return new Response(
80
+ JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to set state' }),
81
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
82
+ );
83
+ }
84
+ }
85
+
86
+ if (path.length === 3 && path[0] === 'stores' && path[2] === 'dispatch') {
87
+ const storeId = path[1];
88
+ const body = await request.json();
89
+
90
+ try {
91
+ await gateway.dispatch(storeId, body.action);
92
+ return Response.json({ ok: true });
93
+ } catch (err) {
94
+ return new Response(
95
+ JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to dispatch' }),
96
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
97
+ );
98
+ }
99
+ }
100
+
101
+ return new Response(JSON.stringify({ error: 'Not found' }), {
102
+ status: 404,
103
+ headers: { 'Content-Type': 'application/json' }
104
+ });
105
+ },
106
+ };
107
+ }