/** * vapor-chamber - Vue Vapor integration * * v1.3.0 — Vue 3.6.0-beta.12 alignment: error recovery (component context, * fallthrough props, render effects restored after setup errors); * VDOM slots interop normalization; no code changes needed here. * v1.1.0 — Vue 3.6.0-beta.10 alignment: defineVaporCustomElement, defineVaporComponent, * defineVaporAsyncComponent detection; improved hydration interop. * v0.4.1 — Added: useCommandGroup (namespace isolation), useCommandError (error boundary). * v0.4.0 — Vue 3.6 Vapor alignment: onScopeDispose, Vapor detection, * defineVaporCommand, createVaporChamberApp. * v0.3.0 — Fixed: signal shim, resetCommandBus, auto-cleanup on Vue unmount. */ import { createCommandBus, disposeAll, type CommandBus, type AsyncCommandBus, type Command, type CommandResult, type CommandMap, type TargetOf, type PayloadOf, type ResultOf, type Handler, type Plugin, type RegisterOptions, type Listener } from './command-bus'; import { configureSignal, signal } from './signal'; import type { Signal } from './signal'; // --------------------------------------------------------------------------- // Signal abstraction // --------------------------------------------------------------------------- // The minimal signal API lives in `./signal` (no module-load side effects, so // transports / plugins / form can import it without dragging Vue feature // detection into ESM consumer bundles). This module adds the heavier behavior: // async dynamic import of Vue, lifecycle hook detection, Vapor APIs. // // When the async probe resolves, applyVueModule() pushes Vue's ref() into the // signal module via configureSignal() so SPA consumers eventually use the // alien-signals-backed ref for real reactivity. // Re-export the signal API so existing import paths (`from 'vapor-chamber'`) // keep working without source change. export type { Signal, CreateSignal } from './signal'; export { configureSignal }; export { signal }; let _vueOnScopeDispose: ((fn: () => void) => void) | null = null; let _vueGetCurrentScope: (() => any) | null = null; let _vueGetCurrentInstance: (() => any) | null = null; let _vueOnActivated: ((fn: () => void) => void) | null = null; let _vueOnDeactivated: ((fn: () => void) => void) | null = null; let _vueProbed = false; // Vue's DEEP ref(), kept separately from the shallowRef() wired into signal(). // Used only by the opt-in vapor-chamber/reactive companion (deepSignal / // useDeepCommandState); the core never touches it. let _vueDeepRefFn: ((v: T) => { value: T }) | null = null; // Vue 3.6+ Vapor detection let _hasVapor = false; let _createVaporAppFn: any = null; let _vaporInteropPluginRef: any = null; // Vue 3.6+ Vapor APIs (introduced across 3.6.0-alpha.3–5) let _defineVaporCustomElementFn: any = null; let _defineVaporComponentFn: any = null; let _defineVaporAsyncComponentFn: any = null; // Dev-only: the "no active Vue scope" warning fires at most once per session. // Composables are routinely used outside setup()/effectScope() in tests and // non-component code, and repeating the warning per call floods the output. let _autoCleanupWarned = false; /** Promise that resolves once Vue detection is complete. Await this in composables * that need Vue APIs to be available before first use. */ let _probePromise: Promise | null = null; function applyVueModule(vue: any): void { if (vue && (typeof vue.shallowRef === 'function' || typeof vue.ref === 'function')) { // Push Vue's shallowRef() into the signal module so signal() returns a real // alien-signals-backed reactive WITHOUT the deep-Proxy wrap that ref() // applies to object/array values via toReactive(). The library only ever // REPLACES a signal's value wholesale (state.value = handler(...), // errors.value = [...], past.value = [...]) — it never mutates nested fields // in place — so shallow tracking is semantically equivalent here while // avoiding the per-write proxy cost. Measured on the real dispatch path: // array-state useCommandState ~3.4x faster, scalar signals ~1.2x. Direct // nested mutation of a returned state (state.value.x = y) would bypass the // command bus anyway, which this library treats as an anti-pattern. // Falls back to ref() if shallowRef is somehow unavailable (Vue < 3.0). configureSignal(vue.shallowRef ?? vue.ref); } // Keep a handle to the DEEP ref() for the opt-in reactive companion. if (vue && typeof vue.ref === 'function') { _vueDeepRefFn = vue.ref; } if (vue && typeof vue.onScopeDispose === 'function') { _vueOnScopeDispose = vue.onScopeDispose; } // getCurrentScope() (Vue 3.2+) — returns the active effect scope or undefined. // Used as the guard before calling onScopeDispose, replacing the try/catch pattern. if (vue && typeof vue.getCurrentScope === 'function') { _vueGetCurrentScope = vue.getCurrentScope; } if (vue && typeof vue.getCurrentInstance === 'function') { _vueGetCurrentInstance = vue.getCurrentInstance; } // KeepAlive lifecycle hooks (Vue 3.x) if (vue && typeof vue.onActivated === 'function') { _vueOnActivated = vue.onActivated; } if (vue && typeof vue.onDeactivated === 'function') { _vueOnDeactivated = vue.onDeactivated; } // Vue 3.6+ Vapor detection if (vue && typeof vue.createVaporApp === 'function') { _hasVapor = true; _createVaporAppFn = vue.createVaporApp; } if (vue && typeof vue.vaporInteropPlugin !== 'undefined') { _vaporInteropPluginRef = vue.vaporInteropPlugin; } // Vue 3.6+: Vapor custom elements and component definitions if (vue && typeof vue.defineVaporCustomElement === 'function') { _defineVaporCustomElementFn = vue.defineVaporCustomElement; } if (vue && typeof vue.defineVaporComponent === 'function') { _defineVaporComponentFn = vue.defineVaporComponent; } if (vue && typeof vue.defineVaporAsyncComponent === 'function') { _defineVaporAsyncComponentFn = vue.defineVaporAsyncComponent; } } function probeVue(): void { if (_vueProbed) return; _vueProbed = true; // 1. Synchronous probe: check globalThis.__VUE__ (set by Vue devtools or bundler) // This gives immediate availability for signal() calls at module init time. if (typeof globalThis !== 'undefined' && (globalThis as any).__VUE__) { try { // If Vue is already loaded as a global (common in MPA / server-rendered // page setups where Vue ships via a `