mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
feat: Introduce Feature for persisted state
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
parent
d56bd1bd69
commit
82b370c09c
6
packages/utility-features/persisted-state/.eslintrc.json
Normal file
6
packages/utility-features/persisted-state/.eslintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@k8slens/eslint-config/eslint",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
1
packages/utility-features/persisted-state/.prettierrc
Normal file
1
packages/utility-features/persisted-state/.prettierrc
Normal file
@ -0,0 +1 @@
|
||||
"@k8slens/eslint-config/prettier"
|
||||
17
packages/utility-features/persisted-state/index.ts
Normal file
17
packages/utility-features/persisted-state/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { feature } from "./src/feature";
|
||||
|
||||
export {
|
||||
createPersistedStateInjectionToken,
|
||||
persistedStateInjectionToken,
|
||||
} from "./src/create-persisted-state/create-persisted-state.injectable";
|
||||
|
||||
export type {
|
||||
CreatePersistedState,
|
||||
CreatePersistedStateConfig,
|
||||
PersistedState,
|
||||
PersistedStateResult,
|
||||
NonPendingPersistedStateResult,
|
||||
PendingPersistedStateResult,
|
||||
} from "./src/create-persisted-state/create-persisted-state.injectable";
|
||||
|
||||
export default feature;
|
||||
1
packages/utility-features/persisted-state/jest.config.js
Normal file
1
packages/utility-features/persisted-state/jest.config.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;
|
||||
45
packages/utility-features/persisted-state/package.json
Normal file
45
packages/utility-features/persisted-state/package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@k8slens/persisted-state",
|
||||
"private": false,
|
||||
"version": "0.1.0",
|
||||
"description": "TBD",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lensapp/lens.git"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"author": {
|
||||
"name": "OpenLens Authors",
|
||||
"email": "info@k8slens.dev"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"scripts": {
|
||||
"build": "lens-webpack-build",
|
||||
"clean": "rimraf dist/",
|
||||
"test:unit": "jest --coverage --runInBand",
|
||||
"lint": "lens-lint",
|
||||
"lint:fix": "lens-lint --fix"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@k8slens/app-paths": "^0.1.0",
|
||||
"@k8slens/file-system": "^0.1.0",
|
||||
"@k8slens/logger": "1.0.0-alpha.6",
|
||||
"@ogre-tools/fp": "^16.1.0",
|
||||
"@ogre-tools/injectable": "^16.1.0",
|
||||
"@ogre-tools/injectable-extension-for-auto-registration": "^16.1.0",
|
||||
"@ogre-tools/injectable-extension-for-mobx": "^16.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"mobx": "^6.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@k8slens/eslint-config": "^6.5.0-alpha.3",
|
||||
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.4",
|
||||
"@k8slens/webpack": "^6.5.0-alpha.6"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
import {
|
||||
getInjectable,
|
||||
getInjectionToken,
|
||||
lifecycleEnum,
|
||||
} from "@ogre-tools/injectable";
|
||||
import type { AnySchema } from "ajv";
|
||||
|
||||
import { computed, IComputedValue, observable, runInAction } from "mobx";
|
||||
import type { JsonValue } from "type-fest";
|
||||
import {
|
||||
readJsonFileInjectionToken,
|
||||
writeJsonFileInjectionToken,
|
||||
} from "@lensapp/fs";
|
||||
import { validateJsonSchema, withThrownFailuresUnless } from "@lensapp/utils";
|
||||
import { constant } from "lodash/fp";
|
||||
import { logErrorInjectionToken } from "@lensapp/logging";
|
||||
import {
|
||||
appPathsInjectionToken,
|
||||
joinPathsInjectionToken,
|
||||
} from "@lensapp/app-paths";
|
||||
|
||||
export type CreatePersistedStateConfig<T extends JsonValue> = {
|
||||
id: string;
|
||||
schema: AnySchema;
|
||||
defaultValue: T;
|
||||
};
|
||||
|
||||
export type NonPendingPersistedStateResult<T extends JsonValue> = {
|
||||
pending: false;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type PendingPersistedStateResult = {
|
||||
pending: true;
|
||||
};
|
||||
|
||||
export type PersistedStateResult<T extends JsonValue> =
|
||||
| NonPendingPersistedStateResult<T>
|
||||
| PendingPersistedStateResult;
|
||||
|
||||
export const persistedStateInjectionToken = getInjectionToken<
|
||||
PersistedState<any>
|
||||
>({
|
||||
id: "persisted-state-injection-token",
|
||||
});
|
||||
|
||||
export const createPersistedStateInjectionToken =
|
||||
getInjectionToken<CreatePersistedState>({
|
||||
id: "create-persisted-state-injection-token",
|
||||
});
|
||||
|
||||
export interface PersistedState<T extends JsonValue> {
|
||||
result: IComputedValue<PersistedStateResult<T>>;
|
||||
getAsyncValue: () => Promise<T>;
|
||||
set: (newValue: T) => void;
|
||||
}
|
||||
|
||||
export type CreatePersistedState = <T extends JsonValue>(
|
||||
config: CreatePersistedStateConfig<T>
|
||||
) => PersistedState<T>;
|
||||
|
||||
export const stateBoxInjectable = getInjectable({
|
||||
id: "persisted-state-box",
|
||||
|
||||
instantiate: () => observable.box(),
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, id: string) => id,
|
||||
}),
|
||||
});
|
||||
|
||||
export const createPersistedStateInjectable = getInjectable({
|
||||
id: "create-persisted-state",
|
||||
|
||||
instantiate: (di) => {
|
||||
const readJsonFileWithThrownFailures = di.inject(
|
||||
readJsonFileInjectionToken
|
||||
);
|
||||
|
||||
const readJsonFile = withThrownFailuresUnless(
|
||||
errorIsAboutMissingFile,
|
||||
errorIsAboutAnythingElse
|
||||
)(readJsonFileWithThrownFailures);
|
||||
|
||||
const writeJsonFile = withThrownFailuresUnless(constant(true))(
|
||||
di.inject(writeJsonFileInjectionToken)
|
||||
);
|
||||
|
||||
const logError = di.inject(logErrorInjectionToken);
|
||||
const appPaths = di.inject(appPathsInjectionToken);
|
||||
const joinPaths = di.inject(joinPathsInjectionToken);
|
||||
|
||||
const alreadyCreated = new Set<string>();
|
||||
|
||||
return <T extends JsonValue>({
|
||||
id: persistedStateId,
|
||||
defaultValue,
|
||||
schema,
|
||||
}: CreatePersistedStateConfig<T>) => {
|
||||
if (alreadyCreated.has(persistedStateId)) {
|
||||
throw new Error(
|
||||
`Tried to create persisted state for "${persistedStateId}", but it was already created`
|
||||
);
|
||||
}
|
||||
|
||||
const validateSchema = validateJsonSchema(getFileSchema(schema));
|
||||
|
||||
alreadyCreated.add(persistedStateId);
|
||||
|
||||
const stateJsonPath = joinPaths(
|
||||
appPaths.userData,
|
||||
`persisted-states/${persistedStateId}.json`
|
||||
);
|
||||
|
||||
const valueBox = observable.box(defaultValue);
|
||||
const pendingBox = observable.box(true);
|
||||
|
||||
let stallAsyncValue = readJsonFile(stateJsonPath).then(
|
||||
(stateValueCall) => {
|
||||
if (!stateValueCall.callWasSuccessful) {
|
||||
if (!errorIsAboutMissingFile(stateValueCall.error.cause)) {
|
||||
logError(
|
||||
`Tried to read persisted states from "${stateJsonPath}" but it failed with:\n\n${stateValueCall.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
pendingBox.set(false);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const response = stateValueCall.response;
|
||||
|
||||
const validated = validateSchema(response);
|
||||
|
||||
if (validated.valid) {
|
||||
runInAction(() => {
|
||||
valueBox.set(response.value);
|
||||
pendingBox.set(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
result: computed(() =>
|
||||
pendingBox.get()
|
||||
? { pending: true as const }
|
||||
: {
|
||||
pending: false as const,
|
||||
value: valueBox.get(),
|
||||
}
|
||||
),
|
||||
|
||||
set: async (newValue: T) => {
|
||||
if (pendingBox.get()) {
|
||||
throw new Error(
|
||||
`Tried to set a persisted state for "${persistedStateId}", but call for the existing persisted state hadn't finished yet`
|
||||
);
|
||||
}
|
||||
|
||||
const latestGoodValue = valueBox.get();
|
||||
|
||||
const validateSchema = validateJsonSchema(schema);
|
||||
|
||||
const validated = validateSchema(newValue);
|
||||
|
||||
if (!validated.valid) {
|
||||
throw new Error(
|
||||
`Tried to set value of persisted state "${persistedStateId}" but validation of new value failed with:\n\n${JSON.stringify(
|
||||
validated.validationErrors,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
valueBox.set(newValue);
|
||||
});
|
||||
|
||||
const resultPromise = writeJsonFile(stateJsonPath, {
|
||||
version: 1,
|
||||
value: newValue,
|
||||
});
|
||||
|
||||
stallAsyncValue = resultPromise as Promise<any>;
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
runInAction(() => {
|
||||
valueBox.set(latestGoodValue);
|
||||
});
|
||||
|
||||
logError(
|
||||
`Tried to persist state to "${stateJsonPath}" but attempt failed with:\n\n${result.error.message}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getAsyncValue: async () => {
|
||||
await stallAsyncValue;
|
||||
|
||||
return valueBox.get();
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
injectionToken: createPersistedStateInjectionToken,
|
||||
});
|
||||
|
||||
const getFileSchema = (valueSchema: AnySchema) => ({
|
||||
type: "object",
|
||||
|
||||
properties: {
|
||||
version: { type: "number" },
|
||||
value: valueSchema,
|
||||
},
|
||||
|
||||
required: ["version", "value"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
const errorIsAboutMissingFile = (error: unknown) =>
|
||||
(error as any).code === "ENOENT";
|
||||
|
||||
const errorIsAboutAnythingElse = () => true;
|
||||
@ -0,0 +1,356 @@
|
||||
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
||||
import {
|
||||
createContainer,
|
||||
DiContainer,
|
||||
getInjectable,
|
||||
} from "@ogre-tools/injectable";
|
||||
import { reaction, runInAction } from "mobx";
|
||||
import {
|
||||
CreatePersistedState,
|
||||
createPersistedStateInjectionToken,
|
||||
PersistedState,
|
||||
PersistedStateResult,
|
||||
} from "./create-persisted-state.injectable";
|
||||
import { registerFeature } from "@lensapp/feature-core";
|
||||
import {
|
||||
ReadJsonFile,
|
||||
readJsonFileInjectionToken,
|
||||
WriteJsonFile,
|
||||
writeJsonFileInjectionToken,
|
||||
} from "@lensapp/fs";
|
||||
import { getSuccess } from "@lensapp/utils";
|
||||
import { logErrorInjectionToken } from "@lensapp/logging";
|
||||
import { appPathsInjectionToken } from "@lensapp/app-paths";
|
||||
import { getPromiseStatus, useFakeTime } from "@lensapp/test-utils";
|
||||
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
|
||||
import { feature } from "../feature";
|
||||
|
||||
describe("create persisted state", () => {
|
||||
let di: DiContainer;
|
||||
let readJsonFileMock: AsyncFnMock<ReadJsonFile>;
|
||||
let writeJsonFileMock: AsyncFnMock<WriteJsonFile>;
|
||||
let logErrorMock: jest.Mock;
|
||||
let createPersistedState: CreatePersistedState;
|
||||
|
||||
beforeEach(() => {
|
||||
useFakeTime();
|
||||
|
||||
di = createContainer("irrelevant");
|
||||
|
||||
registerFeature(di, feature);
|
||||
|
||||
di.register(appPathsFakeInjectable);
|
||||
|
||||
registerMobX(di);
|
||||
|
||||
readJsonFileMock = asyncFn();
|
||||
di.override(readJsonFileInjectionToken, () => readJsonFileMock);
|
||||
|
||||
writeJsonFileMock = asyncFn();
|
||||
di.override(writeJsonFileInjectionToken, () => writeJsonFileMock);
|
||||
|
||||
logErrorMock = jest.fn();
|
||||
di.override(logErrorInjectionToken, () => logErrorMock);
|
||||
|
||||
createPersistedState = di.inject(createPersistedStateInjectionToken);
|
||||
});
|
||||
|
||||
describe("when a persisted state is created", () => {
|
||||
let actualPersistedState: PersistedState<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPersistedState = createPersistedState<string>({
|
||||
id: "some-persisted-state-id",
|
||||
defaultValue: "some-default-value",
|
||||
|
||||
schema: {
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["some-existing-value", "some-changed-value"],
|
||||
},
|
||||
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reads persisted value", () => {
|
||||
expect(readJsonFileMock).toHaveBeenCalledWith(
|
||||
"/some-user-data-directory/persisted-states/some-persisted-state-id.json"
|
||||
);
|
||||
});
|
||||
|
||||
it("when persisted state with conflicting ID is created, throws", () => {
|
||||
expect(() => {
|
||||
createPersistedState({
|
||||
id: "some-persisted-state-id",
|
||||
defaultValue: "irrelevant",
|
||||
schema: {},
|
||||
});
|
||||
}).toThrow(
|
||||
'Tried to create persisted state for "some-persisted-state-id", but it was already created'
|
||||
);
|
||||
});
|
||||
|
||||
describe("when value is accessed as promise instead of observing", () => {
|
||||
let actualPromise: Promise<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPromise = actualPersistedState.getAsyncValue();
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
describe("when existing value resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await readJsonFileMock.resolve(
|
||||
getSuccess({
|
||||
version: 1,
|
||||
value: "some-existing-value",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves as the existing value", async () => {
|
||||
expect(await actualPromise).toBe("some-existing-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when value is accessed as observation instead of promise", () => {
|
||||
let observedResult: PersistedStateResult<string>;
|
||||
let observedValue: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
reaction(
|
||||
() => actualPersistedState.result.get(),
|
||||
|
||||
(newValue) => {
|
||||
observedResult = newValue;
|
||||
observedValue = newValue.pending ? undefined : newValue.value;
|
||||
},
|
||||
|
||||
{ fireImmediately: true }
|
||||
);
|
||||
});
|
||||
|
||||
it("state observes as pending", () => {
|
||||
expect(observedResult.pending).toBe(true);
|
||||
});
|
||||
|
||||
it("when new value is set before existing value has resolved, throws", () => {
|
||||
return expect(actualPersistedState.set("irrelevant")).rejects.toThrow(
|
||||
'Tried to set a persisted state for "some-persisted-state-id", but call for the existing persisted state hadn\'t finished yet'
|
||||
);
|
||||
});
|
||||
|
||||
describe("when existing value resolves", () => {
|
||||
beforeEach(async () => {
|
||||
writeJsonFileMock.mockClear();
|
||||
|
||||
await readJsonFileMock.resolve(
|
||||
getSuccess({
|
||||
version: 1,
|
||||
value: "some-existing-value",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe("when value is accessed as promise instead of observing", () => {
|
||||
let actualPromise: Promise<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
readJsonFileMock.mockClear();
|
||||
actualPromise = actualPersistedState.getAsyncValue();
|
||||
});
|
||||
|
||||
it("resolves as the existing value", async () => {
|
||||
expect(await actualPromise).toBe("some-existing-value");
|
||||
});
|
||||
|
||||
it("does not call for existing values again", () => {
|
||||
expect(readJsonFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("state observes as the existing value", () => {
|
||||
expect(observedValue).toBe("some-existing-value");
|
||||
});
|
||||
|
||||
it("does not persist the value again", () => {
|
||||
expect(writeJsonFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when the state is changed to a valid value", () => {
|
||||
beforeEach(() => {
|
||||
runInAction(async () => {
|
||||
await actualPersistedState.set("some-changed-value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when value is accessed as promise instead of observing", () => {
|
||||
let actualPromise: Promise<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
readJsonFileMock.mockClear();
|
||||
actualPromise = actualPersistedState.getAsyncValue();
|
||||
});
|
||||
|
||||
it("does not resolve yet", async () => {
|
||||
const promiseStatus = await getPromiseStatus(actualPromise);
|
||||
|
||||
expect(promiseStatus.fulfilled).toBe(false);
|
||||
});
|
||||
|
||||
it("does not call for existing values again", () => {
|
||||
expect(readJsonFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when persisting resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await writeJsonFileMock.resolve(getSuccess(undefined));
|
||||
});
|
||||
|
||||
it("resolves as the changed value", async () => {
|
||||
expect(await actualPromise).toBe("some-changed-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("persists the value", () => {
|
||||
expect(writeJsonFileMock).toHaveBeenCalledWith(
|
||||
"/some-user-data-directory/persisted-states/some-persisted-state-id.json",
|
||||
{ version: 1, value: "some-changed-value" }
|
||||
);
|
||||
});
|
||||
|
||||
it("state observes as the changed value", () => {
|
||||
expect(observedValue).toBe("some-changed-value");
|
||||
});
|
||||
|
||||
describe("when persisting resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await writeJsonFileMock.resolve(getSuccess(undefined));
|
||||
});
|
||||
|
||||
it("does not log error", () => {
|
||||
expect(logErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("observed value is still the changed value", () => {
|
||||
expect(observedValue).toBe("some-changed-value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when persisting rejects", () => {
|
||||
beforeEach(async () => {
|
||||
await writeJsonFileMock.reject(new Error("some-error"));
|
||||
});
|
||||
|
||||
it("logs error", () => {
|
||||
expect(logErrorMock).toHaveBeenCalledWith(
|
||||
'Tried to persist state to "/some-user-data-directory/persisted-states/some-persisted-state-id.json" but attempt failed with:\n\nsome-error'
|
||||
);
|
||||
});
|
||||
|
||||
it("observed value is the latest good value", () => {
|
||||
expect(observedValue).toBe("some-existing-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the state is changed to an invalid value", () => {
|
||||
let error: any;
|
||||
|
||||
beforeEach(() => {
|
||||
runInAction(async () => {
|
||||
try {
|
||||
await actualPersistedState.set("some-invalid-value");
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("does not persist the value", () => {
|
||||
expect(writeJsonFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("state observes as latest good value", () => {
|
||||
expect(observedValue).toBe("some-existing-value");
|
||||
});
|
||||
|
||||
it("throws", () => {
|
||||
expect(error.message).toEqual(
|
||||
expect.stringContaining(
|
||||
'Tried to set value of persisted state "some-persisted-state-id" but validation of new value failed with:'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when reading resolves with invalid content", () => {
|
||||
beforeEach(async () => {
|
||||
await readJsonFileMock.resolve(getSuccess({ version: 1, value: 42 }));
|
||||
});
|
||||
|
||||
it("observes as pending (eternally)", () => {
|
||||
expect(observedResult.pending).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when reading rejects with error for non existing file", () => {
|
||||
beforeEach(async () => {
|
||||
const errorAboutMissingFile = new Error("irrelevant");
|
||||
|
||||
(errorAboutMissingFile as any).code = "ENOENT";
|
||||
|
||||
await readJsonFileMock.reject(errorAboutMissingFile);
|
||||
});
|
||||
|
||||
it("does not log error", () => {
|
||||
expect(logErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("state observes as default value", () => {
|
||||
expect(observedValue).toBe("some-default-value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when reading rejects with any other error than for non existing file", () => {
|
||||
beforeEach(async () => {
|
||||
const anyOtherError = new Error("some error");
|
||||
|
||||
await readJsonFileMock.reject(anyOtherError);
|
||||
});
|
||||
|
||||
it("logs error", () => {
|
||||
expect(logErrorMock).toHaveBeenCalledWith(
|
||||
'Tried to read persisted states from "/some-user-data-directory/persisted-states/some-persisted-state-id.json" but it failed with:\n\nsome error'
|
||||
);
|
||||
});
|
||||
|
||||
it("state observes as default value", () => {
|
||||
expect(observedValue).toBe("some-default-value");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const appPathsFakeInjectable = getInjectable({
|
||||
id: "app-paths-fake",
|
||||
instantiate: () =>
|
||||
({
|
||||
userData: "/some-user-data-directory",
|
||||
} as any),
|
||||
injectionToken: appPathsInjectionToken,
|
||||
});
|
||||
18
packages/utility-features/persisted-state/src/feature.ts
Normal file
18
packages/utility-features/persisted-state/src/feature.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
|
||||
import { getFeature } from "@k8slens/feature-core";
|
||||
import fsFeature from "@k8slens/file-system";
|
||||
import loggingFeature from "@k8slens/logger";
|
||||
import appPathsFeature from "@k8slens/app-paths";
|
||||
|
||||
export const feature = getFeature({
|
||||
id: "persisted-state",
|
||||
dependencies: [fsFeature, loggingFeature, appPathsFeature],
|
||||
|
||||
register: (di) => {
|
||||
autoRegister({
|
||||
di,
|
||||
targetModule: module,
|
||||
getRequireContexts: () => [require.context("./", true, /\.injectable\.(ts|tsx)$/)],
|
||||
});
|
||||
},
|
||||
});
|
||||
4
packages/utility-features/persisted-state/tsconfig.json
Normal file
4
packages/utility-features/persisted-state/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@k8slens/typescript/config/base.json",
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
module.exports = require("@k8slens/webpack").configForNode;
|
||||
Loading…
Reference in New Issue
Block a user