feat(kernel): multi-instance support for store (#5083)
This commit is contained in:
parent
1da961cef7
commit
d213bec3ef
12 changed files with 129 additions and 79 deletions
|
|
@ -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<E.Either<StoreError, void>> =>
|
||||
module().set(namespace, key, value, options),
|
||||
get: <T>(
|
||||
): Promise<E.Either<StoreError, void>> => {
|
||||
return module().set(STORE_PATH, namespace, key, value, options)
|
||||
},
|
||||
|
||||
get: async <T>(
|
||||
namespace: string,
|
||||
key: string
|
||||
): Promise<E.Either<StoreError, T | undefined>> =>
|
||||
module().get<T>(namespace, key),
|
||||
remove: (
|
||||
): Promise<E.Either<StoreError, T | undefined>> => {
|
||||
return module().get<T>(STORE_PATH, namespace, key)
|
||||
},
|
||||
|
||||
remove: async (
|
||||
namespace: string,
|
||||
key: string
|
||||
): Promise<E.Either<StoreError, boolean>> =>
|
||||
module().remove(namespace, key),
|
||||
clear: (namespace?: string): Promise<E.Either<StoreError, void>> =>
|
||||
module().clear(namespace),
|
||||
has: (
|
||||
): Promise<E.Either<StoreError, boolean>> => {
|
||||
return module().remove(STORE_PATH, namespace, key)
|
||||
},
|
||||
|
||||
clear: async (namespace?: string): Promise<E.Either<StoreError, void>> => {
|
||||
return module().clear(STORE_PATH, namespace)
|
||||
},
|
||||
|
||||
has: async (
|
||||
namespace: string,
|
||||
key: string
|
||||
): Promise<E.Either<StoreError, boolean>> => module().has(namespace, key),
|
||||
listNamespaces: (): Promise<E.Either<StoreError, string[]>> =>
|
||||
module().listNamespaces(),
|
||||
listKeys: (namespace: string): Promise<E.Either<StoreError, string[]>> =>
|
||||
module().listKeys(namespace),
|
||||
watch: (namespace: string, key: string): StoreEventEmitter<StoreEvents> =>
|
||||
module().watch(namespace, key),
|
||||
): Promise<E.Either<StoreError, boolean>> => {
|
||||
return module().has(STORE_PATH, namespace, key)
|
||||
},
|
||||
|
||||
listNamespaces: async (): Promise<E.Either<StoreError, string[]>> => {
|
||||
return module().listNamespaces(STORE_PATH)
|
||||
},
|
||||
|
||||
listKeys: async (namespace: string): Promise<E.Either<StoreError, string[]>> => {
|
||||
return module().listKeys(STORE_PATH, namespace)
|
||||
},
|
||||
|
||||
watch: async (namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> => {
|
||||
return module().watch(STORE_PATH, namespace, key)
|
||||
},
|
||||
} as const
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export class AgentKernelInterceptorService
|
|||
...effectiveRequest.headers,
|
||||
"User-Agent": existingUserAgentHeader
|
||||
? effectiveRequest.headers[existingUserAgentHeader]
|
||||
: "HoppscotchKernel/0.1.0",
|
||||
: "HoppscotchKernel/0.2.0",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export class NativeKernelInterceptorService
|
|||
...effectiveRequest.headers,
|
||||
"User-Agent": existingUserAgentHeader
|
||||
? effectiveRequest.headers[existingUserAgentHeader]
|
||||
: "HoppscotchKernel/0.1.0",
|
||||
: "HoppscotchKernel/0.2.0",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ Cross-platform persistence with encryption support:
|
|||
interface StoreV1 {
|
||||
readonly capabilities: Set<StoreCapability>
|
||||
set(namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<Either<StoreError, void>>
|
||||
watch(namespace: string, key: string): StoreEventEmitter<StoreEvents>
|
||||
watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -13,28 +13,47 @@ import {
|
|||
StoreEventEmitter,
|
||||
} from '@store/v/1';
|
||||
|
||||
const STORE_PATH = `${window.location.host}.hoppscotch.store`
|
||||
|
||||
type NamespacedData = Record<string, Record<string, StoredData>>;
|
||||
|
||||
class TauriStoreManager {
|
||||
private static instance: TauriStoreManager;
|
||||
private static instances: Map<string, TauriStoreManager> = new Map();
|
||||
private store: Store | null = null;
|
||||
private listeners = new Map<string, Set<(payload: StoreEvents['change']) => 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<void> {
|
||||
const closePromises = Array.from(TauriStoreManager.instances.values())
|
||||
.map(instance => instance.close());
|
||||
await Promise.all(closePromises);
|
||||
TauriStoreManager.instances.clear();
|
||||
}
|
||||
|
||||
static async closeStore(storePath: string): Promise<void> {
|
||||
const instance = TauriStoreManager.instances.get(storePath);
|
||||
if (instance) {
|
||||
await instance.close();
|
||||
TauriStoreManager.instances.delete(storePath);
|
||||
}
|
||||
return TauriStoreManager.instance;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (!this.store) {
|
||||
this.store = await Store.load(STORE_PATH);
|
||||
this.store = await Store.load(this.storePath);
|
||||
const loadedData = await this.store.get<NamespacedData>('data');
|
||||
this.data = loadedData ?? {};
|
||||
|
||||
|
|
@ -123,7 +142,7 @@ class TauriStoreManager {
|
|||
return Object.keys(this.data[namespace] || {});
|
||||
}
|
||||
|
||||
watch(namespace: string, key: string): StoreEventEmitter<StoreEvents> {
|
||||
async watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> {
|
||||
const watchKey = `${namespace}:${key}`;
|
||||
return {
|
||||
on: <K extends keyof StoreEvents>(
|
||||
|
|
@ -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<StoreV1> = {
|
|||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async set(namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<E.Either<StoreError, void>> {
|
||||
async set(storePath: string, namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<E.Either<StoreError, void>> {
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async get<T>(namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>> {
|
||||
async get<T>(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>> {
|
||||
try {
|
||||
const manager = TauriStoreManager.new();
|
||||
const manager = TauriStoreManager.new(storePath);
|
||||
return E.right(await manager.get<T>(namespace, key));
|
||||
} catch (error) {
|
||||
return E.left({
|
||||
|
|
@ -240,9 +260,9 @@ export const implementation: VersionedAPI<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async has(namespace: string, key: string): Promise<E.Either<StoreError, boolean>> {
|
||||
async has(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> {
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async remove(namespace: string, key: string): Promise<E.Either<StoreError, boolean>> {
|
||||
async remove(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>> {
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async clear(namespace?: string): Promise<E.Either<StoreError, void>> {
|
||||
async clear(storePath: string, namespace?: string): Promise<E.Either<StoreError, void>> {
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async listNamespaces(): Promise<E.Either<StoreError, string[]>> {
|
||||
async listNamespaces(storePath: string): Promise<E.Either<StoreError, string[]>> {
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async listKeys(namespace: string): Promise<E.Either<StoreError, string[]>> {
|
||||
async listKeys(storePath: string, namespace: string): Promise<E.Either<StoreError, string[]>> {
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
watch(namespace: string, key: string): StoreEventEmitter<StoreEvents> {
|
||||
const manager = TauriStoreManager.new();
|
||||
async watch(storePath: string, namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> {
|
||||
const manager = TauriStoreManager.new(storePath);
|
||||
return manager.watch(namespace, key);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class BrowserStoreManager {
|
|||
.map(key => key.replace(`${namespace}:`, ''));
|
||||
}
|
||||
|
||||
watch(namespace: string, key: string): StoreEventEmitter<StoreEvents> {
|
||||
async watch(namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>> {
|
||||
const fullKey = this.getFullKey(namespace, key);
|
||||
return {
|
||||
on: (event, handler) => {
|
||||
|
|
@ -129,7 +129,10 @@ export const implementation: VersionedAPI<StoreV1> = {
|
|||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async clear(namespace) {
|
||||
async clear(_storePath, namespace) {
|
||||
try {
|
||||
const manager = BrowserStoreManager.new();
|
||||
await manager.clear(namespace);
|
||||
|
|
@ -225,7 +228,7 @@ export const implementation: VersionedAPI<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
async listNamespaces() {
|
||||
async listNamespaces(_storePath){
|
||||
try {
|
||||
const manager = BrowserStoreManager.new();
|
||||
return E.right(await manager.listNamespaces());
|
||||
|
|
@ -238,7 +241,7 @@ export const implementation: VersionedAPI<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
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<StoreV1> = {
|
|||
}
|
||||
},
|
||||
|
||||
watch(namespace, key) {
|
||||
async watch(_storePath, namespace, key) {
|
||||
const manager = BrowserStoreManager.new();
|
||||
return manager.watch(namespace, key);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,15 +84,15 @@ export interface StoreV1 {
|
|||
readonly id: string
|
||||
readonly capabilities: Set<StoreCapability>
|
||||
|
||||
init(): Promise<E.Either<StoreError, void>>
|
||||
set(namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<E.Either<StoreError, void>>
|
||||
get<T>(namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>>
|
||||
remove(namespace: string, key: string): Promise<E.Either<StoreError, boolean>>
|
||||
clear(namespace?: string): Promise<E.Either<StoreError, void>>
|
||||
has(namespace: string, key: string): Promise<E.Either<StoreError, boolean>>
|
||||
listNamespaces(): Promise<E.Either<StoreError, string[]>>
|
||||
listKeys(namespace: string): Promise<E.Either<StoreError, string[]>>
|
||||
watch(namespace: string, key: string): StoreEventEmitter<StoreEvents>
|
||||
init(storePath: string): Promise<E.Either<StoreError, void>>
|
||||
set(storePath: string, namespace: string, key: string, value: unknown, options?: StorageOptions): Promise<E.Either<StoreError, void>>
|
||||
get<T>(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, T | undefined>>
|
||||
remove(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>>
|
||||
clear(storePath: string, namespace?: string): Promise<E.Either<StoreError, void>>
|
||||
has(storePath: string, namespace: string, key: string): Promise<E.Either<StoreError, boolean>>
|
||||
listNamespaces(storePath: string): Promise<E.Either<StoreError, string[]>>
|
||||
listKeys(storePath: string, namespace: string): Promise<E.Either<StoreError, string[]>>
|
||||
watch(storePath: string, namespace: string, key: string): Promise<StoreEventEmitter<StoreEvents>>
|
||||
}
|
||||
|
||||
export const v1: VersionedAPI<StoreV1> = {
|
||||
|
|
@ -109,7 +109,7 @@ export const v1: VersionedAPI<StoreV1> = {
|
|||
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: () => {}
|
||||
|
|
|
|||
Loading…
Reference in a new issue