@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,244 @@
|
|
|
1
|
+
import { mkdirSync, watch, type FSWatcher } from 'fs';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'fs/promises';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
export interface StoreData<T = unknown> {
|
|
6
|
+
state: T;
|
|
7
|
+
version: number;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
pageId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FileStoreOptions {
|
|
13
|
+
pagesDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PAGE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
17
|
+
|
|
18
|
+
export class InvalidPageIdError extends Error {
|
|
19
|
+
readonly pageId: string;
|
|
20
|
+
|
|
21
|
+
constructor(pageId: string) {
|
|
22
|
+
super(`Invalid pageId "${pageId}". pageId must match /^[a-zA-Z0-9_-]+$/.`);
|
|
23
|
+
this.name = 'InvalidPageIdError';
|
|
24
|
+
this.pageId = pageId;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class VersionConflictError extends Error {
|
|
29
|
+
readonly pageId: string;
|
|
30
|
+
readonly expectedVersion: number;
|
|
31
|
+
readonly actualVersion: number | null;
|
|
32
|
+
|
|
33
|
+
constructor(pageId: string, expectedVersion: number, actualVersion: number | null) {
|
|
34
|
+
super(
|
|
35
|
+
`Version conflict for "${pageId}": expected ${expectedVersion}, actual ${
|
|
36
|
+
actualVersion === null ? 'null' : actualVersion
|
|
37
|
+
}.`
|
|
38
|
+
);
|
|
39
|
+
this.name = 'VersionConflictError';
|
|
40
|
+
this.pageId = pageId;
|
|
41
|
+
this.expectedVersion = expectedVersion;
|
|
42
|
+
this.actualVersion = actualVersion;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validatePageId(pageId: string): void {
|
|
47
|
+
if (!PAGE_ID_PATTERN.test(pageId)) {
|
|
48
|
+
throw new InvalidPageIdError(pageId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class FileStore {
|
|
53
|
+
private pagesDir: string;
|
|
54
|
+
private watchers = new Map<string, FSWatcher>();
|
|
55
|
+
private writeQueue: Array<{
|
|
56
|
+
pageId: string;
|
|
57
|
+
path: string;
|
|
58
|
+
data: StoreData<unknown>;
|
|
59
|
+
expectedVersion?: number;
|
|
60
|
+
resolve: (savedData: StoreData<unknown>) => void;
|
|
61
|
+
reject: (error: unknown) => void;
|
|
62
|
+
}> = [];
|
|
63
|
+
private processing = false;
|
|
64
|
+
private versionClock = new Map<string, number>();
|
|
65
|
+
|
|
66
|
+
constructor(options: FileStoreOptions) {
|
|
67
|
+
this.pagesDir = options.pagesDir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private getStorePath(pageId: string): string {
|
|
71
|
+
return join(this.pagesDir, pageId, 'store.json');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async load<T>(pageId: string): Promise<StoreData<T> | null> {
|
|
75
|
+
validatePageId(pageId);
|
|
76
|
+
const path = this.getStorePath(pageId);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const content = await readFile(path, 'utf8');
|
|
80
|
+
const parsed = JSON.parse(content) as StoreData<T>;
|
|
81
|
+
if (typeof parsed.version === 'number') {
|
|
82
|
+
this.bumpVersionClock(pageId, parsed.version);
|
|
83
|
+
}
|
|
84
|
+
return parsed;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const err = error as NodeJS.ErrnoException;
|
|
87
|
+
if (err.code === 'ENOENT') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async save<T>(pageId: string, data: StoreData<T>, expectedVersion?: number): Promise<StoreData<T>> {
|
|
96
|
+
validatePageId(pageId);
|
|
97
|
+
const path = this.getStorePath(pageId);
|
|
98
|
+
|
|
99
|
+
return new Promise<StoreData<T>>((resolve, reject) => {
|
|
100
|
+
this.writeQueue.push({
|
|
101
|
+
pageId,
|
|
102
|
+
path,
|
|
103
|
+
data: data as StoreData<unknown>,
|
|
104
|
+
expectedVersion,
|
|
105
|
+
resolve: (savedData) => resolve(savedData as StoreData<T>),
|
|
106
|
+
reject,
|
|
107
|
+
});
|
|
108
|
+
void this.processWriteQueue();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async processWriteQueue(): Promise<void> {
|
|
113
|
+
if (this.processing) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.processing = true;
|
|
118
|
+
|
|
119
|
+
while (this.writeQueue.length > 0) {
|
|
120
|
+
const task = this.writeQueue.shift();
|
|
121
|
+
if (!task) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const current = await this.readStoreData(task.path);
|
|
127
|
+
const currentVersion = current?.version ?? null;
|
|
128
|
+
|
|
129
|
+
if (task.expectedVersion !== undefined && currentVersion !== null && currentVersion !== task.expectedVersion) {
|
|
130
|
+
throw new VersionConflictError(task.pageId, task.expectedVersion, currentVersion);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nextVersion = this.nextVersion(task.pageId, current?.version ?? 0);
|
|
134
|
+
const savedData: StoreData<unknown> = {
|
|
135
|
+
...task.data,
|
|
136
|
+
pageId: task.pageId,
|
|
137
|
+
version: nextVersion,
|
|
138
|
+
updatedAt: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await this.writeAtomically(task.path, JSON.stringify(savedData, null, 2));
|
|
142
|
+
task.resolve(savedData);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
task.reject(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.processing = false;
|
|
149
|
+
|
|
150
|
+
if (this.writeQueue.length > 0) {
|
|
151
|
+
void this.processWriteQueue();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async writeAtomically(path: string, content: string): Promise<void> {
|
|
156
|
+
await mkdir(dirname(path), { recursive: true });
|
|
157
|
+
const tmpPath = `${path}.tmp`;
|
|
158
|
+
await writeFile(tmpPath, content, 'utf8');
|
|
159
|
+
await rename(tmpPath, path);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async readStoreData(path: string): Promise<StoreData<unknown> | null> {
|
|
163
|
+
try {
|
|
164
|
+
const content = await readFile(path, 'utf8');
|
|
165
|
+
return JSON.parse(content) as StoreData<unknown>;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const err = error as NodeJS.ErrnoException;
|
|
168
|
+
if (err.code === 'ENOENT') {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private bumpVersionClock(pageId: string, version: number): void {
|
|
176
|
+
const current = this.versionClock.get(pageId) ?? 0;
|
|
177
|
+
if (version > current) {
|
|
178
|
+
this.versionClock.set(pageId, version);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private nextVersion(pageId: string, fileVersion: number): number {
|
|
183
|
+
const clock = this.versionClock.get(pageId) ?? 0;
|
|
184
|
+
const next = Math.max(clock, fileVersion) + 1;
|
|
185
|
+
this.versionClock.set(pageId, next);
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
watch<T>(pageId: string, callback: (data: StoreData<T>) => void): () => void {
|
|
190
|
+
validatePageId(pageId);
|
|
191
|
+
const path = this.getStorePath(pageId);
|
|
192
|
+
const pageDir = dirname(path);
|
|
193
|
+
|
|
194
|
+
const existing = this.watchers.get(pageId);
|
|
195
|
+
if (existing) {
|
|
196
|
+
existing.close();
|
|
197
|
+
this.watchers.delete(pageId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
mkdirSync(pageDir, { recursive: true });
|
|
201
|
+
|
|
202
|
+
const watcher = watch(pageDir, (_, filename) => {
|
|
203
|
+
if (filename && filename.toString() !== 'store.json') {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
void this.load<T>(pageId)
|
|
208
|
+
.then((data) => {
|
|
209
|
+
if (data) {
|
|
210
|
+
callback(data);
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.catch(() => {
|
|
214
|
+
// Ignore transient read/parse errors during file change events.
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
watcher.on('error', () => {
|
|
219
|
+
const activeWatcher = this.watchers.get(pageId);
|
|
220
|
+
if (activeWatcher) {
|
|
221
|
+
activeWatcher.close();
|
|
222
|
+
this.watchers.delete(pageId);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
this.watchers.set(pageId, watcher);
|
|
227
|
+
|
|
228
|
+
return () => {
|
|
229
|
+
const activeWatcher = this.watchers.get(pageId);
|
|
230
|
+
if (activeWatcher) {
|
|
231
|
+
activeWatcher.close();
|
|
232
|
+
this.watchers.delete(pageId);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
destroy(): void {
|
|
238
|
+
for (const watcher of this.watchers.values()) {
|
|
239
|
+
watcher.close();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.watchers.clear();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type * from './types.js';
|
|
2
|
+
export { createBridgeGateway } from './createBridgeGateway.js';
|
|
3
|
+
export { createBridgeApiHandler } from './apiHandler.js';
|
|
4
|
+
export { StoreRegistry } from './registry.js';
|
|
5
|
+
export {
|
|
6
|
+
FileStore,
|
|
7
|
+
InvalidPageIdError,
|
|
8
|
+
VersionConflictError,
|
|
9
|
+
validatePageId,
|
|
10
|
+
type FileStoreOptions,
|
|
11
|
+
type StoreData,
|
|
12
|
+
} from './fileStore.js';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import type {
|
|
3
|
+
StoreId,
|
|
4
|
+
PageId,
|
|
5
|
+
StoreKey,
|
|
6
|
+
RegisteredStore,
|
|
7
|
+
StoreChangeEvent
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
export class StoreRegistry {
|
|
11
|
+
private stores = new Map<StoreId, RegisteredStore>();
|
|
12
|
+
private index = new Map<`${PageId}:${StoreKey}`, StoreId>();
|
|
13
|
+
private byPage = new Map<PageId, Set<StoreId>>();
|
|
14
|
+
private changeHandlers = new Set<(event: StoreChangeEvent) => void>();
|
|
15
|
+
|
|
16
|
+
register(store: RegisteredStore): void {
|
|
17
|
+
const key: `${PageId}:${StoreKey}` = `${store.pageId}:${store.storeKey}`;
|
|
18
|
+
|
|
19
|
+
const existingId = this.index.get(key);
|
|
20
|
+
if (existingId) {
|
|
21
|
+
const existing = this.stores.get(existingId);
|
|
22
|
+
if (existing) {
|
|
23
|
+
this.disconnect(existingId, 'replaced');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.stores.set(store.id, store);
|
|
28
|
+
this.index.set(key, store.id);
|
|
29
|
+
|
|
30
|
+
if (!this.byPage.has(store.pageId)) {
|
|
31
|
+
this.byPage.set(store.pageId, new Set());
|
|
32
|
+
}
|
|
33
|
+
this.byPage.get(store.pageId)!.add(store.id);
|
|
34
|
+
|
|
35
|
+
this.emit({
|
|
36
|
+
type: 'stateChanged',
|
|
37
|
+
storeId: store.id,
|
|
38
|
+
state: store.currentState,
|
|
39
|
+
version: store.version,
|
|
40
|
+
source: 'register',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get(id: StoreId): RegisteredStore | undefined {
|
|
45
|
+
return this.stores.get(id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
find(pageId: PageId, storeKey: StoreKey): RegisteredStore | undefined {
|
|
49
|
+
const key: `${PageId}:${StoreKey}` = `${pageId}:${storeKey}`;
|
|
50
|
+
const storeId = this.index.get(key);
|
|
51
|
+
return storeId ? this.stores.get(storeId) : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
findStoreByKey(pageId: PageId, storeKey: StoreKey): RegisteredStore | undefined {
|
|
55
|
+
return this.find(pageId, storeKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
findByPage(pageId: PageId): RegisteredStore[] {
|
|
59
|
+
const ids = this.byPage.get(pageId);
|
|
60
|
+
if (!ids) return [];
|
|
61
|
+
return Array.from(ids)
|
|
62
|
+
.map(id => this.stores.get(id))
|
|
63
|
+
.filter((s): s is RegisteredStore => s !== undefined);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
list(): RegisteredStore[] {
|
|
67
|
+
return Array.from(this.stores.values());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
updateState(id: StoreId, state: unknown, version: number): void {
|
|
71
|
+
const store = this.stores.get(id);
|
|
72
|
+
if (!store) return;
|
|
73
|
+
|
|
74
|
+
store.currentState = state;
|
|
75
|
+
store.version = version;
|
|
76
|
+
store.lastActivity = new Date();
|
|
77
|
+
|
|
78
|
+
this.emit({
|
|
79
|
+
type: 'stateChanged',
|
|
80
|
+
storeId: id,
|
|
81
|
+
state,
|
|
82
|
+
version,
|
|
83
|
+
source: 'browser',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
disconnect(id: StoreId, reason: string): void {
|
|
88
|
+
const store = this.stores.get(id);
|
|
89
|
+
if (!store) return;
|
|
90
|
+
|
|
91
|
+
for (const sub of store.subscribers) {
|
|
92
|
+
if (sub.readyState === WebSocket.OPEN) {
|
|
93
|
+
sub.send(JSON.stringify({
|
|
94
|
+
type: 'store.disconnected',
|
|
95
|
+
payload: { storeId: id, reason },
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.stores.delete(id);
|
|
101
|
+
|
|
102
|
+
const key: `${PageId}:${StoreKey}` = `${store.pageId}:${store.storeKey}`;
|
|
103
|
+
if (this.index.get(key) === id) {
|
|
104
|
+
this.index.delete(key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pageStores = this.byPage.get(store.pageId);
|
|
108
|
+
if (pageStores) {
|
|
109
|
+
pageStores.delete(id);
|
|
110
|
+
if (pageStores.size === 0) {
|
|
111
|
+
this.byPage.delete(store.pageId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.emit({
|
|
116
|
+
type: 'disconnected',
|
|
117
|
+
storeId: id,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
addSubscriber(id: StoreId, ws: WebSocket): () => void {
|
|
122
|
+
const store = this.stores.get(id);
|
|
123
|
+
if (!store) {
|
|
124
|
+
throw new Error(`Store not found: ${id}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
store.subscribers.add(ws);
|
|
128
|
+
|
|
129
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
130
|
+
ws.send(JSON.stringify({
|
|
131
|
+
type: 'store.stateChanged',
|
|
132
|
+
payload: {
|
|
133
|
+
storeId: id,
|
|
134
|
+
state: store.currentState,
|
|
135
|
+
version: store.version,
|
|
136
|
+
source: 'snapshot',
|
|
137
|
+
},
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
store.subscribers.delete(ws);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onChange(handler: (event: StoreChangeEvent) => void): () => void {
|
|
147
|
+
this.changeHandlers.add(handler);
|
|
148
|
+
return () => this.changeHandlers.delete(handler);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private emit(event: StoreChangeEvent): void {
|
|
152
|
+
for (const handler of this.changeHandlers) {
|
|
153
|
+
handler(event);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
cleanup(): void {
|
|
158
|
+
for (const store of this.stores.values()) {
|
|
159
|
+
for (const sub of store.subscribers) {
|
|
160
|
+
if (sub.readyState !== WebSocket.OPEN) {
|
|
161
|
+
store.subscribers.delete(sub);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Server } from 'http';
|
|
2
|
+
import type WebSocket from 'ws';
|
|
3
|
+
import type {
|
|
4
|
+
StoreId,
|
|
5
|
+
PageId,
|
|
6
|
+
StoreKey,
|
|
7
|
+
StoreDescription,
|
|
8
|
+
StoreChangeEvent,
|
|
9
|
+
GatewayMessage as BrowserMessage,
|
|
10
|
+
ServerMessage as ClientMessage,
|
|
11
|
+
SubscriberMessage,
|
|
12
|
+
SetStateOptions,
|
|
13
|
+
} from '../shared/types.js';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
StoreId,
|
|
17
|
+
PageId,
|
|
18
|
+
StoreKey,
|
|
19
|
+
BrowserMessage,
|
|
20
|
+
ClientMessage,
|
|
21
|
+
SubscriberMessage,
|
|
22
|
+
StoreChangeEvent,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface RegisteredStore {
|
|
26
|
+
id: StoreId;
|
|
27
|
+
pageId: PageId;
|
|
28
|
+
storeKey: StoreKey;
|
|
29
|
+
description: StoreDescription;
|
|
30
|
+
currentState: unknown;
|
|
31
|
+
version: number;
|
|
32
|
+
ws: WebSocket;
|
|
33
|
+
subscribers: Set<WebSocket>;
|
|
34
|
+
connectedAt: Date;
|
|
35
|
+
lastActivity: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Gateway {
|
|
39
|
+
readonly stores: ReadonlyMap<StoreId, RegisteredStore>;
|
|
40
|
+
listStores(): Array<{
|
|
41
|
+
id: StoreId;
|
|
42
|
+
pageId: PageId;
|
|
43
|
+
storeKey: StoreKey;
|
|
44
|
+
version: number;
|
|
45
|
+
connectedAt: Date;
|
|
46
|
+
}>;
|
|
47
|
+
getStore(id: StoreId): RegisteredStore | undefined;
|
|
48
|
+
findStore(pageId: PageId, storeKey: StoreKey): RegisteredStore | undefined;
|
|
49
|
+
findStoreByKey(pageId: PageId, storeKey: StoreKey): RegisteredStore | undefined;
|
|
50
|
+
getDescription(id: StoreId): StoreDescription | undefined;
|
|
51
|
+
getState(id: StoreId): { state: unknown; version: number } | undefined;
|
|
52
|
+
setState(id: StoreId, state: unknown, options?: SetStateOptions): Promise<void>;
|
|
53
|
+
dispatch(id: StoreId, action: { type: string; payload?: unknown }): Promise<void>;
|
|
54
|
+
subscribe(id: StoreId, ws: WebSocket, callback?: (event: StoreChangeEvent) => void): () => void;
|
|
55
|
+
attach(server: Server | import('http2').Http2SecureServer): import('ws').WebSocketServer;
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GatewayOptions {
|
|
60
|
+
wsPath?: string;
|
|
61
|
+
heartbeatTimeout?: number;
|
|
62
|
+
pagesDir?: string;
|
|
63
|
+
ackTimeout?: number;
|
|
64
|
+
ackRetryCount?: number;
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentstage Bridge
|
|
3
|
+
*
|
|
4
|
+
* Agent 控制浏览器页面的核心基础设施
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // Gateway side (TanStack Start)
|
|
8
|
+
* import { createBridgeGateway, createBridgeApiHandler } from 'agent-stage-bridge';
|
|
9
|
+
*
|
|
10
|
+
* // Browser side
|
|
11
|
+
* import { createBridgeStore } from 'agent-stage-bridge/browser';
|
|
12
|
+
*
|
|
13
|
+
* // CLI/SDK side
|
|
14
|
+
* import { BridgeClient } from 'agent-stage-bridge/sdk';
|
|
15
|
+
*
|
|
16
|
+
* // Vite Plugin
|
|
17
|
+
* import { bridgePlugin } from 'agent-stage-bridge/vite';
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type * from './shared/types.js';
|
|
21
|
+
export type * from './gateway/types.js';
|
|
22
|
+
export { createBridgeGateway } from './gateway/createBridgeGateway.js';
|
|
23
|
+
export { createBridgeApiHandler } from './gateway/apiHandler.js';
|
|
24
|
+
export { StoreRegistry } from './gateway/registry.js';
|
|
25
|
+
export {
|
|
26
|
+
FileStore,
|
|
27
|
+
InvalidPageIdError,
|
|
28
|
+
VersionConflictError,
|
|
29
|
+
validatePageId,
|
|
30
|
+
type FileStoreOptions,
|
|
31
|
+
type StoreData,
|
|
32
|
+
} from './gateway/fileStore.js';
|
|
33
|
+
export { bridgePlugin } from './vite/index.js';
|