"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import { UpdateState } from './types';
/**
* History Manager
* Manages the history of updates for undo/redo functionality
*/
export class HistoryManager {
private history: UpdateState[] = [];
/**
* Add an update to history
*/
public add(update: UpdateState) {
this.history.push(update);
}
/**
* Get all history
*/
public getHistory(): UpdateState[] {
return [...this.history];
}
/**
* Clear history
*/
public clear() {
this.history = [];
}
/**
* Undo the last update
* Moves the update from history to redo stack
*/
public undo(): UpdateState | null {
const update = this.history.pop();
if (update) {
update.status = 'reverted';
this.redoStack.push(update);
}
return update || null;
}
/**
* Get the last reverted update for redo
* Note: Since we are changing how undo works (popping), we need to handle redo differently.
* If we want to support redo, we should store the undone updates.
*/
private redoStack: UpdateState[] = [];
/**
* Redo the last undone update
* Moves the update from redo stack to history
*/
public redo(): UpdateState | null {
const update = this.redoStack.pop();
if (update) {
update.status = 'completed';
this.history.push(update);
}
return update || null;
}
// To maintain backward compatibility with the "broken" logic (if it was indeed broken),
// or to fix it. The user asked to refactor, which implies improving maintainability.
// Fixing a bug is also good.
// However, I should be careful not to change behavior if the user relies on it (even if buggy).
// But "redo not working" is hardly a feature.
// I'll implement `undo` to return the update, and let the caller handle the DOM restoration.
// The manager just manages the state.
}

View File

@@ -0,0 +1,221 @@
import { UpdateState, UpdateResult, UpdateManagerConfig } from './types';
/**
* UpdateService
* Handles update processing, validation, and server synchronization
*/
export class UpdateService {
private processingUpdates = new Set<string>();
constructor(
private config: UpdateManagerConfig,
private onComplete: (update: UpdateState) => void,
private onFail: (update: UpdateState) => void
) { }
/**
* Process a single update
*/
public async processUpdate(update: UpdateState): Promise<UpdateResult> {
// Validate update
if (!this.validateUpdate(update)) {
return {
success: false,
element: update.element,
updateId: update.id,
error: 'Update validation failed',
};
}
// Mark as processing
update.status = 'processing';
this.processingUpdates.add(update.id);
try {
// Apply DOM update
this.applyUpdateToDOM(update);
let serverResponse;
// Save to source via API (only if persist is true)
if (update.persist) {
serverResponse = await this.saveToSource(update);
} else {
serverResponse = { success: true, preview: true };
}
// Mark as completed
update.status = 'completed';
this.onComplete(update);
return {
success: true,
element: update.element,
updateId: update.id,
serverResponse,
};
} catch (error) {
update.status = 'failed';
update.error = error instanceof Error ? error.message : 'Unknown error';
// Retry mechanism
if (update.retryCount < this.config.maxRetries) {
update.retryCount++;
return this.processUpdate(update);
}
this.onFail(update);
return {
success: false,
element: update.element,
updateId: update.id,
error: update.error,
};
} finally {
this.processingUpdates.delete(update.id);
}
}
/**
* Process batch update
*/
public async processBatchUpdate(
updates: UpdateState[]
): Promise<UpdateResult[]> {
try {
// Build batch API request
const batchRequest = {
updates: updates.map(update => ({
filePath: update.sourceInfo.fileName,
line: update.sourceInfo.lineNumber,
column: update.sourceInfo.columnNumber,
type: (update.operation === 'style_update' || update.operation === 'class_update') ? 'style' : 'content',
newValue: update.newValue,
originalValue: update.oldValue,
})),
};
// Call batch API
const response = await fetch('/__appdev_design_mode/batch-update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(batchRequest),
});
if (!response.ok) {
throw new Error(`Batch update failed: ${response.statusText}`);
}
const results = await response.json();
// Mark all updates as completed
updates.forEach(update => {
update.status = 'completed';
this.onComplete(update);
});
return results.map((result: any, index: number) => ({
success: result.success,
element: updates[index].element,
updateId: updates[index].id,
serverResponse: result,
}));
} catch (error) {
// Mark all updates as failed
updates.forEach(update => {
update.status = 'failed';
update.error = error instanceof Error ? error.message : 'Unknown error';
this.onFail(update);
});
return updates.map(update => ({
success: false,
element: update.element,
updateId: update.id,
error: update.error,
}));
}
}
/**
* Apply update to DOM
*/
private applyUpdateToDOM(update: UpdateState) {
const { element, operation, newValue } = update;
switch (operation) {
case 'style_update':
case 'class_update':
element.className = newValue;
break;
case 'content_update':
element.innerText = newValue;
break;
case 'attribute_update':
// This should get attribute name from sourceInfo
// Currently simplified to set data-attribute
element.setAttribute('data-updated-value', newValue);
break;
}
}
/**
* Save to source file via API
*/
private async saveToSource(update: UpdateState): Promise<any> {
const response = await fetch('/__appdev_design_mode/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filePath: update.sourceInfo.fileName,
line: update.sourceInfo.lineNumber,
column: update.sourceInfo.columnNumber,
newValue: update.newValue,
originalValue: update.oldValue,
type: (update.operation === 'style_update' || update.operation === 'class_update') ? 'style' : 'content',
}),
});
if (!response.ok) {
throw new Error(`Failed to save source: ${response.statusText}`);
}
return await response.json();
}
/**
* Validate update before processing
*/
private validateUpdate(update: UpdateState): boolean {
// Validate source mapping
if (this.config.validation.validateSource) {
if (
!update.sourceInfo.fileName ||
update.sourceInfo.lineNumber === null ||
update.sourceInfo.columnNumber === null
) {
return false;
}
}
// Validate value
if (this.config.validation.validateValue) {
if (update.newValue.length > this.config.validation.maxLength) {
return false;
}
}
return true;
}
/**
* Check if update is processing
*/
public isProcessing(updateId: string): boolean {
return this.processingUpdates.has(updateId);
}
}

View File

@@ -0,0 +1,37 @@
/**
* Resolved `data-*` names for source mapping (prefix from global config or meta tag).
*/
/** e.g. `data-xagi` */
export declare function getPrefix(): string;
/** Call after changing `window.__APPDEV_DESIGN_MODE_CONFIG__` in tests. */
export declare function resetPrefixCache(): void;
/** `{prefix}-{suffix}` */
export declare function getAttributeName(suffix: string): string;
/** Lazy getters so prefix changes propagate */
export declare const AttributeNames: {
readonly file: string;
readonly line: string;
readonly column: string;
readonly info: string;
readonly elementId: string;
readonly component: string;
readonly function: string;
readonly position: string;
readonly staticContent: string;
/**
* Get the children source attribute name (for tracking where static children come from)
* Example: data-xagi-children-source
*/
readonly childrenSource: string;
readonly contextMenu: string;
readonly contextMenuHover: string;
readonly import: string;
/**
* Get the static-class attribute name (for tracking if className is a pure static string)
* Example: data-xagi-static-class
*/
readonly staticClass: string;
};
/** True for any `{prefix}-*` */
export declare function isSourceMappingAttribute(attributeName: string): boolean;
//# sourceMappingURL=attributeNames.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"attributeNames.d.ts","sourceRoot":"","sources":["attributeNames.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,uBAAuB;AACvB,wBAAgB,SAAS,IAAI,MAAM,CAKlC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,0BAA0B;AAC1B,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,+CAA+C;AAC/C,eAAO,MAAM,cAAc;;;;;;;;;;IA4BzB;;;OAGG;;;;;IAaH;;;OAGG;;CAIK,CAAC;AAEX,gCAAgC;AAChC,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAGvE"}

View File

@@ -0,0 +1,97 @@
/**
* Resolved `data-*` names for source mapping (prefix from global config or meta tag).
*/
const DEFAULT_PREFIX = 'data-xagi';
function getAttributePrefix() {
if (typeof window !== 'undefined') {
const globalConfig = window.__APPDEV_DESIGN_MODE_CONFIG__;
if (globalConfig?.attributePrefix) {
return globalConfig.attributePrefix;
}
const metaTag = document.querySelector('meta[name="appdev-design-mode:attribute-prefix"]');
if (metaTag) {
const prefix = metaTag.getAttribute('content');
if (prefix) {
return prefix;
}
}
}
return DEFAULT_PREFIX;
}
/** Cached prefix */
let cachedPrefix = null;
/** e.g. `data-xagi` */
export function getPrefix() {
if (cachedPrefix === null) {
cachedPrefix = getAttributePrefix();
}
return cachedPrefix;
}
/** Call after changing `window.__APPDEV_DESIGN_MODE_CONFIG__` in tests. */
export function resetPrefixCache() {
cachedPrefix = null;
}
/** `{prefix}-{suffix}` */
export function getAttributeName(suffix) {
const prefix = getPrefix();
return `${prefix}-${suffix}`;
}
/** Lazy getters so prefix changes propagate */
export const AttributeNames = {
get file() {
return getAttributeName('file');
},
get line() {
return getAttributeName('line');
},
get column() {
return getAttributeName('column');
},
get info() {
return getAttributeName('info');
},
get elementId() {
return getAttributeName('element-id');
},
get component() {
return getAttributeName('component');
},
get function() {
return getAttributeName('function');
},
get position() {
return getAttributeName('position');
},
get staticContent() {
return getAttributeName('static-content');
},
/**
* Get the children source attribute name (for tracking where static children come from)
* Example: data-xagi-children-source
*/
get childrenSource() {
return getAttributeName('children-source');
},
get contextMenu() {
return getAttributeName('context-menu');
},
get contextMenuHover() {
return getAttributeName('context-menu-hover');
},
get import() {
return getAttributeName('import');
},
/**
* Get the static-class attribute name (for tracking if className is a pure static string)
* Example: data-xagi-static-class
*/
get staticClass() {
return getAttributeName('static-class');
},
};
/** True for any `{prefix}-*` */
export function isSourceMappingAttribute(attributeName) {
const prefix = getPrefix();
return attributeName.startsWith(`${prefix}-`);
}
//# sourceMappingURL=attributeNames.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"attributeNames.js","sourceRoot":"","sources":["attributeNames.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,cAAc,GAAG,WAAW,CAAC;AAEnC,SAAS,kBAAkB;IACzB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,YAAY,GAAI,MAAc,CAAC,6BAA6B,CAAC;QACnE,IAAI,YAAY,EAAE,eAAe,EAAE,CAAC;YAClC,OAAO,YAAY,CAAC,eAAe,CAAC;QACtC,CAAC;QAED,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,kDAAkD,CAAC,CAAC;QAC3F,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAC/C,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,oBAAoB;AACpB,IAAI,YAAY,GAAkB,IAAI,CAAC;AAEvC,uBAAuB;AACvB,MAAM,UAAU,SAAS;IACvB,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,YAAY,GAAG,kBAAkB,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,gBAAgB;IAC9B,YAAY,GAAG,IAAI,CAAC;AACtB,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,OAAO,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAC/B,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,MAAM;QACR,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,SAAS;QACX,OAAO,gBAAgB,CAAC,YAAY,CAAC,CAAC;IACxC,CAAC;IACD,IAAI,SAAS;QACX,OAAO,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,aAAa;QACf,OAAO,gBAAgB,CAAC,gBAAgB,CAAC,CAAC;IAC5C,CAAC;IACD;;;OAGG;IACH,IAAI,cAAc;QAChB,OAAO,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,WAAW;QACb,OAAO,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,gBAAgB;QAClB,OAAO,gBAAgB,CAAC,oBAAoB,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,MAAM;QACR,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD;;;OAGG;IACH,IAAI,WAAW;QACb,OAAO,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAC1C,CAAC;CACO,CAAC;AAEX,gCAAgC;AAChC,MAAM,UAAU,wBAAwB,CAAC,aAAqB;IAC5D,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,OAAO,aAAa,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAChD,CAAC"}

