@u-devtools/core 0.1.5 → 0.2.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/LICENSE +1 -2
- package/README.md +119 -0
- package/dist/index.cjs.js +46 -1
- package/dist/index.d.ts +871 -64
- package/dist/index.es.js +3113 -56
- package/dist/vite/vite.config.base.cjs.js +1 -0
- package/dist/vite/vite.config.base.d.ts +59 -0
- package/dist/vite/vite.config.base.js +91 -0
- package/dist/vite.config.base.d.ts +42 -0
- package/package.json +26 -18
- package/src/bridge-app.ts +198 -51
- package/src/control.ts +23 -52
- package/src/event-bus.ts +126 -0
- package/src/index.ts +476 -44
- package/src/schemas/rpc.ts +36 -0
- package/src/schemas/settings.ts +125 -0
- package/src/transport.ts +172 -0
- package/src/transports/broadcast-transport.ts +174 -0
- package/src/transports/hmr-transport.ts +82 -0
- package/src/transports/websocket-transport.ts +158 -0
- package/vite/vite.config.base.ts +81 -14
- package/vite/clean-timestamp-plugin.ts +0 -28
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for RPC message validation.
|
|
5
|
+
* Validates the structure of RPC messages used for communication between Server and Client contexts.
|
|
6
|
+
*/
|
|
7
|
+
export const RpcMessageSchema = z.object({
|
|
8
|
+
/** Unique message identifier */
|
|
9
|
+
id: z.string().min(1),
|
|
10
|
+
/** Message type: 'request' for RPC calls, 'response' for replies, 'event' for broadcasts */
|
|
11
|
+
type: z.enum(['request', 'response', 'event']),
|
|
12
|
+
/** RPC method name (for requests and events) */
|
|
13
|
+
method: z.string().optional(),
|
|
14
|
+
/** Message payload data */
|
|
15
|
+
payload: z.unknown().optional(),
|
|
16
|
+
/** Error information (for error responses) */
|
|
17
|
+
error: z.unknown().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type inferred from RpcMessageSchema
|
|
22
|
+
*/
|
|
23
|
+
export type RpcMessageType = z.infer<typeof RpcMessageSchema>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates an unknown value as an RPC message.
|
|
27
|
+
* @param data - Data to validate
|
|
28
|
+
* @returns Validated RPC message or null if validation fails
|
|
29
|
+
*/
|
|
30
|
+
export function validateRpcMessage(data: unknown): RpcMessageType | null {
|
|
31
|
+
const result = RpcMessageSchema.safeParse(data);
|
|
32
|
+
if (result.success) {
|
|
33
|
+
return result.data;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for setting type validation.
|
|
5
|
+
*/
|
|
6
|
+
export const SettingTypeSchema = z.enum(['string', 'number', 'boolean', 'select', 'array']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Zod schema for setting option (used in select type).
|
|
10
|
+
*/
|
|
11
|
+
export const SettingOptionSchema = z.object({
|
|
12
|
+
label: z.string(),
|
|
13
|
+
value: z.unknown(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Zod schema for setting schema definition.
|
|
18
|
+
* Recursive schema using z.lazy for items field.
|
|
19
|
+
*/
|
|
20
|
+
export const SettingSchemaDefSchema: z.ZodType<any> = z.lazy(() =>
|
|
21
|
+
z.object({
|
|
22
|
+
/** Display label for the setting */
|
|
23
|
+
label: z.string().min(1),
|
|
24
|
+
/** Optional description/tooltip text */
|
|
25
|
+
description: z.string().optional(),
|
|
26
|
+
/** Setting type (determines input component) */
|
|
27
|
+
type: SettingTypeSchema,
|
|
28
|
+
/** Default value */
|
|
29
|
+
default: z.unknown().optional(),
|
|
30
|
+
/** Options for 'select' type settings */
|
|
31
|
+
options: z.array(SettingOptionSchema).optional(),
|
|
32
|
+
/** Schema for array items (for 'array' type with object items) */
|
|
33
|
+
items: z.record(z.string(), SettingSchemaDefSchema).optional(),
|
|
34
|
+
/** Item type for 'array' type with primitive items ('string' or 'number') */
|
|
35
|
+
itemType: z.enum(['string', 'number']).optional(),
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Zod schema for plugin settings schema (record of setting definitions).
|
|
41
|
+
*/
|
|
42
|
+
export const PluginSettingsSchemaSchema = z.record(z.string(), SettingSchemaDefSchema);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type inferred from SettingSchemaDefSchema
|
|
46
|
+
*/
|
|
47
|
+
export type SettingSchemaDefType = z.infer<typeof SettingSchemaDefSchema>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type inferred from PluginSettingsSchemaSchema
|
|
51
|
+
*/
|
|
52
|
+
export type PluginSettingsSchemaType = z.infer<typeof PluginSettingsSchemaSchema>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates a setting schema definition.
|
|
56
|
+
* @param data - Data to validate
|
|
57
|
+
* @returns Validated setting schema definition or null if validation fails
|
|
58
|
+
*/
|
|
59
|
+
export function validateSettingSchemaDef(data: unknown): SettingSchemaDefType | null {
|
|
60
|
+
const result = SettingSchemaDefSchema.safeParse(data);
|
|
61
|
+
if (result.success) {
|
|
62
|
+
return result.data;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validates a plugin settings schema.
|
|
69
|
+
* @param data - Data to validate
|
|
70
|
+
* @returns Validated plugin settings schema or null if validation fails
|
|
71
|
+
*/
|
|
72
|
+
export function validatePluginSettingsSchema(data: unknown): PluginSettingsSchemaType | null {
|
|
73
|
+
const result = PluginSettingsSchemaSchema.safeParse(data);
|
|
74
|
+
if (result.success) {
|
|
75
|
+
return result.data;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates a setting value against its schema definition.
|
|
82
|
+
* @param value - Value to validate
|
|
83
|
+
* @param schemaDef - Setting schema definition
|
|
84
|
+
* @returns True if value is valid, false otherwise
|
|
85
|
+
*/
|
|
86
|
+
export function validateSettingValue(value: unknown, schemaDef: SettingSchemaDefType): boolean {
|
|
87
|
+
switch (schemaDef.type) {
|
|
88
|
+
case 'string':
|
|
89
|
+
return typeof value === 'string';
|
|
90
|
+
case 'number':
|
|
91
|
+
return typeof value === 'number' && !Number.isNaN(value);
|
|
92
|
+
case 'boolean':
|
|
93
|
+
return typeof value === 'boolean';
|
|
94
|
+
case 'select':
|
|
95
|
+
if (!schemaDef.options) return false;
|
|
96
|
+
return schemaDef.options.some((opt: { label: string; value: unknown }) => opt.value === value);
|
|
97
|
+
case 'array':
|
|
98
|
+
if (!Array.isArray(value)) return false;
|
|
99
|
+
if (schemaDef.itemType) {
|
|
100
|
+
// Primitive array items
|
|
101
|
+
return value.every((item) => {
|
|
102
|
+
if (schemaDef.itemType === 'string') return typeof item === 'string';
|
|
103
|
+
if (schemaDef.itemType === 'number') return typeof item === 'number';
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (schemaDef.items) {
|
|
108
|
+
// Object array items - validate each item
|
|
109
|
+
const itemsSchema = schemaDef.items;
|
|
110
|
+
return value.every((item) => {
|
|
111
|
+
if (typeof item !== 'object' || item === null) return false;
|
|
112
|
+
for (const [key, itemSchema] of Object.entries(itemsSchema)) {
|
|
113
|
+
const itemValue = (item as Record<string, unknown>)[key];
|
|
114
|
+
if (!validateSettingValue(itemValue, itemSchema)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return true; // Array without constraints
|
|
122
|
+
default:
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { RpcMessage } from './index';
|
|
2
|
+
import { validateRpcMessage } from './schemas/rpc';
|
|
3
|
+
import { extractErrorMessage } from '@u-devtools/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unique ID generator for RPC requests
|
|
7
|
+
*/
|
|
8
|
+
const uuid = () => Math.random().toString(36).substring(2, 15);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Abstract class for message transport.
|
|
12
|
+
* Provides common logic for RPC calls and event subscriptions.
|
|
13
|
+
*/
|
|
14
|
+
export abstract class Transport {
|
|
15
|
+
protected handlers = new Map<
|
|
16
|
+
string,
|
|
17
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
18
|
+
>();
|
|
19
|
+
protected eventListeners = new Map<string, Set<(data: unknown) => void>>();
|
|
20
|
+
protected disposed = false;
|
|
21
|
+
protected timeout = 5000; // Default timeout
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Send a message through the transport
|
|
25
|
+
*/
|
|
26
|
+
protected abstract sendMessage(type: string, data: unknown): void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Subscribe to messages from the transport
|
|
30
|
+
*/
|
|
31
|
+
protected abstract subscribe(type: string, handler: (data: unknown) => void): () => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Unsubscribe from transport messages
|
|
35
|
+
*/
|
|
36
|
+
protected abstract unsubscribe?(type: string, handler: (data: unknown) => void): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* RPC method call
|
|
40
|
+
*/
|
|
41
|
+
call<T = unknown>(method: string, payload?: unknown): Promise<T> {
|
|
42
|
+
if (this.disposed) {
|
|
43
|
+
return Promise.reject(new Error('Transport has been disposed'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const id = uuid();
|
|
47
|
+
return new Promise<T>((resolve, reject) => {
|
|
48
|
+
this.handlers.set(id, {
|
|
49
|
+
resolve: resolve as (value: unknown) => void,
|
|
50
|
+
reject,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Send request
|
|
54
|
+
this.sendMessage('request', { id, type: 'request', method, payload });
|
|
55
|
+
|
|
56
|
+
// Timeout
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
const handler = this.handlers.get(id);
|
|
59
|
+
if (handler) {
|
|
60
|
+
this.handlers.delete(id);
|
|
61
|
+
const error = new Error(`RPC Timeout: ${method}`);
|
|
62
|
+
console.error('[RPC Timeout]', {
|
|
63
|
+
method,
|
|
64
|
+
payload,
|
|
65
|
+
stack: error.stack,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
handler.reject(error);
|
|
69
|
+
}
|
|
70
|
+
}, this.timeout);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Subscribe to events
|
|
76
|
+
*/
|
|
77
|
+
on(event: string, fn: (data: unknown) => void): () => void {
|
|
78
|
+
if (!this.eventListeners.has(event)) {
|
|
79
|
+
this.eventListeners.set(event, new Set());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.eventListeners.get(event)?.add(fn);
|
|
83
|
+
|
|
84
|
+
// Subscribe to transport events
|
|
85
|
+
const unsubscribe = this.subscribe('event', (data: unknown) => {
|
|
86
|
+
const msg = data as RpcMessage;
|
|
87
|
+
if (msg.method === event) {
|
|
88
|
+
fn(msg.payload);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Return unsubscribe function
|
|
93
|
+
return () => {
|
|
94
|
+
const listeners = this.eventListeners.get(event);
|
|
95
|
+
if (listeners) {
|
|
96
|
+
listeners.delete(fn);
|
|
97
|
+
}
|
|
98
|
+
unsubscribe();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Unsubscribe from events
|
|
104
|
+
*/
|
|
105
|
+
off(event: string, fn: (data: unknown) => void) {
|
|
106
|
+
const listeners = this.eventListeners.get(event);
|
|
107
|
+
if (listeners) {
|
|
108
|
+
listeners.delete(fn);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handle incoming messages (called by transport)
|
|
114
|
+
*/
|
|
115
|
+
protected handleMessage(data: unknown) {
|
|
116
|
+
if (this.disposed) return;
|
|
117
|
+
|
|
118
|
+
// Validate message structure using Zod
|
|
119
|
+
const msg = validateRpcMessage(data);
|
|
120
|
+
if (!msg) {
|
|
121
|
+
console.warn('[Transport] Invalid RPC message structure:', data);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle RPC responses
|
|
126
|
+
if (msg.type === 'response') {
|
|
127
|
+
const handler = this.handlers.get(msg.id);
|
|
128
|
+
if (handler) {
|
|
129
|
+
if (msg.error) {
|
|
130
|
+
// Extract error message using utility function
|
|
131
|
+
const errorMessage = extractErrorMessage(msg.error);
|
|
132
|
+
handler.reject(new Error(errorMessage));
|
|
133
|
+
} else {
|
|
134
|
+
handler.resolve(msg.payload);
|
|
135
|
+
}
|
|
136
|
+
this.handlers.delete(msg.id);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle events
|
|
142
|
+
if (msg.type === 'event') {
|
|
143
|
+
const method = msg.method || '';
|
|
144
|
+
const listeners = this.eventListeners.get(method);
|
|
145
|
+
if (listeners && listeners.size > 0) {
|
|
146
|
+
for (const fn of listeners) {
|
|
147
|
+
try {
|
|
148
|
+
fn(msg.payload);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error('[Transport] Error in listener:', err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Clean up all handlers and subscriptions
|
|
159
|
+
*/
|
|
160
|
+
dispose() {
|
|
161
|
+
this.disposed = true;
|
|
162
|
+
|
|
163
|
+
// Reject all pending requests
|
|
164
|
+
for (const [id, handler] of this.handlers.entries()) {
|
|
165
|
+
handler.reject(new Error(`Transport disposed: request ${id} was cancelled`));
|
|
166
|
+
}
|
|
167
|
+
this.handlers.clear();
|
|
168
|
+
|
|
169
|
+
// Clear all event subscriptions
|
|
170
|
+
this.eventListeners.clear();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Transport } from '../transport';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transport based on BroadcastChannel
|
|
5
|
+
* Used for communication between App (window) and Client (iframe)
|
|
6
|
+
* Does not support RPC (call), only events (send/on)
|
|
7
|
+
*/
|
|
8
|
+
export class BroadcastTransport extends Transport {
|
|
9
|
+
private channel: BroadcastChannel;
|
|
10
|
+
private messageHandler?: (e: MessageEvent) => void;
|
|
11
|
+
private namespace: string;
|
|
12
|
+
|
|
13
|
+
constructor(namespace: string) {
|
|
14
|
+
super();
|
|
15
|
+
this.namespace = namespace;
|
|
16
|
+
this.channel = new BroadcastChannel(`u-devtools:${namespace}`);
|
|
17
|
+
|
|
18
|
+
this.messageHandler = (e: MessageEvent) => {
|
|
19
|
+
const { event, data } = e.data as { event: string; data: unknown };
|
|
20
|
+
// BroadcastChannel is only used for events, not for RPC
|
|
21
|
+
const listeners = this.eventListeners.get(event);
|
|
22
|
+
if (listeners) {
|
|
23
|
+
listeners.forEach((fn) => {
|
|
24
|
+
fn(data);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.channel.onmessage = this.messageHandler;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract values from Vue reactive objects (ref, computed, reactive)
|
|
34
|
+
*/
|
|
35
|
+
private unwrapVueReactive(value: unknown): unknown {
|
|
36
|
+
// Check if this is a Vue ref/computed
|
|
37
|
+
if (value && typeof value === 'object') {
|
|
38
|
+
// Vue 3 ref/computed have __v_isRef or _value property
|
|
39
|
+
if ('__v_isRef' in value || '_value' in value) {
|
|
40
|
+
const refValue = (value as { _value?: unknown; value?: unknown })._value ?? (value as { value?: unknown }).value;
|
|
41
|
+
return this.unwrapVueReactive(refValue);
|
|
42
|
+
}
|
|
43
|
+
// Vue reactive objects may have circular references
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.map(item => this.unwrapVueReactive(item));
|
|
46
|
+
}
|
|
47
|
+
// Regular objects - process recursively
|
|
48
|
+
const result: Record<string, unknown> = {};
|
|
49
|
+
const seen = new WeakSet();
|
|
50
|
+
for (const key in value) {
|
|
51
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
52
|
+
const val = (value as Record<string, unknown>)[key];
|
|
53
|
+
// Skip circular references and Vue internal properties
|
|
54
|
+
if (key.startsWith('__') || key.startsWith('_') && key !== '_value') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (val && typeof val === 'object') {
|
|
58
|
+
if (seen.has(val as object)) {
|
|
59
|
+
result[key] = '[Circular]';
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
seen.add(val as object);
|
|
63
|
+
}
|
|
64
|
+
result[key] = this.unwrapVueReactive(val);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Send event (not RPC)
|
|
74
|
+
*/
|
|
75
|
+
send(event: string, data?: unknown): void {
|
|
76
|
+
try {
|
|
77
|
+
// Extract values from Vue reactive objects before sending
|
|
78
|
+
const serializableData = data !== undefined ? this.unwrapVueReactive(data) : undefined;
|
|
79
|
+
this.channel.postMessage({ event, data: serializableData });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Ignore errors if channel is closed
|
|
82
|
+
if (
|
|
83
|
+
e instanceof DOMException &&
|
|
84
|
+
(e.name === 'InvalidStateError' || e.message?.includes('closed'))
|
|
85
|
+
) {
|
|
86
|
+
console.warn(`[BroadcastTransport:${this.namespace}] Cannot send event "${event}": channel is closed`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// DataCloneError - add detailed information
|
|
91
|
+
if (e instanceof Error && (e.name === 'DataCloneError' || e.message?.includes('circular'))) {
|
|
92
|
+
const stack = new Error().stack;
|
|
93
|
+
const dataType = data === null ? 'null' : data === undefined ? 'undefined' : typeof data;
|
|
94
|
+
let dataPreview = '';
|
|
95
|
+
try {
|
|
96
|
+
dataPreview = typeof data === 'object' && data !== null
|
|
97
|
+
? JSON.stringify(data, (key, value) => {
|
|
98
|
+
if (typeof value === 'function') return '[Function]';
|
|
99
|
+
if (value instanceof Node) return `[${value.nodeName}]`;
|
|
100
|
+
if (value instanceof Error) return `[Error: ${value.message}]`;
|
|
101
|
+
// Skip Vue internal properties
|
|
102
|
+
if (key.startsWith('__') || (key.startsWith('_') && key !== '_value')) return undefined;
|
|
103
|
+
return value;
|
|
104
|
+
}, 2).slice(0, 200)
|
|
105
|
+
: String(data).slice(0, 100);
|
|
106
|
+
} catch {
|
|
107
|
+
dataPreview = '[Unable to serialize]';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.error(
|
|
111
|
+
`[BroadcastTransport:${this.namespace}] DataCloneError: Failed to send event "${event}".\n` +
|
|
112
|
+
`Data type: ${dataType}\n` +
|
|
113
|
+
`Data preview: ${dataPreview}\n` +
|
|
114
|
+
`This usually means the data contains non-serializable objects (functions, DOM nodes, Vue refs, etc.).\n` +
|
|
115
|
+
`Stack trace:\n${stack?.split('\n').slice(0, 5).join('\n') || 'N/A'}`
|
|
116
|
+
);
|
|
117
|
+
return; // Don't throw error, just log
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* RPC calls are not supported in BroadcastChannel
|
|
126
|
+
*/
|
|
127
|
+
override call<T = unknown>(_method: string, _payload?: unknown): Promise<T> {
|
|
128
|
+
return Promise.reject(
|
|
129
|
+
new Error('RPC calls are not supported in BroadcastTransport. Use send() for events.')
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
protected sendMessage(_type: string, _data: unknown): void {
|
|
134
|
+
// BroadcastChannel does not support RPC
|
|
135
|
+
throw new Error('sendMessage is not supported in BroadcastTransport. Use send() instead.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected subscribe(event: string, handler: (data: unknown) => void): () => void {
|
|
139
|
+
// Subscription is already handled via messageHandler
|
|
140
|
+
if (!this.eventListeners.has(event)) {
|
|
141
|
+
this.eventListeners.set(event, new Set());
|
|
142
|
+
}
|
|
143
|
+
const listeners = this.eventListeners.get(event);
|
|
144
|
+
if (listeners) {
|
|
145
|
+
listeners.add(handler);
|
|
146
|
+
}
|
|
147
|
+
// Return unsubscribe function
|
|
148
|
+
return () => {
|
|
149
|
+
const listeners = this.eventListeners.get(event);
|
|
150
|
+
if (listeners) {
|
|
151
|
+
listeners.delete(handler);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected unsubscribe(event: string, handler: (data: unknown) => void): void {
|
|
157
|
+
const listeners = this.eventListeners.get(event);
|
|
158
|
+
if (listeners) {
|
|
159
|
+
listeners.delete(handler);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override dispose() {
|
|
164
|
+
super.dispose();
|
|
165
|
+
this.channel.close();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Close channel (alias for dispose)
|
|
170
|
+
*/
|
|
171
|
+
close() {
|
|
172
|
+
this.dispose();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Transport } from '../transport';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transport based on Vite HMR (Hot Module Replacement)
|
|
5
|
+
* Used for communication between client (iframe) and server (Node.js)
|
|
6
|
+
*/
|
|
7
|
+
export class HmrTransport extends Transport {
|
|
8
|
+
private responseHandler?: (data: unknown) => void;
|
|
9
|
+
private eventHandler?: (data: unknown) => void;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private hot: {
|
|
13
|
+
send: (event: string, data: unknown) => void;
|
|
14
|
+
on: (event: string, handler: (data: unknown) => void) => void;
|
|
15
|
+
off?: (event: string, handler: (data: unknown) => void) => void;
|
|
16
|
+
}
|
|
17
|
+
) {
|
|
18
|
+
super();
|
|
19
|
+
if (!hot) throw new Error('Hot Module Replacement is required for HmrTransport');
|
|
20
|
+
|
|
21
|
+
// Listen for responses from server
|
|
22
|
+
this.responseHandler = (data: unknown) => {
|
|
23
|
+
this.handleMessage(data);
|
|
24
|
+
};
|
|
25
|
+
hot.on('u-devtools:response', this.responseHandler);
|
|
26
|
+
|
|
27
|
+
// Listen for events from server
|
|
28
|
+
this.eventHandler = (data: unknown) => {
|
|
29
|
+
this.handleMessage(data);
|
|
30
|
+
};
|
|
31
|
+
hot.on('u-devtools:event', this.eventHandler);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Override on() for HMR, as subscription is already set up in constructor
|
|
36
|
+
*/
|
|
37
|
+
override on(event: string, fn: (data: unknown) => void): () => void {
|
|
38
|
+
if (!this.eventListeners.has(event)) {
|
|
39
|
+
this.eventListeners.set(event, new Set());
|
|
40
|
+
}
|
|
41
|
+
this.eventListeners.get(event)?.add(fn);
|
|
42
|
+
|
|
43
|
+
// Return unsubscribe function
|
|
44
|
+
return () => {
|
|
45
|
+
const listeners = this.eventListeners.get(event);
|
|
46
|
+
if (listeners) {
|
|
47
|
+
listeners.delete(fn);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected sendMessage(type: string, data: unknown): void {
|
|
53
|
+
if (type === 'request') {
|
|
54
|
+
this.hot.send('u-devtools:request', data);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected subscribe(_type: string, _handler: (data: unknown) => void): () => void {
|
|
59
|
+
// HMR is already subscribed in constructor, just return empty function
|
|
60
|
+
// as handling happens through responseHandler and eventHandler
|
|
61
|
+
return () => {
|
|
62
|
+
// Unsubscription is handled in dispose
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected unsubscribe(_type: string, _handler: (data: unknown) => void): void {
|
|
67
|
+
// HMR doesn't support unsubscribing individual handlers directly
|
|
68
|
+
// Unsubscription happens through dispose
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override dispose() {
|
|
72
|
+
super.dispose();
|
|
73
|
+
|
|
74
|
+
// Unsubscribe from HMR events if API supports off
|
|
75
|
+
if (this.hot.off && this.responseHandler) {
|
|
76
|
+
this.hot.off('u-devtools:response', this.responseHandler);
|
|
77
|
+
}
|
|
78
|
+
if (this.hot.off && this.eventHandler) {
|
|
79
|
+
this.hot.off('u-devtools:event', this.eventHandler);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|