Persistence rework

- Removed makePersistent
- Removed Persistent, renamed PersistentRef to Persistent
- createLazyProxy now takes optional base object
  - For use where previously makePersistent would be used
- Added warnings when creating refs outside a layer
- Added warnings when persistent refs aren't included in their layer object
- createLayer now takes id as a parameter rather than an option
This commit is contained in:
thepaperpilot 2022-04-05 22:16:40 -05:00
parent bd084e51c5
commit 5a59aaf4fc
13 changed files with 296 additions and 214 deletions

View file

@ -10,7 +10,7 @@ import {
Visibility
} from "features/feature";
import "game/notifications";
import { Persistent, makePersistent, PersistentState } from "game/persistence";
import { Persistent, PersistentState, persistent } from "game/persistence";
import {
Computable,
GetComputableType,
@ -69,9 +69,10 @@ export type GenericAchievement = Replace<
export function createAchievement<T extends AchievementOptions>(
optionsFunc: () => T & ThisType<Achievement<T>>
): Achievement<T> {
return createLazyProxy(() => {
const achievement: T & Partial<BaseAchievement> = optionsFunc();
makePersistent<boolean>(achievement, false);
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const achievement: Partial<BaseAchievement> & typeof temp = temp;
achievement.id = getUniqueID("achievement-");
achievement.type = AchievementType;
achievement[Component] = AchievementComponent;
@ -122,5 +123,5 @@ export function createAchievement<T extends AchievementOptions>(
}
return achievement as unknown as Achievement<T>;
});
}, persistent<boolean>(false));
}

View file