View File

@@ -0,0 +1,107 @@
/**
* Resolved `data-*` names for source mapping (prefix from global config or meta tag).
*/
const DEFAULT_PREFIX = 'data-xagi';
function getAttributePrefix(): string {
if (typeof window !== 'undefined') {
const globalConfig = (window as any).__APPDEV_DESIGN_MODE_CONFIG__;
if (globalConfig?.attributePrefix) {
return globalConfig.attributePrefix;
}
const metaTag = document.querySelector('meta[name="appdev-design-mode:attribute-prefix"]');
if (metaTag) {
const prefix = metaTag.getAttribute('content');
if (prefix) {
return prefix;
}
}
}
return DEFAULT_PREFIX;
}
/** Cached prefix */
let cachedPrefix: string | null = null;
/** e.g. `data-xagi` */
export function getPrefix(): string {
if (cachedPrefix === null) {
cachedPrefix = getAttributePrefix();
}
return cachedPrefix;
}
/** Call after changing `window.__APPDEV_DESIGN_MODE_CONFIG__` in tests. */
export function resetPrefixCache(): void {
cachedPrefix = null;
}
/** `{prefix}-{suffix}` */
export function getAttributeName(suffix: string): string {
const prefix = getPrefix();
return `${prefix}-${suffix}`;
}
/** Lazy getters so prefix changes propagate */
export const AttributeNames = {
get file() {
return getAttributeName('file');
},
get line() {
return getAttributeName('line');
},
get column() {
return getAttributeName('column');
},
get info() {
return getAttributeName('info');
},
get elementId() {
return getAttributeName('element-id');
},
get component() {
return getAttributeName('component');
},
get function() {
return getAttributeName('function');
},
get position() {
return getAttributeName('position');
},
get staticContent() {
return getAttributeName('static-content');
},
/**
* Get the children source attribute name (for tracking where static children come from)
* Example: data-xagi-children-source
*/
get childrenSource() {
return getAttributeName('children-source');
},
get contextMenu() {
return getAttributeName('context-menu');
},
get contextMenuHover() {
return getAttributeName('context-menu-hover');
},
get import() {
return getAttributeName('import');
},
/**
* Get the static-class attribute name (for tracking if className is a pure static string)
* Example: data-xagi-static-class
*/
get staticClass() {
return getAttributeName('static-class');
},
} as const;
/** True for any `{prefix}-*` */
export function isSourceMappingAttribute(attributeName: string): boolean {
const prefix = getPrefix();
return attributeName.startsWith(`${prefix}-`);
}

View File

@@ -0,0 +1,97 @@
import { DesignModeMessage, MessageValidator, BridgeInterface, BridgeConfig, MessageUtilsInterface, MessageValidationResult } from './messages';
/**
* postMessage bridge: optional request/response, validation hooks, heartbeats.
*/
export declare class EnhancedBridge implements BridgeInterface {
private listeners;
private pendingRequests;
private config;
private _isConnected;
private lastHeartbeat;
private heartbeatTimer?;
private connectionCheckTimer?;
constructor(config?: Partial<BridgeConfig>);
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
private initializeMessageHandling;
/** Notify parent that the child iframe bridge is ready. */
private sendReadyMessage;
/**
* Periodic HEARTBEAT posts when running inside an iframe.
*/
private initializeHeartbeat;
/** Periodically infer disconnect from stale heartbeats. */
private initializeConnectionCheck;
/** True when `window.self !== window.top`. */
private isIframeEnvironment;
/** Snapshot for diagnostics / health. */
getEnvironmentInfo(): {
isIframe: boolean;
isConnected: boolean;
origin: string;
userAgent: string;
location: string;
};
/** Log bridge state to the console (dev aid). */
diagnose(): void;
/** Parent when iframe; `window` when top-level. */
private getTargetWindow;
/** Route incoming postMessage payloads. */
private handleMessage;
/** Resolve or reject pendingRequests entry. */
private handleResponseMessage;
/** Shape check before dispatch. */
private isValidMessage;
/** Known DesignModeMessage union tags. */
private isSupportedMessageType;
/** ACK or carries requestId. */
private isResponseMessage;
/**
* Fire-and-forget postMessage (may no-op if not connected).
*/
send<T extends DesignModeMessage>(message: T): Promise<void>;
/** RPC-style: wait until matching response or timeout. */
sendWithResponse<T extends DesignModeMessage, R extends DesignModeMessage>(message: T, responseType: R['type']): Promise<R>;
/** Ensure requestId + timestamp exist. */
private enhanceMessage;
/** Unique id for correlating responses. */
private generateRequestId;
/** ms since epoch */
private createTimestamp;
/** Best-effort ACK (messageType not wired through yet). */
private sendAcknowledgement;
/** HEARTBEAT post to parent. */
private sendHeartbeat;
/** Mark disconnected if no heartbeats for 3× interval. */
private checkConnection;
/** Subscribe; returns unsubscribe. */
on<T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void): () => void;
/** Remove one handler for `type`. */
off(type: string, handler: Function): void;
/** Invoke all listeners for message.type */
private dispatchMessage;
/** `_isConnected` flag */
isConnected(): boolean;
/** Same as `isConnected` (legacy name). */
isConnectedToTarget(): boolean;
/** Clear timers, reject pendings, remove listener. */
disconnect(): void;
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
healthCheck(): Promise<{
status: string;
details: any;
}>;
/** Guarded by config.debug */
private log;
/** Alias for disconnect(). */
destroy(): void;
}
/** Small helpers for request ids / timestamps. */
export declare const MessageUtils: MessageUtilsInterface;
/** Singleton used by the design-mode client bundle. */
export declare const bridge: EnhancedBridge;
/** Structural validation for a subset of message types. */
export declare class MessageValidatorImpl implements MessageValidator {
validate(message: any): MessageValidationResult;
}
export declare const messageValidator: MessageValidatorImpl;
//# sourceMappingURL=bridge.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAIjB,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,qBAAqB,EACrB,uBAAuB,EAKxB,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,qBAAa,cAAe,YAAW,eAAe;IACpD,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,eAAe,CAKR;IAEf,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,oBAAoB,CAAC,CAAiB;gBAElC,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM;IAe9C,mFAAmF;IACnF,OAAO,CAAC,yBAAyB;IAoBjC,2DAA2D;IAC3D,OAAO,CAAC,gBAAgB;IAkBxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAU3B,2DAA2D;IAC3D,OAAO,CAAC,yBAAyB;IAQjC,8CAA8C;IAC9C,OAAO,CAAC,mBAAmB;IAI3B,yCAAyC;IAClC,kBAAkB,IAAI;QAC3B,QAAQ,EAAE,OAAO,CAAC;QAClB,WAAW,EAAE,OAAO,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB;IAoBD,iDAAiD;IAC1C,QAAQ,IAAI,IAAI;IAYvB,mDAAmD;IACnD,OAAO,CAAC,eAAe;IAIvB,2CAA2C;IAC3C,OAAO,CAAC,aAAa;IAoCrB,+CAA+C;IAC/C,OAAO,CAAC,qBAAqB;IAgB7B,mCAAmC;IACnC,OAAO,CAAC,cAAc;IAQtB,0CAA0C;IAC1C,OAAO,CAAC,sBAAsB;IAa9B,gCAAgC;IAChC,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACU,IAAI,CAAC,CAAC,SAAS,iBAAiB,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCzE,0DAA0D;IAC7C,gBAAgB,CAAC,CAAC,SAAS,iBAAiB,EAAE,CAAC,SAAS,iBAAiB,EACpF,OAAO,EAAE,CAAC,EACV,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,GACtB,OAAO,CAAC,CAAC,CAAC;IAkCb,0CAA0C;IAC1C,OAAO,CAAC,cAAc;IAWtB,2CAA2C;IAC3C,OAAO,CAAC,iBAAiB;IAIzB,qBAAqB;IACrB,OAAO,CAAC,eAAe;IAIvB,2DAA2D;IAC3D,OAAO,CAAC,mBAAmB;IAa3B,gCAAgC;IAChC,OAAO,CAAC,aAAa;IAarB,0DAA0D;IACzD,OAAO,CAAC,eAAe;IAgBxB,sCAAsC;IAC/B,EAAE,CAAC,CAAC,SAAS,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI;IAalG,qCAAqC;IAC9B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI;IAIjD,4CAA4C;IAC5C,OAAO,CAAC,eAAe;IAavB,0BAA0B;IACnB,WAAW,IAAI,OAAO;IAI7B,2CAA2C;IACpC,mBAAmB,IAAI,OAAO;IAIrC,sDAAsD;IAC/C,UAAU,IAAI,IAAI;IA2BzB,oEAAoE;IACvD,WAAW,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,GAAG,CAAA;KAAE,CAAC;IAiErE,8BAA8B;IAC9B,OAAO,CAAC,GAAG;IAOX,8BAA8B;IACvB,OAAO,IAAI,IAAI;CAGvB;AAED,kDAAkD;AAClD,eAAO,MAAM,YAAY,EAAE,qBA4B1B,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,MAAM,gBAAuB,CAAC;AAE3C,2DAA2D;AAC3D,qBAAa,oBAAqB,YAAW,gBAAgB;IAC3D,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,uBAAuB;CA2ChD;AAED,eAAO,MAAM,gBAAgB,sBAA6B,CAAC"}

View File

