diff --git a/packages/hoppscotch-common/src/kernel/store.ts b/packages/hoppscotch-common/src/kernel/store.ts index fa0756bc..0cdbdbe8 100644 --- a/packages/hoppscotch-common/src/kernel/store.ts +++ b/packages/hoppscotch-common/src/kernel/store.ts @@ -7,40 +7,62 @@ import type { import * as E from "fp-ts/Either" import { getModule } from "." +const STORE_PATH = `${window.location.host}.hoppscotch.store` + export const Store = (() => { const module = () => getModule("store") return { capabilities: () => module().capabilities, - init: () => module().init(), - set: ( + + init: async () => { + return module().init(STORE_PATH) + }, + + set: async ( namespace: string, key: string, value: unknown, options?: StorageOptions - ): Promise> => - module().set(namespace, key, value, options), - get: ( + ): Promise> => { + return module().set(STORE_PATH, namespace, key, value, options) + }, + + get: async ( namespace: string, key: string - ): Promise> => - module().get(namespace, key), - remove: ( + ): Promise> => { + return module().get(STORE_PATH, namespace, key) + }, + + remove: async ( namespace: string, key: string - ): Promise> => - module().remove(namespace, key), - clear: (namespace?: string): Promise> => - module().clear(namespace), - has: ( + ): Promise> => { + return module().remove(STORE_PATH, namespace, key) + }, + + clear: async (namespace?: string): Promise> => { + return module().clear(STORE_PATH, namespace) + }, + + has: async ( namespace: string, key: string - ): Promise> => module().has(namespace, key), - listNamespaces: (): Promise> => - module().listNamespaces(), - listKeys: (namespace: string): Promise> => - module().listKeys(namespace), - watch: (namespace: string, key: string): StoreEventEmitter => - module().watch(namespace, key), + ): Promise> => { + return module().has(STORE_PATH, namespace, key) + }, + + listNamespaces: async (): Promise> => { + return module().listNamespaces(STORE_PATH) + }, + + listKeys: async (namespace: string): Promise> => { + return module().listKeys(STORE_PATH, namespace) + }, + + watch: async (namespace: string, key: string): Promise> => { + return module().watch(STORE_PATH, namespace, key) + }, } as const })() diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts index 272635be..5a3771f3 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts @@ -147,7 +147,7 @@ export class AgentKernelInterceptorService ...effectiveRequest.headers, "User-Agent": existingUserAgentHeader ? effectiveRequest.headers[existingUserAgentHeader] - : "HoppscotchKernel/0.1.0", + : "HoppscotchKernel/0.2.0", }, } diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/store.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/store.ts index d786d9ff..416e7481 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/store.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/store.ts @@ -90,8 +90,9 @@ export class KernelInterceptorAgentStore extends Service { } } - private setupWatchers() { - Store.watch(STORE_NAMESPACE, STORE_KEYS.SETTINGS).on( + private async setupWatchers() { + const watcher = await Store.watch(STORE_NAMESPACE, STORE_KEYS.SETTINGS) + watcher.on( "change", async ({ value }) => { if (value) { diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts index 9e8fe28c..ad386d73 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts @@ -88,7 +88,8 @@ export class KernelInterceptorExtensionStore extends Service { this.setupExtensionStatusListener() } - Store.watch(STORE_NAMESPACE, SETTINGS_KEY).on( + const watcher = await Store.watch(STORE_NAMESPACE, SETTINGS_KEY) + watcher.on( "change", async ({ value }) => { if (value) { diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts index c2129b47..428ce9b9 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts @@ -207,7 +207,7 @@ export class NativeKernelInterceptorService ...effectiveRequest.headers, "User-Agent": existingUserAgentHeader ? effectiveRequest.headers[existingUserAgentHeader] - : "HoppscotchKernel/0.1.0", + : "HoppscotchKernel/0.2.0", }, } diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/store.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/store.ts index b9198b94..64995890 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/store.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/store.ts @@ -72,8 +72,9 @@ export class KernelInterceptorNativeStore extends Service { } } - private setupWatchers() { - Store.watch(STORE_NAMESPACE, STORE_KEYS.SETTINGS).on( + private async setupWatchers() { + const watcher = await Store.watch(STORE_NAMESPACE, STORE_KEYS.SETTINGS) + watcher.on( "change", async ({ value }) => { if (value) { diff --git a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts index 9101701b..271d448b 100644 --- a/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts +++ b/packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts @@ -38,7 +38,8 @@ export class KernelInterceptorProxyStore extends Service { await this.loadSettings() - Store.watch(STORE_NAMESPACE, SETTINGS_KEY).on( + const watcher = await Store.watch(STORE_NAMESPACE, SETTINGS_KEY) + watcher.on( "change", async ({ value }) => { if (value) { diff --git a/packages/hoppscotch-kernel/README.md b/packages/hoppscotch-kernel/README.md index bcdccd12..eb83c9ab 100644 --- a/packages/hoppscotch-kernel/README.md +++ b/packages/hoppscotch-kernel/README.md @@ -42,7 +42,7 @@ Cross-platform persistence with encryption support: interface StoreV1 { readonly capabilities: Set set(namespace: string, key: string, value: unknown, options?: StorageOptions): Promise> - watch(namespace: string, key: string): StoreEventEmitter + watch(namespace: string, key: string): Promise> } ``` @@ -86,7 +86,8 @@ await kernel.store.set("collections", "team-a", data, { }) // Watch for changes -kernel.store.watch("collections", "team-a").on("change", +const watcher = await kernel.store.watch("collections", "team-a") +watcher.on("change", (update) => console.log("Collection updated:", update) ) ``` diff --git a/packages/hoppscotch-kernel/package.json b/packages/hoppscotch-kernel/package.json index a15cba5d..c011b365 100644 --- a/packages/hoppscotch-kernel/package.json +++ b/packages/hoppscotch-kernel/package.json @@ -1,6 +1,6 @@ { "name": "@hoppscotch/kernel", - "version": "0.1.0", + "version": "0.2.0", "description": "Cross-platform runtime kernel for Hoppscotch platform-ops", "type": "module", "main": "dist/hoppscotch-kernel.cjs", diff --git a/packages/hoppscotch-kernel/src/store/impl/desktop/v/1.ts b/packages/hoppscotch-kernel/src/store/impl/desktop/v/1.ts index 2340abed..13059d51 100644 --- a/packages/hoppscotch-kernel/src/store/impl/desktop/v/1.ts +++ b/packages/hoppscotch-kernel/src/store/impl/desktop/v/1.ts @@ -13,28 +13,47 @@ import { StoreEventEmitter, } from '@store/v/1'; -const STORE_PATH = `${window.location.host}.hoppscotch.store` - type NamespacedData = Record>; class TauriStoreManager { - private static instance: TauriStoreManager; + private static instances: Map = new Map(); private store: Store | null = null; private listeners = new Map void>>(); private data: NamespacedData = {}; + private storePath: string; - private constructor() {} + private constructor(storePath: string) { + this.storePath = storePath; + } - static new(): TauriStoreManager { - if (!TauriStoreManager.instance) { - TauriStoreManager.instance = new TauriStoreManager(); + static new(storePath: string): TauriStoreManager { + if (TauriStoreManager.instances.has(storePath)) { + return TauriStoreManager.instances.get(storePath)!; + } + + const instance = new TauriStoreManager(storePath); + TauriStoreManager.instances.set(storePath, instance); + return instance; + } + + static async closeAll(): Promise { + const closePromises = Array.from(TauriStoreManager.instances.values()) + .map(instance => instance.close()); + await Promise.all(closePromises); + TauriStoreManager.instances.clear(); + } + + static async closeStore(storePath: string): Promise { + const instance = TauriStoreManager.instances.get(storePath); + if (instance) { + await instance.close(); + TauriStoreManager.instances.delete(storePath); } - return TauriStoreManager.instance; } async init(): Promise { if (!this.store) { - this.store = await Store.load(STORE_PATH); + this.store = await Store.load(this.storePath); const loadedData = await this.store.get('data'); this.data = loadedData ?? {}; @@ -123,7 +142,7 @@ class TauriStoreManager { return Object.keys(this.data[namespace] || {}); } - watch(namespace: string, key: string): StoreEventEmitter { + async watch(namespace: string, key: string): Promise> { const watchKey = `${namespace}:${key}`; return { on: ( @@ -172,6 +191,7 @@ class TauriStoreManager { this.store = null; this.data = {}; this.listeners.clear(); + TauriStoreManager.instances.delete(this.storePath); } } } @@ -182,9 +202,9 @@ export const implementation: VersionedAPI = { id: 'tauri-store', capabilities: new Set(['permanent', 'structured', 'watch', 'namespace', 'secure']), - async init() { + async init(storePath: string) { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); await manager.init(); return E.right(undefined); } catch (error) { @@ -196,9 +216,9 @@ export const implementation: VersionedAPI = { } }, - async set(namespace: string, key: string, value: unknown, options?: StorageOptions): Promise> { + async set(storePath: string, namespace: string, key: string, value: unknown, options?: StorageOptions): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); const existingData = await manager.getRaw(namespace, key); const createdAt = existingData?.metadata.createdAt || new Date().toISOString() const updatedAt = new Date().toISOString() @@ -227,9 +247,9 @@ export const implementation: VersionedAPI = { } }, - async get(namespace: string, key: string): Promise> { + async get(storePath: string, namespace: string, key: string): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); return E.right(await manager.get(namespace, key)); } catch (error) { return E.left({ @@ -240,9 +260,9 @@ export const implementation: VersionedAPI = { } }, - async has(namespace: string, key: string): Promise> { + async has(storePath: string, namespace: string, key: string): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); return E.right(await manager.has(namespace, key)); } catch (error) { return E.left({ @@ -253,9 +273,9 @@ export const implementation: VersionedAPI = { } }, - async remove(namespace: string, key: string): Promise> { + async remove(storePath: string, namespace: string, key: string): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); return E.right(await manager.delete(namespace, key)); } catch (error) { return E.left({ @@ -266,9 +286,9 @@ export const implementation: VersionedAPI = { } }, - async clear(namespace?: string): Promise> { + async clear(storePath: string, namespace?: string): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); await manager.clear(namespace); return E.right(undefined); } catch (error) { @@ -280,9 +300,9 @@ export const implementation: VersionedAPI = { } }, - async listNamespaces(): Promise> { + async listNamespaces(storePath: string): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); return E.right(await manager.listNamespaces()); } catch (error) { return E.left({ @@ -293,9 +313,9 @@ export const implementation: VersionedAPI = { } }, - async listKeys(namespace: string): Promise> { + async listKeys(storePath: string, namespace: string): Promise> { try { - const manager = TauriStoreManager.new(); + const manager = TauriStoreManager.new(storePath); return E.right(await manager.listKeys(namespace)); } catch (error) { return E.left({ @@ -306,8 +326,8 @@ export const implementation: VersionedAPI = { } }, - watch(namespace: string, key: string): StoreEventEmitter { - const manager = TauriStoreManager.new(); + async watch(storePath: string, namespace: string, key: string): Promise> { + const manager = TauriStoreManager.new(storePath); return manager.watch(namespace, key); }, }, diff --git a/packages/hoppscotch-kernel/src/store/impl/web/v/1.ts b/packages/hoppscotch-kernel/src/store/impl/web/v/1.ts index 0c0b892b..d2568d46 100644 --- a/packages/hoppscotch-kernel/src/store/impl/web/v/1.ts +++ b/packages/hoppscotch-kernel/src/store/impl/web/v/1.ts @@ -91,7 +91,7 @@ class BrowserStoreManager { .map(key => key.replace(`${namespace}:`, '')); } - watch(namespace: string, key: string): StoreEventEmitter { + async watch(namespace: string, key: string): Promise> { const fullKey = this.getFullKey(namespace, key); return { on: (event, handler) => { @@ -129,7 +129,10 @@ export const implementation: VersionedAPI = { id: 'browser-store', capabilities: new Set(['permanent', 'structured', 'watch', 'namespace']), - async init() { + // `init` and other methods in `web` don't `storePath` + // but having a consistent API where first param of every method + // is the path that filteres to the "realm" makes it easier to reason around + async init(_storePath) { try { return E.right(undefined); } catch (e) { @@ -141,7 +144,7 @@ export const implementation: VersionedAPI = { } }, - async set(namespace, key, value, options) { + async set(_storePath, namespace, key, value, options) { try { const manager = BrowserStoreManager.new(); const existingData = await manager.getRaw(namespace, key); @@ -172,7 +175,7 @@ export const implementation: VersionedAPI = { } }, - async get(namespace, key) { + async get(_storePath, namespace, key) { try { const manager = BrowserStoreManager.new(); return E.right(await manager.get(namespace, key)); @@ -185,7 +188,7 @@ export const implementation: VersionedAPI = { } }, - async has(namespace, key) { + async has(_storePath, namespace, key) { try { const manager = BrowserStoreManager.new(); return E.right(await manager.has(namespace, key)); @@ -198,7 +201,7 @@ export const implementation: VersionedAPI = { } }, - async remove(namespace, key) { + async remove(_storePath, namespace, key) { try { const manager = BrowserStoreManager.new(); return E.right(await manager.delete(namespace, key)); @@ -211,7 +214,7 @@ export const implementation: VersionedAPI = { } }, - async clear(namespace) { + async clear(_storePath, namespace) { try { const manager = BrowserStoreManager.new(); await manager.clear(namespace); @@ -225,7 +228,7 @@ export const implementation: VersionedAPI = { } }, - async listNamespaces() { + async listNamespaces(_storePath){ try { const manager = BrowserStoreManager.new(); return E.right(await manager.listNamespaces()); @@ -238,7 +241,7 @@ export const implementation: VersionedAPI = { } }, - async listKeys(namespace) { + async listKeys(_storePath, namespace) { try { const manager = BrowserStoreManager.new(); return E.right(await manager.listKeys(namespace)); @@ -251,7 +254,7 @@ export const implementation: VersionedAPI = { } }, - watch(namespace, key) { + async watch(_storePath, namespace, key) { const manager = BrowserStoreManager.new(); return manager.watch(namespace, key); }, diff --git a/packages/hoppscotch-kernel/src/store/v/1.ts b/packages/hoppscotch-kernel/src/store/v/1.ts index 0feff87a..6fc1cdac 100644 --- a/packages/hoppscotch-kernel/src/store/v/1.ts +++ b/packages/hoppscotch-kernel/src/store/v/1.ts @@ -84,15 +84,15 @@ export interface StoreV1 { readonly id: string readonly capabilities: Set - init(): Promise> - set(namespace: string, key: string, value: unknown, options?: StorageOptions): Promise> - get(namespace: string, key: string): Promise> - remove(namespace: string, key: string): Promise> - clear(namespace?: string): Promise> - has(namespace: string, key: string): Promise> - listNamespaces(): Promise> - listKeys(namespace: string): Promise> - watch(namespace: string, key: string): StoreEventEmitter + init(storePath: string): Promise> + set(storePath: string, namespace: string, key: string, value: unknown, options?: StorageOptions): Promise> + get(storePath: string, namespace: string, key: string): Promise> + remove(storePath: string, namespace: string, key: string): Promise> + clear(storePath: string, namespace?: string): Promise> + has(storePath: string, namespace: string, key: string): Promise> + listNamespaces(storePath: string): Promise> + listKeys(storePath: string, namespace: string): Promise> + watch(storePath: string, namespace: string, key: string): Promise> } export const v1: VersionedAPI = { @@ -109,7 +109,7 @@ export const v1: VersionedAPI = { has: async () => E.left({ kind: 'version', message: 'Not implemented' }), listNamespaces: async () => E.left({ kind: 'version', message: 'Not implemented' }), listKeys: async () => E.left({ kind: 'version', message: 'Not implemented' }), - watch: () => ({ + watch: async () => ({ on: () => () => {}, once: () => () => {}, off: () => {}