2023-07-07 19:22:30 +00:00
|
|
|
// PIZZAX --- A lightweight store
|
|
|
|
|
|
|
|
import { onUnmounted, Ref, ref, watch } from "vue";
|
|
|
|
import { $i } from "./account";
|
|
|
|
import { api } from "./os";
|
|
|
|
import { stream } from "./stream";
|
|
|
|
|
|
|
|
type StateDef = Record<
|
2023-07-23 13:31:28 +00:00
|
|
|
string,
|
|
|
|
{
|
|
|
|
where: "account" | "device" | "deviceAccount";
|
|
|
|
default: any;
|
|
|
|
}
|
2023-07-07 19:22:30 +00:00
|
|
|
>;
|
|
|
|
|
|
|
|
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
|
|
|
|
|
|
|
const connection = $i && stream.useChannel("main");
|
|
|
|
|
|
|
|
export class Storage<T extends StateDef> {
|
2023-07-23 13:31:28 +00:00
|
|
|
public readonly key: string;
|
|
|
|
public readonly keyForLocalStorage: string;
|
|
|
|
|
|
|
|
public readonly def: T;
|
|
|
|
|
|
|
|
// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
|
|
|
|
public readonly state: {
|
|
|
|
[K in keyof T]: T[K]["default"];
|
|
|
|
};
|
|
|
|
public readonly reactiveState: {
|
|
|
|
[K in keyof T]: Ref<T[K]["default"]>;
|
|
|
|
};
|
|
|
|
|
|
|
|
constructor(key: string, def: T) {
|
|
|
|
this.key = key;
|
|
|
|
this.keyForLocalStorage = `pizzax::${key}`;
|
|
|
|
this.def = def;
|
|
|
|
|
|
|
|
// TODO: indexedDBにする
|
|
|
|
const deviceState = JSON.parse(
|
2024-04-08 01:10:23 +00:00
|
|
|
localStorage.getItem(this.keyForLocalStorage) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
const deviceAccountState = $i
|
|
|
|
? JSON.parse(
|
|
|
|
localStorage.getItem(
|
2024-04-08 01:10:23 +00:00
|
|
|
`${this.keyForLocalStorage}::${$i.id}`,
|
|
|
|
) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
)
|
|
|
|
: {};
|
|
|
|
const registryCache = $i
|
|
|
|
? JSON.parse(
|
|
|
|
localStorage.getItem(
|
2024-04-08 01:10:23 +00:00
|
|
|
`${this.keyForLocalStorage}::cache::${$i.id}`,
|
|
|
|
) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
)
|
|
|
|
: {};
|
|
|
|
|
|
|
|
const state = {};
|
|
|
|
const reactiveState = {};
|
|
|
|
for (const [k, v] of Object.entries(def)) {
|
|
|
|
if (
|
|
|
|
v.where === "device" &&
|
|
|
|
Object.prototype.hasOwnProperty.call(deviceState, k)
|
|
|
|
) {
|
|
|
|
state[k] = deviceState[k];
|
|
|
|
} else if (
|
|
|
|
v.where === "account" &&
|
|
|
|
$i &&
|
|
|
|
Object.prototype.hasOwnProperty.call(registryCache, k)
|
|
|
|
) {
|
|
|
|
state[k] = registryCache[k];
|
|
|
|
} else if (
|
|
|
|
v.where === "deviceAccount" &&
|
|
|
|
Object.prototype.hasOwnProperty.call(deviceAccountState, k)
|
|
|
|
) {
|
|
|
|
state[k] = deviceAccountState[k];
|
|
|
|
} else {
|
|
|
|
state[k] = v.default;
|
|
|
|
if (_DEV_) console.log("Use default value", k, v.default);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const [k, v] of Object.entries(state)) {
|
|
|
|
reactiveState[k] = ref(v);
|
|
|
|
}
|
|
|
|
this.state = state as any;
|
|
|
|
this.reactiveState = reactiveState as any;
|
|
|
|
|
|
|
|
if ($i) {
|
|
|
|
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
|
|
|
|
window.setTimeout(() => {
|
|
|
|
api("i/registry/get-all", { scope: ["client", this.key] }).then(
|
|
|
|
(kvs) => {
|
|
|
|
const cache = {};
|
|
|
|
for (const [k, v] of Object.entries(def)) {
|
|
|
|
if (v.where === "account") {
|
|
|
|
if (
|
|
|
|
Object.prototype.hasOwnProperty.call(kvs, k)
|
|
|
|
) {
|
|
|
|
state[k] = kvs[k];
|
|
|
|
reactiveState[k].value = kvs[k];
|
|
|
|
cache[k] = kvs[k];
|
|
|
|
} else {
|
|
|
|
state[k] = v.default;
|
|
|
|
reactiveState[k].value = v.default;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
localStorage.setItem(
|
|
|
|
`${this.keyForLocalStorage}::cache::${$i.id}`,
|
2024-04-08 01:10:23 +00:00
|
|
|
JSON.stringify(cache),
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
2024-04-08 01:10:23 +00:00
|
|
|
},
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
}, 1);
|
|
|
|
// streamingのuser storage updateイベントを監視して更新
|
|
|
|
connection?.on(
|
|
|
|
"registryUpdated",
|
|
|
|
({
|
|
|
|
scope,
|
|
|
|
key,
|
|
|
|
value,
|
|
|
|
}: {
|
|
|
|
scope: string[];
|
|
|
|
key: keyof T;
|
|
|
|
value: T[typeof key]["default"];
|
|
|
|
}) => {
|
|
|
|
if (
|
|
|
|
scope.length !== 2 ||
|
|
|
|
scope[0] !== "client" ||
|
|
|
|
scope[1] !== this.key ||
|
|
|
|
this.state[key] === value
|
|
|
|
)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.state[key] = value;
|
|
|
|
this.reactiveState[key].value = value;
|
|
|
|
|
|
|
|
const cache = JSON.parse(
|
|
|
|
localStorage.getItem(
|
2024-04-08 01:10:23 +00:00
|
|
|
`${this.keyForLocalStorage}::cache::${$i.id}`,
|
|
|
|
) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
if (cache[key] !== value) {
|
|
|
|
cache[key] = value;
|
|
|
|
localStorage.setItem(
|
|
|
|
`${this.keyForLocalStorage}::cache::${$i.id}`,
|
2024-04-08 01:10:23 +00:00
|
|
|
JSON.stringify(cache),
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
}
|
2024-04-08 01:10:23 +00:00
|
|
|
},
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public set<K extends keyof T>(key: K, value: T[K]["default"]): void {
|
|
|
|
if (_DEV_) console.log("set", key, value);
|
|
|
|
|
|
|
|
this.state[key] = value;
|
|
|
|
this.reactiveState[key].value = value;
|
|
|
|
|
|
|
|
switch (this.def[key].where) {
|
|
|
|
case "device": {
|
|
|
|
const deviceState = JSON.parse(
|
2024-04-08 01:10:23 +00:00
|
|
|
localStorage.getItem(this.keyForLocalStorage) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
deviceState[key] = value;
|
|
|
|
localStorage.setItem(
|
|
|
|
this.keyForLocalStorage,
|
2024-04-08 01:10:23 +00:00
|
|
|
JSON.stringify(deviceState),
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "deviceAccount": {
|
|
|
|
if ($i == null) break;
|
|
|
|
const deviceAccountState = JSON.parse(
|
|
|
|
localStorage.getItem(
|
2024-04-08 01:10:23 +00:00
|
|
|
`${this.keyForLocalStorage}::${$i.id}`,
|
|
|
|
) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
deviceAccountState[key] = value;
|
|
|
|
localStorage.setItem(
|
|
|
|
`${this.keyForLocalStorage}::${$i.id}`,
|
2024-04-08 01:10:23 +00:00
|
|
|
JSON.stringify(deviceAccountState),
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "account": {
|
|
|
|
if ($i == null) break;
|
|
|
|
const cache = JSON.parse(
|
|
|
|
localStorage.getItem(
|
2024-04-08 01:10:23 +00:00
|
|
|
`${this.keyForLocalStorage}::cache::${$i.id}`,
|
|
|
|
) || "{}",
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
cache[key] = value;
|
|
|
|
localStorage.setItem(
|
|
|
|
`${this.keyForLocalStorage}::cache::${$i.id}`,
|
2024-04-08 01:10:23 +00:00
|
|
|
JSON.stringify(cache),
|
2023-07-23 13:31:28 +00:00
|
|
|
);
|
|
|
|
api("i/registry/set", {
|
|
|
|
scope: ["client", this.key],
|
|
|
|
key: key,
|
|
|
|
value: value,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public push<K extends keyof T>(
|
|
|
|
key: K,
|
2024-04-08 01:10:23 +00:00
|
|
|
value: ArrayElement<T[K]["default"]>,
|
2023-07-23 13:31:28 +00:00
|
|
|
): void {
|
|
|
|
const currentState = this.state[key];
|
|
|
|
this.set(key, [...currentState, value]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public reset(key: keyof T) {
|
|
|
|
this.set(key, this.def[key].default);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 特定のキーの、簡易的なgetter/setterを作ります
|
|
|
|
* 主にvue場で設定コントロールのmodelとして使う用
|
|
|
|
*/
|
|
|
|
public makeGetterSetter<K extends keyof T>(
|
|
|
|
key: K,
|
|
|
|
getter?: (v: T[K]) => unknown,
|
2024-04-08 01:10:23 +00:00
|
|
|
setter?: (v: unknown) => T[K],
|
2023-07-23 13:31:28 +00:00
|
|
|
) {
|
|
|
|
const valueRef = ref(this.state[key]);
|
|
|
|
|
|
|
|
const stop = watch(this.reactiveState[key], (val) => {
|
|
|
|
valueRef.value = val;
|
|
|
|
});
|
|
|
|
|
|
|
|
// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
|
|
|
|
onUnmounted(() => {
|
|
|
|
stop();
|
|
|
|
});
|
|
|
|
|
|
|
|
// TODO: VueのcustomRef使うと良い感じになるかも
|
|
|
|
return {
|
|
|
|
get: () => {
|
|
|
|
if (getter) {
|
|
|
|
return getter(valueRef.value);
|
|
|
|
} else {
|
|
|
|
return valueRef.value;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
set: (value: unknown) => {
|
|
|
|
const val = setter ? setter(value) : value;
|
|
|
|
this.set(key, val);
|
|
|
|
valueRef.value = val;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2023-07-07 19:22:30 +00:00
|
|
|
}
|