@@ -0,0 +1,503 @@
/**
* postMessage bridge: optional request/response, validation hooks, heartbeats.
*/
export class EnhancedBridge {
constructor(config = {}) {
this.listeners = new Map();
this.pendingRequests = new Map();
this._isConnected = false;
this.lastHeartbeat = 0;
this.config = {
timeout: 10000,
retryAttempts: 3,
retryDelay: 1000,
heartbeatInterval: 30000,
debug: false,
...config
};
this.initializeMessageHandling();
this.initializeHeartbeat();
this.initializeConnectionCheck();
}
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
initializeMessageHandling() {
if (typeof window === 'undefined')
return;
window.addEventListener('message', this.handleMessage.bind(this));
if (this.isIframeEnvironment()) {
setTimeout(() => {
this._isConnected = true;
this.log('Bridge initialized and connected (iframe mode)');
this.sendReadyMessage();
}, 200);
}
else {
setTimeout(() => {
this._isConnected = true;
this.log('Bridge initialized and connected (main window mode)');
}, 100);
}
}
/** Notify parent that the child iframe bridge is ready. */
sendReadyMessage() {
try {
const readyMessage = {
type: 'BRIDGE_READY',
payload: {
timestamp: this.createTimestamp(),
environment: 'iframe'
},
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(readyMessage, '*');
this.log('Sent ready message to parent');
}
catch (error) {
this.log('Failed to send ready message:', error);
}
}
/**
* Periodic HEARTBEAT posts when running inside an iframe.
*/
initializeHeartbeat() {
if (typeof window === 'undefined')
return;
if (this.isIframeEnvironment()) {
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.config.heartbeatInterval);
}
}
/** Periodically infer disconnect from stale heartbeats. */
initializeConnectionCheck() {
if (typeof window === 'undefined')
return;
this.connectionCheckTimer = setInterval(() => {
this.checkConnection();
}, this.config.heartbeatInterval * 2);
}
/** True when `window.self !== window.top`. */
isIframeEnvironment() {
return typeof window !== 'undefined' && window.self !== window.top;
}
/** Snapshot for diagnostics / health. */
getEnvironmentInfo() {
if (typeof window === 'undefined') {
return {
isIframe: false,
isConnected: false,
origin: '',
userAgent: '',
location: ''
};
}
return {
isIframe: this.isIframeEnvironment(),
isConnected: this._isConnected,
origin: window.location.origin,
userAgent: navigator.userAgent.substring(0, 100),
location: window.location.href
};
}
/** Log bridge state to the console (dev aid). */
diagnose() {
const env = this.getEnvironmentInfo();
console.group('[EnhancedBridge] Bridge Diagnosis');
console.log('Environment:', env);
console.log('Connection Status:', this._isConnected);
console.log('Pending Requests:', this.pendingRequests.size);
console.log('Message Listeners:', Array.from(this.listeners.keys()));
console.log('Config:', this.config);
console.groupEnd();
}
/** Parent when iframe; `window` when top-level. */
getTargetWindow() {
return this.isIframeEnvironment() ? window.parent : window;
}
/** Route incoming postMessage payloads. */
handleMessage(event) {
// Optional origin filter (disabled by default):
// if (event.origin !== window.location.origin && window.location.origin !== 'null') {
// if (!event.origin.startsWith('http') && !event.origin.startsWith('https')) {
// this.log('Received message from different origin, allowing:', event.origin);
// } else {
// this.log('Skipping message from different origin:', event.origin);
// return;
// }
// }
const message = event.data;
// Drop malformed payloads
if (!this.isValidMessage(message)) {
this.log('Invalid message received:', message);
return;
}
// Child handshake
if (message.type === 'BRIDGE_READY') {
this.log('Received ready message from child');
this._isConnected = true;
return;
}
// Completes a pending sendWithResponse
if (this.isResponseMessage(message)) {
this.handleResponseMessage(message);
return;
}
// Fan-out to on(type) handlers
this.dispatchMessage(message);
}
/** Resolve or reject pendingRequests entry. */
handleResponseMessage(message) {
const { requestId } = message;
if (this.pendingRequests.has(requestId)) {
const request = this.pendingRequests.get(requestId);
clearTimeout(request.timeout);
this.pendingRequests.delete(requestId);
if (message.type === 'ACKNOWLEDGEMENT') {
request.resolve({ success: true, acknowledged: true });
}
else {
request.resolve(message);
}
}
}
/** Shape check before dispatch. */
isValidMessage(message) {
return (message &&
typeof message.type === 'string' &&
this.isSupportedMessageType(message.type));
}
/** Known DesignModeMessage union tags. */
isSupportedMessageType(type) {
const supportedTypes = [
// Iframe to Parent
'ELEMENT_SELECTED', 'ELEMENT_DESELECTED', 'CONTENT_UPDATED', 'STYLE_UPDATED',
'DESIGN_MODE_CHANGED', 'ELEMENT_STATE_RESPONSE', 'ERROR', 'ACKNOWLEDGEMENT',
'HEARTBEAT', 'HEALTH_CHECK_RESPONSE', 'BRIDGE_READY', 'ADD_TO_CHAT',
// Parent to Iframe
'TOGGLE_DESIGN_MODE', 'UPDATE_STYLE', 'UPDATE_CONTENT', 'BATCH_UPDATE',
'HEALTH_CHECK'
];
return supportedTypes.includes(type);
}
/** ACK or carries requestId. */
isResponseMessage(message) {
return message &&
(message.type === 'ACKNOWLEDGEMENT' ||
message.requestId !== undefined) &&
(message.timestamp !== undefined);
}
/**
* Fire-and-forget postMessage (may no-op if not connected).
*/
async send(message) {
if (!this._isConnected && this.isIframeEnvironment()) {
this.log('Bridge not connected, attempting to reconnect...');
await new Promise(resolve => setTimeout(resolve, 100));
if (!this._isConnected) {
console.warn('[EnhancedBridge] Bridge still not connected, message will be queued');
return;
}
}
if (!this._isConnected && !this.isIframeEnvironment()) {
this.log('Running in main window, bridge connection not applicable');
return;
}
const enhancedMessage = this.enhanceMessage(message);
try {
this.log('Sending message:', enhancedMessage);
this.getTargetWindow().postMessage(enhancedMessage, '*');
if (this.isIframeEnvironment() && enhancedMessage.requestId) {
this.sendAcknowledgement(enhancedMessage.requestId);
}
}
catch (error) {
this.log('Error sending message:', error);
// Dev: surface send failures
if (process.env.NODE_ENV === 'development') {
throw error;
}
}
}
/** RPC-style: wait until matching response or timeout. */
async sendWithResponse(message, responseType) {
if (!this._isConnected) {
throw new Error('Bridge is not connected');
}
const enhancedMessage = this.enhanceMessage(message);
const requestId = enhancedMessage.requestId;
return new Promise((resolve, reject) => {
// Timeout
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error(`Request timeout after ${this.config.timeout}ms`));
}, this.config.timeout);
// Pending request map
this.pendingRequests.set(requestId, {
resolve,
reject,
timeout,
responseType
});
try {
this.log('Sending request with response:', enhancedMessage);
this.getTargetWindow().postMessage(enhancedMessage, '*');
}
catch (error) {
this.pendingRequests.delete(requestId);
clearTimeout(timeout);
reject(error);
}
});
}
/** Ensure requestId + timestamp exist. */
enhanceMessage(message) {
const requestId = message.requestId || this.generateRequestId();
const timestamp = message.timestamp || this.createTimestamp();
return {
...message,
requestId,
timestamp
};
}
/** Unique id for correlating responses. */
generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/** ms since epoch */
createTimestamp() {
return Date.now();
}
/** Best-effort ACK (messageType not wired through yet). */
sendAcknowledgement(requestId) {
const acknowledgement = {
type: 'ACKNOWLEDGEMENT',
payload: {
messageType: 'UNKNOWN', // TODO: propagate originating type
},
requestId,
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(acknowledgement, '*');
}
/** HEARTBEAT post to parent. */
sendHeartbeat() {
const heartbeat = {
type: 'HEARTBEAT',
payload: {
timestamp: this.createTimestamp()
},
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(heartbeat, '*');
this.lastHeartbeat = Date.now();
}
/** Mark disconnected if no heartbeats for 3× interval. */
checkConnection() {
const now = Date.now();
const timeSinceLastHeartbeat = now - this.lastHeartbeat;
if (timeSinceLastHeartbeat > this.config.heartbeatInterval * 3) {
this.log('Connection appears to be lost');
this._isConnected = false;
// Optimistic reconnect
setTimeout(() => {
this._isConnected = true;
this.log('Connection restored');
}, 1000);
}
}
/** Subscribe; returns unsubscribe. */
on(type, handler) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type).add(handler);
// Unsubscribe
return () => {
this.listeners.get(type)?.delete(handler);
};
}
/** Remove one handler for `type`. */
off(type, handler) {
this.listeners.get(type)?.delete(handler);
}
/** Invoke all listeners for message.type */
dispatchMessage(message) {
const handlers = this.listeners.get(message.type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(message);
}
catch (error) {
this.log('Error in message handler:', error);
}
});
}
}
/** `_isConnected` flag */
isConnected() {
return this._isConnected;
}
/** Same as `isConnected` (legacy name). */
isConnectedToTarget() {
return this._isConnected;
}
/** Clear timers, reject pendings, remove listener. */
disconnect() {
this._isConnected = false;
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
if (this.connectionCheckTimer) {
clearInterval(this.connectionCheckTimer);
}
// Reject pending RPCs
this.pendingRequests.forEach(request => {
clearTimeout(request.timeout);
request.reject(new Error('Connection disconnected'));
});
this.pendingRequests.clear();
// Remove global listener
if (typeof window !== 'undefined') {
window.removeEventListener('message', this.handleMessage.bind(this));
}
this.listeners.clear();
this.log('Bridge disconnected');
}
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
async healthCheck() {
try {
const env = this.getEnvironmentInfo();
// Top-level window
if (!env.isIframe && this._isConnected) {
return {
status: 'healthy',
details: {
...env,
message: 'Bridge is healthy and running in main window'
}
};
}
if (env.isIframe && this._isConnected) {
// Iframe: ping parent
try {
const response = await this.sendWithResponse({
type: 'HEALTH_CHECK',
requestId: '',
timestamp: this.createTimestamp()
}, 'HEALTH_CHECK_RESPONSE');
return {
status: 'healthy',
details: {
...env,
response: response.payload,
message: 'Bridge is healthy and responsive'
}
};
}
catch (error) {
return {
status: 'degraded',
details: {
...env,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Bridge is connected but not responding to health checks'
}
};
}
}
return {
status: env.isIframe ? 'connecting' : 'unnecessary',
details: {
...env,
message: env.isIframe ? 'Bridge is initializing' : 'Bridge not needed in main window'
}
};
}
catch (error) {
return {
status: 'unhealthy',
details: {
error: error instanceof Error ? error.message : 'Unknown error',
environment: this.getEnvironmentInfo()
}
};
}
}
/** Guarded by config.debug */
log(...args) {
if (this.config.debug) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [EnhancedBridge]`, ...args);
}
}
/** Alias for disconnect(). */
destroy() {
this.disconnect();
}
}
/** Small helpers for request ids / timestamps. */
export const MessageUtils = {
generateRequestId: () => {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
},
createTimestamp: () => {
return Date.now();
},
isValidMessage: (message) => {
return message &&
typeof message.type === 'string' &&
typeof message.timestamp === 'number';
},
createResponse: function (originalMessage, payload, success = true, error) {
return {
payload,
type: payload?.type,
requestId: originalMessage.requestId || '',
timestamp: this.createTimestamp()
};
}
};
/** Singleton used by the design-mode client bundle. */
export const bridge = new EnhancedBridge();
/** Structural validation for a subset of message types. */
export class MessageValidatorImpl {
validate(message) {
const errors = [];
if (!message) {
errors.push('Message is null or undefined');
return { isValid: false, errors };
}
if (typeof message.type !== 'string') {
errors.push('Message type must be a string');
}
if (typeof message.timestamp !== 'number') {
errors.push('Message timestamp must be a number');
}
// Per-type payload checks
switch (message.type) {
case 'ELEMENT_SELECTED':
if (!message.payload?.elementInfo) {
errors.push('ELEMENT_SELECTED must have elementInfo in payload');
}
break;
case 'UPDATE_STYLE':
case 'UPDATE_CONTENT':
if (!message.payload?.sourceInfo) {
errors.push(`${message.type} must have sourceInfo in payload`);
}
break;
case 'BATCH_UPDATE':
if (!Array.isArray(message.payload?.updates)) {
errors.push('BATCH_UPDATE must have updates array in payload');
}
break;
}
return {
isValid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined
};
}
}
export const messageValidator = new MessageValidatorImpl();
//# sourceMappingURL=bridge.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,651 @@
import {
DesignModeMessage,
IframeToParentMessage,
ParentToIframeMessage,
RequestResponseMessage,
MessageValidator,
BridgeInterface,
BridgeConfig,
MessageUtilsInterface,
MessageValidationResult,
AcknowledgementMessage,
HealthCheckMessage,
HealthCheckResponseMessage,
HeartbeatMessage
} from './messages';
/**
* postMessage bridge: optional request/response, validation hooks, heartbeats.
*/
export class EnhancedBridge implements BridgeInterface {
private listeners: Map<string, Set<Function>> = new Map();
private pendingRequests: Map<string, {
resolve: Function;
reject: Function;
timeout: NodeJS.Timeout;
responseType: string;
}> = new Map();
private config: BridgeConfig;
private _isConnected = false;
private lastHeartbeat = 0;
private heartbeatTimer?: NodeJS.Timeout;
private connectionCheckTimer?: NodeJS.Timeout;
constructor(config: Partial<BridgeConfig> = {}) {
this.config = {
timeout: 10000,
retryAttempts: 3,
retryDelay: 1000,
heartbeatInterval: 30000,
debug: false,
...config
};
this.initializeMessageHandling();
this.initializeHeartbeat();
this.initializeConnectionCheck();
}
/** Listen for `message` and mark connected after a short delay (iframe vs top). */
private initializeMessageHandling() {
if (typeof window === 'undefined') return;
window.addEventListener('message', this.handleMessage.bind(this));
if (this.isIframeEnvironment()) {
setTimeout(() => {
this._isConnected = true;
this.log('Bridge initialized and connected (iframe mode)');
this.sendReadyMessage();
}, 200);
} else {
setTimeout(() => {
this._isConnected = true;
this.log('Bridge initialized and connected (main window mode)');
}, 100);
}
}
/** Notify parent that the child iframe bridge is ready. */
private sendReadyMessage() {
try {
const readyMessage = {
type: 'BRIDGE_READY',
payload: {
timestamp: this.createTimestamp(),
environment: 'iframe'
},
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(readyMessage, '*');
this.log('Sent ready message to parent');
} catch (error) {
this.log('Failed to send ready message:', error);
}
}
/**
* Periodic HEARTBEAT posts when running inside an iframe.
*/
private initializeHeartbeat() {
if (typeof window === 'undefined') return;
if (this.isIframeEnvironment()) {
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.config.heartbeatInterval);
}
}
/** Periodically infer disconnect from stale heartbeats. */
private initializeConnectionCheck() {
if (typeof window === 'undefined') return;
this.connectionCheckTimer = setInterval(() => {
this.checkConnection();
}, this.config.heartbeatInterval * 2);
}
/** True when `window.self !== window.top`. */
private isIframeEnvironment(): boolean {
return typeof window !== 'undefined' && window.self !== window.top;
}
/** Snapshot for diagnostics / health. */
public getEnvironmentInfo(): {
isIframe: boolean;
isConnected: boolean;
origin: string;
userAgent: string;
location: string;
} {
if (typeof window === 'undefined') {
return {
isIframe: false,
isConnected: false,
origin: '',
userAgent: '',
location: ''
};
}
return {
isIframe: this.isIframeEnvironment(),
isConnected: this._isConnected,
origin: window.location.origin,
userAgent: navigator.userAgent.substring(0, 100),
location: window.location.href
};
}
/** Log bridge state to the console (dev aid). */
public diagnose(): void {
// Diagnose output is explicitly gated by debug mode to avoid noisy runtime logs.
if (!this.config.debug) return;
const env = this.getEnvironmentInfo();
console.group('[EnhancedBridge] Bridge Diagnosis');
console.log('Environment:', env);
console.log('Connection Status:', this._isConnected);
console.log('Pending Requests:', this.pendingRequests.size);
console.log('Message Listeners:', Array.from(this.listeners.keys()));
console.log('Config:', this.config);
console.groupEnd();
}
/** Parent when iframe; `window` when top-level. */
private getTargetWindow(): Window {
return this.isIframeEnvironment() ? window.parent : window;
}
/** Route incoming postMessage payloads. */
private handleMessage(event: MessageEvent) {
// Optional origin filter (disabled by default):
// if (event.origin !== window.location.origin && window.location.origin !== 'null') {
// if (!event.origin.startsWith('http') && !event.origin.startsWith('https')) {
// this.log('Received message from different origin, allowing:', event.origin);
// } else {
// this.log('Skipping message from different origin:', event.origin);
// return;
// }
// }
const message = event.data;
// Drop malformed payloads
if (!this.isValidMessage(message)) {
this.log('Invalid message received:', message);
return;
}
// Child handshake
if (message.type === 'BRIDGE_READY') {
this.log('Received ready message from child');
this._isConnected = true;
return;
}
// 父窗口 -> iframe 的控制类消息:必须优先走业务分发,不能误判为 RPC response。
// 否则像 TOGGLE_DESIGN_MODE 这类同样带 requestId/timestamp 的消息会被 isResponseMessage
// 吞掉,导致 bridge.on('TOGGLE_DESIGN_MODE') 永远不触发,父页面一直等不到 DESIGN_MODE_CHANGED。
if (this.isIframeEnvironment() && this.isParentToIframeCommand(message)) {
this.dispatchMessage(message);
return;
}
// Completes a pending sendWithResponse仅处理真正的响应类型避免与父到子命令冲突
if (this.isResponseMessage(message)) {
this.handleResponseMessage(message);
return;
}
// Fan-out to on(type) handlers
this.dispatchMessage(message);
}
/** Resolve or reject pendingRequests entry. */
private handleResponseMessage(message: RequestResponseMessage) {
const { requestId } = message;
if (this.pendingRequests.has(requestId)) {
const request = this.pendingRequests.get(requestId)!;
clearTimeout(request.timeout);
this.pendingRequests.delete(requestId);
if (message.type === 'ACKNOWLEDGEMENT') {
request.resolve({ success: true, acknowledged: true });
} else {
request.resolve(message);
}
}
}
/** Shape check before dispatch. */
private isValidMessage(message: any): message is DesignModeMessage {
return (
message &&
typeof message.type === 'string' &&
this.isSupportedMessageType(message.type)
);
}
/** Known DesignModeMessage union tags. */
private isSupportedMessageType(type: string): boolean {
const supportedTypes = [
// Iframe to Parent
'ELEMENT_SELECTED', 'ELEMENT_DESELECTED', 'CONTENT_UPDATED', 'STYLE_UPDATED',
'DESIGN_MODE_CHANGED', 'ELEMENT_STATE_RESPONSE', 'ERROR', 'ACKNOWLEDGEMENT',
'HEARTBEAT', 'HEALTH_CHECK_RESPONSE', 'BRIDGE_READY', 'ADD_TO_CHAT',
// Parent to Iframe
'TOGGLE_DESIGN_MODE', 'UPDATE_STYLE', 'UPDATE_CONTENT', 'BATCH_UPDATE',
'HEALTH_CHECK'
];
return supportedTypes.includes(type);
}
/**
* 是否为 sendWithResponse / ACK 通道的响应消息。
* 注意:不能仅凭 requestId + timestamp 判断,否则父页面发来的 TOGGLE_DESIGN_MODE
*(同样带 requestId会被误判为 response从而跳过 dispatchMessage。
*/
private isResponseMessage(message: any): message is RequestResponseMessage {
if (!message || typeof message.type !== 'string') {
return false;
}
if (message.requestId === undefined || message.timestamp === undefined) {
return false;
}
return (
message.type === 'ACKNOWLEDGEMENT' ||
message.type === 'HEALTH_CHECK_RESPONSE'
);
}
/** 父窗口发往 iframe 的控制命令(在 iframe 内必须由业务 handler 处理) */
private isParentToIframeCommand(message: DesignModeMessage): boolean {
const t = (message as { type?: string }).type;
return (
t === 'TOGGLE_DESIGN_MODE' ||
t === 'UPDATE_STYLE' ||
t === 'UPDATE_CONTENT' ||
t === 'BATCH_UPDATE' ||
t === 'HEALTH_CHECK'
);
}
/**
* Fire-and-forget postMessage (may no-op if not connected).
*/
public async send<T extends DesignModeMessage>(message: T): Promise<void> {
if (!this._isConnected && this.isIframeEnvironment()) {
this.log('Bridge not connected, attempting to reconnect...');
await new Promise(resolve => setTimeout(resolve, 100));
if (!this._isConnected) return;
}
if (!this._isConnected && !this.isIframeEnvironment()) {
this.log('Running in main window, bridge connection not applicable');
return;
}
const enhancedMessage = this.enhanceMessage(message);
try {
this.log('Sending message:', enhancedMessage);
this.getTargetWindow().postMessage(enhancedMessage, '*');
if (this.isIframeEnvironment() && enhancedMessage.requestId) {
this.sendAcknowledgement(enhancedMessage.requestId);
}
} catch (error) {
this.log('Error sending message:', error);
// Dev: surface send failures
if (process.env.NODE_ENV === 'development') {
throw error;
}
}
}
/** RPC-style: wait until matching response or timeout. */
public async sendWithResponse<T extends DesignModeMessage, R extends DesignModeMessage>(
message: T,
responseType: R['type']
): Promise<R> {
if (!this._isConnected) {
throw new Error('Bridge is not connected');
}
const enhancedMessage = this.enhanceMessage(message);
const requestId = enhancedMessage.requestId!;
return new Promise((resolve, reject) => {
// Timeout
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error(`Request timeout after ${this.config.timeout}ms`));
}, this.config.timeout);
// Pending request map
this.pendingRequests.set(requestId, {
resolve,
reject,
timeout,
responseType
});
try {
this.log('Sending request with response:', enhancedMessage);
this.getTargetWindow().postMessage(enhancedMessage, '*');
} catch (error) {
this.pendingRequests.delete(requestId);
clearTimeout(timeout);
reject(error);
}
});
}
/** Ensure requestId + timestamp exist. */
private enhanceMessage<T extends DesignModeMessage>(message: T): T & { requestId?: string; timestamp: number } {
const requestId = (message as any).requestId || this.generateRequestId();
const timestamp = (message as any).timestamp || this.createTimestamp();
return {
...message,
requestId,
timestamp
};
}
/** Unique id for correlating responses. */
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/** ms since epoch */
private createTimestamp(): number {
return Date.now();
}
/** Best-effort ACK (messageType not wired through yet). */
private sendAcknowledgement(requestId: string) {
const acknowledgement: AcknowledgementMessage = {
type: 'ACKNOWLEDGEMENT',
payload: {
messageType: 'UNKNOWN', // TODO: propagate originating type
},
requestId,
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(acknowledgement, '*');
}
/** HEARTBEAT post to parent. */
private sendHeartbeat() {
const heartbeat: HeartbeatMessage = {
type: 'HEARTBEAT',
payload: {
timestamp: this.createTimestamp()
},
timestamp: this.createTimestamp()
};
this.getTargetWindow().postMessage(heartbeat, '*');
this.lastHeartbeat = Date.now();
}
/** Mark disconnected if no heartbeats for 3× interval. */
private checkConnection() {
const now = Date.now();
const timeSinceLastHeartbeat = now - this.lastHeartbeat;
if (timeSinceLastHeartbeat > this.config.heartbeatInterval * 3) {
this.log('Connection appears to be lost');
this._isConnected = false;
// Optimistic reconnect
setTimeout(() => {
this._isConnected = true;
this.log('Connection restored');
}, 1000);
}
}
/** Subscribe; returns unsubscribe. */
public on<T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void): () => void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(handler);
// Unsubscribe
return () => {
this.listeners.get(type)?.delete(handler);
};
}
/** Remove one handler for `type`. */
public off(type: string, handler: Function): void {
this.listeners.get(type)?.delete(handler);
}
/** Invoke all listeners for message.type */
private dispatchMessage(message: DesignModeMessage) {
const handlers = this.listeners.get(message.type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(message);
} catch (error) {
this.log('Error in message handler:', error);
}
});
}
}
/** `_isConnected` flag */
public isConnected(): boolean {
return this._isConnected;
}
/** Same as `isConnected` (legacy name). */
public isConnectedToTarget(): boolean {
return this._isConnected;
}
/** Clear timers, reject pendings, remove listener. */
public disconnect(): void {
this._isConnected = false;
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
if (this.connectionCheckTimer) {
clearInterval(this.connectionCheckTimer);
}
// Reject pending RPCs
this.pendingRequests.forEach(request => {
clearTimeout(request.timeout);
request.reject(new Error('Connection disconnected'));
});
this.pendingRequests.clear();
// Remove global listener
if (typeof window !== 'undefined') {
window.removeEventListener('message', this.handleMessage.bind(this));
}
this.listeners.clear();
this.log('Bridge disconnected');
}
/** Summarize connectivity; may issue HEALTH_CHECK RPC in iframe. */
public async healthCheck(): Promise<{ status: string; details: any }> {
try {
const env = this.getEnvironmentInfo();
// Top-level window
if (!env.isIframe && this._isConnected) {
return {
status: 'healthy',
details: {
...env,
message: 'Bridge is healthy and running in main window'
}
};
}
if (env.isIframe && this._isConnected) {
// Iframe: ping parent
try {
const response = await this.sendWithResponse<HealthCheckMessage, HealthCheckResponseMessage>(
{
type: 'HEALTH_CHECK',
requestId: '',
timestamp: this.createTimestamp()
},
'HEALTH_CHECK_RESPONSE'
);
return {
status: 'healthy',
details: {
...env,
response: response.payload,
message: 'Bridge is healthy and responsive'
}
};
} catch (error) {
return {
status: 'degraded',
details: {
...env,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Bridge is connected but not responding to health checks'
}
};
}
}
return {
status: env.isIframe ? 'connecting' : 'unnecessary',
details: {
...env,
message: env.isIframe ? 'Bridge is initializing' : 'Bridge not needed in main window'
}
};
} catch (error) {
return {
status: 'unhealthy',
details: {
error: error instanceof Error ? error.message : 'Unknown error',
environment: this.getEnvironmentInfo()
}
};
}
}
/** Guarded by config.debug */
private log(...args: any[]) {
// Keep debug logging capability for explicit troubleshooting sessions.
if (this.config.debug) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [EnhancedBridge]`, ...args);
}
}
/** Alias for disconnect(). */
public destroy(): void {
this.disconnect();
}
}
/** Small helpers for request ids / timestamps. */
export const MessageUtils: MessageUtilsInterface = {
generateRequestId: (): string => {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
},
createTimestamp: (): number => {
return Date.now();
},
isValidMessage: (message: any): boolean => {
return message &&
typeof message.type === 'string' &&
typeof message.timestamp === 'number';
},
createResponse: function<T extends IframeToParentMessage>(
originalMessage: ParentToIframeMessage,
payload: T extends { payload: infer P } ? P : never,
success: boolean = true,
error?: string
): T {
return {
payload,
type: (payload as any)?.type,
requestId: (originalMessage as any).requestId || '',
timestamp: this.createTimestamp()
} as unknown as T;
}
};
/** Singleton used by the design-mode client bundle. */
export const bridge = new EnhancedBridge();
/** Structural validation for a subset of message types. */
export class MessageValidatorImpl implements MessageValidator {
validate(message: any): MessageValidationResult {
const errors: string[] = [];
if (!message) {
errors.push('Message is null or undefined');
return { isValid: false, errors };
}
if (typeof message.type !== 'string') {
errors.push('Message type must be a string');
}
if (typeof message.timestamp !== 'number') {
errors.push('Message timestamp must be a number');
}
// Per-type payload checks
switch (message.type) {
case 'ELEMENT_SELECTED':
if (!message.payload?.elementInfo) {
errors.push('ELEMENT_SELECTED must have elementInfo in payload');
}
break;
case 'UPDATE_STYLE':
case 'UPDATE_CONTENT':
if (!message.payload?.sourceInfo) {
errors.push(`${message.type} must have sourceInfo in payload`);
}
break;
case 'BATCH_UPDATE':
if (!Array.isArray(message.payload?.updates)) {
errors.push('BATCH_UPDATE must have updates array in payload');
}
break;
}
return {
isValid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined
};
}
}
export const messageValidator = new MessageValidatorImpl();

