wu-framework 2.1.1 → 2.5.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/README.md +6 -1
- package/dist/adapters/alpine/index.d.ts +1 -1
- package/dist/adapters/angular/index.d.ts +1 -1
- package/dist/adapters/htmx/index.d.ts +1 -1
- package/dist/adapters/lit/index.d.ts +1 -1
- package/dist/adapters/lit/index.js +2 -2
- package/dist/adapters/lit/index.js.map +1 -1
- package/dist/adapters/preact/index.d.ts +1 -1
- package/dist/adapters/preact/index.js +1 -1
- package/dist/adapters/preact/index.js.map +1 -1
- package/dist/adapters/qwik/index.d.ts +3 -10
- package/dist/adapters/qwik/index.js +1 -1
- package/dist/adapters/qwik/index.js.map +1 -1
- package/dist/adapters/react/index.js +1 -1
- package/dist/adapters/react/index.js.map +1 -1
- package/dist/adapters/shared.d.ts +44 -0
- package/dist/adapters/shared.js +1 -1
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/solid/index.d.ts +1 -1
- package/dist/adapters/solid/index.js +1 -1
- package/dist/adapters/solid/index.js.map +1 -1
- package/dist/adapters/stencil/index.d.ts +1 -1
- package/dist/adapters/stimulus/index.d.ts +1 -1
- package/dist/adapters/svelte/index.d.ts +1 -1
- package/dist/adapters/svelte/index.js +1 -1
- package/dist/adapters/svelte/index.js.map +1 -1
- package/dist/adapters/vanilla/index.d.ts +1 -1
- package/dist/adapters/vanilla/index.js +1 -1
- package/dist/adapters/vanilla/index.js.map +1 -1
- package/dist/adapters/vue/index.js +1 -1
- package/dist/adapters/vue/index.js.map +1 -1
- package/dist/ai/wu-ai.js +1 -1
- package/dist/ai/wu-ai.js.map +1 -1
- package/dist/core/wu-devtools.js +2 -0
- package/dist/core/wu-devtools.js.map +1 -0
- package/dist/core/wu-html-parser.js +1 -1
- package/dist/core/wu-html-parser.js.map +1 -1
- package/dist/core/wu-iframe-sandbox.js +1 -1
- package/dist/core/wu-iframe-sandbox.js.map +1 -1
- package/dist/core/wu-loader.js +1 -1
- package/dist/core/wu-loader.js.map +1 -1
- package/dist/core/wu-logger.js +2 -0
- package/dist/core/wu-logger.js.map +1 -0
- package/dist/core/wu-mcp-bridge.js +1 -1
- package/dist/core/wu-mcp-bridge.js.map +1 -1
- package/dist/core/wu-script-executor.js +1 -1
- package/dist/core/wu-script-executor.js.map +1 -1
- package/dist/core/wu-store-sync.js +2 -0
- package/dist/core/wu-store-sync.js.map +1 -0
- package/dist/core/wu-timeline.js +2 -0
- package/dist/core/wu-timeline.js.map +1 -0
- package/dist/index.d.cts +739 -0
- package/dist/index.d.ts +295 -1
- package/dist/wu-ai-browser-primitives-CaUCk1Xl.js +2 -0
- package/dist/wu-ai-browser-primitives-CaUCk1Xl.js.map +1 -0
- package/dist/wu-framework.cjs +3 -0
- package/dist/wu-framework.cjs.map +1 -0
- package/dist/wu-framework.dev.js +1207 -275
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +2 -2
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +2 -2
- package/dist/wu-framework.umd.js.map +1 -1
- package/integrations/astro/WuApp.astro +16 -11
- package/integrations/astro/WuShell.astro +11 -3
- package/package.json +14 -6
- package/dist/wu-ai-browser-primitives-BDKXJlwc.js +0 -2
- package/dist/wu-ai-browser-primitives-BDKXJlwc.js.map +0 -1
- package/dist/wu-framework.cjs.js +0 -3
- package/dist/wu-framework.cjs.js.map +0 -1
- package/dist/wu-logger-fJfUHBGA.js +0 -2
- package/dist/wu-logger-fJfUHBGA.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wu-mcp-bridge.js","sources":["../../src/core/wu-mcp-bridge.js"],"sourcesContent":["/**\n * WU-MCP Bridge (Browser Side)\n *\n * Connects to the wu-mcp-server via WebSocket and executes\n * commands using wu.* APIs. This is the \"eyes and hands\" of\n * the MCP server inside the browser.\n *\n * Security:\n * - Optional auth token sent on first message (handshake)\n * - All state/event/mount operations check wu.ai permissions\n * - Mutating operations emit audit events\n * - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted\n *\n * @example\n * // Connect with auth token\n * wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });\n *\n * // Connect without auth (development only)\n * wu.mcp.connect();\n */\n\nimport {\n ensureInterceptors,\n networkLog,\n consoleLog,\n captureScreenshot,\n buildA11yTree,\n clickElement,\n typeIntoElement,\n getFilteredNetwork,\n getFilteredConsole,\n} from '../ai/wu-ai-browser-primitives.js';\nimport { logger } from './wu-logger.js';\n\n/**\n * Create the MCP bridge for a Wu instance.\n *\n * @param {object} wu - The Wu Framework instance (window.wu)\n * @returns {object} Bridge API: { connect, disconnect, isConnected }\n */\nexport function createMcpBridge(wu) {\n let ws = null;\n let reconnectTimer = null;\n let reconnectAttempts = 0;\n let authenticated = false;\n let authToken = null;\n const MAX_RECONNECT_ATTEMPTS = 10;\n const RECONNECT_DELAY = 2000;\n\n // Closure-capture our internal token. Required so strictMode accepts\n // emits as appName='wu-mcp-bridge' (and rejects spoofers under the same name).\n const _bridgeToken = wu.eventBus?.getInternalToken?.('wu-mcp-bridge') || null;\n const _emitInternal = (event, data) =>\n wu.eventBus?.emit(event, data, { appName: 'wu-mcp-bridge', token: _bridgeToken });\n\n // Event log for wu_list_events\n const eventLog = [];\n const MAX_EVENT_LOG = 200;\n\n // Capture events for history\n if (wu.eventBus) {\n wu.eventBus.on('*', (event) => {\n eventLog.push({\n name: event.name,\n data: event.data,\n timestamp: event.timestamp || Date.now(),\n source: event.source || 'unknown',\n });\n if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();\n });\n }\n\n // Install shared interceptors (idempotent — safe if wu-ai-browser already did it)\n ensureInterceptors();\n\n // ── Permission helpers ──\n\n /**\n * Check a permission flag via wu.ai.permissions if available.\n * Falls back to deny if wu.ai is not initialized.\n */\n function _checkPermission(perm) {\n if (wu.ai && wu.ai.permissions) {\n return wu.ai.permissions.check(perm);\n }\n // If AI module not initialized, deny write operations, allow reads\n const readPerms = ['readStore', 'executeActions'];\n return readPerms.includes(perm);\n }\n\n /**\n * Emit an audit event for bridge operations.\n */\n function _audit(operation, params, result) {\n if (wu.eventBus) {\n wu.eventBus.emit('mcp:bridge:operation', {\n operation,\n params,\n result: result?.error ? { error: result.error } : { success: true },\n timestamp: Date.now(),\n }, { appName: 'wu-mcp-bridge' });\n }\n }\n\n // ── Command handlers ──\n\n const handlers = {\n // ── Read-only operations (no permission gates) ──\n\n status() {\n return {\n connected: true,\n framework: 'wu-framework',\n apps: _getAppList(),\n storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],\n actionsCount: wu.ai ? wu.ai.tools().length : 0,\n eventLogSize: eventLog.length,\n };\n },\n\n list_apps() {\n return _getAppList();\n },\n\n list_events({ limit = 20 }) {\n return eventLog.slice(-limit);\n },\n\n list_actions() {\n if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };\n const tools = wu.ai.tools();\n return { actions: tools, count: tools.length };\n },\n\n snapshot({ appName }) {\n try {\n const target = appName\n ? document.querySelector(`[data-wu-app=\"${appName}\"]`) || document.querySelector(`#wu-app-${appName}`)\n : document.body;\n\n if (!target) return { error: `App \"${appName}\" not found in DOM` };\n\n return {\n app: appName || '(page)',\n snapshot: buildA11yTree(target, 0, 5),\n timestamp: Date.now(),\n };\n } catch (err) {\n return { error: err.message };\n }\n },\n\n console({ level = 'all', limit = 50 }) {\n return getFilteredConsole(level, limit);\n },\n\n async screenshot({ selector, quality = 0.8 }) {\n const result = await captureScreenshot(selector, quality);\n if (!result.error) result.timestamp = Date.now();\n return result;\n },\n\n network({ method, status, limit = 50 }) {\n return getFilteredNetwork(method, status, limit);\n },\n\n // ── Permission-gated operations ──\n\n get_state({ path }) {\n if (!wu.store) return { error: 'wu.store not available' };\n if (!_checkPermission('readStore')) {\n return { error: 'Permission denied: readStore is disabled' };\n }\n const value = wu.store.get(path || '');\n return { path: path || '(root)', value };\n },\n\n set_state({ path, value }) {\n if (!wu.store) return { error: 'wu.store not available' };\n if (!path) return { error: 'path is required' };\n if (!_checkPermission('writeStore')) {\n _audit('set_state', { path }, { error: 'Permission denied' });\n return { error: 'Permission denied: writeStore is disabled' };\n }\n wu.store.set(path, value);\n _audit('set_state', { path, value }, { success: true });\n return { path, value, updated: true };\n },\n\n emit_event({ event, data }) {\n if (!wu.eventBus) return { error: 'wu.eventBus not available' };\n if (!event) return { error: 'event name is required' };\n if (!_checkPermission('emitEvents')) {\n _audit('emit_event', { event }, { error: 'Permission denied' });\n return { error: 'Permission denied: emitEvents is disabled' };\n }\n _emitInternal(event, data);\n _audit('emit_event', { event, data }, { success: true });\n return { emitted: event, data };\n },\n\n navigate({ route }) {\n if (!route) return { error: 'Route is required' };\n if (!_checkPermission('emitEvents')) {\n _audit('navigate', { route }, { error: 'Permission denied: emitEvents' });\n return { error: 'Permission denied: emitEvents is disabled' };\n }\n _emitInternal('shell:navigate', { route });\n if (wu.store && _checkPermission('writeStore')) {\n wu.store.set('currentPath', route);\n }\n _audit('navigate', { route }, { success: true });\n return { navigated: route };\n },\n\n mount_app({ appName, container }) {\n if (!appName) return { error: 'appName is required' };\n if (!_checkPermission('modifyDOM')) {\n _audit('mount_app', { appName }, { error: 'Permission denied' });\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n try {\n if (wu.mount) {\n wu.mount(appName, container);\n _audit('mount_app', { appName, container }, { success: true });\n return { mounted: appName, container };\n }\n return { error: 'wu.mount not available' };\n } catch (err) {\n return { error: err.message };\n }\n },\n\n unmount_app({ appName }) {\n if (!appName) return { error: 'appName is required' };\n if (!_checkPermission('modifyDOM')) {\n _audit('unmount_app', { appName }, { error: 'Permission denied' });\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n try {\n if (wu.unmount) {\n wu.unmount(appName);\n _audit('unmount_app', { appName }, { success: true });\n return { unmounted: appName };\n }\n return { error: 'wu.unmount not available' };\n } catch (err) {\n return { error: err.message };\n }\n },\n\n click({ selector, text }) {\n if (!_checkPermission('modifyDOM')) {\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n const result = clickElement(selector, text);\n _audit('click', { selector, text }, result);\n return result;\n },\n\n type({ selector, text, clear = false, submit = false }) {\n if (!_checkPermission('modifyDOM')) {\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n const result = typeIntoElement(selector, text, { clear, submit });\n _audit('type', { selector, textLength: text?.length }, result);\n return result;\n },\n\n async execute_action({ action, params }) {\n if (!wu.ai) return { error: 'wu.ai not available' };\n if (!action) return { error: 'action name is required' };\n\n try {\n // Execute through public API (respects permissions, validation, audit)\n const result = await wu.ai.execute(action, params || {});\n return { action, ...result };\n } catch (err) {\n return { error: err.message };\n }\n },\n };\n\n // ── WebSocket connection ──\n\n function connect(url = 'ws://localhost:19100', options = {}) {\n if (ws && ws.readyState <= 1) {\n logger.warn('[wu-mcp-bridge] Already connected or connecting');\n return;\n }\n\n authToken = options.token || null;\n authenticated = !authToken; // No token = auto-authenticated (dev mode)\n\n try {\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n logger.debug('[wu-mcp-bridge] Connected to wu-mcp-server');\n reconnectAttempts = 0;\n\n // Send auth handshake if token provided\n if (authToken) {\n ws.send(JSON.stringify({\n type: 'auth',\n token: authToken,\n }));\n }\n };\n\n ws.onmessage = async (event) => {\n try {\n const msg = JSON.parse(event.data);\n\n // Handle auth response\n if (msg.type === 'auth_result') {\n authenticated = msg.success === true;\n if (!authenticated) {\n console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');\n disconnect();\n } else {\n logger.debug('[wu-mcp-bridge] Authenticated successfully');\n }\n return;\n }\n\n // Reject commands if not authenticated\n if (!authenticated) {\n if (msg.id) {\n _respond(msg.id, null, 'Not authenticated. Send auth token first.');\n }\n return;\n }\n\n const { id, command, params } = msg;\n\n if (!id || !command) {\n logger.warn('[wu-mcp-bridge] Invalid message:', msg);\n return;\n }\n\n const handler = handlers[command];\n if (!handler) {\n _respond(id, null, `Unknown command: ${command}`);\n return;\n }\n\n try {\n const result = await handler(params || {});\n _respond(id, result);\n } catch (err) {\n _respond(id, null, err.message);\n }\n } catch (err) {\n console.error('[wu-mcp-bridge] Failed to handle message:', err);\n }\n };\n\n ws.onclose = () => {\n logger.debug('[wu-mcp-bridge] Disconnected');\n ws = null;\n authenticated = false;\n _scheduleReconnect(url, options);\n };\n\n ws.onerror = () => {\n // onclose will fire after this\n };\n } catch (err) {\n console.error('[wu-mcp-bridge] Connection failed:', err.message);\n _scheduleReconnect(url, options);\n }\n }\n\n function disconnect() {\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // prevent reconnect\n if (ws) {\n ws.close();\n ws = null;\n }\n authenticated = false;\n }\n\n function isConnected() {\n return ws !== null && ws.readyState === 1 && authenticated;\n }\n\n // ── Private helpers ──\n\n function _respond(id, result, error) {\n if (!ws || ws.readyState !== 1) return;\n const msg = error ? { id, error } : { id, result };\n ws.send(JSON.stringify(msg));\n }\n\n function _scheduleReconnect(url, options) {\n if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;\n reconnectAttempts++;\n const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);\n reconnectTimer = setTimeout(() => connect(url, options), delay);\n }\n\n function _getAppList() {\n const apps = [];\n\n if (wu._apps) {\n for (const [name, app] of Object.entries(wu._apps)) {\n apps.push({\n name,\n mounted: app.mounted || app.isMounted || false,\n url: app.url || app.info?.url || '',\n status: app.status || app.info?.status || 'unknown',\n });\n }\n }\n\n // Fallback: scan DOM for wu-app elements\n if (apps.length === 0) {\n document.querySelectorAll('[data-wu-app]').forEach((el) => {\n apps.push({\n name: el.getAttribute('data-wu-app'),\n mounted: true,\n container: `#${el.id || '(no-id)'}`,\n });\n });\n }\n\n return apps;\n }\n\n return { connect, disconnect, isConnected };\n}\n"],"names":["createMcpBridge","wu","ws","reconnectTimer","reconnectAttempts","authenticated","authToken","_bridgeToken","eventBus","getInternalToken","_emitInternal","event","data","emit","appName","token","eventLog","_checkPermission","perm","ai","permissions","check","includes","_audit","operation","params","result","error","success","timestamp","Date","now","on","push","name","source","length","shift","ensureInterceptors","handlers","status","connected","framework","apps","_getAppList","storeKeys","store","Object","keys","get","actionsCount","tools","eventLogSize","list_apps","list_events","limit","slice","list_actions","actions","note","count","snapshot","target","document","querySelector","body","app","buildA11yTree","err","message","console","level","getFilteredConsole","screenshot","selector","quality","captureScreenshot","network","method","getFilteredNetwork","get_state","path","value","set_state","set","updated","emit_event","emitted","navigate","route","navigated","mount_app","container","mount","mounted","unmount_app","unmount","unmounted","click","text","clickElement","type","clear","submit","typeIntoElement","textLength","execute_action","action","execute","connect","url","options","readyState","logger","warn","WebSocket","onopen","debug","send","JSON","stringify","onmessage","async","msg","parse","reason","disconnect","id","_respond","command","handler","onclose","_scheduleReconnect","onerror","clearTimeout","close","delay","Math","min","setTimeout","_apps","entries","isMounted","info","querySelectorAll","forEach","el","getAttribute","isConnected"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"wu-mcp-bridge.js","sources":["../../src/core/wu-mcp-bridge.js"],"sourcesContent":["/**\n * WU-MCP Bridge (Browser Side)\n *\n * Connects to the wu-mcp-server via WebSocket and executes\n * commands using wu.* APIs. This is the \"eyes and hands\" of\n * the MCP server inside the browser.\n *\n * Security:\n * - Optional auth token sent on first message (handshake)\n * - All state/event/mount operations check wu.ai permissions\n * - Mutating operations emit audit events\n * - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted\n *\n * @example\n * // Connect with auth token\n * wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });\n *\n * // Connect without auth (development only)\n * wu.mcp.connect();\n */\n\nimport {\n ensureInterceptors,\n networkLog,\n consoleLog,\n captureScreenshot,\n buildA11yTree,\n clickElement,\n typeIntoElement,\n getFilteredNetwork,\n getFilteredConsole,\n} from '../ai/wu-ai-browser-primitives.js';\nimport { logger } from './wu-logger.js';\n\n/**\n * Create the MCP bridge for a Wu instance.\n *\n * @param {object} wu - The Wu Framework instance (window.wu)\n * @returns {object} Bridge API: { connect, disconnect, isConnected }\n */\nexport function createMcpBridge(wu) {\n let ws = null;\n let reconnectTimer = null;\n let reconnectAttempts = 0;\n let authenticated = false;\n let authToken = null;\n const MAX_RECONNECT_ATTEMPTS = 10;\n const RECONNECT_DELAY = 2000;\n\n // Closure-capture our internal token. Required so strictMode accepts\n // emits as appName='wu-mcp-bridge' (and rejects spoofers under the same name).\n const _bridgeToken = wu.eventBus?.getInternalToken?.('wu-mcp-bridge') || null;\n const _emitInternal = (event, data) =>\n wu.eventBus?.emit(event, data, { appName: 'wu-mcp-bridge', token: _bridgeToken });\n\n // Event log for wu_list_events\n const eventLog = [];\n const MAX_EVENT_LOG = 200;\n\n // Capture events for history\n if (wu.eventBus) {\n wu.eventBus.on('*', (event) => {\n eventLog.push({\n name: event.name,\n data: event.data,\n timestamp: event.timestamp || Date.now(),\n source: event.source || 'unknown',\n });\n if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();\n });\n }\n\n // Install shared interceptors (idempotent — safe if wu-ai-browser already did it)\n ensureInterceptors();\n\n // ── Permission helpers ──\n\n /**\n * Check a permission flag via wu.ai.permissions if available.\n * Falls back to deny if wu.ai is not initialized.\n */\n function _checkPermission(perm) {\n if (wu.ai && wu.ai.permissions) {\n return wu.ai.permissions.check(perm);\n }\n // If AI module not initialized, deny write operations, allow reads\n const readPerms = ['readStore', 'executeActions'];\n return readPerms.includes(perm);\n }\n\n /**\n * Emit an audit event for bridge operations.\n */\n function _audit(operation, params, result) {\n if (wu.eventBus) {\n _emitInternal('mcp:bridge:operation', {\n operation,\n params,\n result: result?.error ? { error: result.error } : { success: true },\n timestamp: Date.now(),\n });\n }\n }\n\n // ── Command handlers ──\n\n const handlers = {\n // ── Read-only operations (no permission gates) ──\n\n status() {\n return {\n connected: true,\n framework: 'wu-framework',\n apps: _getAppList(),\n storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],\n actionsCount: wu.ai ? wu.ai.tools().length : 0,\n eventLogSize: eventLog.length,\n };\n },\n\n list_apps() {\n return _getAppList();\n },\n\n list_events({ limit = 20 }) {\n return eventLog.slice(-limit);\n },\n\n list_actions() {\n if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };\n const tools = wu.ai.tools();\n return { actions: tools, count: tools.length };\n },\n\n snapshot({ appName }) {\n try {\n const target = appName\n ? document.querySelector(`[data-wu-app=\"${appName}\"]`) || document.querySelector(`#wu-app-${appName}`)\n : document.body;\n\n if (!target) return { error: `App \"${appName}\" not found in DOM` };\n\n return {\n app: appName || '(page)',\n snapshot: buildA11yTree(target, 0, 5),\n timestamp: Date.now(),\n };\n } catch (err) {\n return { error: err.message };\n }\n },\n\n console({ level = 'all', limit = 50 }) {\n return getFilteredConsole(level, limit);\n },\n\n async screenshot({ selector, quality = 0.8 }) {\n const result = await captureScreenshot(selector, quality);\n if (!result.error) result.timestamp = Date.now();\n return result;\n },\n\n network({ method, status, limit = 50 }) {\n return getFilteredNetwork(method, status, limit);\n },\n\n // ── Permission-gated operations ──\n\n get_state({ path }) {\n if (!wu.store) return { error: 'wu.store not available' };\n if (!_checkPermission('readStore')) {\n return { error: 'Permission denied: readStore is disabled' };\n }\n const value = wu.store.get(path || '');\n return { path: path || '(root)', value };\n },\n\n set_state({ path, value }) {\n if (!wu.store) return { error: 'wu.store not available' };\n if (!path) return { error: 'path is required' };\n if (!_checkPermission('writeStore')) {\n _audit('set_state', { path }, { error: 'Permission denied' });\n return { error: 'Permission denied: writeStore is disabled' };\n }\n wu.store.set(path, value);\n _audit('set_state', { path, value }, { success: true });\n return { path, value, updated: true };\n },\n\n emit_event({ event, data }) {\n if (!wu.eventBus) return { error: 'wu.eventBus not available' };\n if (!event) return { error: 'event name is required' };\n if (!_checkPermission('emitEvents')) {\n _audit('emit_event', { event }, { error: 'Permission denied' });\n return { error: 'Permission denied: emitEvents is disabled' };\n }\n _emitInternal(event, data);\n _audit('emit_event', { event, data }, { success: true });\n return { emitted: event, data };\n },\n\n navigate({ route }) {\n if (!route) return { error: 'Route is required' };\n if (!_checkPermission('emitEvents')) {\n _audit('navigate', { route }, { error: 'Permission denied: emitEvents' });\n return { error: 'Permission denied: emitEvents is disabled' };\n }\n _emitInternal('shell:navigate', { route });\n if (wu.store && _checkPermission('writeStore')) {\n wu.store.set('currentPath', route);\n }\n _audit('navigate', { route }, { success: true });\n return { navigated: route };\n },\n\n mount_app({ appName, container }) {\n if (!appName) return { error: 'appName is required' };\n if (!_checkPermission('modifyDOM')) {\n _audit('mount_app', { appName }, { error: 'Permission denied' });\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n try {\n if (wu.mount) {\n wu.mount(appName, container);\n _audit('mount_app', { appName, container }, { success: true });\n return { mounted: appName, container };\n }\n return { error: 'wu.mount not available' };\n } catch (err) {\n return { error: err.message };\n }\n },\n\n unmount_app({ appName }) {\n if (!appName) return { error: 'appName is required' };\n if (!_checkPermission('modifyDOM')) {\n _audit('unmount_app', { appName }, { error: 'Permission denied' });\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n try {\n if (wu.unmount) {\n wu.unmount(appName);\n _audit('unmount_app', { appName }, { success: true });\n return { unmounted: appName };\n }\n return { error: 'wu.unmount not available' };\n } catch (err) {\n return { error: err.message };\n }\n },\n\n click({ selector, text }) {\n if (!_checkPermission('modifyDOM')) {\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n const result = clickElement(selector, text);\n _audit('click', { selector, text }, result);\n return result;\n },\n\n type({ selector, text, clear = false, submit = false }) {\n if (!_checkPermission('modifyDOM')) {\n return { error: 'Permission denied: modifyDOM is disabled' };\n }\n const result = typeIntoElement(selector, text, { clear, submit });\n _audit('type', { selector, textLength: text?.length }, result);\n return result;\n },\n\n async execute_action({ action, params }) {\n if (!wu.ai) return { error: 'wu.ai not available' };\n if (!action) return { error: 'action name is required' };\n\n try {\n // Execute through public API (respects permissions, validation, audit)\n const result = await wu.ai.execute(action, params || {});\n return { action, ...result };\n } catch (err) {\n return { error: err.message };\n }\n },\n };\n\n // ── WebSocket connection ──\n\n function connect(url = 'ws://localhost:19100', options = {}) {\n if (ws && ws.readyState <= 1) {\n logger.warn('[wu-mcp-bridge] Already connected or connecting');\n return;\n }\n\n authToken = options.token || null;\n authenticated = !authToken; // No token = auto-authenticated (dev mode)\n\n try {\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n logger.debug('[wu-mcp-bridge] Connected to wu-mcp-server');\n reconnectAttempts = 0;\n\n // Send auth handshake if token provided\n if (authToken) {\n ws.send(JSON.stringify({\n type: 'auth',\n token: authToken,\n }));\n }\n };\n\n ws.onmessage = async (event) => {\n try {\n const msg = JSON.parse(event.data);\n\n // Handle auth response\n if (msg.type === 'auth_result') {\n authenticated = msg.success === true;\n if (!authenticated) {\n console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');\n disconnect();\n } else {\n logger.debug('[wu-mcp-bridge] Authenticated successfully');\n }\n return;\n }\n\n // Reject commands if not authenticated\n if (!authenticated) {\n if (msg.id) {\n _respond(msg.id, null, 'Not authenticated. Send auth token first.');\n }\n return;\n }\n\n const { id, command, params } = msg;\n\n if (!id || !command) {\n logger.warn('[wu-mcp-bridge] Invalid message:', msg);\n return;\n }\n\n const handler = handlers[command];\n if (!handler) {\n _respond(id, null, `Unknown command: ${command}`);\n return;\n }\n\n try {\n const result = await handler(params || {});\n _respond(id, result);\n } catch (err) {\n _respond(id, null, err.message);\n }\n } catch (err) {\n console.error('[wu-mcp-bridge] Failed to handle message:', err);\n }\n };\n\n ws.onclose = () => {\n logger.debug('[wu-mcp-bridge] Disconnected');\n ws = null;\n authenticated = false;\n _scheduleReconnect(url, options);\n };\n\n ws.onerror = () => {\n // onclose will fire after this\n };\n } catch (err) {\n console.error('[wu-mcp-bridge] Connection failed:', err.message);\n _scheduleReconnect(url, options);\n }\n }\n\n function disconnect() {\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // prevent reconnect\n if (ws) {\n ws.close();\n ws = null;\n }\n authenticated = false;\n }\n\n function isConnected() {\n return ws !== null && ws.readyState === 1 && authenticated;\n }\n\n // ── Private helpers ──\n\n function _respond(id, result, error) {\n if (!ws || ws.readyState !== 1) return;\n const msg = error ? { id, error } : { id, result };\n ws.send(JSON.stringify(msg));\n }\n\n function _scheduleReconnect(url, options) {\n if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;\n reconnectAttempts++;\n const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);\n reconnectTimer = setTimeout(() => connect(url, options), delay);\n }\n\n function _getAppList() {\n const apps = [];\n\n if (wu._apps) {\n for (const [name, app] of Object.entries(wu._apps)) {\n apps.push({\n name,\n mounted: app.mounted || app.isMounted || false,\n url: app.url || app.info?.url || '',\n status: app.status || app.info?.status || 'unknown',\n });\n }\n }\n\n // Fallback: scan DOM for wu-app elements\n if (apps.length === 0) {\n document.querySelectorAll('[data-wu-app]').forEach((el) => {\n apps.push({\n name: el.getAttribute('data-wu-app'),\n mounted: true,\n container: `#${el.id || '(no-id)'}`,\n });\n });\n }\n\n return apps;\n }\n\n return { connect, disconnect, isConnected };\n}\n"],"names":["createMcpBridge","wu","ws","reconnectTimer","reconnectAttempts","authenticated","authToken","_bridgeToken","eventBus","getInternalToken","_emitInternal","event","data","emit","appName","token","eventLog","_checkPermission","perm","ai","permissions","check","includes","_audit","operation","params","result","error","success","timestamp","Date","now","on","push","name","source","length","shift","ensureInterceptors","handlers","status","connected","framework","apps","_getAppList","storeKeys","store","Object","keys","get","actionsCount","tools","eventLogSize","list_apps","list_events","limit","slice","list_actions","actions","note","count","snapshot","target","document","querySelector","body","app","buildA11yTree","err","message","console","level","getFilteredConsole","screenshot","selector","quality","captureScreenshot","network","method","getFilteredNetwork","get_state","path","value","set_state","set","updated","emit_event","emitted","navigate","route","navigated","mount_app","container","mount","mounted","unmount_app","unmount","unmounted","click","text","clickElement","type","clear","submit","typeIntoElement","textLength","execute_action","action","execute","connect","url","options","readyState","logger","warn","WebSocket","onopen","debug","send","JSON","stringify","onmessage","async","msg","parse","reason","disconnect","id","_respond","command","handler","onclose","_scheduleReconnect","onerror","clearTimeout","close","delay","Math","min","setTimeout","_apps","entries","isMounted","info","querySelectorAll","forEach","el","getAttribute","isConnected"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{logger as t}from"./wu-logger.js";class r{static DANGEROUS_PATTERNS=[{pattern:/constructor\s*\[\s*['"`]constructor['"`]\s*\]/,label:"constructor chain access (sandbox escape)"},{pattern:/__proto__/,label:"__proto__ access (prototype pollution)"},{pattern:/Object\s*\.\s*getPrototypeOf\s*\(\s*proxy\s*\)/,label:"Object.getPrototypeOf(proxy) (sandbox escape)"},{pattern:/Function\s*\(\s*['"`]/,label:"Function() constructor (dynamic code generation)"},{pattern:/\beval\s*\(/,label:"eval() (dynamic code execution)"},{pattern:/\bimport\s*\(/,label:"import() (dynamic import escapes sandbox)"},{pattern:/document\s*\.\s*cookie/,label:"document.cookie (direct cookie access)"}];_validateScript(o,e){for(const{pattern:c,label:n}of r.DANGEROUS_PATTERNS)if(c.test(o)){const r=`[ScriptExecutor] Blocked dangerous pattern in "${e}": ${n}`;throw t.wuError(r),new Error(r)}}execute(r,o,e,c={}){const{strictGlobal:n=!0,sourceUrl:s=""}=c;if(!r||!r.trim())return;this._validateScript(r,o);const i=s?`\n//# sourceURL=wu-sandbox:///${o}/${s}\n`:"";let a,p;a=n?`;(function(window, self, globalThis, top, parent) {\n with(window) {\n ;${r}${i}\n }\n}).call(proxy, proxy, proxy, proxy, proxy, proxy);`:`;(function(window, self, globalThis, top, parent) {\n ;${r}${i}\n}).call(proxy, proxy, proxy, proxy, proxy, proxy);`;try{p=new Function("proxy",a)}catch(s){if(n)return t.wuWarn(`[ScriptExecutor] strictGlobal failed for ${o}, retrying without with(): ${s.message}`),this.execute(r,o,e,{...c,strictGlobal:!1});throw t.wuError(`[ScriptExecutor] Execution failed for ${o}:`,s),s}try{return p(e)}catch(r){throw t.wuError(`[ScriptExecutor] Execution failed for ${o}:`,r),r}}async fetchScript(t){const r=await fetch(t);if(!r.ok)throw new Error(`Failed to fetch script ${t}: HTTP ${r.status}`);return r.text()}async executeAll(r,o,e,c={}){for(const n of r){let r=n.content;!r&&n.src&&(t.wuDebug(`[ScriptExecutor] Fetching external script: ${n.src}`),r=await this.fetchScript(n.src)),r&&r.trim()&&this.execute(r,o,e,{...c,sourceUrl:n.src||c.sourceUrl||""})}t.wuDebug(`[ScriptExecutor] Executed ${r.length} scripts for ${o}`)}}export{r as WuScriptExecutor};
|
|
2
2
|
//# sourceMappingURL=wu-script-executor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wu-script-executor.js","sources":["../../src/core/wu-script-executor.js"],"sourcesContent":["/**\n * WU-SCRIPT-EXECUTOR: Execute scripts inside a Proxy sandbox.\n *\n * Two isolation levels:\n * - strictGlobal: true → with(proxy) { code } — all global access goes through proxy\n * - strictGlobal: false → (function(window){ code })(proxy) — only explicit window.xxx\n *\n * This is what makes the sandbox REAL instead of decorative.\n * Without this, import() runs code in global scope and the proxy is just a cleanup tracker.\n * With this, code receives the proxy as \"window\" and every setTimeout, addEventListener,\n * document.querySelector, localStorage access goes through the proxy's traps.\n */\n\nimport { logger } from './wu-logger.js';\n\nexport class WuScriptExecutor {\n\n /**\n * Dangerous patterns that indicate prototype pollution, sandbox escape,\n * or direct access to sensitive APIs. Each entry is a regex paired with\n * a human-readable label used in error messages.\n *\n * This is a tripwire, not a full parser. It catches the most common\n * attack vectors without the overhead of AST analysis.\n */\n static DANGEROUS_PATTERNS = [\n // Prototype pollution vectors\n { pattern: /constructor\\s*\\[\\s*['\"`]constructor['\"`]\\s*\\]/, label: 'constructor chain access (sandbox escape)' },\n { pattern: /__proto__/, label: '__proto__ access (prototype pollution)' },\n\n // Sandbox escape via proxy introspection\n { pattern: /Object\\s*\\.\\s*getPrototypeOf\\s*\\(\\s*proxy\\s*\\)/, label: 'Object.getPrototypeOf(proxy) (sandbox escape)' },\n\n // Dynamic code generation that bypasses the sandbox\n { pattern: /Function\\s*\\(\\s*['\"`]/, label: 'Function() constructor (dynamic code generation)' },\n { pattern: /\\beval\\s*\\(/, label: 'eval() (dynamic code execution)' },\n\n // Dynamic import escapes the sandbox entirely (runs in global scope)\n { pattern: /\\bimport\\s*\\(/, label: 'import() (dynamic import escapes sandbox)' },\n\n // Direct cookie access (should go through proxy traps, not raw document)\n { pattern: /document\\s*\\.\\s*cookie/, label: 'document.cookie (direct cookie access)' },\n ];\n\n /**\n * Validate script text against known dangerous patterns before execution.\n * Throws if any pattern matches. This is intentionally lightweight --\n * pattern detection only, not a full parse.\n *\n * @param {string} scriptText - The raw script to validate\n * @param {string} appName - App identifier (for error context)\n * @throws {Error} If a dangerous pattern is detected\n */\n _validateScript(scriptText, appName) {\n for (const { pattern, label } of WuScriptExecutor.DANGEROUS_PATTERNS) {\n if (pattern.test(scriptText)) {\n const msg = `[ScriptExecutor] Blocked dangerous pattern in \"${appName}\": ${label}`;\n logger.wuError(msg);\n throw new Error(msg);\n }\n }\n }\n\n /**\n * Execute a script string inside the proxy sandbox.\n *\n * @param {string} scriptText - JavaScript code to execute\n * @param {string} appName - App identifier (for logging)\n * @param {Proxy} proxy - The activated proxy sandbox\n * @param {Object} [options]\n * @param {boolean} [options.strictGlobal=true] - Use with(proxy) for maximum isolation\n * @param {string} [options.sourceUrl=''] - Source URL for devtools (//# sourceURL)\n * @returns {*} Return value of the executed code\n */\n execute(scriptText, appName, proxy, options = {}) {\n const { strictGlobal = true, sourceUrl = '' } = options;\n\n if (!scriptText || !scriptText.trim()) return;\n\n this._validateScript(scriptText, appName);\n\n const sourceComment = sourceUrl ? `\\n//# sourceURL=wu-sandbox:///${appName}/${sourceUrl}\\n` : '';\n\n let wrappedCode;\n\n if (strictGlobal) {\n // MAXIMUM ISOLATION\n // with(window) makes ALL unqualified identifiers (setTimeout, fetch, document, etc.)\n // resolve through the proxy's has/get traps, not the real window.\n // Note: 'use strict' inside the with block becomes a no-op string expression,\n // so bundled code with strict mode still works.\n wrappedCode = `;(function(window, self, globalThis, top, parent) {\n with(window) {\n ;${scriptText}${sourceComment}\n }\n}).call(proxy, proxy, proxy, proxy, proxy, proxy);`;\n } else {\n // IIFE ONLY — only explicit window.xxx goes through proxy\n wrappedCode = `;(function(window, self, globalThis, top, parent) {\n ;${scriptText}${sourceComment}\n}).call(proxy, proxy, proxy, proxy, proxy, proxy);`;\n }\n\n try {\n // new Function('proxy', code) creates a function with 'proxy' as the single param.\n // This avoids polluting scope — the only bridge to the sandbox is the proxy argument.\n
|
|
1
|
+
{"version":3,"file":"wu-script-executor.js","sources":["../../src/core/wu-script-executor.js"],"sourcesContent":["/**\n * WU-SCRIPT-EXECUTOR: Execute scripts inside a Proxy sandbox.\n *\n * Two isolation levels:\n * - strictGlobal: true → with(proxy) { code } — all global access goes through proxy\n * - strictGlobal: false → (function(window){ code })(proxy) — only explicit window.xxx\n *\n * This is what makes the sandbox REAL instead of decorative.\n * Without this, import() runs code in global scope and the proxy is just a cleanup tracker.\n * With this, code receives the proxy as \"window\" and every setTimeout, addEventListener,\n * document.querySelector, localStorage access goes through the proxy's traps.\n */\n\nimport { logger } from './wu-logger.js';\n\nexport class WuScriptExecutor {\n\n /**\n * Dangerous patterns that indicate prototype pollution, sandbox escape,\n * or direct access to sensitive APIs. Each entry is a regex paired with\n * a human-readable label used in error messages.\n *\n * This is a tripwire, not a full parser. It catches the most common\n * attack vectors without the overhead of AST analysis.\n */\n static DANGEROUS_PATTERNS = [\n // Prototype pollution vectors\n { pattern: /constructor\\s*\\[\\s*['\"`]constructor['\"`]\\s*\\]/, label: 'constructor chain access (sandbox escape)' },\n { pattern: /__proto__/, label: '__proto__ access (prototype pollution)' },\n\n // Sandbox escape via proxy introspection\n { pattern: /Object\\s*\\.\\s*getPrototypeOf\\s*\\(\\s*proxy\\s*\\)/, label: 'Object.getPrototypeOf(proxy) (sandbox escape)' },\n\n // Dynamic code generation that bypasses the sandbox\n { pattern: /Function\\s*\\(\\s*['\"`]/, label: 'Function() constructor (dynamic code generation)' },\n { pattern: /\\beval\\s*\\(/, label: 'eval() (dynamic code execution)' },\n\n // Dynamic import escapes the sandbox entirely (runs in global scope)\n { pattern: /\\bimport\\s*\\(/, label: 'import() (dynamic import escapes sandbox)' },\n\n // Direct cookie access (should go through proxy traps, not raw document)\n { pattern: /document\\s*\\.\\s*cookie/, label: 'document.cookie (direct cookie access)' },\n ];\n\n /**\n * Validate script text against known dangerous patterns before execution.\n * Throws if any pattern matches. This is intentionally lightweight --\n * pattern detection only, not a full parse.\n *\n * @param {string} scriptText - The raw script to validate\n * @param {string} appName - App identifier (for error context)\n * @throws {Error} If a dangerous pattern is detected\n */\n _validateScript(scriptText, appName) {\n for (const { pattern, label } of WuScriptExecutor.DANGEROUS_PATTERNS) {\n if (pattern.test(scriptText)) {\n const msg = `[ScriptExecutor] Blocked dangerous pattern in \"${appName}\": ${label}`;\n logger.wuError(msg);\n throw new Error(msg);\n }\n }\n }\n\n /**\n * Execute a script string inside the proxy sandbox.\n *\n * @param {string} scriptText - JavaScript code to execute\n * @param {string} appName - App identifier (for logging)\n * @param {Proxy} proxy - The activated proxy sandbox\n * @param {Object} [options]\n * @param {boolean} [options.strictGlobal=true] - Use with(proxy) for maximum isolation\n * @param {string} [options.sourceUrl=''] - Source URL for devtools (//# sourceURL)\n * @returns {*} Return value of the executed code\n */\n execute(scriptText, appName, proxy, options = {}) {\n const { strictGlobal = true, sourceUrl = '' } = options;\n\n if (!scriptText || !scriptText.trim()) return;\n\n this._validateScript(scriptText, appName);\n\n const sourceComment = sourceUrl ? `\\n//# sourceURL=wu-sandbox:///${appName}/${sourceUrl}\\n` : '';\n\n let wrappedCode;\n\n if (strictGlobal) {\n // MAXIMUM ISOLATION\n // with(window) makes ALL unqualified identifiers (setTimeout, fetch, document, etc.)\n // resolve through the proxy's has/get traps, not the real window.\n // Note: 'use strict' inside the with block becomes a no-op string expression,\n // so bundled code with strict mode still works.\n wrappedCode = `;(function(window, self, globalThis, top, parent) {\n with(window) {\n ;${scriptText}${sourceComment}\n }\n}).call(proxy, proxy, proxy, proxy, proxy, proxy);`;\n } else {\n // IIFE ONLY — only explicit window.xxx goes through proxy\n wrappedCode = `;(function(window, self, globalThis, top, parent) {\n ;${scriptText}${sourceComment}\n}).call(proxy, proxy, proxy, proxy, proxy, proxy);`;\n }\n\n let fn;\n try {\n // new Function('proxy', code) creates a function with 'proxy' as the single param.\n // This avoids polluting scope — the only bridge to the sandbox is the proxy argument.\n fn = new Function('proxy', wrappedCode);\n } catch (error) {\n // If strictGlobal failed to compile (rare edge case with with-statement), retry without it.\n // Only construction errors retry — runtime throws must NOT re-run side effects.\n if (strictGlobal) {\n logger.wuWarn(`[ScriptExecutor] strictGlobal failed for ${appName}, retrying without with(): ${error.message}`);\n return this.execute(scriptText, appName, proxy, { ...options, strictGlobal: false });\n }\n logger.wuError(`[ScriptExecutor] Execution failed for ${appName}:`, error);\n throw error;\n }\n\n try {\n return fn(proxy);\n } catch (error) {\n logger.wuError(`[ScriptExecutor] Execution failed for ${appName}:`, error);\n throw error;\n }\n }\n\n /**\n * Fetch script content from a URL.\n * @param {string} url - Script URL\n * @returns {Promise<string>} Script text\n */\n async fetchScript(url) {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to fetch script ${url}: HTTP ${response.status}`);\n }\n return response.text();\n }\n\n /**\n * Execute an array of scripts in sequence inside the proxy.\n * External scripts (with src) are fetched first.\n *\n * @param {Array<{content?: string, src?: string}>} scripts\n * @param {string} appName\n * @param {Proxy} proxy\n * @param {Object} [options]\n */\n async executeAll(scripts, appName, proxy, options = {}) {\n for (const script of scripts) {\n let text = script.content;\n\n if (!text && script.src) {\n logger.wuDebug(`[ScriptExecutor] Fetching external script: ${script.src}`);\n text = await this.fetchScript(script.src);\n }\n\n if (text && text.trim()) {\n this.execute(text, appName, proxy, {\n ...options,\n sourceUrl: script.src || options.sourceUrl || ''\n });\n }\n }\n\n logger.wuDebug(`[ScriptExecutor] Executed ${scripts.length} scripts for ${appName}`);\n }\n}\n"],"names":["WuScriptExecutor","static","pattern","label","_validateScript","scriptText","appName","DANGEROUS_PATTERNS","test","msg","logger","wuError","Error","execute","proxy","options","strictGlobal","sourceUrl","trim","this","sourceComment","wrappedCode","fn","Function","error","wuWarn","message","fetchScript","url","response","fetch","ok","status","text","executeAll","scripts","script","content","src","wuDebug","length"],"mappings":"wCAeO,MAAMA,EAUXC,0BAA4B,CAE1B,CAAEC,QAAS,gDAAiDC,MAAO,6CACnE,CAAED,QAAS,YAAaC,MAAO,0CAG/B,CAAED,QAAS,iDAAkDC,MAAO,iDAGpE,CAAED,QAAS,wBAAyBC,MAAO,oDAC3C,CAAED,QAAS,cAAeC,MAAO,mCAGjC,CAAED,QAAS,gBAAiBC,MAAO,6CAGnC,CAAED,QAAS,yBAA0BC,MAAO,2CAY9C,eAAAC,CAAgBC,EAAYC,GAC1B,IAAK,MAAMJ,QAAEA,EAAOC,MAAEA,KAAWH,EAAiBO,mBAChD,GAAIL,EAAQM,KAAKH,GAAa,CAC5B,MAAMI,EAAM,kDAAkDH,OAAaH,IAE3E,MADAO,EAAOC,QAAQF,GACT,IAAIG,MAAMH,EAClB,CAEJ,CAaA,OAAAI,CAAQR,EAAYC,EAASQ,EAAOC,EAAU,CAAA,GAC5C,MAAMC,aAAEA,GAAe,EAAIC,UAAEA,EAAY,IAAOF,EAEhD,IAAKV,IAAeA,EAAWa,OAAQ,OAEvCC,KAAKf,gBAAgBC,EAAYC,GAEjC,MAAMc,EAAgBH,EAAY,iCAAiCX,KAAWW,MAAgB,GAE9F,IAAII,EAoBAC,EAZFD,EANEL,EAMY,+EAEbX,IAAae,6DAKA,2DACff,IAAae,wDAKd,IAGEE,EAAK,IAAIC,SAAS,QAASF,EAC7B,CAAE,MAAOG,GAGP,GAAIR,EAEF,OADAN,EAAOe,OAAO,4CAA4CnB,+BAAqCkB,EAAME,WAC9FP,KAAKN,QAAQR,EAAYC,EAASQ,EAAO,IAAKC,EAASC,cAAc,IAG9E,MADAN,EAAOC,QAAQ,yCAAyCL,KAAYkB,GAC9DA,CACR,CAEA,IACE,OAAOF,EAAGR,EACZ,CAAE,MAAOU,GAEP,MADAd,EAAOC,QAAQ,yCAAyCL,KAAYkB,GAC9DA,CACR,CACF,CAOA,iBAAMG,CAAYC,GAChB,MAAMC,QAAiBC,MAAMF,GAC7B,IAAKC,EAASE,GACZ,MAAM,IAAInB,MAAM,0BAA0BgB,WAAaC,EAASG,UAElE,OAAOH,EAASI,MAClB,CAWA,gBAAMC,CAAWC,EAAS7B,EAASQ,EAAOC,EAAU,CAAA,GAClD,IAAK,MAAMqB,KAAUD,EAAS,CAC5B,IAAIF,EAAOG,EAAOC,SAEbJ,GAAQG,EAAOE,MAClB5B,EAAO6B,QAAQ,8CAA8CH,EAAOE,OACpEL,QAAad,KAAKQ,YAAYS,EAAOE,MAGnCL,GAAQA,EAAKf,QACfC,KAAKN,QAAQoB,EAAM3B,EAASQ,EAAO,IAC9BC,EACHE,UAAWmB,EAAOE,KAAOvB,EAAQE,WAAa,IAGpD,CAEAP,EAAO6B,QAAQ,6BAA6BJ,EAAQK,sBAAsBlC,IAC5E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{logger as t}from"./wu-logger.js";function e(t){if(null===t||"object"!=typeof t)return t;if("function"==typeof structuredClone)try{return structuredClone(t)}catch{}try{return JSON.parse(JSON.stringify(t))}catch{return t}}function s(e){let s=null,n=[];const o=()=>{for(;n.length&&1===e.readyState;)e.send(n.shift())},i=t=>{try{s&&s("string"==typeof t.data?JSON.parse(t.data):t.data)}catch{}};return e.addEventListener("open",o),e.addEventListener("message",i),{send(s){let o;try{o=JSON.stringify(s)}catch(e){return void t.wuWarn(`[WuStoreSync] message not JSON-serializable, dropped: ${e?.message||e}`)}1!==e.readyState?e.readyState>=2||(n.length>=1e3&&n.shift(),n.push(o)):e.send(o)},onMessage(t){s=t},close(){try{e.removeEventListener("open",o),e.removeEventListener("message",i)}catch{}s=null,n=[];try{e.close()}catch{}}}}function n(t){const e=t.transport;if(e&&"function"==typeof e.send&&"function"==typeof e.onMessage)return e;if(void 0===e||"broadcast"===e)return function(t){if("undefined"==typeof BroadcastChannel)throw new Error("[WuStoreSync] BroadcastChannel is not available in this environment.");const e=new BroadcastChannel(`wu-store:${t}`);let s=null;const n=t=>{s&&s(t.data)};return e.addEventListener("message",n),{send(t){e.postMessage(t)},onMessage(t){s=t},close(){try{e.removeEventListener("message",n)}catch{}s=null;try{e.close()}catch{}}}}(t.room||"default");if("undefined"!=typeof WebSocket&&e instanceof WebSocket)return s(e);if("string"==typeof e&&/^wss?:\/\//.test(e)){if("undefined"==typeof WebSocket)throw new Error("[WuStoreSync] WebSocket not available.");return s(new WebSocket(e))}throw new Error('[WuStoreSync] Unknown transport. Use "broadcast", a WebSocket/URL, or { send, onMessage, close }.')}class o{constructor(t){this.store=t,this.site=function(){if("undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=()=>Math.random().toString(36).slice(2,10);return`${Date.now().toString(36)}-${t()}${t()}`}(),this._lamport=0,this._clocks=new Map,this._transport=null,this._untap=null,this._applyingRemote=!1,this._outbox=[],this._flushScheduled=!1,this._connected=!1,this._peers=new Set,this.stats={sent:0,received:0,applied:0,ignored:0,dropped:0}}connect(s={}){return this._connected||(this._transport=n(s),this._transport.onMessage(t=>this._receive(t)),this._untap=this.store.tap((s,n,o)=>{if(!this._applyingRemote&&n){if(!function(t){const e=typeof t;return!("bigint"===e||"function"===e||"symbol"===e)}(o))return t.wuWarn(`[WuStoreSync] '${n}' not synced: value type '${typeof o}' is not serializable.`),void(this.stats.dropped+=1);this._pruneDescendants(n),this._lamport+=1,this._clocks.set(n,{l:this._lamport,site:this.site}),this._outbox.push({path:n,value:e(o),l:this._lamport,site:this.site}),this._scheduleFlush()}}),this._connected=!0,this._send({t:"hello"})),this}stop(){return this._untap&&(this._untap(),this._untap=null),this._outbox.length=0,this._flushScheduled=!1,this._connected=!1,this._transport&&(this._transport.close(),this._transport=null),this}status(){return{connected:this._connected,site:this.site,lamport:this._lamport,peers:this._peers.size,tracked:this._clocks.size,...this.stats}}_scheduleFlush(){this._flushScheduled||(this._flushScheduled=!0,queueMicrotask(()=>{this._flushScheduled=!1,this._connected&&this._outbox.length&&this._send({t:"op",ops:this._outbox.splice(0)})}))}_send(t){if(this._transport){t.from=this.site;try{this._transport.send(t),this.stats.sent+=1}catch{}}}_receive(t){if(this._connected&&t&&t.from!==this.site)if(this.stats.received+=1,t.from&&this._peers.size<1024&&this._peers.add(t.from),"op"===t.t)for(const e of t.ops||[])this._applyOp(e);else if("hello"===t.t){const e=this._snapshotOps();e.length&&this._send({t:"op",ops:e,to:t.from})}}_applyOp(t){if(!t||"string"!=typeof t.path||!t.path||"number"!=typeof t.l)return;this._lamport=Math.max(this._lamport,t.l);const e=this._clocks.get(t.path);if(!e||this._isNewer(t,e)){this._pruneDescendants(t.path),this._clocks.set(t.path,{l:t.l,site:t.site}),this._applyingRemote=!0;try{this.store.set(t.path,t.value),this.stats.applied+=1}catch{this._clocks.delete(t.path),this.stats.ignored+=1}finally{this._applyingRemote=!1}}else this.stats.ignored+=1}_isNewer(t,e){return t.l!==e.l?t.l>e.l:String(t.site)>String(e.site)}_pruneDescendants(t){const e=`${t}.`;for(const t of this._clocks.keys())t.startsWith(e)&&this._clocks.delete(t)}_snapshotOps(){const t=[];for(const[s,n]of this._clocks){const o=this.store.get(s);void 0!==o&&t.push({path:s,value:e(o),l:n.l,site:n.site})}return t.sort((t,e)=>t.path.split(".").length-e.path.split(".").length),t}}export{o as WuStoreSync};
|
|
2
|
+
//# sourceMappingURL=wu-store-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wu-store-sync.js","sources":["../../src/core/wu-store-sync.js"],"sourcesContent":["/**\n * WU-STORE-SYNC: real-time collaborative state (CRDT)\n *\n * Synchronizes the shared WuStore across replicas — tabs (BroadcastChannel),\n * workers, and clients (WebSocket) — conflict-free. Every write that flows\n * through `store.set()` is captured via `store.tap()`, stamped with a Lamport\n * clock + site id, and shipped over a transport. Remote ops merge with a\n * per-path Last-Writer-Wins register keyed on the total order (lamport, site):\n * two replicas that write the same path concurrently converge to the same\n * value, and BOTH agree on which. Because the substrate is framework-agnostic,\n * a React app and a Vue app on the same page (or two users' browsers) sync\n * through the same store with no glue — something Redux needs bespoke\n * middleware for and Module Federation has no model for at all.\n *\n * This is roadmap item #2, built on the convergence primitives proven by\n * WuTimeline (#1): the Lamport clock + site id, and the LWW total order.\n *\n * Honest scope (v1):\n * - LWW per path. Concurrent writes to the SAME path: highest (lamport, site)\n * wins (no value-level merge — opt-in merge strategies are future work).\n * - PATH GRANULARITY: each written path is an independent register. Writing a\n * parent object (set('user', {...})) prunes its tracked sub-paths, so the\n * common \"replace the whole object\" pattern is safe. But two replicas writing\n * OVERLAPPING paths concurrently (one 'user', another 'user.name') is not\n * guaranteed to converge under reordering — prefer non-overlapping paths, or\n * a consistent granularity (always the object, or always leaf keys).\n * - Only writes made AFTER sync() are tracked. State that predates sync() is\n * NOT propagated to late joiners — seed shared state by writing it after\n * sync(). Late joiners receive every tracked path via a snapshot exchange.\n * - Full-state replaces (empty-path set) are NOT synced.\n * - Values must be serializable. Top-level BigInt/Function/Symbol are skipped\n * (warned, never silently dropped). Over the WebSocket (JSON) transport, rich\n * types (Date → ISO string, Map/Set → {}, NaN/Infinity → null) degrade per\n * JSON; BroadcastChannel (structured clone) preserves them. Use JSON-safe\n * values for transport-independent fidelity.\n *\n * Lazy chunk — never in the main bundle. Loaded on first `wu.store.sync()`.\n */\n\nimport { logger } from './wu-logger.js';\n\n/* ─────────────────────────── pure helpers ─────────────────────────── */\n\nfunction safeClone(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof structuredClone === 'function') {\n try { return structuredClone(value); } catch { /* unclonable members */ }\n }\n try { return JSON.parse(JSON.stringify(value)); } catch { return value; }\n}\n\n/** Top-level types that can't ride any wire (BigInt throws JSON; Function/\n * Symbol vanish). Cheap check — nested throwers are caught by the WS transport. */\nfunction isWireSafe(value) {\n const t = typeof value;\n return !(t === 'bigint' || t === 'function' || t === 'symbol');\n}\n\nfunction genSiteId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n const seg = () => Math.random().toString(36).slice(2, 10);\n return `${Date.now().toString(36)}-${seg()}${seg()}`;\n}\n\n/* ────────────────────────── transport adapters ────────────────────── */\n\n/** Cross-tab transport (same origin). Does not echo to the sender. */\nfunction makeBroadcastTransport(room) {\n if (typeof BroadcastChannel === 'undefined') {\n throw new Error('[WuStoreSync] BroadcastChannel is not available in this environment.');\n }\n const bc = new BroadcastChannel(`wu-store:${room}`);\n let handler = null;\n const onMsg = (e) => { if (handler) handler(e.data); };\n bc.addEventListener('message', onMsg);\n return {\n send(msg) { bc.postMessage(msg); },\n onMessage(cb) { handler = cb; },\n close() {\n try { bc.removeEventListener('message', onMsg); } catch { /* noop */ }\n handler = null;\n try { bc.close(); } catch { /* already closed */ }\n },\n };\n}\n\n/** Cross-client transport over a WebSocket. Needs a relay that broadcasts to\n * the other peers (the server must echo to everyone but the sender). */\nfunction makeWebSocketTransport(ws) {\n const MAX_QUEUE = 1000; // bound the pre-open buffer for a never-opening socket\n let handler = null;\n let queue = [];\n const onOpen = () => { while (queue.length && ws.readyState === 1) ws.send(queue.shift()); };\n const onMsg = (e) => {\n try { if (handler) handler(typeof e.data === 'string' ? JSON.parse(e.data) : e.data); }\n catch { /* non-JSON frame — ignore */ }\n };\n ws.addEventListener('open', onOpen);\n ws.addEventListener('message', onMsg);\n return {\n send(msg) {\n let s;\n try { s = JSON.stringify(msg); }\n catch (e) { logger.wuWarn(`[WuStoreSync] message not JSON-serializable, dropped: ${e?.message || e}`); return; }\n if (ws.readyState === 1) { ws.send(s); return; }\n if (ws.readyState >= 2) return; // CLOSING/CLOSED — don't buffer\n if (queue.length >= MAX_QUEUE) queue.shift(); // drop oldest, stay bounded\n queue.push(s);\n },\n onMessage(cb) { handler = cb; },\n close() {\n try { ws.removeEventListener('open', onOpen); ws.removeEventListener('message', onMsg); }\n catch { /* noop */ }\n handler = null;\n queue = [];\n try { ws.close(); } catch { /* already closing */ }\n },\n };\n}\n\nfunction resolveTransport(opts) {\n const t = opts.transport;\n // Custom transport: anything exposing the duck-typed interface.\n if (t && typeof t.send === 'function' && typeof t.onMessage === 'function') return t;\n if (t === undefined || t === 'broadcast') return makeBroadcastTransport(opts.room || 'default');\n if (typeof WebSocket !== 'undefined' && t instanceof WebSocket) return makeWebSocketTransport(t);\n if (typeof t === 'string' && /^wss?:\\/\\//.test(t)) {\n if (typeof WebSocket === 'undefined') throw new Error('[WuStoreSync] WebSocket not available.');\n return makeWebSocketTransport(new WebSocket(t));\n }\n throw new Error('[WuStoreSync] Unknown transport. Use \"broadcast\", a WebSocket/URL, or { send, onMessage, close }.');\n}\n\n/* ──────────────────────────────── class ───────────────────────────── */\n\nexport class WuStoreSync {\n /**\n * @param {object} store - The WuStore instance to synchronize.\n */\n constructor(store) {\n this.store = store;\n this.site = genSiteId();\n\n this._lamport = 0;\n // LWW register version per path: path -> { l, site }. The total order\n // (l, site) decides every conflict; both replicas compute it identically.\n this._clocks = new Map();\n this._transport = null;\n this._untap = null;\n this._applyingRemote = false; // true while applying a remote op (no echo)\n this._outbox = [];\n this._flushScheduled = false;\n this._connected = false;\n this._peers = new Set();\n this.stats = { sent: 0, received: 0, applied: 0, ignored: 0, dropped: 0 };\n }\n\n /**\n * Start syncing. Taps local writes, opens the transport, and announces\n * presence so existing peers send a snapshot of their state.\n */\n connect(opts = {}) {\n if (this._connected) return this;\n this._transport = resolveTransport(opts); // may throw — caller handles\n this._transport.onMessage((msg) => this._receive(msg));\n\n this._untap = this.store.tap((seq, path, value) => {\n if (this._applyingRemote) return; // don't re-broadcast applied ops\n if (!path) return; // full-state replaces aren't synced\n if (!isWireSafe(value)) { // BigInt/Function/Symbol → skip + warn\n logger.wuWarn(`[WuStoreSync] '${path}' not synced: value type '${typeof value}' is not serializable.`);\n this.stats.dropped += 1;\n return;\n }\n this._pruneDescendants(path); // a parent write supersedes its sub-path registers\n this._lamport += 1;\n this._clocks.set(path, { l: this._lamport, site: this.site });\n this._outbox.push({ path, value: safeClone(value), l: this._lamport, site: this.site });\n this._scheduleFlush();\n });\n\n this._connected = true;\n this._send({ t: 'hello' }); // ask peers for their state\n return this;\n }\n\n /** Stop syncing: detach the store tap, drop pending ops, close the transport. */\n stop() {\n if (this._untap) { this._untap(); this._untap = null; }\n // Drop any batched-but-unsent ops so they can't leak onto a later session.\n this._outbox.length = 0;\n this._flushScheduled = false;\n this._connected = false; // _receive bails on this, so late frames can't mutate\n if (this._transport) { this._transport.close(); this._transport = null; }\n return this;\n }\n\n status() {\n return {\n connected: this._connected,\n site: this.site,\n lamport: this._lamport,\n peers: this._peers.size,\n tracked: this._clocks.size,\n ...this.stats,\n };\n }\n\n /* ─────────────────────────────── private ────────────────────────── */\n\n _scheduleFlush() {\n if (this._flushScheduled) return;\n this._flushScheduled = true;\n // Coalesce rapid writes (e.g. store.batch()) into one message.\n queueMicrotask(() => {\n this._flushScheduled = false;\n if (this._connected && this._outbox.length) this._send({ t: 'op', ops: this._outbox.splice(0) });\n });\n }\n\n _send(msg) {\n if (!this._transport) return;\n msg.from = this.site;\n try { this._transport.send(msg); this.stats.sent += 1; }\n catch { /* transport down — a reconnecting transport buffers itself */ }\n }\n\n _receive(msg) {\n if (!this._connected) return; // a late frame after stop() must not mutate\n if (!msg || msg.from === this.site) return; // ignore our own echo\n this.stats.received += 1;\n if (msg.from && this._peers.size < 1024) this._peers.add(msg.from); // bounded\n\n if (msg.t === 'op') {\n for (const op of msg.ops || []) this._applyOp(op);\n } else if (msg.t === 'hello') {\n // A peer joined — send our tracked state so it can catch up. Reuses the\n // op-apply path: LWW makes redundant snapshots idempotent.\n const ops = this._snapshotOps();\n if (ops.length) this._send({ t: 'op', ops, to: msg.from });\n }\n }\n\n _applyOp(op) {\n if (!op || typeof op.path !== 'string' || !op.path || typeof op.l !== 'number') return;\n // Advance our Lamport clock past anything we've seen (causality).\n this._lamport = Math.max(this._lamport, op.l);\n\n const cur = this._clocks.get(op.path);\n if (cur && !this._isNewer(op, cur)) { this.stats.ignored += 1; return; }\n\n this._pruneDescendants(op.path); // applying a parent write clears sub-path registers\n this._clocks.set(op.path, { l: op.l, site: op.site });\n this._applyingRemote = true;\n try {\n this.store.set(op.path, op.value);\n this.stats.applied += 1;\n } catch {\n // WuStore.set throws on prototype-pollution paths (__proto__, …) — a\n // malicious/garbled remote op is rejected here, never mutating internals.\n this._clocks.delete(op.path);\n this.stats.ignored += 1;\n } finally {\n this._applyingRemote = false;\n }\n }\n\n /** Total order on (lamport, site): higher lamport wins; ties break on site. */\n _isNewer(op, cur) {\n if (op.l !== cur.l) return op.l > cur.l;\n return String(op.site) > String(cur.site);\n }\n\n /** A write to `path` replaces its subtree, so its tracked descendant\n * registers are obsolete — drop them (prevents orphaned sub-path clocks\n * re-surfacing in snapshots as phantom keys). */\n _pruneDescendants(path) {\n const prefix = `${path}.`;\n for (const tracked of this._clocks.keys()) {\n if (tracked.startsWith(prefix)) this._clocks.delete(tracked);\n }\n }\n\n /** Every tracked path as an op, for snapshotting a late joiner. Skips paths\n * whose value is gone (never resurrect a phantom key) and orders ancestors\n * before descendants so the receiver applies them coherently. */\n _snapshotOps() {\n const ops = [];\n for (const [path, stamp] of this._clocks) {\n const value = this.store.get(path);\n if (value === undefined) continue;\n ops.push({ path, value: safeClone(value), l: stamp.l, site: stamp.site });\n }\n ops.sort((a, b) => a.path.split('.').length - b.path.split('.').length);\n return ops;\n }\n}\n"],"names":["safeClone","value","structuredClone","JSON","parse","stringify","makeWebSocketTransport","ws","handler","queue","onOpen","length","readyState","send","shift","onMsg","e","data","addEventListener","msg","s","logger","wuWarn","message","push","onMessage","cb","close","removeEventListener","resolveTransport","opts","t","transport","undefined","room","BroadcastChannel","Error","bc","postMessage","makeBroadcastTransport","WebSocket","test","WuStoreSync","constructor","store","this","site","crypto","randomUUID","seg","Math","random","toString","slice","Date","now","genSiteId","_lamport","_clocks","Map","_transport","_untap","_applyingRemote","_outbox","_flushScheduled","_connected","_peers","Set","stats","sent","received","applied","ignored","dropped","connect","_receive","tap","seq","path","isWireSafe","_pruneDescendants","set","l","_scheduleFlush","_send","stop","status","connected","lamport","peers","size","tracked","queueMicrotask","ops","splice","from","add","op","_applyOp","_snapshotOps","to","max","cur","get","_isNewer","delete","String","prefix","keys","startsWith","stamp","sort","a","b","split"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{logger as t}from"./wu-logger.js";const s="wu-timeline/1";function e(t){return"__proto__"!==t&&"constructor"!==t&&"prototype"!==t}function i(t){if(null===t||"object"!=typeof t)return t;if("function"==typeof structuredClone)try{return structuredClone(t)}catch{}try{return JSON.parse(JSON.stringify(t))}catch{return t}}function n(t,s,i){if(!s)return i;const n=String(s).split(".");if(!n.every(e))return t;let r=t;for(let t=0;t<n.length-1;t++){const s=n[t];Object.prototype.hasOwnProperty.call(r,s)&&"object"==typeof r[s]&&null!==r[s]||(r[s]={}),r=r[s]}return r[n[n.length-1]]=i,t}function r(t=[],s=[]){const e=new Set,i=[];for(const n of[...t,...s]){if(!n||"number"!=typeof n.l||!n.site)continue;const t=`${n.l}|${n.site}`;e.has(t)||(e.add(t),i.push(n))}return i.sort((t,s)=>t.l-s.l||(t.site<s.site?-1:t.site>s.site?1:0)),i}function o(t=[],s={}){let e=i(s);null!==e&&"object"==typeof e||(e={});for(const s of t)s&&"store"===s.kind&&(e=n(e,s.path,i(s.value)));return e}class a{constructor(t){this.wu=t,this.site=function(){if("undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=()=>Math.random().toString(36).slice(2,10);return`${Date.now().toString(36)}-${t()}${t()}`}(),this._entries=[],this._snapshots=[],this._baselineApps=[],this._lamport=0,this._position=0,this._recording=!1,this._applying=!1,this._storeWritesSinceSnap=0,this._untapStore=null,this._untapBus=null,this._seekChain=Promise.resolve(),this._pastWarned=!1,this._opts={snapshotEvery:64,maxEntries:5e3}}record(s={}){return this._recording||(this._opts={...this._opts,...s},this._entries=[],this._snapshots=[{at:0,state:i(this.wu.store.get())||{}}],this._baselineApps=this._captureApps(),this._position=0,this._storeWritesSinceSnap=0,this._pastWarned=!1,this._recording=!0,this._untapStore=this.wu.store.tap((t,s,e)=>{this._shouldJournal()&&this._push({kind:"store",seq:t,path:s,value:i(e)})}),this._untapBus=this.wu.eventBus.tap(t=>{if(!this._shouldJournal())return;if("string"==typeof t.name&&t.name.startsWith("timeline:"))return;const s={kind:"event",name:t.name,appName:t.appName};if("app:mounted"===t.name)s.container=this.wu.mounted.get(t.data?.appName)?.containerSelector||null;else if("app:updated"===t.name){const e=this.wu.mounted.get(t.data?.appName)||this.wu.hidden.get(t.data?.appName);s.props=i(e?.props??t.data?.props??null)}this._push(s)}),this._emit("timeline:record",{site:this.site}),t.wuInfo("[WuTimeline] Recording started")),this.status()}stop(){return this._untapStore&&(this._untapStore(),this._untapStore=null),this._untapBus&&(this._untapBus(),this._untapBus=null),this._recording&&(this._recording=!1,this._emit("timeline:stop",{length:this._entries.length}),t.wuInfo(`[WuTimeline] Recording stopped (${this._entries.length} entries)`)),this.status()}clear(){return this._entries=[],this._snapshots=[{at:0,state:i(this.wu.store.get())||{}}],this._baselineApps=this._captureApps(),this._position=0,this._storeWritesSinceSnap=0,this.status()}_shouldJournal(){return!(this._applying||!this._recording)&&(!(this._position<this._entries.length)||(this._pastWarned||(this._pastWarned=!0,t.wuWarn("[WuTimeline] Writes while positioned in the past are not journaled — they will be discarded when you return to live().")),!1))}_push(t){this._lamport+=1,this._entries.push({l:this._lamport,site:this.site,t:Date.now(),...t}),"store"===t.kind&&(this._storeWritesSinceSnap+=1,this._storeWritesSinceSnap>=this._opts.snapshotEvery&&(this._storeWritesSinceSnap=0,this._snapshots.push({at:this._entries.length,state:i(this.wu.store.get())||{}}))),this._entries.length>this._opts.maxEntries&&this._trim(),this._position=this._entries.length}_trim(){const t=this._entries.length-this._opts.maxEntries;if(t<=0)return;const s=this._stateAt(t),e=this._appsAt(t);this._entries.splice(0,t),this._baselineApps=[...e].map(([t,s])=>({name:t,container:s.container,props:s.props})),this._snapshots=this._snapshots.map(s=>({at:s.at-t,state:s.state})).filter(t=>t.at>0),this._snapshots.unshift({at:0,state:s}),this._position>0&&(this._position=Math.max(0,this._position-t))}seek(s,e={}){return this._seekChain=this._seekChain.then(()=>this._doSeek("function"==typeof s?s():s,e)).catch(s=>(t.wuError("[WuTimeline] seek failed:",s),this.status())),this._seekChain}live(){return this.seek(()=>this._entries.length)}stepBack(){return this.seek(()=>this._position-1)}stepForward(){return this.seek(()=>this._position+1)}async _doSeek(s,{store:e=!0,apps:i=!0,props:n=!0}={}){const r=this._entries.length;s=Math.max(0,Math.min(r,Math.floor(Number(s)||0))),this._applying=!0;try{if(e){const t=this._stateAt(s);this.wu.store.hydrate(t,{notifyPaths:this._touchedPaths(t)})}if(i||n){const e=this._appsAt(s);if(i){for(const s of[...this.wu.mounted.keys()])if(!e.has(s))try{await this.wu.unmount(s,{force:!0})}catch(e){t.wuWarn(`[WuTimeline] seek unmount('${s}') failed:`,e)}for(const[s,i]of e)if(!this.wu.mounted.has(s)&&i.container)try{await this.wu.mount(s,i.container)}catch(e){t.wuWarn(`[WuTimeline] seek mount('${s}') failed:`,e)}}if(n)for(const[s,i]of e){if(!this.wu.mounted.has(s))continue;const e=i.props||{},n=this.wu.mounted.get(s).props||{},r={...e};for(const t of Object.keys(n))t in e||(r[t]=void 0);if(Object.keys(r).length)try{await this.wu.update(s,r)}catch(e){t.wuWarn(`[WuTimeline] seek update('${s}') failed:`,e)}}}this._position=s,this._emit("timeline:seek",{position:s,length:r,live:s===r})}finally{this._applying=!1}return this.status()}_stateAt(t){let s=this._snapshots[0]||{at:0,state:{}};for(const e of this._snapshots)e.at<=t&&e.at>=s.at&&(s=e);let e=i(s.state);null!==e&&"object"==typeof e||(e={});for(let r=s.at;r<t;r++){const t=this._entries[r];"store"===t.kind&&(e=n(e,t.path,i(t.value)))}return e}_appsAt(t){const s=new Map;for(const t of this._baselineApps)s.set(t.name,{container:t.container,props:t.props??null});for(let e=0;e<t;e++){const t=this._entries[e];"event"===t.kind&&t.appName&&("app:mounted"===t.name?s.set(t.appName,{container:t.container||s.get(t.appName)?.container||null,props:s.get(t.appName)?.props??null}):"app:unmounted"===t.name?s.delete(t.appName):"app:updated"===t.name&&s.has(t.appName)&&(s.get(t.appName).props=t.props??null))}return s}_touchedPaths(t){const s=new Set;let e=!1;for(const t of this._entries)if("store"===t.kind)if(t.path)s.add(t.path);else if(e=!0,t.value&&"object"==typeof t.value)for(const e of Object.keys(t.value))s.add(e);if(e){const e=this._snapshots[0]?.state;if(e&&"object"==typeof e)for(const t of Object.keys(e))s.add(t);if(t&&"object"==typeof t)for(const e of Object.keys(t))s.add(e)}return[...s]}export(){return{format:s,wu:this.wu.version||null,site:this.site,exportedAt:Date.now(),baselineApps:i(this._baselineApps),entries:i(this._entries),snapshots:i(this._snapshots)}}import(t){if(!t||t.format!==s)throw new Error(`[WuTimeline] Unsupported journal format: ${t?.format??typeof t}`);this.stop(),this._entries=Array.isArray(t.entries)?t.entries.filter(Boolean):[],this._baselineApps=Array.isArray(t.baselineApps)?t.baselineApps:[];const e=(Array.isArray(t.snapshots)?t.snapshots:[]).filter(t=>t&&"number"==typeof t.at&&t.at>=0&&t.at<=this._entries.length).sort((t,s)=>t.at-s.at);return e.length&&0===e[0].at||e.unshift({at:0,state:{}}),this._snapshots=e,this._lamport=this._entries.reduce((t,s)=>Math.max(t,s.l||0),this._lamport),this._position=this._entries.length,this._storeWritesSinceSnap=0,this._emit("timeline:import",{length:this._entries.length,site:t.site}),this.status()}ingest(t=[]){return this._entries=r(this._entries,t),this._lamport=this._entries.reduce((t,s)=>Math.max(t,s.l||0),this._lamport),this._snapshots=[{at:0,state:{}}],this._position=this._entries.length,this._storeWritesSinceSnap=0,this._entries.length>this._opts.maxEntries&&this._trim(),this.status()}entries(){return this._entries.slice()}status(){return{loaded:!0,recording:this._recording,live:this._position===this._entries.length,position:this._position,length:this._entries.length,site:this.site,lamport:this._lamport,snapshots:this._snapshots.length}}_captureApps(){const t=[];for(const[s,e]of this.wu.mounted)t.push({name:s,container:e.containerSelector||null,props:i(e.props??null)});return t}_emit(t,s){try{this.wu.eventBus.emit(t,s,{appName:"wu-core",token:this.wu.eventBus.getInternalToken?.("wu-core")||void 0,history:!1})}catch{}}}export{s as TIMELINE_FORMAT,a as WuTimeline,o as materializeState,r as mergeJournals,i as safeClone,n as setPath};
|
|
2
|
+
//# sourceMappingURL=wu-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wu-timeline.js","sources":["../../src/core/wu-timeline.js"],"sourcesContent":["/**\n * WU-TIMELINE: cross-framework time travel\n *\n * Records every mutation that flows through the Wu substrate — store writes\n * (already sequence-numbered by the ring buffer), event-bus messages, and app\n * lifecycle (mount/unmount/live-props updates) — into one append-only journal.\n * Seeking re-materializes the store at any past position, re-pushes the\n * historical live-props into every mounted app, and diff-mounts/unmounts apps\n * so the WHOLE multi-framework page rewinds in lockstep. Per-framework\n * devtools structurally cannot do this: each only sees its own island.\n *\n * Honest scope (v1):\n * - What rewinds is what flows through the substrate (store / bus / lifecycle).\n * App-internal side effects (a setTimeout inside a micro-app) do not.\n * - While positioned in the past, new writes are NOT journaled — returning to\n * live() discards them (Redux-DevTools-style \"no branching\" policy).\n * - Journal values are deep-cloned (structuredClone → JSON fallback).\n * Unclonable values (functions, DOM nodes) degrade per the fallback chain.\n *\n * Sync-ready by design (#2 roadmap): every entry carries a Lamport clock and\n * a site id. `mergeJournals()` is a pure LWW merge over the (lamport, site)\n * total order — the convergence primitive a future wu.store.sync() transport\n * (BroadcastChannel / WebSocket) will ship entries through. See ROADMAP.md.\n *\n * Lazy chunk — never in the main bundle. Loaded on first wu.timeline.* call.\n */\n\nimport { logger } from './wu-logger.js';\n\nexport const TIMELINE_FORMAT = 'wu-timeline/1';\n\n/* ──────────────────────────── pure helpers ──────────────────────────── */\n\nfunction isSafeKey(key) {\n return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';\n}\n\n/**\n * Deep-clone a journal value. structuredClone first (handles Maps, Dates,\n * cycles), JSON second (drops functions), raw reference as the last honest\n * resort — better a live ref than a crash, and export() will stringify it.\n */\nexport function safeClone(value) {\n if (value === null || typeof value !== 'object') return value;\n if (typeof structuredClone === 'function') {\n try { return structuredClone(value); } catch { /* unclonable members */ }\n }\n try { return JSON.parse(JSON.stringify(value)); } catch { return value; }\n}\n\n/**\n * Apply one store write to a plain state object, mirroring WuStore's\n * updateState semantics (empty path = full-state replace, prototype-pollution\n * keys rejected). Returns the resulting state (a new root on full replace).\n */\nexport function setPath(state, path, value) {\n if (!path) return value;\n const keys = String(path).split('.');\n if (!keys.every(isSafeKey)) return state;\n let target = state;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n if (\n !Object.prototype.hasOwnProperty.call(target, k) ||\n typeof target[k] !== 'object' ||\n target[k] === null\n ) {\n target[k] = {};\n }\n target = target[k];\n }\n target[keys[keys.length - 1]] = value;\n return state;\n}\n\n/**\n * Merge two journals into one, deduped by (lamport, site) identity and\n * totally ordered by (lamport, site). Pure function — the CRDT convergence\n * core for multiplayer sync (#2): merge(a, b) ≡ merge(b, a), element-wise.\n */\nexport function mergeJournals(a = [], b = []) {\n const seen = new Set();\n const out = [];\n for (const e of [...a, ...b]) {\n if (!e || typeof e.l !== 'number' || !e.site) continue;\n const key = `${e.l}|${e.site}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(e);\n }\n out.sort((x, y) => (x.l - y.l) || (x.site < y.site ? -1 : x.site > y.site ? 1 : 0));\n return out;\n}\n\n/**\n * Materialize the store state produced by replaying a journal's store entries\n * over a base state. Pure — used by seek internally and by convergence tests.\n */\nexport function materializeState(entries = [], base = {}) {\n let state = safeClone(base);\n if (state === null || typeof state !== 'object') state = {};\n for (const e of entries) {\n if (e && e.kind === 'store') state = setPath(state, e.path, safeClone(e.value));\n }\n return state;\n}\n\nfunction genSiteId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n // ~96 bits across two segments. mergeJournals dedupes on (lamport, site), so\n // a colliding site id would silently drop a distinct entry — full-width ids\n // keep that birthday bound far out of reach (an 8-char id collided ~1/2 at\n // only ~77k replicas).\n const seg = () => Math.random().toString(36).slice(2, 10);\n return `${Date.now().toString(36)}-${seg()}${seg()}`;\n}\n\n/* ──────────────────────────────── class ─────────────────────────────── */\n\nexport class WuTimeline {\n /**\n * @param {object} wu - The WuCore instance to record/rewind.\n */\n constructor(wu) {\n this.wu = wu;\n this.site = genSiteId();\n\n this._entries = [];\n this._snapshots = []; // { at, state } — state AFTER applying entries[0..at-1]\n this._baselineApps = []; // [{ name, container, props }] mounted when record() began\n this._lamport = 0;\n this._position = 0; // entries applied; === _entries.length means \"live\"\n this._recording = false;\n this._applying = false; // true while seek() pushes state back into the page\n this._storeWritesSinceSnap = 0;\n this._untapStore = null;\n this._untapBus = null;\n this._seekChain = Promise.resolve();\n this._pastWarned = false;\n\n this._opts = { snapshotEvery: 64, maxEntries: 5000 };\n }\n\n /* ───────────────────────────── recording ──────────────────────────── */\n\n /**\n * Start recording. Resets the journal, snapshots the current store state\n * and mounted-apps set as the baseline. Idempotent while recording.\n *\n * @param {object} [opts]\n * @param {number} [opts.snapshotEvery=64] - Store writes between state snapshots\n * @param {number} [opts.maxEntries=5000] - Journal cap; oldest entries fold into the baseline\n */\n record(opts = {}) {\n if (this._recording) return this.status();\n this._opts = { ...this._opts, ...opts };\n\n this._entries = [];\n this._snapshots = [{ at: 0, state: safeClone(this.wu.store.get()) || {} }];\n this._baselineApps = this._captureApps();\n this._position = 0;\n this._storeWritesSinceSnap = 0;\n this._pastWarned = false;\n this._recording = true;\n\n this._untapStore = this.wu.store.tap((seq, path, value) => {\n if (!this._shouldJournal()) return;\n this._push({ kind: 'store', seq, path, value: safeClone(value) });\n });\n\n this._untapBus = this.wu.eventBus.tap((event) => {\n if (!this._shouldJournal()) return;\n if (typeof event.name === 'string' && event.name.startsWith('timeline:')) return;\n const entry = { kind: 'event', name: event.name, appName: event.appName };\n if (event.name === 'app:mounted') {\n // The bus payload has no container — enrich from the live mount record\n // so seek() can remount the app in the right place.\n entry.container = this.wu.mounted.get(event.data?.appName)?.containerSelector || null;\n } else if (event.name === 'app:updated') {\n // Journal the ABSOLUTE (cumulative) props, not the delta. core.update()\n // merges the delta onto the mount record BEFORE emitting (wu-core.js),\n // so mounted.props is the full prop set at this point. Seek needs the\n // absolute set to reproduce a position AND to clear keys that only\n // appeared later — a delta cannot express removal.\n const rec = this.wu.mounted.get(event.data?.appName) || this.wu.hidden.get(event.data?.appName);\n entry.props = safeClone(rec?.props ?? event.data?.props ?? null);\n }\n this._push(entry);\n });\n\n this._emit('timeline:record', { site: this.site });\n logger.wuInfo('[WuTimeline] Recording started');\n return this.status();\n }\n\n /**\n * Stop recording. The journal is retained — seek/export still work.\n * Calling record() again starts a fresh journal.\n */\n stop() {\n if (this._untapStore) { this._untapStore(); this._untapStore = null; }\n if (this._untapBus) { this._untapBus(); this._untapBus = null; }\n if (this._recording) {\n this._recording = false;\n this._emit('timeline:stop', { length: this._entries.length });\n logger.wuInfo(`[WuTimeline] Recording stopped (${this._entries.length} entries)`);\n }\n return this.status();\n }\n\n /** Reset the journal in place, re-baselining on the current page state. */\n clear() {\n this._entries = [];\n this._snapshots = [{ at: 0, state: safeClone(this.wu.store.get()) || {} }];\n this._baselineApps = this._captureApps();\n this._position = 0;\n this._storeWritesSinceSnap = 0;\n return this.status();\n }\n\n /** Journal only while recording AND live (no branching from the past). */\n _shouldJournal() {\n if (this._applying || !this._recording) return false;\n if (this._position < this._entries.length) {\n if (!this._pastWarned) {\n this._pastWarned = true;\n logger.wuWarn(\n '[WuTimeline] Writes while positioned in the past are not journaled — ' +\n 'they will be discarded when you return to live().'\n );\n }\n return false;\n }\n return true;\n }\n\n _push(partial) {\n this._lamport += 1;\n this._entries.push({ l: this._lamport, site: this.site, t: Date.now(), ...partial });\n\n if (partial.kind === 'store') {\n this._storeWritesSinceSnap += 1;\n if (this._storeWritesSinceSnap >= this._opts.snapshotEvery) {\n this._storeWritesSinceSnap = 0;\n this._snapshots.push({ at: this._entries.length, state: safeClone(this.wu.store.get()) || {} });\n }\n }\n\n if (this._entries.length > this._opts.maxEntries) this._trim();\n this._position = this._entries.length;\n }\n\n /** Fold the oldest entries into the baseline so the journal stays bounded. */\n _trim() {\n const over = this._entries.length - this._opts.maxEntries;\n if (over <= 0) return;\n // Compute the new baseline BEFORE dropping the entries it folds in.\n const baseState = this._stateAt(over);\n const baseApps = this._appsAt(over);\n this._entries.splice(0, over);\n this._baselineApps = [...baseApps].map(([name, info]) => ({\n name, container: info.container, props: info.props,\n }));\n this._snapshots = this._snapshots\n .map((s) => ({ at: s.at - over, state: s.state }))\n .filter((s) => s.at > 0);\n this._snapshots.unshift({ at: 0, state: baseState });\n if (this._position > 0) this._position = Math.max(0, this._position - over);\n }\n\n /* ─────────────────────────────── seeking ──────────────────────────── */\n\n /**\n * Rewind/forward the page to a journal position (0..length). Seeks are\n * serialized — rapid scrubber drags apply in order, none dropped.\n *\n * @param {number} pos - Number of journal entries applied\n * @param {object} [opts]\n * @param {boolean} [opts.store=true] - Re-materialize the store\n * @param {boolean} [opts.apps=true] - Diff-mount/unmount apps to match the position\n * @param {boolean} [opts.props=true] - Re-push historical live-props\n * @returns {Promise<object>} status()\n */\n seek(pos, opts = {}) {\n // `pos` may be a thunk. live()/stepBack()/stepForward() pass one so their\n // target is read at EXECUTION time — once the chain has drained — not at\n // call time. Reading eagerly let back-to-back calls (rapid scrubber drag,\n // repeated button clicks) compute targets from stale position/length: two\n // stepBack()s could both resolve to the same index, and a live() racing a\n // queued seek + a fresh write could strand the user in the past.\n this._seekChain = this._seekChain\n .then(() => this._doSeek(typeof pos === 'function' ? pos() : pos, opts))\n .catch((err) => {\n logger.wuError('[WuTimeline] seek failed:', err);\n return this.status();\n });\n return this._seekChain;\n }\n\n /** Return to the present: seek to the journal end; recording resumes. */\n live() {\n return this.seek(() => this._entries.length);\n }\n\n stepBack() { return this.seek(() => this._position - 1); }\n stepForward() { return this.seek(() => this._position + 1); }\n\n async _doSeek(pos, { store = true, apps = true, props = true } = {}) {\n const len = this._entries.length;\n pos = Math.max(0, Math.min(len, Math.floor(Number(pos) || 0)));\n\n this._applying = true;\n try {\n if (store) {\n const state = this._stateAt(pos);\n this.wu.store.hydrate(state, { notifyPaths: this._touchedPaths(state) });\n }\n\n if (apps || props) {\n const desired = this._appsAt(pos);\n\n if (apps) {\n // Unmount apps that should not exist at this position…\n for (const name of [...this.wu.mounted.keys()]) {\n if (!desired.has(name)) {\n try { await this.wu.unmount(name, { force: true }); }\n catch (err) { logger.wuWarn(`[WuTimeline] seek unmount('${name}') failed:`, err); }\n }\n }\n // …and remount the ones that should (imported journals may reference\n // apps this page never registered — warn and continue).\n for (const [name, info] of desired) {\n if (!this.wu.mounted.has(name) && info.container) {\n try { await this.wu.mount(name, info.container); }\n catch (err) { logger.wuWarn(`[WuTimeline] seek mount('${name}') failed:`, err); }\n }\n }\n }\n\n if (props) {\n for (const [name, info] of desired) {\n if (!this.wu.mounted.has(name)) continue;\n const target = info.props || {};\n // core.update() MERGES, so reproducing an absolute prop set means\n // also nulling keys the app currently carries that didn't exist at\n // this position. We only touch keys WE journaled (mounted.props),\n // never the app's initial mount props (which aren't in the journal).\n const current = this.wu.mounted.get(name).props || {};\n const patch = { ...target };\n for (const k of Object.keys(current)) {\n if (!(k in target)) patch[k] = undefined;\n }\n if (Object.keys(patch).length) {\n try { await this.wu.update(name, patch); }\n catch (err) { logger.wuWarn(`[WuTimeline] seek update('${name}') failed:`, err); }\n }\n }\n }\n }\n\n this._position = pos;\n this._emit('timeline:seek', { position: pos, length: len, live: pos === len });\n } finally {\n this._applying = false;\n }\n return this.status();\n }\n\n /** Store state at a position: nearest snapshot ≤ pos + replay to pos. */\n _stateAt(pos) {\n let snap = this._snapshots[0] || { at: 0, state: {} };\n for (const s of this._snapshots) {\n if (s.at <= pos && s.at >= snap.at) snap = s;\n }\n let state = safeClone(snap.state);\n if (state === null || typeof state !== 'object') state = {};\n for (let i = snap.at; i < pos; i++) {\n const e = this._entries[i];\n if (e.kind === 'store') state = setPath(state, e.path, safeClone(e.value));\n }\n return state;\n }\n\n /** Mounted-apps map (name → {container, props}) at a position. */\n _appsAt(pos) {\n const apps = new Map();\n for (const a of this._baselineApps) {\n apps.set(a.name, { container: a.container, props: a.props ?? null });\n }\n for (let i = 0; i < pos; i++) {\n const e = this._entries[i];\n if (e.kind !== 'event' || !e.appName) continue;\n if (e.name === 'app:mounted') {\n apps.set(e.appName, {\n container: e.container || apps.get(e.appName)?.container || null,\n props: apps.get(e.appName)?.props ?? null,\n });\n } else if (e.name === 'app:unmounted') {\n apps.delete(e.appName);\n } else if (e.name === 'app:updated' && apps.has(e.appName)) {\n apps.get(e.appName).props = e.props ?? null;\n }\n }\n return apps;\n }\n\n /**\n * Paths whose listeners must be re-notified on seek: every path the journal\n * ever wrote (a path written after `pos` needs resetting when rewinding\n * before it). Full-state replaces ('' path) expand to the state's top-level\n * keys, mirroring WuStore's exact+parents notification semantics.\n */\n _touchedPaths(state) {\n const paths = new Set();\n let sawFullReplace = false;\n for (const e of this._entries) {\n if (e.kind !== 'store') continue;\n if (e.path) { paths.add(e.path); continue; }\n // Full-state replace ('' path): every top-level key the replacement\n // introduced must stay notifiable, so a key that exists only in a LATER\n // replace state is re-notified (with its now-undefined value) when we\n // rewind before it.\n sawFullReplace = true;\n if (e.value && typeof e.value === 'object') {\n for (const k of Object.keys(e.value)) paths.add(k);\n }\n }\n if (sawFullReplace) {\n const base = this._snapshots[0]?.state;\n if (base && typeof base === 'object') for (const k of Object.keys(base)) paths.add(k);\n if (state && typeof state === 'object') for (const k of Object.keys(state)) paths.add(k);\n }\n return [...paths];\n }\n\n /* ─────────────────────────── export / import ──────────────────────── */\n\n /**\n * Serializable journal — the reproducible bug report. JSON.stringify it to\n * get one file; values that resisted cloning degrade to their JSON form.\n */\n export() {\n return {\n format: TIMELINE_FORMAT,\n wu: this.wu.version || null,\n site: this.site,\n exportedAt: Date.now(),\n baselineApps: safeClone(this._baselineApps),\n entries: safeClone(this._entries),\n snapshots: safeClone(this._snapshots),\n };\n }\n\n /**\n * Load a journal exported elsewhere (or earlier). Stops any active\n * recording; the page can then seek through the imported history.\n */\n import(data) {\n if (!data || data.format !== TIMELINE_FORMAT) {\n throw new Error(`[WuTimeline] Unsupported journal format: ${data?.format ?? typeof data}`);\n }\n this.stop();\n this._entries = Array.isArray(data.entries) ? data.entries.filter(Boolean) : [];\n this._baselineApps = Array.isArray(data.baselineApps) ? data.baselineApps : [];\n const snaps = (Array.isArray(data.snapshots) ? data.snapshots : [])\n .filter((s) => s && typeof s.at === 'number' && s.at >= 0 && s.at <= this._entries.length)\n .sort((a, b) => a.at - b.at);\n if (!snaps.length || snaps[0].at !== 0) snaps.unshift({ at: 0, state: {} });\n this._snapshots = snaps;\n this._lamport = this._entries.reduce((m, e) => Math.max(m, e.l || 0), this._lamport);\n this._position = this._entries.length;\n this._storeWritesSinceSnap = 0;\n this._emit('timeline:import', { length: this._entries.length, site: data.site });\n return this.status();\n }\n\n /**\n * Merge remote journal entries into the local one (sync seed, #2 roadmap).\n * Dedupes by (lamport, site), re-sorts the total order, advances the local\n * Lamport clock past everything seen. Mid-journal snapshots are invalidated\n * by interleaving and dropped (the baseline survives).\n */\n ingest(remoteEntries = []) {\n this._entries = mergeJournals(this._entries, remoteEntries);\n this._lamport = this._entries.reduce((m, e) => Math.max(m, e.l || 0), this._lamport);\n // Convergence needs a SHARED base. Keeping each site's local baseline\n // snapshot would make byte-identical merged journals materialize to\n // DIFFERENT store states. Anchor replay on {} and let the journal's own\n // entries carry everything — so two replicas that diverge and re-merge\n // converge to the same state. Sync journals must therefore be recorded\n // from a shared base (or lead with a full-replace entry). See ROADMAP.md (#2).\n this._snapshots = [{ at: 0, state: {} }];\n this._position = this._entries.length;\n this._storeWritesSinceSnap = 0;\n // Streaming sync calls ingest() per remote batch — enforce the journal cap\n // here too (it lived only in _push()), or _entries would grow unbounded.\n if (this._entries.length > this._opts.maxEntries) this._trim();\n return this.status();\n }\n\n /* ─────────────────────────────── info ─────────────────────────────── */\n\n /** Shallow copy of the journal (safe to iterate, not to mutate values). */\n entries() {\n return this._entries.slice();\n }\n\n status() {\n return {\n loaded: true,\n recording: this._recording,\n live: this._position === this._entries.length,\n position: this._position,\n length: this._entries.length,\n site: this.site,\n lamport: this._lamport,\n snapshots: this._snapshots.length,\n };\n }\n\n /* ─────────────────────────────── private ──────────────────────────── */\n\n _captureApps() {\n const list = [];\n for (const [name, rec] of this.wu.mounted) {\n list.push({\n name,\n container: rec.containerSelector || null,\n props: safeClone(rec.props ?? null),\n });\n }\n return list;\n }\n\n _emit(name, data) {\n try {\n this.wu.eventBus.emit(name, data, {\n appName: 'wu-core',\n token: this.wu.eventBus.getInternalToken?.('wu-core') || undefined,\n // Keep timeline:* lifecycle out of the bounded bus history / inspector\n // — a scrubber drag would otherwise evict every real app event.\n history: false,\n });\n } catch { /* bus may be torn down during destroy */ }\n }\n}\n"],"names":["TIMELINE_FORMAT","isSafeKey","key","safeClone","value","structuredClone","JSON","parse","stringify","setPath","state","path","keys","String","split","every","target","i","length","k","Object","prototype","hasOwnProperty","call","mergeJournals","a","b","seen","Set","out","e","l","site","has","add","push","sort","x","y","materializeState","entries","base","kind","WuTimeline","constructor","wu","this","crypto","randomUUID","seg","Math","random","toString","slice","Date","now","genSiteId","_entries","_snapshots","_baselineApps","_lamport","_position","_recording","_applying","_storeWritesSinceSnap","_untapStore","_untapBus","_seekChain","Promise","resolve","_pastWarned","_opts","snapshotEvery","maxEntries","record","opts","at","store","get","_captureApps","tap","seq","_shouldJournal","_push","eventBus","event","name","startsWith","entry","appName","container","mounted","data","containerSelector","rec","hidden","props","_emit","logger","wuInfo","status","stop","clear","wuWarn","partial","t","_trim","over","baseState","_stateAt","baseApps","_appsAt","splice","map","info","s","filter","unshift","max","seek","pos","then","_doSeek","catch","err","wuError","live","stepBack","stepForward","apps","len","min","floor","Number","hydrate","notifyPaths","_touchedPaths","desired","unmount","force","mount","current","patch","undefined","update","position","snap","Map","set","delete","paths","sawFullReplace","format","version","exportedAt","baselineApps","snapshots","import","Error","Array","isArray","Boolean","snaps","reduce","m","ingest","remoteEntries","loaded","recording","lamport","list","emit","token","getInternalToken","history"],"mappings":"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"}
|