{"version":3,"sources":["jsdelivr-header.js","/npm/@channel-state/core@0.0.5/src/ChannelState.ts"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,gEC+EO,MAwBL,WAAA,CAAY,GAvBZ,KAAQ,IAA0B,KAClC,KAAQ,aAAA,IAAmB,IAC3B,KAAQ,mBAAA,IAAyB,IAMjC,KAAiB,OAAS,QAG1B,KAAQ,4BACN,KAKF,KAAA,OAAsB,eA2GtB,KAAQ,sBACN,IAEA,GAAoB,cAAhB,KAAK,OACP,OAGF,MAAM,EAAU,EAAa,KAE7B,GAAI,EAAQ,WAAa,KAAK,YAI9B,OAAQ,EAAQ,MACd,IAAK,qBACH,KAAK,SAAS,YAAY,CACxB,KAAM,sBACN,QAAS,KAAK,OACd,SAAU,KAAK,cAEjB,MAEF,IAAK,sBAEiB,iBAAhB,KAAK,SACH,KAAK,8BACP,aAAa,KAAK,6BAClB,KAAK,4BAA8B,MAErC,KAAK,OAAS,EAAQ,QACtB,KAAK,OAAS,QACd,KAAK,qBACL,KAAK,4BAEP,MAEF,IAAK,eAEiB,UAAhB,KAAK,SACP,KAAK,OAAS,EAAQ,QACtB,KAAK,sBAEP,EA9IJ,KAAK,MAAQ,EAAQ,KACrB,KAAK,SAAW,EAAQ,UAAW,EACnC,KAAK,SAAW,EAAQ,QACxB,KAAK,cAAgB,kBAAkB,KAAK,QAC5C,KAAK,YAAc,OAAO,aAE1B,KAAK,OAAS,gBAAgB,KAAK,UAEnC,KAAK,SAAW,IAAI,iBAAiB,KAAK,eAC1C,KAAK,SAAS,iBAAiB,UAAW,KAAK,uBAE3C,KAAK,SACP,KAAK,UAEL,KAAK,mCACP,CAGM,OAAA,GACN,GAAoB,cAAhB,KAAK,OACP,OAEF,MAAM,EAAU,UAAU,KAAK,KAAK,cAAe,GAEnD,EAAQ,gBAAkB,KACxB,MAAM,EAAK,EAAQ,OACd,EAAG,iBAAiB,SAAS,KAAK,gBACrC,EAAG,kBAAkB,KAAK,cAAa,EAI3C,EAAQ,UAAY,KAClB,KAAK,IAAM,EAAQ,OACnB,KAAK,kBAAiB,EAGxB,EAAQ,QAAU,KAChB,QAAQ,MAAM,yBAA0B,EAAQ,OAChD,KAAK,OAAS,QACd,KAAK,0BAAyB,CAChC,CAGM,gBAAA,GACN,GAAoB,cAAhB,KAAK,OACP,OAEF,IAAK,KAAK,IAAK,OAEf,MAGM,EAHK,KAAK,IAAI,YAAY,KAAK,cAAe,YACnC,YAAY,KAAK,eAEhB,IAAI,KAAK,QAC3B,EAAI,UAAY,KACd,GAAoB,UAAhB,KAAK,OACP,OAEF,MAAM,EAAM,EAAI,YACJ,IAAR,IACF,KAAK,OAAS,GAEhB,KAAK,OAAS,QACd,KAAK,qBACL,KAAK,0BAAyB,EAEhC,EAAI,QAAU,KAEZ,KAAK,mCAAkC,CACzC,CAGM,iCAAA,GACc,cAAhB,KAAK,SAGT,KAAK,SAAS,YAAY,CACxB,KAAM,qBACN,SAAU,KAAK,cAGjB,KAAK,4BAA8B,YAAW,KACxB,UAAhB,KAAK,SACP,KAAK,OAAS,QACd,KAAK,qBACL,KAAK,4BAEP,KAAK,4BAA8B,OAClC,KAAG,CA+DA,kBAAA,GACN,KAAK,aAAa,SAAS,IACzB,EAAW,KAAK,OAAM,GACvB,CAGK,wBAAA,GACN,KAAK,mBAAmB,SAAS,IAC/B,EAAW,KAAK,OAAM,GACvB,CAQK,cAAA,GACc,cAAhB,KAAK,SAGT,KAAK,SAAS,YAAY,CACxB,KAAM,eACN,QAAS,KAAK,OACd,SAAU,KAAK,cAEjB,KAAK,qBAAmB,CAO1B,GAAA,GACE,OAAO,KAAK,MAAA,CAed,GAAA,CAAI,GACkB,cAAhB,KAAK,SAIW,iBAAhB,KAAK,SACP,KAAK,OAAS,QACd,KAAK,4BAGP,KAAK,OAAS,EAET,KAAK,UAAyB,OAAb,KAAK,IAKtB,IAAI,SAAc,CAAC,EAAS,KAC/B,MAAM,EAAK,KAAK,IAEhB,IAAK,EAEH,YADA,EAAO,IAAI,MAAM,6BAInB,MAEM,EAFK,EAAG,YAAY,KAAK,cAAe,aAC7B,YAAY,KAAK,eAChB,IAAI,EAAO,KAAK,QAElC,EAAI,UAAY,KACd,KAAK,iBACL,GAAQ,EAEV,EAAI,QAAU,KACZ,EAAO,IAAI,MAAM,EAAI,OAAO,SAAW,iBAAgB,CACzD,IAtBA,KAAK,iBAuBN,CAQH,SAAA,CAAU,GACR,GAAoB,cAAhB,KAAK,OACP,MAAM,IAAI,MAAM,6BAIlB,OAFA,KAAK,aAAa,IAAI,GAEf,KACL,KAAK,aAAa,OAAO,EAAQ,CACnC,CAQF,eAAA,CAAgB,GACd,GAAoB,cAAhB,KAAK,OACP,MAAM,IAAI,MAAM,6BAIlB,OAFA,KAAK,mBAAmB,IAAI,GAErB,KACL,KAAK,mBAAmB,OAAO,EAAQ,CACzC,CAOF,OAAA,GACsB,cAAhB,KAAK,SAGT,KAAK,OAAS,YACd,KAAK,2BACL,KAAK,SAAS,QACd,KAAK,aAAa,QAClB,KAAK,mBAAmB,QACxB,KAAK,KAAK,QACN,KAAK,8BACP,aAAa,KAAK,6BAClB,KAAK,4BAA8B,MACrC,CAOF,KAAA,GACE,MAAoB,cAAhB,KAAK,OACA,QAAQ,WAEjB,KAAK,OAAS,gBAAgB,KAAK,UAE9B,KAAK,IAKH,IAAI,SAAQ,CAAC,EAAS,KAC3B,MAAM,EAAK,KAAK,IAEhB,IAAK,EAEH,YADA,EAAO,IAAI,MAAM,+BAInB,MAEM,EAFK,EAAG,YAAY,KAAK,cAAe,aAC7B,YAAY,KAAK,eAChB,IAAI,KAAK,SAAU,KAAK,QAE1C,EAAI,UAAY,KACd,KAAK,iBACL,GAAQ,EAEV,EAAI,QAAU,KACZ,EAAO,IAAI,MAAM,EAAI,OAAO,SAAW,iBAAgB,CACzD,KAtBA,KAAK,iBACE,QAAQ,WAsBhB,G","file":"/npm/@channel-state/core@0.0.5/dist/index.global.js","sourceRoot":"","sourcesContent":["/**\n * Minified by jsDelivr using Terser v5.39.0.\n * Original file: /npm/@channel-state/core@0.0.5/dist/index.global.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n","/*\n * Copyright 2025 ronny1020\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Represents the types of messages that can be sent between stores.\n * - 'REQUEST_INIT_STATE': A request from a new store instance asking for the current state.\n * - 'RESPONSE_INIT_STATE': A response from an existing store, providing its state to the new instance.\n * - 'STATE_UPDATE': A regular state update broadcast to all other stores.\n */\nexport type StoreBroadcastMessageType =\n  | 'REQUEST_INIT_STATE'\n  | 'RESPONSE_INIT_STATE'\n  | 'STATE_UPDATE'\n\n/**\n * Represents a message sent between stores.\n * @template T The type of the payload.\n * @remarks This interface is used for both sending and receiving messages between stores.\n **/\nexport interface StoreBroadcastMessage<T> {\n  type: StoreBroadcastMessageType\n  senderId: string\n  payload: T\n}\n\n/**\n * Represents the status of the ChannelStore.\n * - 'initializing': The store is initializing.\n * - 'ready': The store is ready to be used.\n * - 'destroyed': The store has been destroyed.\n */\nexport type StoreStatus = 'initializing' | 'ready' | 'destroyed'\n\n/**\n * Callback function type for store status changes.\n */\nexport type StoreStatusCallback = (status: StoreStatus) => void\n\n/**\n * Callback function type for store changes.\n */\ntype StoreChangeCallback<T> = (value: T) => void\n\n/**\n * Options for configuring a ChannelStore instance.\n * @template T The type of the state managed by the store.\n */\nexport interface ChannelStoreOptions<T> {\n  /**\n   * The name of the channel. This is used for both BroadcastChannel and IndexedDB.\n   * @remarks Required.\n   */\n  name: string\n  /**\n   * Whether the store should persist its state to IndexedDB.\n   * @remarks Defaults to `false`.\n   */\n  persist?: boolean\n  /**\n   * The initial state of the store.\n   * @remarks Required.\n   */\n  initial: T\n}\n\n/**\n * A class that manages and shares state across different browser tabs or windows\n * using BroadcastChannel and IndexedDB for persistence.\n * @template T The type of the state managed by the store.\n *\n * @property {StoreStatus} status The current status of the store, indicating its readiness and lifecycle phase.\n */\nexport class ChannelStore<T> {\n  private _db: IDBDatabase | null = null\n  private _subscribers = new Set<StoreChangeCallback<T>>()\n  private _statusSubscribers = new Set<StoreStatusCallback>()\n  private _value: T\n  private readonly _name: string\n  private readonly _persist: boolean\n  private readonly _initial: T\n  private readonly _channel: BroadcastChannel\n  private readonly _dbKey = 'state' // Fixed key for storing the single state object\n  private readonly _prefixedName: string\n  private _instanceId: string\n  private _initialStateRequestTimeout: ReturnType<typeof setTimeout> | null =\n    null\n\n  /**\n   * The current status of the store.\n   */\n  status: StoreStatus = 'initializing'\n\n  /**\n   * Creates an instance of ChannelStore.\n   * @param options The options for configuring the store.\n   */\n  constructor(options: ChannelStoreOptions<T>) {\n    this._name = options.name\n    this._persist = options.persist ?? false\n    this._initial = options.initial\n    this._prefixedName = `channel-state__${this._name}`\n    this._instanceId = crypto.randomUUID()\n\n    this._value = structuredClone(this._initial)\n\n    this._channel = new BroadcastChannel(this._prefixedName)\n    this._channel.addEventListener('message', this._handleChannelMessage)\n\n    if (this._persist) {\n      this._initDB()\n    } else {\n      this._requestInitialStateFromOtherTabs()\n    }\n  }\n\n  private _initDB() {\n    if (this.status === 'destroyed') {\n      return\n    }\n    const request = indexedDB.open(this._prefixedName, 1)\n\n    request.onupgradeneeded = () => {\n      const db = request.result\n      if (!db.objectStoreNames.contains(this._prefixedName)) {\n        db.createObjectStore(this._prefixedName)\n      }\n    }\n\n    request.onsuccess = () => {\n      this._db = request.result\n      this._loadCacheFromDB()\n    }\n\n    request.onerror = () => {\n      console.error('IndexedDB init failed:', request.error)\n      this.status = 'ready' // Fallback to initial values cache\n      this._notifyStatusSubscribers()\n    }\n  }\n\n  private _loadCacheFromDB() {\n    if (this.status === 'destroyed') {\n      return\n    }\n    if (!this._db) return\n\n    const tx = this._db.transaction(this._prefixedName, 'readonly')\n    const store = tx.objectStore(this._prefixedName)\n\n    const req = store.get(this._dbKey) as IDBRequest<T>\n    req.onsuccess = () => {\n      if (this.status === 'ready') {\n        return\n      }\n      const val = req.result\n      if (val !== undefined) {\n        this._value = val\n      }\n      this.status = 'ready'\n      this._notifySubscribers()\n      this._notifyStatusSubscribers()\n    }\n    req.onerror = () => {\n      // If IndexedDB read fails, request from other tabs\n      this._requestInitialStateFromOtherTabs()\n    }\n  }\n\n  private _requestInitialStateFromOtherTabs() {\n    if (this.status === 'destroyed') {\n      return\n    }\n    this._channel.postMessage({\n      type: 'REQUEST_INIT_STATE',\n      senderId: this._instanceId,\n    })\n\n    this._initialStateRequestTimeout = setTimeout(() => {\n      if (this.status !== 'ready') {\n        this.status = 'ready'\n        this._notifySubscribers()\n        this._notifyStatusSubscribers()\n      }\n      this._initialStateRequestTimeout = null\n    }, 500) // Wait for 500ms for a response\n  }\n\n  /**\n   * Processes messages received from the BroadcastChannel.\n   *\n   * This method handles three types of messages:\n   * - 'REQUEST_INIT_STATE': Responds to other tabs by sending the current state.\n   * - 'RESPONSE_INIT_STATE': Sets the store's initial state if the store is still 'initializing'.\n   * - 'STATE_UPDATE': Updates the store's state if the store is 'ready'.\n   * @param messageEvent The MessageEvent containing the broadcasted data.\n   * @private\n   */\n  private _handleChannelMessage = (\n    messageEvent: MessageEvent<StoreBroadcastMessage<T>>,\n  ) => {\n    if (this.status === 'destroyed') {\n      return\n    }\n\n    const message = messageEvent.data\n\n    if (message.senderId === this._instanceId) {\n      return // Ignore messages from self\n    }\n\n    switch (message.type) {\n      case 'REQUEST_INIT_STATE':\n        this._channel.postMessage({\n          type: 'RESPONSE_INIT_STATE',\n          payload: this._value,\n          senderId: this._instanceId,\n        })\n        break\n\n      case 'RESPONSE_INIT_STATE':\n        // Only accept this if we are still initializing\n        if (this.status === 'initializing') {\n          if (this._initialStateRequestTimeout) {\n            clearTimeout(this._initialStateRequestTimeout)\n            this._initialStateRequestTimeout = null\n          }\n          this._value = message.payload\n          this.status = 'ready'\n          this._notifySubscribers()\n          this._notifyStatusSubscribers()\n        }\n        break\n\n      case 'STATE_UPDATE':\n        // Only accept this if we are already ready\n        if (this.status === 'ready') {\n          this._value = message.payload\n          this._notifySubscribers()\n        }\n        break\n    }\n  }\n\n  /**\n   * Notifies all registered subscribers about a change in the store's state.\n   * @private\n   */\n  private _notifySubscribers() {\n    this._subscribers.forEach((subscriber) => {\n      subscriber(this._value)\n    })\n  }\n\n  private _notifyStatusSubscribers() {\n    this._statusSubscribers.forEach((subscriber) => {\n      subscriber(this.status)\n    })\n  }\n\n  /**\n   * Triggers a change notification by posting the current cache to the BroadcastChannel\n   * and notifying local subscribers.\n   * @private\n   */\n  private _triggerChange() {\n    if (this.status === 'destroyed') {\n      return\n    }\n    this._channel.postMessage({\n      type: 'STATE_UPDATE',\n      payload: this._value,\n      senderId: this._instanceId,\n    })\n    this._notifySubscribers()\n  }\n\n  /**\n   * Synchronously retrieves the current state from the cache.\n   * @returns The current state of the store.\n   */\n  get(): T {\n    return this._value\n  }\n\n  /**\n   * Sets a new value for the store's state.\n   *\n   * This method updates the value, broadcasts the change to other tabs, and\n   * persists the new value to IndexedDB if persistence is enabled.\n   *\n   * If `set()` is called while the store is still `initializing`, it will\n   * immediately transition the store to the `ready` state with the new value,\n   * cancelling any pending initial state synchronization.\n   *\n   * @param value The new state value to set.\n   */\n  set(value: T): void {\n    if (this.status === 'destroyed') {\n      return\n    }\n\n    if (this.status === 'initializing') {\n      this.status = 'ready'\n      this._notifyStatusSubscribers()\n    }\n\n    this._value = value\n\n    if (!this._persist || this._db === null) {\n      this._triggerChange()\n      return\n    }\n\n    void new Promise<void>((resolve, reject) => {\n      const db = this._db\n\n      if (!db) {\n        reject(new Error('Database not initialized'))\n        return\n      }\n\n      const tx = db.transaction(this._prefixedName, 'readwrite')\n      const store = tx.objectStore(this._prefixedName)\n      const req = store.put(value, this._dbKey)\n\n      req.onsuccess = () => {\n        this._triggerChange()\n        resolve()\n      }\n      req.onerror = () => {\n        reject(new Error(req.error?.message ?? 'unknown error'))\n      }\n    })\n  }\n\n  /**\n   * Subscribes a callback function to state changes.\n   * @param callback The function to call when the state changes.\n   * @returns A function that can be called to unsubscribe the callback.\n   */\n  subscribe(callback: StoreChangeCallback<T>): () => void {\n    if (this.status === 'destroyed') {\n      throw new Error('ChannelStore is destroyed')\n    }\n    this._subscribers.add(callback)\n\n    return () => {\n      this._subscribers.delete(callback)\n    }\n  }\n\n  /**\n   * Subscribes a callback function to status changes.\n   * @param callback The function to call when the status changes.\n   * @returns A function that can be called to unsubscribe the callback.\n   */\n  subscribeStatus(callback: StoreStatusCallback): () => void {\n    if (this.status === 'destroyed') {\n      throw new Error('ChannelStore is destroyed')\n    }\n    this._statusSubscribers.add(callback)\n\n    return () => {\n      this._statusSubscribers.delete(callback)\n    }\n  }\n\n  /**\n   * Cleans up resources used by the ChannelStore, including closing the BroadcastChannel\n   * and IndexedDB connection, and clearing subscribers.\n   */\n  destroy() {\n    if (this.status === 'destroyed') {\n      return\n    }\n    this.status = 'destroyed'\n    this._notifyStatusSubscribers()\n    this._channel.close()\n    this._subscribers.clear()\n    this._statusSubscribers.clear()\n    this._db?.close()\n    if (this._initialStateRequestTimeout) {\n      clearTimeout(this._initialStateRequestTimeout)\n      this._initialStateRequestTimeout = null\n    }\n  }\n\n  /**\n   * Resets the store's state to its initial value.\n   * @returns A Promise that resolves when the state has been reset.\n   */\n  reset(): Promise<void> {\n    if (this.status === 'destroyed') {\n      return Promise.resolve()\n    }\n    this._value = structuredClone(this._initial)\n\n    if (!this._db) {\n      this._triggerChange()\n      return Promise.resolve()\n    }\n\n    return new Promise((resolve, reject) => {\n      const db = this._db\n\n      if (!db) {\n        reject(new Error('IndexedDB is not available'))\n        return\n      }\n\n      const tx = db.transaction(this._prefixedName, 'readwrite')\n      const store = tx.objectStore(this._prefixedName)\n      const req = store.put(this._initial, this._dbKey)\n\n      req.onsuccess = () => {\n        this._triggerChange()\n        resolve()\n      }\n      req.onerror = () => {\n        reject(new Error(req.error?.message ?? 'unknown error'))\n      }\n    })\n  }\n}\n"]}