@rahul_ur/devlink-bridge 1.0.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.
@@ -0,0 +1,87 @@
1
+ export type BrowserMessage = {
2
+ type: 'handshake';
3
+ protocolVersion: string;
4
+ clientId: string;
5
+ } | {
6
+ type: 'readFile';
7
+ id: string;
8
+ path: string;
9
+ } | {
10
+ type: 'writeFile';
11
+ id: string;
12
+ path: string;
13
+ content: string;
14
+ writeId: string;
15
+ } | {
16
+ type: 'readDir';
17
+ id: string;
18
+ path: string;
19
+ recursive?: boolean;
20
+ } | {
21
+ type: 'watchFile';
22
+ id: string;
23
+ path: string;
24
+ } | {
25
+ type: 'unwatchFile';
26
+ id: string;
27
+ path: string;
28
+ } | {
29
+ type: 'ping';
30
+ id: string;
31
+ };
32
+ export type ExtensionMessage = {
33
+ type: 'handshakeAck';
34
+ protocolVersion: string;
35
+ workspaceRoot: string;
36
+ } | {
37
+ type: 'response';
38
+ id: string;
39
+ ok: true;
40
+ result: unknown;
41
+ } | {
42
+ type: 'response';
43
+ id: string;
44
+ ok: false;
45
+ error: string;
46
+ } | {
47
+ type: 'fileChanged';
48
+ path: string;
49
+ content: string;
50
+ writeId?: string;
51
+ } | {
52
+ type: 'fileDeleted';
53
+ path: string;
54
+ } | {
55
+ type: 'pong';
56
+ id: string;
57
+ };
58
+ export interface CacheEntry {
59
+ content: string;
60
+ version: number;
61
+ lastFetched: number;
62
+ watchActive: boolean;
63
+ }
64
+ export interface BridgeOptions {
65
+ /** Port range to try. Defaults to 7100–7200 */
66
+ portRange?: [number, number];
67
+ /** Specific port to try first */
68
+ port?: number;
69
+ /** Heartbeat interval in ms. Defaults to 5000 */
70
+ heartbeatMs?: number;
71
+ /** Request timeout in ms. Defaults to 10000 */
72
+ timeoutMs?: number;
73
+ /** Max reconnect attempts before giving up. Defaults to Infinity */
74
+ maxReconnectAttempts?: number;
75
+ /** Base delay for reconnect backoff in ms. Defaults to 500 */
76
+ reconnectBaseMs?: number;
77
+ /** Called when connection state changes */
78
+ onStatusChange?: (status: BridgeStatus) => void;
79
+ }
80
+ export type BridgeStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
81
+ export interface DirEntry {
82
+ name: string;
83
+ path: string;
84
+ type: 'file' | 'directory';
85
+ children?: DirEntry[];
86
+ }
87
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjF;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAIjC,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAIjC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB;AAID,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,MAAM,YAAY,GACpB,YAAY,GACZ,WAAW,GACX,cAAc,GACd,cAAc,CAAC;AAEnB,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // ─── Message types (browser → extension) ─────────────────────────────────────
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,gFAAgF"}
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevlinkBridge = void 0;
4
+ const cache_1 = require("./cache");
5
+ const PROTOCOL_VERSION = '1.0';
6
+ const DEFAULT_PORTS = [7100, 7101, 7102, 7103, 7104];
7
+ const PORT_RANGE_DEFAULT = [7100, 7200];
8
+ class DevlinkBridge {
9
+ constructor(opts = {}) {
10
+ var _a, _b, _c, _d, _e, _f;
11
+ this.ws = null;
12
+ this.status = 'disconnected';
13
+ this.pending = new Map();
14
+ this.fileHandlers = new Map();
15
+ this.deleteHandlers = new Map();
16
+ this.statusHandlers = new Set();
17
+ this.eventHandlers = new Map();
18
+ this.cache = new cache_1.FileCache();
19
+ this.reconnectAttempts = 0;
20
+ this.reconnectTimer = null;
21
+ this.heartbeatTimer = null;
22
+ this.missedPongs = 0;
23
+ this.activePort = null;
24
+ this.lastWriteId = null;
25
+ this.workspaceRoot = null;
26
+ this.destroyed = false;
27
+ this.opts = {
28
+ portRange: (_a = opts.portRange) !== null && _a !== void 0 ? _a : PORT_RANGE_DEFAULT,
29
+ port: (_b = opts.port) !== null && _b !== void 0 ? _b : 0,
30
+ heartbeatMs: (_c = opts.heartbeatMs) !== null && _c !== void 0 ? _c : 5000,
31
+ timeoutMs: (_d = opts.timeoutMs) !== null && _d !== void 0 ? _d : 10000,
32
+ maxReconnectAttempts: (_e = opts.maxReconnectAttempts) !== null && _e !== void 0 ? _e : Infinity,
33
+ reconnectBaseMs: (_f = opts.reconnectBaseMs) !== null && _f !== void 0 ? _f : 500,
34
+ onStatusChange: opts.onStatusChange,
35
+ };
36
+ this.clientId = `devlink-${Math.random().toString(36).slice(2, 10)}`;
37
+ this.connect();
38
+ }
39
+ // ─── Connection lifecycle ───────────────────────────────────────────────────
40
+ async connect() {
41
+ if (this.destroyed)
42
+ return;
43
+ this.setStatus('connecting');
44
+ const port = await this.findOpenPort();
45
+ if (port === null) {
46
+ console.warn('[devlink-bridge] No VSCode extension found on ports', this.opts.portRange);
47
+ this.scheduleReconnect();
48
+ return;
49
+ }
50
+ this.activePort = port;
51
+ const url = `ws://127.0.0.1:${port}`;
52
+ try {
53
+ this.ws = new WebSocket(url);
54
+ }
55
+ catch {
56
+ this.scheduleReconnect();
57
+ return;
58
+ }
59
+ this.ws.onopen = () => {
60
+ this.reconnectAttempts = 0;
61
+ this.missedPongs = 0;
62
+ this.send({ type: 'handshake', protocolVersion: PROTOCOL_VERSION, clientId: this.clientId });
63
+ this.startHeartbeat();
64
+ };
65
+ this.ws.onmessage = (event) => {
66
+ try {
67
+ const msg = JSON.parse(event.data);
68
+ this.handleMessage(msg);
69
+ }
70
+ catch (e) {
71
+ console.error('[devlink-bridge] Failed to parse message:', e);
72
+ }
73
+ };
74
+ this.ws.onclose = () => {
75
+ this.stopHeartbeat();
76
+ this.rejectAllPending('Connection closed');
77
+ if (!this.destroyed)
78
+ this.scheduleReconnect();
79
+ };
80
+ this.ws.onerror = () => {
81
+ // onerror is always followed by onclose, so we handle there
82
+ };
83
+ }
84
+ async findOpenPort() {
85
+ // Try specific port first if provided
86
+ if (this.opts.port) {
87
+ if (await this.canConnect(this.opts.port))
88
+ return this.opts.port;
89
+ }
90
+ // Try default ports first (fast path)
91
+ for (const p of DEFAULT_PORTS) {
92
+ if (await this.canConnect(p))
93
+ return p;
94
+ }
95
+ // Full range scan
96
+ const [min, max] = this.opts.portRange;
97
+ for (let p = min; p <= max; p++) {
98
+ if (DEFAULT_PORTS.includes(p))
99
+ continue; // already tried
100
+ if (await this.canConnect(p))
101
+ return p;
102
+ }
103
+ return null;
104
+ }
105
+ canConnect(port) {
106
+ return new Promise(resolve => {
107
+ const ws = new WebSocket(`ws://127.0.0.1:${port}`);
108
+ const timer = setTimeout(() => { ws.close(); resolve(false); }, 2000);
109
+ ws.onopen = () => { clearTimeout(timer); ws.close(); resolve(true); };
110
+ ws.onerror = () => { clearTimeout(timer); resolve(false); };
111
+ });
112
+ }
113
+ scheduleReconnect() {
114
+ if (this.destroyed)
115
+ return;
116
+ if (this.reconnectAttempts >= this.opts.maxReconnectAttempts) {
117
+ this.setStatus('disconnected');
118
+ return;
119
+ }
120
+ this.setStatus('reconnecting');
121
+ this.reconnectAttempts++;
122
+ // Exponential backoff capped at 30s
123
+ const delay = Math.min(this.opts.reconnectBaseMs * Math.pow(2, this.reconnectAttempts - 1), 30000);
124
+ this.reconnectTimer = setTimeout(() => this.connect(), delay);
125
+ }
126
+ startHeartbeat() {
127
+ this.stopHeartbeat();
128
+ this.heartbeatTimer = setInterval(() => {
129
+ var _a;
130
+ if (this.missedPongs >= 2) {
131
+ // Extension is gone — force reconnect
132
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.close();
133
+ return;
134
+ }
135
+ this.missedPongs++;
136
+ this.send({ type: 'ping', id: `ping-${Date.now()}` });
137
+ }, this.opts.heartbeatMs);
138
+ }
139
+ stopHeartbeat() {
140
+ if (this.heartbeatTimer) {
141
+ clearInterval(this.heartbeatTimer);
142
+ this.heartbeatTimer = null;
143
+ }
144
+ }
145
+ // ─── Message handling ───────────────────────────────────────────────────────
146
+ handleMessage(msg) {
147
+ var _a, _b;
148
+ switch (msg.type) {
149
+ case 'handshakeAck': {
150
+ this.workspaceRoot = msg.workspaceRoot;
151
+ this.setStatus('connected');
152
+ // Re-establish watchers for all files that were being watched before disconnect
153
+ this.restoreWatchers();
154
+ break;
155
+ }
156
+ case 'response': {
157
+ const pending = this.pending.get(msg.id);
158
+ if (!pending)
159
+ return;
160
+ clearTimeout(pending.timer);
161
+ this.pending.delete(msg.id);
162
+ if (msg.ok) {
163
+ pending.resolve(msg.result);
164
+ }
165
+ else {
166
+ pending.reject(new Error(msg.error));
167
+ }
168
+ break;
169
+ }
170
+ case 'fileChanged': {
171
+ // Dedup our own saves — if writeId matches last write, skip UI notification
172
+ const updated = this.cache.update(msg.path, msg.content, (_a = msg.writeId) !== null && _a !== void 0 ? _a : undefined, (_b = this.lastWriteId) !== null && _b !== void 0 ? _b : undefined);
173
+ if (updated) {
174
+ // Notify all registered handlers for this path
175
+ const handlers = this.fileHandlers.get(msg.path);
176
+ handlers === null || handlers === void 0 ? void 0 : handlers.forEach(h => h(msg.path, msg.content));
177
+ }
178
+ break;
179
+ }
180
+ case 'fileDeleted': {
181
+ this.cache.delete(msg.path);
182
+ const handlers = this.deleteHandlers.get(msg.path);
183
+ handlers === null || handlers === void 0 ? void 0 : handlers.forEach(h => h(msg.path));
184
+ break;
185
+ }
186
+ case 'pong': {
187
+ this.missedPongs = 0;
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ async restoreWatchers() {
193
+ const paths = this.cache.watchedPaths();
194
+ for (const path of paths) {
195
+ try {
196
+ await this.request('watchFile', { path });
197
+ }
198
+ catch {
199
+ // Non-fatal — file may have been deleted during disconnect
200
+ }
201
+ }
202
+ }
203
+ // ─── Core request/response ──────────────────────────────────────────────────
204
+ request(type, payload) {
205
+ return new Promise((resolve, reject) => {
206
+ if (this.status !== 'connected') {
207
+ reject(new Error(`[devlink-bridge] Not connected (status: ${this.status})`));
208
+ return;
209
+ }
210
+ const id = `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
211
+ const timer = setTimeout(() => {
212
+ var _a;
213
+ this.pending.delete(id);
214
+ reject(new Error(`[devlink-bridge] Request timed out: ${type} ${(_a = payload.path) !== null && _a !== void 0 ? _a : ''}`));
215
+ }, this.opts.timeoutMs);
216
+ this.pending.set(id, {
217
+ resolve: resolve,
218
+ reject,
219
+ timer,
220
+ });
221
+ this.send({ type, id, ...payload });
222
+ });
223
+ }
224
+ send(msg) {
225
+ var _a;
226
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
227
+ this.ws.send(JSON.stringify(msg));
228
+ }
229
+ }
230
+ rejectAllPending(reason) {
231
+ for (const [id, pending] of this.pending) {
232
+ clearTimeout(pending.timer);
233
+ pending.reject(new Error(`[devlink-bridge] ${reason}`));
234
+ this.pending.delete(id);
235
+ }
236
+ }
237
+ setStatus(status) {
238
+ var _a, _b;
239
+ if (this.status === status)
240
+ return;
241
+ this.status = status;
242
+ (_b = (_a = this.opts).onStatusChange) === null || _b === void 0 ? void 0 : _b.call(_a, status);
243
+ this.statusHandlers.forEach(h => h(status));
244
+ }
245
+ // ─── Public API ─────────────────────────────────────────────────────────────
246
+ /** Read a file. Returns cached content if available, otherwise fetches. */
247
+ async readFile(path) {
248
+ const cached = this.cache.getContent(path);
249
+ if (cached !== undefined)
250
+ return cached;
251
+ const content = await this.request('readFile', { path });
252
+ this.cache.set(path, content, false);
253
+ return content;
254
+ }
255
+ /**
256
+ * Write a file. Debounce on the caller side (500ms recommended).
257
+ * The extension will push the change back via fileChanged,
258
+ * but we dedup it so the editor doesn't re-render its own save.
259
+ */
260
+ async writeFile(path, content) {
261
+ var _a, _b;
262
+ // Generate a writeId to dedup the incoming push
263
+ const writeId = `write-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
264
+ this.lastWriteId = writeId;
265
+ // Optimistically update cache so the editor stays responsive
266
+ this.cache.set(path, content, (_b = (_a = this.cache.get(path)) === null || _a === void 0 ? void 0 : _a.watchActive) !== null && _b !== void 0 ? _b : false);
267
+ await this.request('writeFile', { path, content, writeId });
268
+ }
269
+ /** Read a directory tree */
270
+ async readDir(path, recursive = false) {
271
+ return this.request('readDir', { path, recursive });
272
+ }
273
+ /**
274
+ * Watch a file for changes pushed from the extension.
275
+ * Handler is called whenever the file changes on disk.
276
+ * Returns an unwatch function.
277
+ */
278
+ async watchFile(path, handler) {
279
+ var _a;
280
+ if (!this.fileHandlers.has(path)) {
281
+ this.fileHandlers.set(path, new Set());
282
+ }
283
+ this.fileHandlers.get(path).add(handler);
284
+ if (!((_a = this.cache.get(path)) === null || _a === void 0 ? void 0 : _a.watchActive)) {
285
+ await this.request('watchFile', { path });
286
+ this.cache.markWatched(path);
287
+ }
288
+ return () => this.unwatchFile(path, handler);
289
+ }
290
+ /** Watch a file for deletion */
291
+ onFileDeleted(path, handler) {
292
+ if (!this.deleteHandlers.has(path)) {
293
+ this.deleteHandlers.set(path, new Set());
294
+ }
295
+ this.deleteHandlers.get(path).add(handler);
296
+ return () => {
297
+ var _a;
298
+ (_a = this.deleteHandlers.get(path)) === null || _a === void 0 ? void 0 : _a.delete(handler);
299
+ };
300
+ }
301
+ async unwatchFile(path, handler) {
302
+ const handlers = this.fileHandlers.get(path);
303
+ if (!handlers)
304
+ return;
305
+ handlers.delete(handler);
306
+ if (handlers.size === 0) {
307
+ this.fileHandlers.delete(path);
308
+ this.cache.markUnwatched(path);
309
+ try {
310
+ await this.request('unwatchFile', { path });
311
+ }
312
+ catch {
313
+ // Non-fatal
314
+ }
315
+ }
316
+ }
317
+ /** Subscribe to arbitrary events from the bridge server */
318
+ on(type, handler) {
319
+ if (!this.eventHandlers.has(type)) {
320
+ this.eventHandlers.set(type, new Set());
321
+ }
322
+ this.eventHandlers.get(type).add(handler);
323
+ return () => { var _a; return (_a = this.eventHandlers.get(type)) === null || _a === void 0 ? void 0 : _a.delete(handler); };
324
+ }
325
+ /** Send an event to the bridge server without expecting a response */
326
+ publish(type, payload) {
327
+ this.send({ type, ...payload });
328
+ }
329
+ /** Subscribe to connection status changes */
330
+ onStatusChange(handler) {
331
+ this.statusHandlers.add(handler);
332
+ handler(this.status); // emit current status immediately
333
+ return () => this.statusHandlers.delete(handler);
334
+ }
335
+ /** Current connection status */
336
+ getStatus() {
337
+ return this.status;
338
+ }
339
+ /** Workspace root path (available after handshake) */
340
+ getWorkspaceRoot() {
341
+ return this.workspaceRoot;
342
+ }
343
+ /** Active port (available after connection) */
344
+ getActivePort() {
345
+ return this.activePort;
346
+ }
347
+ /** Cache snapshot for debugging */
348
+ getCacheSnapshot() {
349
+ return this.cache.snapshot();
350
+ }
351
+ /** Destroy the bridge — cleans up all timers and closes the socket */
352
+ destroy() {
353
+ var _a;
354
+ this.destroyed = true;
355
+ this.stopHeartbeat();
356
+ if (this.reconnectTimer)
357
+ clearTimeout(this.reconnectTimer);
358
+ this.rejectAllPending('Bridge destroyed');
359
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.close();
360
+ this.ws = null;
361
+ this.setStatus('disconnected');
362
+ }
363
+ }
364
+ exports.DevlinkBridge = DevlinkBridge;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileCache = void 0;
4
+ class FileCache {
5
+ constructor(maxSize = 500) {
6
+ this.store = new Map();
7
+ this.maxSize = maxSize;
8
+ }
9
+ get(path) {
10
+ return this.store.get(path);
11
+ }
12
+ getContent(path) {
13
+ var _a;
14
+ return (_a = this.store.get(path)) === null || _a === void 0 ? void 0 : _a.content;
15
+ }
16
+ has(path) {
17
+ return this.store.has(path);
18
+ }
19
+ set(path, content, watchActive = false) {
20
+ // Evict oldest entry if at capacity
21
+ if (!this.store.has(path) && this.store.size >= this.maxSize) {
22
+ const oldest = this.store.keys().next().value;
23
+ if (oldest)
24
+ this.store.delete(oldest);
25
+ }
26
+ const existing = this.store.get(path);
27
+ const entry = {
28
+ content,
29
+ version: existing ? existing.version + 1 : 1,
30
+ lastFetched: Date.now(),
31
+ watchActive,
32
+ };
33
+ this.store.set(path, entry);
34
+ return entry;
35
+ }
36
+ // Called when extension pushes a file change
37
+ // Returns false if writeId matches the last write (dedup own saves)
38
+ update(path, content, writeId, lastWriteId) {
39
+ var _a;
40
+ if (writeId && writeId === lastWriteId) {
41
+ // This push was triggered by our own save — skip
42
+ return false;
43
+ }
44
+ const existing = this.store.get(path);
45
+ const entry = {
46
+ content,
47
+ version: existing ? existing.version + 1 : 1,
48
+ lastFetched: Date.now(),
49
+ watchActive: (_a = existing === null || existing === void 0 ? void 0 : existing.watchActive) !== null && _a !== void 0 ? _a : true,
50
+ };
51
+ this.store.set(path, entry);
52
+ return true;
53
+ }
54
+ delete(path) {
55
+ this.store.delete(path);
56
+ }
57
+ markWatched(path) {
58
+ const entry = this.store.get(path);
59
+ if (entry)
60
+ entry.watchActive = true;
61
+ }
62
+ markUnwatched(path) {
63
+ const entry = this.store.get(path);
64
+ if (entry)
65
+ entry.watchActive = false;
66
+ }
67
+ // Returns all currently watched paths (needed after reconnect)
68
+ watchedPaths() {
69
+ const paths = [];
70
+ for (const [path, entry] of this.store) {
71
+ if (entry.watchActive)
72
+ paths.push(path);
73
+ }
74
+ return paths;
75
+ }
76
+ clear() {
77
+ this.store.clear();
78
+ }
79
+ size() {
80
+ return this.store.size;
81
+ }
82
+ // Debug snapshot
83
+ snapshot() {
84
+ const out = {};
85
+ for (const [path, entry] of this.store) {
86
+ out[path] = {
87
+ version: entry.version,
88
+ size: entry.content.length,
89
+ watched: entry.watchActive,
90
+ };
91
+ }
92
+ return out;
93
+ }
94
+ }
95
+ exports.FileCache = FileCache;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileCache = exports.DevlinkBridge = void 0;
4
+ var bridge_1 = require("./bridge");
5
+ Object.defineProperty(exports, "DevlinkBridge", { enumerable: true, get: function () { return bridge_1.DevlinkBridge; } });
6
+ var cache_1 = require("./cache");
7
+ Object.defineProperty(exports, "FileCache", { enumerable: true, get: function () { return cache_1.FileCache; } });
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // ─── Message types (browser → extension) ─────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@rahul_ur/devlink-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Browser SDK + local bridge server for devlink — connects your web app to VSCode over a local WebSocket",
5
+ "main": "dist-cjs/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./dist-cjs/index.js",
11
+ "import": "./dist/index.mjs",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./server.mjs": "./server.mjs",
15
+ "./setup-devlink.mjs": "./setup-devlink.mjs"
16
+ },
17
+ "bin": {
18
+ "devlink-setup": "setup-devlink.mjs",
19
+ "devlink-server": "server.mjs"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "dist-cjs",
24
+ "server.mjs",
25
+ "setup-devlink.mjs",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/rename-esm.mjs",
30
+ "dev": "tsc --watch",
31
+ "test": "jest --no-coverage --passWithNoTests",
32
+ "prepublishOnly": "npm run build && npm test && npm pack --dry-run"
33
+ },
34
+ "keywords": [
35
+ "devlink",
36
+ "bridge",
37
+ "websocket",
38
+ "vscode",
39
+ "devtools",
40
+ "inspector"
41
+ ],
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "ws": "^8.20.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/jest": "^29.5.0",
48
+ "@types/node": "^20.0.0",
49
+ "@types/ws": "^8.5.0",
50
+ "jest": "^29.7.0",
51
+ "ts-jest": "^29.1.0",
52
+ "typescript": "^5.2.0"
53
+ },
54
+ "jest": {
55
+ "preset": "ts-jest",
56
+ "testEnvironment": "node",
57
+ "transform": {
58
+ "^.+\\.tsx?$": [
59
+ "ts-jest",
60
+ {
61
+ "tsconfig": {
62
+ "module": "commonjs"
63
+ }
64
+ }
65
+ ]
66
+ }
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/BiginerCoder/Devlink-Doc.git"
71
+ },
72
+ "author": "rahul urmaliya",
73
+ "bugs": {
74
+ "url": "https://github.com/BiginerCoder/Devlink-Doc/issues"
75
+ },
76
+ "homepage": "https://github.com/BiginerCoder/Devlink-Doc#readme"
77
+ }