View File

@@ -0,0 +1,5 @@
/**
* True when the element has only text child nodes (no element children).
*/
export declare function isPureStaticText(element: HTMLElement): boolean;
//# sourceMappingURL=elementUtils.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"elementUtils.d.ts","sourceRoot":"","sources":["elementUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAa9D"}

View File

@@ -0,0 +1,16 @@
/**
* True when the element has only text child nodes (no element children).
*/
export function isPureStaticText(element) {
if (element.children.length > 0) {
return false;
}
for (let i = 0; i < element.childNodes.length; i++) {
const node = element.childNodes[i];
if (node.nodeType !== Node.TEXT_NODE) {
return false;
}
}
return true;
}
//# sourceMappingURL=elementUtils.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"elementUtils.js","sourceRoot":"","sources":["elementUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAoB;IACnD,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnD,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACrC,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}

View File

@@ -0,0 +1,17 @@
/**
* True when the element has only text child nodes (no element children).
*/
export function isPureStaticText(element: HTMLElement): boolean {
if (element.children.length > 0) {
return false;
}
for (let i = 0; i < element.childNodes.length; i++) {
const node = element.childNodes[i];
if (node.nodeType !== Node.TEXT_NODE) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,20 @@
// Export all types
export * from './types';
export * from './messages';
// Export attribute names utilities
export * from './attributeNames';
// Export bridge
export * from './bridge';
// Export source info utilities
export * from './sourceInfo';
export * from './sourceInfoResolver';
// Export element utilities
export * from './elementUtils';
// Export services
export * from './UpdateService';
export * from './HistoryManager';

View File

@@ -0,0 +1,265 @@
export interface SourceInfo {
fileName: string;
lineNumber: number;
columnNumber: number;
elementType?: string;
componentName?: string;
functionName?: string;
elementId?: string;
importPath?: string;
isUIComponent?: boolean;
}
export interface ElementInfo {
tagName: string;
className: string;
textContent: string;
sourceInfo: SourceInfo;
isStaticText: boolean;
isStaticClass?: boolean;
componentName?: string;
componentPath?: string;
props?: Record<string, string>;
hierarchy?: {
tagName: string;
componentName?: string;
fileName?: string;
}[];
}
export interface MessageValidationResult {
isValid: boolean;
errors?: string[];
}
export interface MessageValidator {
validate: (message: any) => MessageValidationResult;
}
export interface BridgeReadyMessage {
type: 'BRIDGE_READY';
payload: {
timestamp: number;
environment: 'iframe' | 'main';
};
timestamp?: number;
requestId?: string;
}
export interface ElementSelectedMessage {
type: 'ELEMENT_SELECTED';
payload: {
elementInfo: ElementInfo;
};
requestId?: string;
timestamp?: number;
}
export interface ElementDeselectedMessage {
type: 'ELEMENT_DESELECTED';
requestId?: string;
timestamp?: number;
payload?: null;
}
export interface ContentUpdatedMessage {
type: 'CONTENT_UPDATED';
payload: {
sourceInfo: SourceInfo;
oldValue: string;
newValue: string;
realtime?: boolean;
};
requestId?: string;
timestamp?: number;
}
export interface ContentUpdatedCallbackMessage {
type: 'CONTENT_UPDATED_CALLBACK';
payload: {
sourceInfo: SourceInfo;
oldValue: string;
newValue: string;
realtime?: boolean;
};
requestId?: string;
timestamp?: number;
}
export interface StyleUpdatedMessage {
type: 'STYLE_UPDATED';
payload: {
sourceInfo: SourceInfo;
oldClass: string;
newClass: string;
};
requestId?: string;
timestamp?: number;
}
export interface DesignModeChangedMessage {
type: 'DESIGN_MODE_CHANGED';
enabled: boolean;
requestId?: string;
timestamp?: number;
}
export interface ToggleDesignModeMessage {
type: 'TOGGLE_DESIGN_MODE';
enabled: boolean;
requestId?: string;
timestamp?: number;
}
export interface UpdateStyleMessage {
type: 'UPDATE_STYLE';
payload: {
sourceInfo: SourceInfo;
newClass: string;
persist?: boolean;
};
requestId?: string;
timestamp?: number;
enabled?: boolean;
}
export interface UpdateContentMessage {
type: 'UPDATE_CONTENT';
payload: {
sourceInfo: SourceInfo;
newContent: string;
persist?: boolean;
};
requestId?: string;
timestamp?: number;
}
export interface BatchUpdateItem {
type: 'style' | 'content';
sourceInfo: SourceInfo;
newValue: string;
originalValue?: string;
}
export interface BatchUpdateMessage {
type: 'BATCH_UPDATE';
payload: {
updates: BatchUpdateItem[];
};
requestId?: string;
timestamp?: number;
}
export interface AddToChatMessage {
type: 'ADD_TO_CHAT';
payload: {
content: string;
context?: {
elementInfo?: ElementInfo;
sourceInfo?: SourceInfo;
};
};
requestId?: string;
timestamp?: number;
}
export interface CopyElementMessage {
type: 'COPY_ELEMENT';
payload: {
elementInfo: {
tagName: string;
className: string;
content: string;
sourceInfo?: SourceInfo;
};
textContent: string;
success: boolean;
error?: string;
};
requestId?: string;
timestamp?: number;
}
export interface ErrorMessage {
type: 'ERROR';
payload: {
code: string;
message: string;
details?: any;
};
requestId?: string;
timestamp?: number;
}
export interface AcknowledgementMessage {
type: 'ACKNOWLEDGEMENT';
payload: {
messageType: string;
};
requestId: string;
timestamp?: number;
}
export interface HeartbeatMessage {
type: 'HEARTBEAT';
payload?: {
timestamp: number;
};
timestamp?: number;
}
export interface HealthCheckMessage {
type: 'HEALTH_CHECK';
requestId?: string;
timestamp?: number;
}
export interface HealthCheckResponseMessage {
type: 'HEALTH_CHECK_RESPONSE';
payload: {
status: 'healthy' | 'unhealthy' | 'degraded' | 'connecting' | 'unnecessary';
version?: string;
uptime?: number;
message?: string;
response?: any;
error?: string;
isIframe?: boolean;
isConnected?: boolean;
origin?: string;
userAgent?: string;
location?: string;
};
requestId: string;
timestamp?: number;
}
export type IframeToParentMessage = ElementSelectedMessage | ElementDeselectedMessage | ContentUpdatedMessage | StyleUpdatedMessage | DesignModeChangedMessage | ErrorMessage | AcknowledgementMessage | HeartbeatMessage | HealthCheckResponseMessage | BridgeReadyMessage | AddToChatMessage | CopyElementMessage | ContentUpdatedCallbackMessage;
export type ParentToIframeMessage = ToggleDesignModeMessage | UpdateStyleMessage | UpdateContentMessage | BatchUpdateMessage | HealthCheckMessage | HeartbeatMessage;
export type DesignModeMessage = IframeToParentMessage | ParentToIframeMessage;
export type RequestMessage = ParentToIframeMessage & {
requestId: string;
timestamp: number;
};
export type ResponseMessage = IframeToParentMessage & {
requestId: string;
success?: boolean;
error?: string;
timestamp: number;
};
export type RequestResponseMessage = RequestMessage | ResponseMessage | AcknowledgementMessage;
export interface MessageHandler<T extends DesignModeMessage = DesignModeMessage> {
type: T['type'];
handler: (message: T) => Promise<any> | any;
validator?: MessageValidator;
}
export interface IframeModeConfig {
enabled: boolean;
hideUI: boolean;
enableSelection: boolean;
enableDirectEdit: boolean;
}
export interface BatchUpdateConfig {
enabled: boolean;
debounceMs: number;
}
export interface BridgeConfig {
timeout: number;
retryAttempts: number;
retryDelay: number;
heartbeatInterval: number;
debug: boolean;
}
export interface MessageUtilsInterface {
generateRequestId: () => string;
createTimestamp: () => number;
isValidMessage: (message: any) => boolean;
createResponse: <T extends IframeToParentMessage>(originalMessage: ParentToIframeMessage, payload: T extends {
payload: infer P;
} ? P : never, success?: boolean, error?: string) => T;
}
export interface BridgeInterface {
send: <T extends DesignModeMessage>(message: T) => Promise<void>;
sendWithResponse: <T extends DesignModeMessage, R extends DesignModeMessage>(message: T, responseType: R['type']) => Promise<R>;
on: <T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void) => void;
off: (type: string, handler: Function) => void;
isConnected: () => boolean;
disconnect: () => void;
}
//# sourceMappingURL=messages.d.ts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
// Message types for iframe ↔ parent window communication
export {};
//# sourceMappingURL=messages.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"messages.js","sourceRoot":"","sources":["messages.ts"],"names":[],"mappings":"AAAA,yDAAyD"}

View File

@@ -0,0 +1,340 @@
// Message types for iframe ↔ parent window communication
export interface SourceInfo {
fileName: string;
lineNumber: number;
columnNumber: number;
elementType?: string;
componentName?: string;
functionName?: string;
elementId?: string;
importPath?: string;
isUIComponent?: boolean; // Whether this is a UI-kit component (e.g. under components/ui)
}
export interface ElementInfo {
tagName: string;
className: string;
textContent: string;
sourceInfo: SourceInfo;
isStaticText: boolean;
isStaticClass?: boolean; // Whether className is a pure static string (editable)
componentName?: string;
componentPath?: string;
props?: Record<string, string>;
hierarchy?: {
tagName: string;
componentName?: string;
fileName?: string;
}[];
}
// Message validation types
export interface MessageValidationResult {
isValid: boolean;
errors?: string[];
}
export interface MessageValidator {
validate: (message: any) => MessageValidationResult;
}
// Bridge Ready Message
export interface BridgeReadyMessage {
type: 'BRIDGE_READY';
payload: {
timestamp: number;
environment: 'iframe' | 'main';
};
timestamp?: number;
requestId?: string;
}
// iframe → Parent messages
export interface ElementSelectedMessage {
type: 'ELEMENT_SELECTED';
payload: {
elementInfo: ElementInfo;
};
requestId?: string;
timestamp?: number;
}
export interface ElementDeselectedMessage {
type: 'ELEMENT_DESELECTED';
requestId?: string;
timestamp?: number;
payload?: null; // Added optional payload to match usage
}
export interface ContentUpdatedMessage {
type: 'CONTENT_UPDATED';
payload: {
sourceInfo: SourceInfo;
oldValue: string;
newValue: string;
realtime?: boolean; // Whether this is a realtime (throttled) update
};
requestId?: string;
timestamp?: number;
}
export interface ContentUpdatedCallbackMessage {
type: 'CONTENT_UPDATED_CALLBACK';
payload: {
sourceInfo: SourceInfo;
oldValue: string;
newValue: string;
realtime?: boolean; // Whether this is a realtime (throttled) update
};
requestId?: string;
timestamp?: number;
}
export interface StyleUpdatedMessage {
type: 'STYLE_UPDATED';
payload: {
sourceInfo: SourceInfo;
oldClass: string;
newClass: string;
};
requestId?: string;
timestamp?: number;
}
export interface DesignModeChangedMessage {
type: 'DESIGN_MODE_CHANGED';
enabled: boolean;
requestId?: string;
timestamp?: number;
}
// Parent → iframe messages
export interface ToggleDesignModeMessage {
type: 'TOGGLE_DESIGN_MODE';
enabled: boolean;
requestId?: string;
timestamp?: number;
}
export interface UpdateStyleMessage {
type: 'UPDATE_STYLE';
payload: {
sourceInfo: SourceInfo;
newClass: string;
persist?: boolean;
};
requestId?: string;
timestamp?: number;
enabled?: boolean; // Added to fix TS error where enabled was accessed on this type (though likely a bug in usage, adding optional property avoids strict error if discriminated union fails)
}
export interface UpdateContentMessage {
type: 'UPDATE_CONTENT';
payload: {
sourceInfo: SourceInfo;
newContent: string;
persist?: boolean;
};
requestId?: string;
timestamp?: number;
}
export interface BatchUpdateItem {
type: 'style' | 'content';
sourceInfo: SourceInfo;
newValue: string;
originalValue?: string;
}
export interface BatchUpdateMessage {
type: 'BATCH_UPDATE';
payload: {
updates: BatchUpdateItem[];
};
requestId?: string;
timestamp?: number;
}
// Add to Chat Message
export interface AddToChatMessage {
type: 'ADD_TO_CHAT';
payload: {
content: string;
context?: {
elementInfo?: ElementInfo;
sourceInfo?: SourceInfo;
};
};
requestId?: string;
timestamp?: number;
}
// Copy Element Message
export interface CopyElementMessage {
type: 'COPY_ELEMENT';
payload: {
elementInfo: {
tagName: string;
className: string;
content: string;
sourceInfo?: SourceInfo;
};
textContent: string; // JSON string for clipboard
success: boolean; // Whether copy succeeded
error?: string; // Error message when copy failed
};
requestId?: string;
timestamp?: number;
}
// Error message
export interface ErrorMessage {
type: 'ERROR';
payload: {
code: string;
message: string;
details?: any;
};
requestId?: string;
timestamp?: number;
}
// Acknowledgement message
export interface AcknowledgementMessage {
type: 'ACKNOWLEDGEMENT';
payload: {
messageType: string;
};
requestId: string; // Response MUST have requestId
timestamp?: number;
}
// Heartbeat and health-check messages
export interface HeartbeatMessage {
type: 'HEARTBEAT';
payload?: {
timestamp: number;
};
timestamp?: number;
}
export interface HealthCheckMessage {
type: 'HEALTH_CHECK';
requestId?: string; // Made optional for sender
timestamp?: number; // Made optional for sender
}
export interface HealthCheckResponseMessage {
type: 'HEALTH_CHECK_RESPONSE';
payload: {
status: 'healthy' | 'unhealthy' | 'degraded' | 'connecting' | 'unnecessary';
version?: string;
uptime?: number;
message?: string;
response?: any;
error?: string;
isIframe?: boolean;
isConnected?: boolean;
origin?: string;
userAgent?: string;
location?: string;
};
requestId: string; // Response MUST have requestId
timestamp?: number;
}
// Union types
export type IframeToParentMessage =
| ElementSelectedMessage
| ElementDeselectedMessage
| ContentUpdatedMessage
| StyleUpdatedMessage
| DesignModeChangedMessage
| ErrorMessage
| AcknowledgementMessage
| HeartbeatMessage
| HealthCheckResponseMessage
| BridgeReadyMessage
| AddToChatMessage
| CopyElementMessage
| ContentUpdatedCallbackMessage;
export type ParentToIframeMessage =
| ToggleDesignModeMessage
| UpdateStyleMessage
| UpdateContentMessage
| BatchUpdateMessage
| HealthCheckMessage
| HeartbeatMessage; // Added HeartbeatMessage here too for bidirectional support
export type DesignModeMessage = IframeToParentMessage | ParentToIframeMessage;
// Promise-based request/response types
export type RequestMessage = ParentToIframeMessage & {
requestId: string;
timestamp: number;
};
export type ResponseMessage = IframeToParentMessage & {
requestId: string;
success?: boolean;
error?: string;
timestamp: number;
};
export type RequestResponseMessage = RequestMessage | ResponseMessage | AcknowledgementMessage;
// Message handler type
export interface MessageHandler<T extends DesignModeMessage = DesignModeMessage> {
type: T['type'];
handler: (message: T) => Promise<any> | any;
validator?: MessageValidator;
}
// Config-related types
export interface IframeModeConfig {
enabled: boolean;
hideUI: boolean;
enableSelection: boolean;
enableDirectEdit: boolean;
}
export interface BatchUpdateConfig {
enabled: boolean;
debounceMs: number;
}
export interface BridgeConfig {
timeout: number;
retryAttempts: number;
retryDelay: number;
heartbeatInterval: number;
debug: boolean;
}
// Message utility helpers
export interface MessageUtilsInterface {
generateRequestId: () => string;
createTimestamp: () => number;
isValidMessage: (message: any) => boolean;
createResponse: <T extends IframeToParentMessage>(
originalMessage: ParentToIframeMessage,
payload: T extends { payload: infer P } ? P : never,
success?: boolean,
error?: string
) => T;
}
// Bridge interface
export interface BridgeInterface {
send: <T extends DesignModeMessage>(message: T) => Promise<void>;
sendWithResponse: <T extends DesignModeMessage, R extends DesignModeMessage>(
message: T,
responseType: R['type']
) => Promise<R>;
on: <T extends DesignModeMessage>(type: T['type'], handler: (message: T) => void) => void;
off: (type: string, handler: Function) => void;
isConnected: () => boolean;
disconnect: () => void;
}

View File

@@ -0,0 +1,10 @@
import { SourceInfo } from './messages';
/**
* Whether the element carries the compact JSON `{prefix}-info` attribute.
*/
export declare function hasSourceMapping(element: HTMLElement): boolean;
/**
* Parse `{prefix}-info` JSON into `SourceInfo`, or null when missing/invalid.
*/
export declare function extractSourceInfo(element: HTMLElement): SourceInfo | null;
//# sourceMappingURL=sourceInfo.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sourceInfo.d.ts","sourceRoot":"","sources":["sourceInfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAE9D;AAqCD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,CA4BzE"}

View File

@@ -0,0 +1,69 @@
import { AttributeNames } from './attributeNames';
/**
* Whether the element carries the compact JSON `{prefix}-info` attribute.
*/
export function hasSourceMapping(element) {
return element.hasAttribute(AttributeNames.info);
}
function toNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number.parseInt(value, 10);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function normalizeSourceInfo(raw) {
const fileName = typeof raw.fileName === 'string' ? raw.fileName : null;
const lineNumber = toNumber(raw.lineNumber ?? raw.line);
const columnNumber = toNumber(raw.columnNumber ?? raw.column);
if (!fileName || lineNumber === null || columnNumber === null) {
return null;
}
return {
fileName,
lineNumber,
columnNumber,
elementType: typeof raw.elementType === 'string' ? raw.elementType : undefined,
componentName: typeof raw.componentName === 'string' ? raw.componentName : undefined,
functionName: typeof raw.functionName === 'string' ? raw.functionName : undefined,
elementId: typeof raw.elementId === 'string' ? raw.elementId : undefined,
importPath: typeof raw.importPath === 'string' ? raw.importPath : undefined,
isUIComponent: typeof raw.isUIComponent === 'boolean' ? raw.isUIComponent : undefined,
};
}
/**
* Parse `{prefix}-info` JSON into `SourceInfo`, or null when missing/invalid.
*/
export function extractSourceInfo(element) {
const sourceInfoStr = element.getAttribute(AttributeNames.info);
if (sourceInfoStr) {
try {
const parsed = JSON.parse(sourceInfoStr);
const normalized = normalizeSourceInfo(parsed);
if (normalized) {
return normalized;
}
}
catch (e) {
console.warn(`[sourceInfo] Failed to parse ${AttributeNames.info}:`, e);
}
}
// Legacy per-field attrs.
const fileName = element.getAttribute(AttributeNames.file);
const line = toNumber(element.getAttribute(AttributeNames.line));
const column = toNumber(element.getAttribute(AttributeNames.column));
if (fileName && line !== null && column !== null) {
return {
fileName,
lineNumber: line,
columnNumber: column,
};
}
return null;
}
//# sourceMappingURL=sourceInfo.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sourceInfo.js","sourceRoot":"","sources":["sourceInfo.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,77 @@
import { SourceInfo } from './messages';
import { AttributeNames } from './attributeNames';
/**
* Whether the element carries the compact JSON `{prefix}-info` attribute.
*/
export function hasSourceMapping(element: HTMLElement): boolean {
return element.hasAttribute(AttributeNames.info);
}
function toNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number.parseInt(value, 10);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function normalizeSourceInfo(raw: Record<string, unknown>): SourceInfo | null {
const fileName = typeof raw.fileName === 'string' ? raw.fileName : null;
const lineNumber = toNumber(raw.lineNumber ?? raw.line);
const columnNumber = toNumber(raw.columnNumber ?? raw.column);
if (!fileName || lineNumber === null || columnNumber === null) {
return null;
}
return {
fileName,
lineNumber,
columnNumber,
elementType: typeof raw.elementType === 'string' ? raw.elementType : undefined,
componentName: typeof raw.componentName === 'string' ? raw.componentName : undefined,
functionName: typeof raw.functionName === 'string' ? raw.functionName : undefined,
elementId: typeof raw.elementId === 'string' ? raw.elementId : undefined,
importPath: typeof raw.importPath === 'string' ? raw.importPath : undefined,
isUIComponent: typeof raw.isUIComponent === 'boolean' ? raw.isUIComponent : undefined,
};
}
/**
* Parse `{prefix}-info` JSON into `SourceInfo`, or null when missing/invalid.
*/
export function extractSourceInfo(element: HTMLElement): SourceInfo | null {
const sourceInfoStr = element.getAttribute(AttributeNames.info);
if (sourceInfoStr) {
try {
const parsed = JSON.parse(sourceInfoStr) as Record<string, unknown>;
const normalized = normalizeSourceInfo(parsed);
if (normalized) {
return normalized;
}
} catch (e) {
console.warn(`[sourceInfo] Failed to parse ${AttributeNames.info}:`, e);
}
}
// Legacy per-field attrs.
const fileName = element.getAttribute(AttributeNames.file);
const line = toNumber(element.getAttribute(AttributeNames.line));
const column = toNumber(element.getAttribute(AttributeNames.column));
if (fileName && line !== null && column !== null) {
return {
fileName,
lineNumber: line,
columnNumber: column,
};
}
return null;
}

View File

@@ -0,0 +1,13 @@
import { SourceInfo } from './types';
/**
* Resolve the **usage-site** `SourceInfo` for an element.
*
* Pass-through/static-content nodes may be compiled inside a UI-kit file; this walks
* parents / `children-source` / cross-file children so selections map to the caller file.
*/
export declare function resolveSourceInfo(element: HTMLElement): SourceInfo | null;
/**
* Heuristic: node lives in a component **definition** (both parent/child static, same file).
*/
export declare function isInComponentDefinition(element: HTMLElement): boolean;
//# sourceMappingURL=sourceInfoResolver.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sourceInfoResolver.d.ts","sourceRoot":"","sources":["sourceInfoResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAkDrC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,CAuGzE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAmBrE"}

View File

@@ -0,0 +1,157 @@
import { AttributeNames } from './attributeNames';
import { extractSourceInfo } from './sourceInfo';
/**
* BFS for the first descendant whose `{prefix}-info` points at a different file than `currentFile`.
* Used to detect wrapper components (e.g. `<button>` inside a design-system `Button`).
*
* @param element Root to search under
* @param currentFile File path of the current elements mapping
*/
function findChildFromDifferentFile(element, currentFile) {
const queue = [];
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i];
if (child instanceof HTMLElement) {
queue.push(child);
}
}
let depth = 0;
const maxDepth = 5;
while (queue.length > 0 && depth < maxDepth) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const child = queue.shift();
if (!child)
continue;
const childInfo = extractSourceInfo(child);
if (childInfo && childInfo.fileName !== currentFile) {
return child;
}
for (let j = 0; j < child.children.length; j++) {
const grandChild = child.children[j];
if (grandChild instanceof HTMLElement) {
queue.push(grandChild);
}
}
}
depth++;
}
return null;
}
/**
* Resolve the **usage-site** `SourceInfo` for an element.
*
* Pass-through/static-content nodes may be compiled inside a UI-kit file; this walks
* parents / `children-source` / cross-file children so selections map to the caller file.
*/
export function resolveSourceInfo(element) {
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
const currentInfo = extractSourceInfo(element);
const currentFile = currentInfo?.fileName;
if (currentInfo?.isUIComponent) {
let parent = element.parentElement;
let depth = 0;
while (parent && depth < 20) {
const parentInfo = extractSourceInfo(parent);
if (parentInfo?.fileName && !parentInfo.isUIComponent) {
return {
fileName: parentInfo.fileName,
lineNumber: parentInfo.lineNumber,
columnNumber: parentInfo.columnNumber,
elementType: element.tagName?.toLowerCase() || 'unknown',
componentName: currentInfo?.componentName,
functionName: currentInfo?.functionName
};
}
parent = parent.parentElement;
depth++;
}
}
const candidates = [
'data-xagi-children-source',
'data-source-children-source',
AttributeNames.childrenSource
];
let childrenSource = null;
for (const attr of candidates) {
const val = element.getAttribute(attr);
if (val) {
childrenSource = val;
break;
}
}
if (childrenSource) {
const parts = childrenSource.split(':');
if (parts.length >= 3) {
const fileName = parts.slice(0, -2).join(':'); // Paths may contain `:`
const lineNumber = parseInt(parts[parts.length - 2], 10);
const columnNumber = parseInt(parts[parts.length - 1], 10);
if (!isNaN(lineNumber) && !isNaN(columnNumber)) {
return {
fileName,
lineNumber,
columnNumber,
elementType: element.tagName?.toLowerCase() || 'unknown',
componentName: currentInfo?.componentName,
functionName: currentInfo?.functionName
};
}
}
}
if (currentFile) {
const childWithDifferentSource = findChildFromDifferentFile(element, currentFile);
if (childWithDifferentSource) {
const childInfo = extractSourceInfo(childWithDifferentSource);
if (childInfo && childInfo.fileName !== currentFile) {
return {
fileName: childInfo.fileName,
lineNumber: childInfo.lineNumber,
columnNumber: childInfo.columnNumber,
elementType: element.tagName?.toLowerCase() || 'unknown',
componentName: currentInfo?.componentName,
functionName: currentInfo?.functionName
};
}
}
}
if (!hasStaticContent) {
return extractSourceInfo(element);
}
let currentElement = element;
let depth = 0;
while (currentElement && depth < 20) {
const parent = currentElement.parentElement;
if (!parent) {
break;
}
const parentInfo = extractSourceInfo(parent);
const parentFile = parentInfo?.fileName;
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
if (parentFile && (!parentHasStaticContent || parentFile !== currentFile)) {
return parentInfo;
}
currentElement = parent;
depth++;
}
return extractSourceInfo(element);
}
/**
* Heuristic: node lives in a component **definition** (both parent/child static, same file).
*/
export function isInComponentDefinition(element) {
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
const sourceInfo = extractSourceInfo(element);
const sourceFile = sourceInfo?.fileName;
if (!hasStaticContent || !sourceFile) {
return false;
}
const parent = element.parentElement;
if (!parent) {
return false;
}
const parentInfo = extractSourceInfo(parent);
const parentFile = parentInfo?.fileName;
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
return parentHasStaticContent && parentFile === sourceFile;
}
//# sourceMappingURL=sourceInfoResolver.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sourceInfoResolver.js","sourceRoot":"","sources":["sourceInfoResolver.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,190 @@
import { SourceInfo } from './types';
import { AttributeNames } from './attributeNames';
import { extractSourceInfo } from './sourceInfo';
/**
* BFS for the first descendant whose `{prefix}-info` points at a different file than `currentFile`.
* Used to detect wrapper components (e.g. `<button>` inside a design-system `Button`).
*
* @param element Root to search under
* @param currentFile File path of the current elements mapping
*/
function findChildFromDifferentFile(element: HTMLElement, currentFile: string): HTMLElement | null {
const queue: HTMLElement[] = [];
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i];
if (child instanceof HTMLElement) {
queue.push(child);
}
}
let depth = 0;
const maxDepth = 5;
while (queue.length > 0 && depth < maxDepth) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const child = queue.shift();
if (!child) continue;
const childInfo = extractSourceInfo(child);
if (childInfo && childInfo.fileName !== currentFile) {
return child;
}
for (let j = 0; j < child.children.length; j++) {
const grandChild = child.children[j];
if (grandChild instanceof HTMLElement) {
queue.push(grandChild);
}
}
}
depth++;
}
return null;
}
/**
* Resolve the **usage-site** `SourceInfo` for an element.
*
* Pass-through/static-content nodes may be compiled inside a UI-kit file; this walks
* parents / `children-source` / cross-file children so selections map to the caller file.
*/
export function resolveSourceInfo(element: HTMLElement): SourceInfo | null {
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
const currentInfo = extractSourceInfo(element);
const currentFile = currentInfo?.fileName;
if (currentInfo?.isUIComponent) {
let parent = element.parentElement;
let depth = 0;
while (parent && depth < 20) {
const parentInfo = extractSourceInfo(parent);
if (parentInfo?.fileName && !parentInfo.isUIComponent) {
return {
fileName: parentInfo.fileName,
lineNumber: parentInfo.lineNumber,
columnNumber: parentInfo.columnNumber,
elementType: element.tagName?.toLowerCase() || 'unknown',
componentName: currentInfo?.componentName,
functionName: currentInfo?.functionName
};
}
parent = parent.parentElement;
depth++;
}
}
const candidates = [
'data-xagi-children-source',
'data-source-children-source',
AttributeNames.childrenSource
];
let childrenSource: string | null = null;
for (const attr of candidates) {
const val = element.getAttribute(attr);
if (val) {
childrenSource = val;
break;
}
}
if (childrenSource) {
const parts = childrenSource.split(':');
if (parts.length >= 3) {
const fileName = parts.slice(0, -2).join(':'); // Paths may contain `:`
const lineNumber = parseInt(parts[parts.length - 2], 10);
const columnNumber = parseInt(parts[parts.length - 1], 10);
if (!isNaN(lineNumber) && !isNaN(columnNumber)) {
return {
fileName,
lineNumber,
columnNumber,
elementType: element.tagName?.toLowerCase() || 'unknown',
componentName: currentInfo?.componentName,
functionName: currentInfo?.functionName
};
}
}
}
if (currentFile) {
const childWithDifferentSource = findChildFromDifferentFile(element, currentFile);
if (childWithDifferentSource) {
const childInfo = extractSourceInfo(childWithDifferentSource);
if (childInfo && childInfo.fileName !== currentFile) {
return {
fileName: childInfo.fileName,
lineNumber: childInfo.lineNumber,
columnNumber: childInfo.columnNumber,
elementType: element.tagName?.toLowerCase() || 'unknown',
componentName: currentInfo?.componentName,
functionName: currentInfo?.functionName
};
}
}
}
if (!hasStaticContent) {
return extractSourceInfo(element);
}
// Only walk up for elements inside component definitions (both parent and child
// have static-content in the same file). Regular nested HTML should use its own sourceInfo.
if (!isInComponentDefinition(element)) {
return extractSourceInfo(element);
}
let currentElement: HTMLElement | null = element;
let depth = 0;
while (currentElement && depth < 20) {
const parent: HTMLElement | null = currentElement.parentElement;
if (!parent) {
break;
}
const parentInfo = extractSourceInfo(parent);
const parentFile = parentInfo?.fileName;
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
if (parentFile && (!parentHasStaticContent || parentFile !== currentFile)) {
return parentInfo;
}
currentElement = parent;
depth++;
}
return extractSourceInfo(element);
}
/**
* Heuristic: node lives in a component **definition** (both parent/child static, same file).
*/
export function isInComponentDefinition(element: HTMLElement): boolean {
const hasStaticContent = element.hasAttribute(AttributeNames.staticContent);
const sourceInfo = extractSourceInfo(element);
const sourceFile = sourceInfo?.fileName;
if (!hasStaticContent || !sourceFile) {
return false;
}
const parent = element.parentElement;
if (!parent) {
return false;
}
const parentInfo = extractSourceInfo(parent);
const parentFile = parentInfo?.fileName;
const parentHasStaticContent = parent.hasAttribute(AttributeNames.staticContent);
return parentHasStaticContent && parentFile === sourceFile;
}