@ -10,7 +10,7 @@ import {
Visibility
} from "features/feature";
import { globalBus } from "game/events";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence";
import { State, Persistent, PersistentState, persistent } from "game/persistence";
import { isFunction } from "util/common";
import {
Computable,
@ -199,13 +199,11 @@ export type GenericBoard = Replace<
export function createBoard<T extends BoardOptions>(
optionsFunc: () => T & ThisType<Board<T>>
): Board<T> {
return createLazyProxy(() => {
const board: T & Partial<BaseBoard> = optionsFunc();
makePersistent<BoardData>(board, {
nodes: [],
selectedNode: null,
selectedAction: null
});
return createLazyProxy(
persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const board: Partial<BaseBoard> & typeof temp = temp;
board.id = getUniqueID("board-");
board.type = BoardType;
board[Component] = BoardComponent;
@ -236,7 +234,10 @@ export function createBoard<T extends BoardOptions>(
if (processedBoard.selectedAction.value == null) {
return null;
}
if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) {
if (
processedBoard.selectedAction.value.links &&
processedBoard.selectedNode.value
) {
return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
@ -327,7 +328,13 @@ export function createBoard<T extends BoardOptions>(
// This is necessary because board.types is different from T and Board
const processedBoard = board as unknown as Board<T>;
return processedBoard;
});
},
persistent<BoardData>({
nodes: [],
selectedNode: null,
selectedAction: null
})
);
}
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {

View file

@ -1,6 +1,6 @@
import ClickableComponent from "features/clickables/Clickable.vue";
import { Resource } from "features/resources/resource";
import { Persistent, makePersistent, PersistentState } from "game/persistence";
import { Persistent, PersistentState, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import {
Computable,
@ -89,8 +89,10 @@ export type GenericBuyable = Replace<
export function createBuyable<T extends BuyableOptions>(
optionsFunc: () => T & ThisType<Buyable<T>>
): Buyable<T> {
return createLazyProxy(() => {
const buyable: T & Partial<BaseBuyable> = optionsFunc();
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const buyable: Partial<BaseBuyable> & typeof temp = temp;
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn(
@ -100,7 +102,6 @@ export function createBuyable<T extends BuyableOptions>(
throw "Cannot create buyable without a canPurchase property or a resource and cost property";
}
makePersistent<DecimalSource>(buyable, 0);
buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType;
buyable[Component] = ClickableComponent;
@ -239,5 +240,5 @@ export function createBuyable<T extends BuyableOptions>(
};
return buyable as unknown as Buyable<T>;
});
}, persistent<DecimalSource>(0));
}

View file

@ -15,7 +15,7 @@ import {
import { GenericReset } from "features/reset";
import { Resource } from "features/resources/resource";
import { globalBus } from "game/events";
import { persistent, PersistentRef } from "game/persistence";
import { Persistent, persistent } from "game/persistence";
import settings, { registerSettingField } from "game/settings";
import Decimal, { DecimalSource } from "util/bignum";
import {
@ -58,10 +58,10 @@ export interface ChallengeOptions {
export interface BaseChallenge {
id: string;
completions: PersistentRef<DecimalSource>;
completions: Persistent<DecimalSource>;
completed: Ref<boolean>;
maxed: Ref<boolean>;
active: PersistentRef<boolean>;
active: Persistent<boolean>;
toggle: VoidFunction;
complete: (remainInChallenge?: boolean) => void;
type: typeof ChallengeType;
@ -98,6 +98,8 @@ export type GenericChallenge = Replace<
export function createChallenge<T extends ChallengeOptions>(
optionsFunc: () => T & ThisType<Challenge<T>>
): Challenge<T> {
const completions = persistent(0);
const active = persistent(false);
return createLazyProxy(() => {
const challenge: T & Partial<BaseChallenge> = optionsFunc();
@ -116,8 +118,8 @@ export function createChallenge<T extends ChallengeOptions>(
challenge.type = ChallengeType;
challenge[Component] = ChallengeComponent;
challenge.completions = persistent(0);
challenge.active = persistent(false);
challenge.completions = completions;
challenge.active = active;
challenge.completed = computed(() =>
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
);

View file

@ -19,7 +19,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence";
import { State, Persistent, PersistentState, persistent } from "game/persistence";
export const GridType = Symbol("Grid");
@ -243,9 +243,10 @@ export type GenericGrid = Replace<
export function createGrid<T extends GridOptions>(
optionsFunc: () => T & ThisType<Grid<T>>
): Grid<T> {
return createLazyProxy(() => {
const grid: T & Partial<BaseGrid> = optionsFunc();
makePersistent(grid, {});
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const grid: Partial<BaseGrid> & typeof temp = temp;
grid.id = getUniqueID("grid-");
grid[Component] = GridComponent;
@ -301,5 +302,5 @@ export function createGrid<T extends GridOptions>(
};
return grid as unknown as Grid<T>;
});
}, persistent({}));
}

View file

@ -18,7 +18,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence";
import { Persistent, PersistentState, persistent } from "game/persistence";
export const InfoboxType = Symbol("Infobox");
@ -65,9 +65,10 @@ export type GenericInfobox = Replace<
export function createInfobox<T extends InfoboxOptions>(
optionsFunc: () => T & ThisType<Infobox<T>>
): Infobox<T> {
return createLazyProxy(() => {
const infobox: T & Partial<BaseInfobox> = optionsFunc();
makePersistent<boolean>(infobox, false);
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const infobox: Partial<BaseInfobox> & typeof temp = temp;
infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent;
@ -112,5 +113,5 @@ export function createInfobox<T extends InfoboxOptions>(
};
return infobox as unknown as Infobox<T>;
});
}, persistent<boolean>(false));
}

View file

@ -13,7 +13,7 @@ import {
import MilestoneComponent from "features/milestones/Milestone.vue";
import { globalBus } from "game/events";
import "game/notifications";
import { makePersistent, Persistent, PersistentState } from "game/persistence";
import { persistent, Persistent, PersistentState } from "game/persistence";
import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common";
import {
@ -85,9 +85,10 @@ export type GenericMilestone = Replace<
export function createMilestone<T extends MilestoneOptions>(
optionsFunc: () => T & ThisType<Milestone<T>>
): Milestone<T> {
return createLazyProxy(() => {
const milestone: T & Partial<BaseMilestone> = optionsFunc();
makePersistent<boolean>(milestone, false);
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const milestone: Partial<BaseMilestone> & typeof temp = temp;
milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent;
@ -168,7 +169,7 @@ export function createMilestone<T extends MilestoneOptions>(
}
return milestone as unknown as Milestone<T>;
});
}, persistent<boolean>(false));
}
declare module "game/settings" {

View file

@ -10,7 +10,7 @@ import {
} from "features/feature";
import TabButtonComponent from "features/tabs/TabButton.vue";
import TabFamilyComponent from "features/tabs/TabFamily.vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence";
import { Persistent, PersistentState, persistent } from "game/persistence";
import {
Computable,
GetComputableType,
@ -60,13 +60,13 @@ export type GenericTabButton = Replace<
export interface TabFamilyOptions {
visibility?: Computable<Visibility>;
tabs: Record<string, TabButtonOptions>;
classes?: Computable<Record<string, boolean>>;
style?: Computable<StyleValue>;
}
export interface BaseTabFamily extends Persistent<string> {
id: string;
tabs: Record<string, TabButtonOptions>;
activeTab: Ref<GenericTab | CoercableComponent | null>;
selected: Ref<string>;
type: typeof TabFamilyType;
@ -90,21 +90,41 @@ export type GenericTabFamily = Replace<
>;
export function createTabFamily<T extends TabFamilyOptions>(
tabs: Record<string, () => TabButtonOptions>,
optionsFunc: () => T & ThisType<TabFamily<T>>
): TabFamily<T> {
return createLazyProxy(() => {
const tabFamily: T & Partial<BaseTabFamily> = optionsFunc();
if (Object.keys(tabFamily.tabs).length === 0) {
console.warn("Cannot create tab family with 0 tabs", tabFamily);
if (Object.keys(tabs).length === 0) {
console.warn("Cannot create tab family with 0 tabs");
throw "Cannot create tab family with 0 tabs";
}
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const tabFamily: Partial<BaseTabFamily> & typeof temp = temp;
tabFamily.id = getUniqueID("tabFamily-");
tabFamily.type = TabFamilyType;
tabFamily[Component] = TabFamilyComponent;
makePersistent<string>(tabFamily, Object.keys(tabFamily.tabs)[0]);
tabFamily.tabs = Object.keys(tabs).reduce<Record<string, GenericTabButton>>(
(parsedTabs, tab) => {
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabs[tab]();
tabButton.type = TabButtonType;
tabButton[Component] = TabButtonComponent;
processComputable(tabButton as TabButtonOptions, "visibility");
setDefault(tabButton, "visibility", Visibility.Visible);
processComputable(tabButton as TabButtonOptions, "tab");
processComputable(tabButton as TabButtonOptions, "display");
processComputable(tabButton as TabButtonOptions, "classes");
processComputable(tabButton as TabButtonOptions, "style");
processComputable(tabButton as TabButtonOptions, "glowColor");
parsedTabs[tab] = tabButton as GenericTabButton;
return parsedTabs;
},
{}
);
tabFamily.selected = tabFamily[PersistentState];
tabFamily.activeTab = computed(() => {
const tabs = unref(processedTabFamily.tabs);
@ -129,20 +149,6 @@ export function createTabFamily<T extends TabFamilyOptions>(
processComputable(tabFamily as T, "classes");
processComputable(tabFamily as T, "style");
for (const tab in tabFamily.tabs) {
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabFamily.tabs[tab];
tabButton.type = TabButtonType;
tabButton[Component] = TabButtonComponent;
processComputable(tabButton as TabButtonOptions, "visibility");
setDefault(tabButton, "visibility", Visibility.Visible);
processComputable(tabButton as TabButtonOptions, "tab");
processComputable(tabButton as TabButtonOptions, "display");
processComputable(tabButton as TabButtonOptions, "classes");
processComputable(tabButton as TabButtonOptions, "style");
processComputable(tabButton as TabButtonOptions, "glowColor");
}
tabFamily[GatherProps] = function (this: GenericTabFamily) {
const { visibility, activeTab, selected, tabs, style, classes } = this;
return { visibility, activeTab, selected, tabs, style: unref(style), classes };
@ -151,5 +157,5 @@ export function createTabFamily<T extends TabFamilyOptions>(
// This is necessary because board.types is different from T and TabFamily
const processedTabFamily = tabFamily as unknown as TabFamily<T>;
return processedTabFamily;
});
}, persistent(Object.keys(tabs)[0]));
}

View file

@ -13,7 +13,7 @@ import { GenericReset } from "features/reset";
import { displayResource, Resource } from "features/resources/resource";
import { Tooltip } from "features/tooltip";
import TreeComponent from "features/trees/Tree.vue";
import { persistent } from "game/persistence";
import { deletePersistent, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import {
Computable,
@ -76,16 +76,18 @@ export type GenericTreeNode = Replace<
export function createTreeNode<T extends TreeNodeOptions>(
optionsFunc: () => T & ThisType<TreeNode<T>>
): TreeNode<T> {
const forceTooltip = persistent(false);
return createLazyProxy(() => {
const treeNode: T & Partial<BaseTreeNode> = optionsFunc();
treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType;
if (treeNode.tooltip) {
treeNode.forceTooltip = persistent(false);
treeNode.forceTooltip = forceTooltip;
} else {
// If we don't have a tooltip, no point in making this persistent
treeNode.forceTooltip = ref(false);
deletePersistent(forceTooltip);
}
processComputable(treeNode as T, "visibility");

View file

@ -23,7 +23,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence";
import { persistent, Persistent, PersistentState } from "game/persistence";
export const UpgradeType = Symbol("Upgrade");
@ -80,9 +80,10 @@ export type GenericUpgrade = Replace<
export function createUpgrade<T extends UpgradeOptions>(
optionsFunc: () => T & ThisType<Upgrade<T>>
): Upgrade<T> {
return createLazyProxy(() => {
const upgrade: T & Partial<BaseUpgrade> = optionsFunc();
makePersistent<boolean>(upgrade, false);
return createLazyProxy(persistent => {
// Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const upgrade: Partial<BaseUpgrade> & typeof temp = temp;
upgrade.id = getUniqueID("upgrade-");
upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent;
@ -167,7 +168,7 @@ export function createUpgrade<T extends UpgradeOptions>(
};
return upgrade as unknown as Upgrade<T>;
});
}, persistent<boolean>(false));
}
export function setupAutoPurchase(

View file

@ -18,7 +18,7 @@ import { createLazyProxy } from "util/proxies";
import { createNanoEvents, Emitter } from "nanoevents";
import { InjectionKey, Ref, ref, unref } from "vue";
import { globalBus } from "./events";
import { persistent, PersistentRef } from "./persistence";
import { Persistent, persistent } from "./persistence";
import player from "./player";
export interface FeatureNode {
@ -58,7 +58,6 @@ export interface Position {
}
export interface LayerOptions {
id: string;
color?: Computable<string>;
display: Computable<CoercableComponent>;
classes?: Computable<Record<string, boolean>>;
@ -70,7 +69,8 @@ export interface LayerOptions {
}
export interface BaseLayer {
minimized: PersistentRef<boolean>;
id: string;
minimized: Persistent<boolean>;
emitter: Emitter<LayerEvents>;
on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
emit: <K extends keyof LayerEvents>(event: K, ...args: Parameters<LayerEvents[K]>) => void;
@ -84,7 +84,7 @@ export type Layer<T extends LayerOptions> = Replace<
display: GetComputableType<T["display"]>;
classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>;
name: GetComputableTypeWithDefault<T["name"], T["id"]>;
name: GetComputableTypeWithDefault<T["name"], string>;
minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>;
minimizable: GetComputableTypeWithDefault<T["minimizable"], true>;
forceHideGoBack: GetComputableType<T["forceHideGoBack"]>;
@ -100,7 +100,10 @@ export type GenericLayer = Replace<
}
>;
export const persistentRefs: Record<string, Set<Persistent>> = {};
export const addingLayers: string[] = [];
export function createLayer<T extends LayerOptions>(
id: string,
optionsFunc: (() => T) & ThisType<BaseLayer>
): Layer<T> {
return createLazyProxy(() => {
@ -109,10 +112,19 @@ export function createLayer<T extends LayerOptions>(
layer.on = emitter.on.bind(emitter);
layer.emit = emitter.emit.bind(emitter);
layer.nodes = ref({});
layer.id = id;
addingLayers.push(id);
persistentRefs[id] = new Set();
layer.minimized = persistent(false);
Object.assign(layer, optionsFunc.call(layer));
if (
addingLayers[addingLayers.length - 1] == null ||
addingLayers[addingLayers.length - 1] !== id
) {
throw `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`;
}
addingLayers.pop();
processComputable(layer as T, "color");
processComputable(layer as T, "display");

View file

@ -3,10 +3,12 @@ import Decimal, { DecimalSource } from "util/bignum";
import { ProxyState } from "util/proxies";
import { isArray } from "@vue/shared";
import { isReactive, isRef, Ref, ref } from "vue";
import { GenericLayer } from "./layers";
import { addingLayers, GenericLayer, persistentRefs } from "./layers";
export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue");
export const StackTrace = Symbol("StackTrace");
export const Deleted = Symbol("Deleted");
// Note: This is a union of things that should be safely stringifiable without needing
// special processes for knowing what to load them in as
@ -20,31 +22,52 @@ export type State =
| { [key: string]: State }
| { [key: number]: State };
export type Persistent<T extends State = State> = {
export type Persistent<T extends State = State> = Ref<T> & {
[PersistentState]: Ref<T>;
[DefaultValue]: T;
[StackTrace]: string;
[Deleted]: boolean;
};
export type PersistentRef<T extends State = State> = Ref<T> & Persistent<T>;
export function persistent<T extends State>(defaultValue: T | Ref<T>): PersistentRef<T> {
function getStackTrace() {
return (
new Error().stack
?.split("\n")
.slice(3, 5)
.map(line => line.trim())
.join("\n") || ""
);
}
export function persistent<T extends State>(defaultValue: T | Ref<T>): Persistent<T> {
const persistent = (
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
) as PersistentRef<T>;
) as Persistent<T>;
persistent[PersistentState] = persistent;
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
return persistent as PersistentRef<T>;
persistent[StackTrace] = getStackTrace();
persistent[Deleted] = false;
if (addingLayers.length === 0) {
console.warn(
"Creating a persistent ref outside of a layer. This is not officially supported",
persistent,
"\nCreated at:\n" + persistent[StackTrace]
);
} else {
persistentRefs[addingLayers[addingLayers.length - 1]].add(persistent);
}
return persistent as Persistent<T>;
}
export function makePersistent<T extends State>(
obj: unknown,
defaultValue: T
): asserts obj is Persistent<T> {
const persistent = obj as Partial<Persistent<T>>;
const state = ref(defaultValue) as Ref<T>;
persistent[PersistentState] = state;
persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue;
export function deletePersistent(persistent: Persistent) {
if (addingLayers.length === 0) {
console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent);
}
persistentRefs[addingLayers[addingLayers.length - 1]].delete(persistent);
persistent[Deleted] = true;
}
globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => {
@ -56,6 +79,19 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
if (value && typeof value === "object") {
if (PersistentState in value) {
foundPersistent = true;
if ((value as Persistent)[Deleted]) {
console.warn(
"Deleted persistent ref present in returned object. Ignoring...",
value,
"\nCreated at:\n" + (value as Persistent)[StackTrace]
);
return;
}
persistentRefs[layer.id].delete(
ProxyState in value
? ((value as any)[ProxyState] as Persistent)
: (value as Persistent)
);
// Construct save path if it doesn't exist
const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => {
@ -122,4 +158,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
return foundPersistent;
};
handleObject(layer);
persistentRefs[layer.id].forEach(persistent => {
console.error(
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
persistent,
"\nCreated at:\n" + persistent[StackTrace]
);
});
persistentRefs[layer.id].clear();
});

View file

@ -17,15 +17,18 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any
// Takes a function that returns an object and pretends to be that object
// Note that the object is lazily calculated
export function createLazyProxy<T extends object>(objectFunc: () => T): T {
const obj: T | Record<string, never> = {};
export function createLazyProxy<T extends object, S>(
objectFunc: (baseObject: S) => T & S,
baseObject: S = {} as S
): T {
const obj: S & Partial<T> = baseObject;
let calculated = false;
function calculateObj(): T {
if (!calculated) {
Object.assign(obj, objectFunc());
Object.assign(obj, objectFunc(obj));
calculated = true;
}
return obj as T;
return obj as S & T;
}
return new Proxy(obj, {
@ -51,10 +54,10 @@ export function createLazyProxy<T extends object>(objectFunc: () => T): T {
},
getOwnPropertyDescriptor(target, key) {
if (!calculated) {
Object.assign(obj, objectFunc());
Object.assign(obj, objectFunc(obj));
calculated = true;
}
return Object.getOwnPropertyDescriptor(target, key);
}
}) as T;
}) as S & T;
}