View File

@@ -0,0 +1,50 @@
import type { SourceInfo, ElementInfo } from './messages';
export type { SourceInfo, ElementInfo };
/** High-level update kind tracked by `UpdateManager`. */
export type UpdateOperation = 'style_update' | 'content_update' | 'attribute_update' | 'class_update' | 'batch_update';
/** One in-flight or historical mutation. */
export interface UpdateState {
id: string;
operation: UpdateOperation;
sourceInfo: SourceInfo;
element: HTMLElement;
oldValue: string;
newValue: string;
status: 'pending' | 'processing' | 'completed' | 'failed' | 'reverted';
timestamp: number;
error?: string;
retryCount: number;
persist?: boolean;
}
/** Result of persisting / previewing an update. */
export interface UpdateResult {
success: boolean;
element: HTMLElement;
updateId: string;
error?: string;
serverResponse?: any;
}
/** One row in a batch POST body for UpdateManager. */
export interface UpdateManagerBatchItem {
element: HTMLElement;
type: 'style' | 'content' | 'attribute';
sourceInfo: SourceInfo;
newValue: string;
originalValue?: string;
selector?: string;
}
/** `UpdateManager` runtime options. */
export interface UpdateManagerConfig {
enableDirectEdit: boolean;
enableBatching: boolean;
batchDebounceMs: number;
maxRetries: number;
autoSave: boolean;
saveDelay: number;
validation: {
validateSource: boolean;
validateValue: boolean;
maxLength: number;
};
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG1D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;AAExC,yDAAyD;AACzD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,gBAAgB,GAChB,kBAAkB,GAClB,cAAc,GACd,cAAc,CAAC;AAEnB,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,GAAG,CAAC;CACtB;AAED,sDAAsD;AACtD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE;QACV,cAAc,EAAE,OAAO,CAAC;QACxB,aAAa,EAAE,OAAO,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,62 @@
// Import shared types from messages
import type { SourceInfo, ElementInfo } from './messages';
// Re-export for convenience
export type { SourceInfo, ElementInfo };
/** High-level update kind tracked by `UpdateManager`. */
export type UpdateOperation =
| 'style_update'
| 'content_update'
| 'attribute_update'
| 'class_update'
| 'batch_update';
/** One in-flight or historical mutation. */
export interface UpdateState {
id: string;
operation: UpdateOperation;
sourceInfo: SourceInfo;
element: HTMLElement;
oldValue: string;
newValue: string;
status: 'pending' | 'processing' | 'completed' | 'failed' | 'reverted';
timestamp: number;
error?: string;
retryCount: number;
persist?: boolean;
}
/** Result of persisting / previewing an update. */
export interface UpdateResult {
success: boolean;
element: HTMLElement;
updateId: string;
error?: string;
serverResponse?: any;
}
/** One row in a batch POST body for UpdateManager. */
export interface UpdateManagerBatchItem {
element: HTMLElement;
type: 'style' | 'content' | 'attribute';
sourceInfo: SourceInfo;
newValue: string;
originalValue?: string;
selector?: string;
}
/** `UpdateManager` runtime options. */
export interface UpdateManagerConfig {
enableDirectEdit: boolean;
enableBatching: boolean;
batchDebounceMs: number;
maxRetries: number;
autoSave: boolean;
saveDelay: number;
validation: {
validateSource: boolean;
validateValue: boolean;
maxLength: number;
};
}