diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/package.json b/package.json index c5fde01..c3b2b25 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "tailwindcss-textshadow": "^2.1.3", "ts-node": "^10.7.0", "type-fest": "^2.12.2", - "typescript": "^4.6.3", + "typescript": "^4.7.2", "webpack": "^5.72.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.8.1" @@ -101,7 +101,8 @@ "react-resize-detector": "^7.0.0", "react-spring-carousel-js": "^1.9.32", "react-use": "^17.3.2", - "recoil": "^0.7.2", + "recoil": "^0.7.3-alpha.2", + "recoil-sync": "^0.0.1-alpha.2", "stream-json": "^1.7.4", "webchimera.js": "patch:webchimera.js@^0.5.2#patches/webchimera.js+0.5.2.patch", "zod": "^3.14.4" diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 092dea5..a4ad59f 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -1,4 +1,5 @@ import _Recoil from "recoil" +import _RecoilSync from "recoil-sync" import { Preload } from "../types/ipc" import { InternalPluginDefineInRenderer, DefineAtom } from "../types/plugin" import { PluginDatum } from "../types/struct" @@ -15,6 +16,11 @@ declare global { // eslint-disable-next-line no-var declare var Recoil: typeof _Recoil + // eslint-disable-next-line no-var + declare var RecoilSync: typeof _RecoilSync - declare const structuredClone: (obj: T) => T + declare function structuredClone( + obj: T, + options?: StructuredSerializeOptions + ): T } diff --git a/src/@types/recoil.d.ts b/src/@types/recoil.d.ts new file mode 100644 index 0000000..1e5233b --- /dev/null +++ b/src/@types/recoil.d.ts @@ -0,0 +1,598 @@ +import "recoil-sync" + +declare module "recoil-sync" { + // https://github.com/facebookexperimental/Recoil/blob/284f9be594db4d62e718334a59c3f5fbe1fd96aa/typescript/refine.d.ts + export namespace refine { + // Minimum TypeScript Version: 3.7 + + /** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+recoil + */ + + type CheckerResult = V extends Checker + ? Result + : V extends OptionalPropertyChecker + ? OptionalResult + : never + + export type CheckerReturnType = V extends Checker + ? Result + : never + + /** + * This file is a manual translation of the flow types, which are the source of truth, so we should not introduce new terminology or behavior in this file. + */ + + export class Path { + constructor(parent?: Path | null, field?: string) + extend(field: string): Path + toString(): string + } + + /** + * the result of failing to match a value to its expected type + */ + export type CheckFailure = Readonly<{ + type: "failure" + message: string + path: Path + }> + + /** + * the result of successfully matching a value to its expected type + */ + export type CheckSuccess = Readonly<{ + type: "success" + value: V + // if using `nullable` with the `nullWithWarningWhenInvalid` option, + // failures will be appended here + warnings: ReadonlyArray + }> + + /** + * the result of checking whether a type matches an expected value + */ + export type CheckResult = CheckSuccess | CheckFailure + + /** + * a function which checks if a given mixed value matches a type V, + * returning the value if it does, otherwise a failure message. + */ + export type Checker = (value: unknown, path?: Path) => CheckResult + + /** + * wrap value in an object signifying successful checking + */ + export function success( + value: V, + warnings: ReadonlyArray + ): CheckSuccess + + /** + * indicate typecheck failed + */ + export function failure(message: string, path: Path): CheckFailure + + /** + * utility function for composing checkers + */ + export function compose( + checker: Checker, + next: (success: CheckSuccess, path: Path) => CheckResult + ): Checker + + /** + * function to assert that a given value matches a checker + */ + export type AssertionFunction = (value: unknown) => V + + /** + * function to coerce a given value to a checker type, returning null if invalid + */ + export type CoercionFunction = (value: unknown) => V | null | undefined + + /** + * create a function to assert a value matches a checker, throwing otherwise + * + * For example, + * + * ``` + * const assert = assertion(array(number())); + * const value: Array = assert([1,2]); + * + * try { + * // should throw with `Refine.js assertion failed: ...` + * const invalid = assert('test'); + * } catch { + * } + * ``` + */ + export function assertion( + checker: Checker, + errorMessage?: string + ): AssertionFunction + + /** + * create a CoercionFunction given a checker. + * + * Allows for null-coercing a value to a given type using a checker. Optionally + * provide a callback which receives the full check + * result object (e.g. for logging). + * + * Example: + * + * ```javascript + * import {coercion, record, string} from 'refine'; + * import MyLogger from './MyLogger'; + * + * const Person = record({ + * name: string(), + * hobby: string(), + * }); + * + * const coerce = coercion(Person, result => MyLogger.log(result)); + * + * declare value: mixed; + * + * // ?Person + * const person = coerce(value); + * ``` + */ + export function coercion( + checker: Checker, + onResult?: (checker: CheckResult) => void + ): CoercionFunction + + /** + * checker to assert if a mixed value is an array of + * values determined by a provided checker + */ + export function array( + valueChecker: Checker + ): Checker> + + /** + * checker to assert if a mixed value is a tuple of values + * determined by provided checkers. Extra entries are ignored. + * + * Example: + * ```jsx + * const checker = tuple( number(), string() ); + * ``` + * + * Example with optional trailing entry: + * ```jsx + * const checker = tuple( number(), voidable(string())); + * ``` + */ + export function tuple( + ...checkers: Checkers + ): Checker }>> + + /** + * checker to assert if a mixed value is a string-keyed dict of + * values determined by a provided checker + */ + export function dict( + valueChecker: Checker + ): Checker> + + // expose opaque version of optional property as public api, + // forcing consistent usage of built-in `optional` to define optional properties + declare const __opaque: unique symbol + export interface OptionalPropertyChecker { + readonly [__opaque]: T + } + + /** + * checker which can only be used with `object` or `writablObject`. Marks a + * field as optional, skipping the key in the result if it doesn't + * exist in the input. + * + * @example + * ```jsx + * import {object, string, optional} from 'refine'; + * + * const checker = object({a: string(), b: optional(string())}); + * assert(checker({a: 1}).type === 'success'); + * ``` + */ + export function optional(checker: Checker): OptionalPropertyChecker + + type CheckerObject = Readonly<{ + [key: string]: Checker | OptionalPropertyChecker + }> + + type CheckersToValues = { + [K in keyof Checkers]: CheckerResult + } + + type WhereValue = Pick< + Checkers, + { + [Key in keyof Checkers]: Checkers[Key] extends Condition ? Key : never + }[keyof Checkers] + > + + type RequiredCheckerProperties = WhereValue< + Checkers, + Checker + > + + type OptionalCheckerProperties = Partial< + WhereValue> + > + + type ObjectCheckerResult = CheckersToValues< + RequiredCheckerProperties & OptionalCheckerProperties + > + + /** + * checker to assert if a mixed value is a fixed-property object, + * with key-value pairs determined by a provided object of checkers. + * Any extra properties in the input object values are ignored. + * Class instances are not supported, use the custom() checker for those. + * + * Example: + * ```jsx + * const myObject = object({ + * name: string(), + * job: object({ + * years: number(), + * title: string(), + * }), + * }); + * ``` + * + * Properties can be optional using `voidable()` or have default values + * using `withDefault()`: + * ```jsx + * const customer = object({ + * name: string(), + * reference: voidable(string()), + * method: withDefault(string(), 'email'), + * }); + * ``` + */ + export function object( + checkers: Checkers + ): Checker>> + + /** + * checker to assert if a mixed value is a Set type + */ + export function set(checker: Checker): Checker> + + /** + * checker to assert if a mixed value is a Map. + */ + export function map( + keyChecker: Checker, + valueChecker: Checker + ): Checker> + + /** + * identical to `array()` except the resulting value is a writable flow type. + */ + export function writableArray(valueChecker: Checker): Checker + + /** + * identical to `dict()` except the resulting value is a writable flow type. + */ + export function writableDict( + valueChecker: Checker + ): Checker<{ [key: string]: V }> + + /** + * identical to `object()` except the resulting value is a writable flow type. + */ + export function writableObject( + checkers: Checkers + ): Checker> + + /** + * function which takes a json string, parses it, + * and matches it with a checker (returning null if no match) + */ + export type JSONParser = (input: string | null | undefined) => T + + /** + * creates a JSON parser which will error if the resulting value is invalid + */ + export function jsonParserEnforced( + checker: Checker, + suffix?: string + ): JSONParser + + /** + * convienience function to wrap a checker in a function + * for easy JSON string parsing. + */ + export function jsonParser( + checker: Checker + ): JSONParser + + /** + * a mixed (i.e. untyped) value + */ + export function mixed(): Checker + + /** + * checker to assert if a mixed value matches a literal value + */ + export function literal< + T extends string | boolean | number | null | undefined + >(literalValue: T): Checker + + /** + * boolean value checker + */ + export function boolean(): Checker + + /** + * checker to assert if a mixed value is a number + */ + export function number(): Checker + + /** + * Checker to assert if a mixed value is a string. + * + * Provide an optional RegExp template to match string against. + */ + export function string(regex?: RegExp): Checker + + /** + * checker to assert if a mixed value matches a union of string literals. + * Legal values are provided as keys in an object and may be translated by + * providing values in the object. + * + * For example: + * ```jsx + * ``` + */ + export function stringLiterals(enumValues: { + readonly [key: string]: T + }): Checker + + /** + * checker to assert if a mixed value is a Date object + */ + export function date(): Checker + + /** + * Cast the type of a value after passing a given checker + * + * For example: + * + * ```javascript + * import {string, asType} from 'refine'; + * + * opaque type ID = string; + * + * const IDChecker: Checker = asType(string(), s => (s: ID)); + * ``` + */ + export function asType( + checker: Checker, + cast: (input: A) => B + ): Checker + + /** + * checker which asserts the value matches + * at least one of the two provided checkers + */ + export function or( + aChecker: Checker, + bChecker: Checker + ): Checker + + type ElementType = T extends unknown[] ? T[number] : T + + /** + * checker which asserts the value matches + * at least one of the provided checkers + */ + export function union>>( + ...checkers: Checkers + ): Checker< + ElementType<{ [K in keyof Checkers]: CheckerResult }> + > + + /** + * Provide a set of checkers to check in sequence to use the first match. + * This is similar to union(), but all checkers must have the same type. + * + * This can be helpful for supporting backward compatibility. For example the + * following loads a string type, but can also convert from a number as the + * previous version or pull from an object as an even older version: + * + * ```jsx + * const backwardCompatibilityChecker: Checker = match( + * string(), + * asType(number(), num => `${num}`), + * asType(record({num: number()}), obj => `${obj.num}`), + * ); + * ``` + */ + export function match(...checkers: ReadonlyArray>): Checker + + /** + * wraps a given checker, making the valid value nullable + * + * By default, a value passed to nullable must match the checker spec exactly + * when it is not null, or it will fail. + * + * passing the `nullWithWarningWhenInvalid` enables gracefully handling invalid + * values that are less important -- if the provided checker is invalid, + * the new checker will return null. + * + * For example: + * + * ```javascript + * import {nullable, record, string} from 'refine'; + * + * const Options = record({ + * // this must be a non-null string, + * // or Options is not valid + * filename: string(), + * + * // if this field is not a string, + * // it will be null and Options will pass the checker + * description: nullable(string(), { + * nullWithWarningWhenInvalid: true, + * }) + * }) + * + * const result = Options({filename: 'test', description: 1}); + * + * invariant(result.type === 'success'); + * invariant(result.value.description === null); + * invariant(result.warnings.length === 1); // there will be a warning + * ``` + */ + export function nullable( + checker: Checker, + options?: Readonly<{ + // if this is true, the checker will not fail + // validation if an invalid value is provided, instead + // returning null and including a warning as to the invalid type. + nullWithWarningWhenInvalid?: boolean + }> + ): Checker + + /** + * wraps a given checker, making the valid value voidable + * + * By default, a value passed to voidable must match the checker spec exactly + * when it is not undefined, or it will fail. + * + * passing the `undefinedWithWarningWhenInvalid` enables gracefully handling invalid + * values that are less important -- if the provided checker is invalid, + * the new checker will return undefined. + * + * For example: + * + * ```javascript + * import {voidable, record, string} from 'refine'; + * + * const Options = record({ + * // this must be a string, or Options is not valid + * filename: string(), + * + * // this must be a string or undefined, + * // or Options is not valid + * displayName: voidable(string()), + * + * // if this field is not a string, + * // it will be undefined and Options will pass the checker + * description: voidable(string(), { + * undefinedWithWarningWhenInvalid: true, + * }) + * }) + * + * const result = Options({filename: 'test', description: 1}); + * + * invariant(result.type === 'success'); + * invariant(result.value.description === undefined); + * invariant(result.warnings.length === 1); // there will be a warning + * ``` + */ + export function voidable( + checker: Checker, + options?: Readonly<{ + // if this is true, the checker will not fail + // validation if an invalid value is provided, instead + // returning undefined and including a warning as to the invalid type. + undefinedWithWarningWhenInvalid?: boolean + }> + ): Checker + + /** + * a checker that provides a withDefault value if the provided value is nullable. + * + * For example: + * ```jsx + * const objPropertyWithDefault = record({ + * foo: withDefault(number(), 123), + * }); + * ``` + * Both `{}` and `{num: 123}` will refine to `{num: 123}` + */ + export function withDefault(checker: Checker, fallback: T): Checker + + /** + * wraps a checker with a logical constraint. + * + * Predicate function can return either a boolean result or + * a tuple with a result and message + * + * For example: + * + * ```javascript + * import {number, constraint} from 'refine'; + * + * const evenNumber = constraint( + * number(), + * n => n % 2 === 0 + * ); + * + * const passes = evenNumber(2); + * // passes.type === 'success'; + * + * const fails = evenNumber(1); + * // fails.type === 'failure'; + * ``` + */ + export function constraint( + checker: Checker, + predicate: (value: T) => boolean | [boolean, string] + ): Checker + + /** + * wrapper to allow for passing a lazy checker value. This enables + * recursive types by allowing for passing in the returned value of + * another checker. For example: + * + * ```javascript + * const user = record({ + * id: number(), + * name: string(), + * friends: array(lazy(() => user)) + * }); + * ``` + * + * Example of array with arbitrary nesting depth: + * ```jsx + * const entry = or(number(), array(lazy(() => entry))); + * const nestedArray = array(entry); + * ``` + */ + export function lazy(getChecker: () => Checker): Checker + + /** + * helper to create a custom checker from a provided function. + * If the function returns a non-nullable value, the checker succeeds. + * + * ```jsx + * const myClassChecker = custom(x => x instanceof MyClass ? x : null); + * ``` + * + * Nullable custom types can be created by composing with `nullable()` or + * `voidable()` checkers: + * + * ```jsx + * const maybeMyClassChecker = + * nullable(custom(x => x instanceof MyClass ? x : null)); + * ``` + */ + export function custom( + checkValue: (value: unknown) => null | T, + failureMessage?: string + ): Checker + } +} diff --git a/src/Plugin.tsx b/src/Plugin.tsx index dcbe233..3ef000a 100644 --- a/src/Plugin.tsx +++ b/src/Plugin.tsx @@ -34,8 +34,8 @@ import { } from "./atoms/mirakurunSelectors" import { Splash } from "./components/global/Splash" import { - RECOIL_SHARED_ATOM_KEYS, - RECOIL_STORED_ATOM_KEYS, + RECOIL_SYNC_SHARED_KEY, + RECOIL_SYNC_STORED_KEY, } from "./constants/recoil" import { ROUTES } from "./constants/routes" import { @@ -59,8 +59,6 @@ export const PluginLoader: React.VFC<{ disabledPluginFileNames: string[] }> = ({ states, pluginData, fonts, disabledPluginFileNames }) => { const [isLoading, setIsLoading] = useState(true) - const [sharedAtoms, setSharedAtoms] = useState(RECOIL_SHARED_ATOM_KEYS) - const [storedAtoms, setStoredAtoms] = useState(RECOIL_STORED_ATOM_KEYS) useEffect(() => { if (isLoading === false) { return @@ -127,6 +125,12 @@ export const PluginLoader: React.VFC<{ mirakurunVersionSelector, mirakurunServicesSelector, }, + constants: { + recoil: { + storedKey: RECOIL_SYNC_STORED_KEY, + sharedKey: RECOIL_SYNC_SHARED_KEY, + }, + }, } window.pluginData = pluginData window.disabledPluginFileNames = disabledPluginFileNames @@ -158,11 +162,7 @@ export const PluginLoader: React.VFC<{ `[Plugin] 読込中: ${plugin.name} (${plugin.id}, ${plugin.version})` ) if ( - ![ - ...plugin.storedAtoms, - ...plugin.sharedAtoms, - ...plugin.exposedAtoms, - ].every( + !plugin.exposedAtoms.every( (atomDef) => (atomDef.type === "atom" && atomDef.atom.key.startsWith("plugins.")) || @@ -196,40 +196,6 @@ export const PluginLoader: React.VFC<{ ) try { await plugin.setup({ plugins: openedPlugins }) - plugin.sharedAtoms - .map((atomDef) => - "key" in atomDef - ? atomDef - : { ...atomDef, key: atomDef.atom.key } - ) - .forEach((atom) => { - const mached = atoms.find((_atom) => - _atom.type === "atom" - ? _atom.atom.key === atom.key - : _atom.key === atom.key - ) - if (!mached) { - atoms.push(atom) - } - setSharedAtoms((atoms) => [...atoms, atom.key]) - }) - plugin.storedAtoms - .map((atomDef) => - "key" in atomDef - ? atomDef - : { ...atomDef, key: atomDef.atom.key } - ) - .forEach((atom) => { - const mached = atoms.find((_atom) => - _atom.type === "atom" - ? _atom.atom.key === atom.key - : _atom.key === atom.key - ) - if (!mached) { - atoms.push(atom) - } - setStoredAtoms((atoms) => [...atoms, atom.key]) - }) plugin.exposedAtoms .map((atomDef) => "key" in atomDef @@ -273,12 +239,5 @@ export const PluginLoader: React.VFC<{ if (isLoading) { return } - return ( - - ) + return } diff --git a/src/State.tsx b/src/State.tsx index f716245..b5276ff 100644 --- a/src/State.tsx +++ b/src/State.tsx @@ -3,8 +3,8 @@ import { QueryClient, QueryClientProvider } from "react-query" import { RecoilRoot } from "recoil" import { Router } from "./Router" import { PluginPositionComponents } from "./components/common/PluginPositionComponents" -import { RecoilApplier } from "./components/global/RecoilApplier" -import { RecoilObserver } from "./components/global/RecoilObserver" +import { RecoilSharedSync } from "./components/global/RecoilSharedSync" +import { RecoilStoredSync } from "./components/global/RecoilStoredSync" import { ObjectLiteral } from "./types/struct" import { initializeState } from "./utils/recoil" @@ -18,21 +18,17 @@ const queryClient = new QueryClient({ export const StateRoot: React.VFC<{ states: ObjectLiteral - sharedAtoms: string[] - storedAtoms: string[] fonts: string[] -}> = ({ states, sharedAtoms, storedAtoms, fonts }) => { +}> = ({ states, fonts }) => { return ( - - + +
({ export const contentPlayerBoundsAtom = atom({ key: `${prefix}.bounds`, default: null, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: $.nullable($.mixed()), + }), + ], }) export const contentPlayerSubtitleEnabledAtom = atom({ key: `${prefix}.subtitleEnabled`, default: false, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: $.boolean(), + }), + ], }) export const contentPlayerIsPlayingAtom = atom({ @@ -31,6 +45,9 @@ export const contentPlayerIsPlayingAtom = atom({ export const contentPlayerVolumeAtom = atom({ key: `${prefix}.volume`, default: 100, + effects: [ + syncEffect({ storeKey: RECOIL_SYNC_STORED_KEY, refine: $.number() }), + ], }) export const contentPlayerSpeedAtom = atom({ @@ -108,6 +125,17 @@ export const contentPlayerKeyForRestorationAtom = atom({ key: `${prefix}.keyForRestoration`, default: null, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: $.nullable( + $.object({ + contentType: $.string(), + serviceId: $.number(), + }) + ), + }), + ], }) export const lastEpgUpdatedAtom = atom({ diff --git a/src/atoms/global.ts b/src/atoms/global.ts index c1678c2..a620f5b 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -1,8 +1,9 @@ import { atom } from "recoil" +import { syncEffect, refine as $ } from "recoil-sync" import pkg from "../../package.json" import { - RECOIL_SHARED_ATOM_KEYS, - RECOIL_STORED_ATOM_KEYS, + RECOIL_SYNC_SHARED_KEY, + RECOIL_SYNC_STORED_KEY, } from "../constants/recoil" import { globalActiveContentPlayerIdAtomKey, @@ -13,37 +14,63 @@ import { const prefix = `${pkg.name}.global` -export const globalSharedAtomsAtom = atom({ - key: `${prefix}.sharedAtoms`, - default: RECOIL_SHARED_ATOM_KEYS, -}) - -export const globalStoredAtomsAtom = atom({ - key: `${prefix}.storedAtoms`, - default: RECOIL_STORED_ATOM_KEYS, -}) - export const globalActiveContentPlayerIdAtom = atom({ key: globalActiveContentPlayerIdAtomKey, default: null, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.nullable($.number()), + }), + ], }) export const globalContentPlayerIdsAtom = atom({ key: globalContentPlayerIdsAtomKey, default: [], + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.array($.number()), + }), + ], }) export const globalFontsAtom = atom({ key: `${prefix}.fonts`, default: [], + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.array($.string()), + }), + ], }) export const globalLastEpgUpdatedAtom = atom({ key: globalLastEpgUpdatedAtomKey, default: 0, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.number(), + }), + ], }) +const globalDisabledPluginFileNamesAtomRefine = $.array($.string()) + export const globalDisabledPluginFileNamesAtom = atom({ key: globalDisabledPluginFileNamesAtomKey, default: [], + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: globalDisabledPluginFileNamesAtomRefine, + }), + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: globalDisabledPluginFileNamesAtomRefine, + }), + ], }) diff --git a/src/atoms/globalFamilies.ts b/src/atoms/globalFamilies.ts index 123a446..17c46db 100644 --- a/src/atoms/globalFamilies.ts +++ b/src/atoms/globalFamilies.ts @@ -1,4 +1,6 @@ import { atomFamily } from "recoil" +import { syncEffect, refine as $ } from "recoil-sync" +import { RECOIL_SYNC_SHARED_KEY } from "../constants/recoil" import { Service } from "../infra/mirakurun/api" import { ContentPlayerPlayingContent } from "../types/contentPlayer" @@ -13,6 +15,19 @@ export const globalContentPlayerPlayingContentFamily = atomFamily< >({ key: globalContentPlayerPlayingContentFamilyKey, default: null, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.nullable( + $.object({ + contentType: $.string(), + url: $.string(), + program: $.voidable($.mixed()), + service: $.voidable($.mixed()), + }) + ), + }), + ], }) export const globalContentPlayerSelectedServiceFamily = atomFamily< @@ -21,4 +36,10 @@ export const globalContentPlayerSelectedServiceFamily = atomFamily< >({ key: globalContentPlayerSelectedServiceFamilyKey, default: null, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.nullable($.mixed()), + }), + ], }) diff --git a/src/atoms/mirakurun.ts b/src/atoms/mirakurun.ts index 5b37540..79c8fee 100644 --- a/src/atoms/mirakurun.ts +++ b/src/atoms/mirakurun.ts @@ -1,5 +1,7 @@ import { atom } from "recoil" +import { syncEffect, refine as $ } from "recoil-sync" import pkg from "../../package.json" +import { RECOIL_SYNC_SHARED_KEY } from "../constants/recoil" import { MirakurunCompatibilityTypes, ServiceWithLogoData, @@ -21,4 +23,10 @@ export const mirakurunVersionAtom = atom({ export const mirakurunServicesAtom = atom({ key: `${prefix}.services`, default: null, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: $.nullable($.array($.mixed())), + }), + ], }) diff --git a/src/atoms/settings.ts b/src/atoms/settings.ts index a29449a..46149fd 100644 --- a/src/atoms/settings.ts +++ b/src/atoms/settings.ts @@ -1,6 +1,11 @@ import { atom } from "recoil" +import { syncEffect, refine as $ } from "recoil-sync" import pkg from "../../package.json" import { SUBTITLE_DEFAULT_FONT } from "../constants/font" +import { + RECOIL_SYNC_SHARED_KEY, + RECOIL_SYNC_STORED_KEY, +} from "../constants/recoil" import type { ControllerSetting, ExperimentalSetting, @@ -15,16 +20,42 @@ import { const prefix = `${pkg.name}.settings` +const mirakurunSettingRefine = $.object({ + isEnableServiceTypeFilter: $.boolean(), + baseUrl: $.voidable($.string()), + userAgent: $.voidable($.string()), +}) + export const mirakurunSetting = atom({ key: `${prefix}.mirakurun`, default: { isEnableServiceTypeFilter: true, }, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: mirakurunSettingRefine, + }), + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: mirakurunSettingRefine, + }), + ], }) export const mirakurunUrlHistory = atom({ key: `${prefix}.mirakurunUrlHistory`, default: [], + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: $.array($.string()), + }), + ], +}) + +const controllerSettingRefine = $.object({ + volumeRange: $.array($.number()), }) export const controllerSetting = atom({ @@ -32,6 +63,20 @@ export const controllerSetting = atom({ default: { volumeRange: [0, 150], }, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: controllerSettingRefine, + }), + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: controllerSettingRefine, + }), + ], +}) + +const subtitleSettingRefine = $.object({ + font: $.string(), }) export const subtitleSetting = atom({ @@ -39,6 +84,23 @@ export const subtitleSetting = atom({ default: { font: SUBTITLE_DEFAULT_FONT, }, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: subtitleSettingRefine, + }), + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: subtitleSettingRefine, + }), + ], +}) + +const screenshotSettingRefine = $.object({ + saveAsAFile: $.boolean(), + includeSubtitle: $.boolean(), + keepQuality: $.boolean(), + basePath: $.voidable($.string()), }) export const screenshotSetting = atom({ @@ -48,6 +110,24 @@ export const screenshotSetting = atom({ includeSubtitle: true, keepQuality: true, }, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: screenshotSettingRefine, + }), + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: screenshotSettingRefine, + }), + ], +}) + +const experimentalSettingRefine = $.object({ + isWindowDragMoveEnabled: $.boolean(), + isVlcAvCodecHwAny: $.boolean(), + vlcNetworkCaching: $.number(), + isDualMonoAutoAdjustEnabled: $.boolean(), + globalScreenshotAccelerator: $.or($.string(), $.boolean()), }) export const experimentalSetting = atom({ @@ -59,4 +139,14 @@ export const experimentalSetting = atom({ isDualMonoAutoAdjustEnabled: true, globalScreenshotAccelerator: false, }, + effects: [ + syncEffect({ + storeKey: RECOIL_SYNC_SHARED_KEY, + refine: experimentalSettingRefine, + }), + syncEffect({ + storeKey: RECOIL_SYNC_STORED_KEY, + refine: experimentalSettingRefine, + }), + ], }) diff --git a/src/components/global/RecoilApplier.tsx b/src/components/global/RecoilApplier.tsx deleted file mode 100644 index 67926bc..0000000 --- a/src/components/global/RecoilApplier.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useEffect, useState } from "react" -import { useSetRecoilState } from "recoil" -import type { SetterOrUpdater } from "recoil" -import { ALL_ATOMS, ALL_FAMILIES } from "../../atoms" -import { SerializableKV } from "../../types/ipc" -import { Atom, AtomFamily } from "../../types/plugin" - -const AtomSetter: React.VFC<{ - payload: SerializableKV -}> = ({ payload }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let setter: SetterOrUpdater - if (payload) { - if (payload.key.includes("__")) { - const [, ...splited] = payload.key.split("__") - const args = JSON.parse(splited.join("__")) - const atom = [ - ...ALL_FAMILIES.map((atom) => atom(args)), - ...( - window.atoms?.filter( - (atomDef): atomDef is AtomFamily => atomDef.type === "family" - ) || [] - ).map(({ atom }) => atom(args)), - ].find((atom) => atom.key === payload.key) - if (atom) { - setter = useSetRecoilState(atom) - } - } else { - const atom = [ - ...ALL_ATOMS, - ...(( - (window.atoms?.filter((atomDef) => atomDef.type === "atom") || - []) as Atom[] - ).map(({ atom }) => atom) || []), - ].find((atom) => atom.key === payload.key) - if (atom) { - setter = useSetRecoilState(atom) - } - } - } - - useEffect(() => { - if (payload && setter !== undefined) { - payload && setter && setter(payload.value) - } else { - console.warn("対象の atom が見つかりません", payload) - } - }, [payload]) - return <> -} - -export const RecoilApplier: React.VFC<{}> = () => { - const [payload, setPayload] = useState(null) - useEffect(() => { - const fn = (payload: SerializableKV) => { - if (!payload.key) return - const { key, value } = payload - setPayload({ key, value }) - } - const off = window.Preload.onRecoilStateUpdate(fn) - return () => { - off() - } - }, []) - if (payload) { - return - } - return <> -} diff --git a/src/components/global/RecoilObserver.tsx b/src/components/global/RecoilObserver.tsx deleted file mode 100644 index c069af4..0000000 --- a/src/components/global/RecoilObserver.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react" -import { useRecoilTransactionObserver_UNSTABLE, useRecoilValue } from "recoil" -import type { SerializableParam } from "recoil" -import { - globalSharedAtomsAtom, - globalStoredAtomsAtom, -} from "../../atoms/global" - -export const RecoilObserver: React.VFC<{}> = () => { - const sharedAtoms = useRecoilValue(globalSharedAtomsAtom) - const storedAtoms = useRecoilValue(globalStoredAtomsAtom) - useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => { - for (const atom of snapshot.getNodes_UNSTABLE({ isModified: true })) { - const key = atom.key.split("__").shift() - if (!key) throw new Error("key error: " + atom.key) - if (sharedAtoms.includes(key)) { - const value = snapshot.getLoadable(atom).getValue() as SerializableParam - window.Preload.recoilStateUpdate({ - key: atom.key, - value, - }) - } - if (storedAtoms.includes(atom.key)) { - try { - const value = snapshot.getLoadable(atom).getValue() - window.Preload.store.set(atom.key, value) - } catch (error) { - console.error(error) - } - } - } - }) - return <> -} diff --git a/src/components/global/RecoilSharedSync.tsx b/src/components/global/RecoilSharedSync.tsx new file mode 100644 index 0000000..a3288da --- /dev/null +++ b/src/components/global/RecoilSharedSync.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useRef } from "react" +import type { SerializableParam } from "recoil" +import { DefaultValue } from "recoil" +import { useRecoilSync } from "recoil-sync" +import { RECOIL_SYNC_SHARED_KEY } from "../../constants/recoil" +import { SerializableKV } from "../../types/ipc" +import { ObjectLiteral } from "../../types/struct" + +export const RecoilSharedSync: React.FC<{ initialStates: ObjectLiteral }> = ({ + initialStates, +}) => { + const statesRef = useRef(new Map(Object.entries(initialStates))) + const broadcastChannelRef = useRef(null) + useEffect(() => { + const broadcastChannel = new BroadcastChannel("recoil-sync") + broadcastChannelRef.current = broadcastChannel + return () => { + broadcastChannelRef.current = null + broadcastChannel.close() + } + }, []) + useRecoilSync({ + storeKey: RECOIL_SYNC_SHARED_KEY, + read: (key) => { + const state = statesRef.current + if (!state.has(key)) { + return new DefaultValue() + } + return state.get(key) + }, + write: ({ diff }) => { + broadcastChannelRef.current?.postMessage(diff) + for (const [key, value] of diff.entries()) { + window.Preload.recoilStateUpdate({ + key, + value: value as SerializableParam, + }) + statesRef.current.set(key, value) + } + }, + listen: ({ updateItem }) => { + const broadcastChannel = broadcastChannelRef.current + if (!broadcastChannel) { + return + } + const listener = (event: MessageEvent>) => { + for (const [key, value] of event.data.entries()) { + updateItem(key, value) + } + } + broadcastChannel.addEventListener("message", listener) + const onPayloadFromMain = (payload: SerializableKV) => { + if (!payload.key) return + const { key, value } = payload + updateItem(key, value) + } + const off = window.Preload.onRecoilStateUpdate(onPayloadFromMain) + return () => { + broadcastChannel.removeEventListener("message", listener) + off() + } + }, + }) + return null +} diff --git a/src/components/global/RecoilStoredSync.tsx b/src/components/global/RecoilStoredSync.tsx new file mode 100644 index 0000000..d3c5212 --- /dev/null +++ b/src/components/global/RecoilStoredSync.tsx @@ -0,0 +1,34 @@ +import React, { useRef } from "react" +import { DefaultValue } from "recoil" +import { useRecoilSync } from "recoil-sync" +import { RECOIL_SYNC_STORED_KEY } from "../../constants/recoil" + +export const RecoilStoredSync: React.FC<{}> = () => { + const mapRef = useRef(new Map()) + useRecoilSync({ + storeKey: RECOIL_SYNC_STORED_KEY, + read: (key) => { + const map = mapRef.current + if (map.has(key)) { + return map.get(key) + } + const value = window.Preload.store.get(key) + if (typeof value === "undefined") { + return new DefaultValue() + } + map.set(key, value) + return value + }, + write: ({ diff }) => { + for (const [key, value] of diff.entries()) { + mapRef.current.set(key, value) + if (typeof value === "undefined") { + window.Preload.store.delete(key) + } else { + window.Preload.store.set(key, value) + } + } + }, + }) + return null +} diff --git a/src/constants/recoil.ts b/src/constants/recoil.ts index 0d3480e..dd273cc 100644 --- a/src/constants/recoil.ts +++ b/src/constants/recoil.ts @@ -1,50 +1,3 @@ -import { - contentPlayerBoundsAtom, - contentPlayerKeyForRestorationAtom, - contentPlayerSubtitleEnabledAtom, - contentPlayerVolumeAtom, -} from "../atoms/contentPlayer" -import { - globalContentPlayerPlayingContentFamilyKey, - globalContentPlayerSelectedServiceFamilyKey, -} from "../atoms/globalFamilyKeys" -import { - globalActiveContentPlayerIdAtomKey, - globalDisabledPluginFileNamesAtomKey, -} from "../atoms/globalKeys" -import { mirakurunServicesAtom } from "../atoms/mirakurun" -import { - controllerSetting, - experimentalSetting, - mirakurunSetting, - mirakurunUrlHistory, - screenshotSetting, - subtitleSetting, -} from "../atoms/settings" +export const RECOIL_SYNC_SHARED_KEY = "shared" -export const RECOIL_SHARED_ATOM_KEYS = [ - mirakurunSetting.key, - controllerSetting.key, - subtitleSetting.key, - screenshotSetting.key, - experimentalSetting.key, - mirakurunServicesAtom.key, - globalActiveContentPlayerIdAtomKey, - globalContentPlayerPlayingContentFamilyKey, - globalContentPlayerSelectedServiceFamilyKey, - globalDisabledPluginFileNamesAtomKey, -] - -export const RECOIL_STORED_ATOM_KEYS = [ - mirakurunSetting.key, - mirakurunUrlHistory.key, - controllerSetting.key, - subtitleSetting.key, - screenshotSetting.key, - experimentalSetting.key, - contentPlayerVolumeAtom.key, - contentPlayerKeyForRestorationAtom.key, - contentPlayerBoundsAtom.key, - contentPlayerSubtitleEnabledAtom.key, - globalDisabledPluginFileNamesAtomKey, -] +export const RECOIL_SYNC_STORED_KEY = "stored" diff --git a/src/index.web.tsx b/src/index.web.tsx index 24b2c2f..c2cc36b 100644 --- a/src/index.web.tsx +++ b/src/index.web.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react" import ReactDOM from "react-dom" import Recoil from "recoil" +import RecoilSync from "recoil-sync" import { PluginLoader } from "./Plugin" import { Splash } from "./components/global/Splash" import { InitialData } from "./types/struct" @@ -8,6 +9,7 @@ import "./index.scss" global.React = React global.Recoil = Recoil +global.RecoilSync = RecoilSync const WebRoot: React.VFC<{}> = () => { const [unmounted, setUnmounted] = useState(false) diff --git a/src/main/index.electron.ts b/src/main/index.electron.ts index 2e8389e..dcade4b 100644 --- a/src/main/index.electron.ts +++ b/src/main/index.electron.ts @@ -593,15 +593,14 @@ ipcMain.handle(UPDATE_IS_PLAYING_STATE, (event, isPlaying: boolean) => { const states: ObjectLiteral = {} const statesHash: ObjectLiteral = {} -const recoilStateUpdate = (source: number, payload: SerializableKV) => { +const recoilStateUpdate = (payload: SerializableKV) => { for (const window of BrowserWindow.getAllWindows()) { - if (window.webContents.id === source) continue window.webContents.send(RECOIL_STATE_UPDATE, payload) } } const updateContentPlayerIds = () => { - recoilStateUpdate(-1, { + recoilStateUpdate({ key: GLOBAL_CONTENT_PLAYER_IDS, value: contentPlayerWindows.map((w) => w.id), }) @@ -616,7 +615,6 @@ ipcMain.handle(RECOIL_STATE_UPDATE, (event, payload: SerializableKV) => { if (hash !== statesHash[key]) { statesHash[key] = hash states[key] = value - recoilStateUpdate(event.sender.id, payload) } }) @@ -840,7 +838,7 @@ const openWindow = ({ const value = contentPlayerWindows.slice(0).shift()?.id ?? null // 生Recoil states[globalActiveContentPlayerIdAtomKey] = value - recoilStateUpdate(_id, { + recoilStateUpdate({ key: globalActiveContentPlayerIdAtomKey, value, }) @@ -950,11 +948,18 @@ ipcMain.handle( } ) +let lastEpgUpdated = 0 + new EPGManager(ipcMain, () => { - const value = Date.now() + const value = Math.floor(performance.now()) + // 5秒に1回のみ + if (value - lastEpgUpdated < 5000) { + return + } + lastEpgUpdated = value // 生Recoil states[globalLastEpgUpdatedAtomKey] = value - recoilStateUpdate(0, { + recoilStateUpdate({ key: globalLastEpgUpdatedAtomKey, value, }) diff --git a/src/main/preload.ts b/src/main/preload.ts index c9949b9..17cfb6b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -235,6 +235,9 @@ const preload: Preload = { set: (key: string, value: unknown) => { store.set(key, value) }, + delete: (key) => { + store.delete(key as never) + }, openConfig() { store.openInEditor() }, diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 477878c..a234e3d 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -72,6 +72,7 @@ export type Preload = { store: { set: (key: string, value: T) => void get: (key: string) => T + delete: (key: string) => void openConfig: () => void } onUpdateIsPlayingState: (listener: (isPlaying: boolean) => void) => () => void diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 36666bd..750ec17 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -22,22 +22,28 @@ export type CustomComponent = { component: React.VFC<{}> } +/** すべてのウィンドウに展開される。見えない。 */ export type OnBackgroundComponent = { position: "onBackground" -} & CustomComponent // すべてのウィンドウに展開される。見えない。 +} & CustomComponent -export type OnSplashComponent = { position: "onSplash" } & CustomComponent // ほぼ見えない。バックグラウンド実行用などに +/** ほぼ見えない。バックグラウンド実行用などに */ +export type OnSplashComponent = { position: "onSplash" } & CustomComponent +/** プラグイン設定画面 */ export type OnSettingComponent = { position: "onSetting" label: string -} & CustomComponent // 設定画面 +} & CustomComponent -export type OnPlayerComponent = { position: "onPlayer" } & CustomComponent // プレイヤーの上、字幕より後ろ +/** プレイヤーの上、字幕より後ろ */ +export type OnPlayerComponent = { position: "onPlayer" } & CustomComponent -export type OnSubtitleComponent = { position: "onSubtitle" } & CustomComponent // 字幕より上、コントローラーより後ろ +/** 字幕より上、コントローラーより後ろ */ +export type OnSubtitleComponent = { position: "onSubtitle" } & CustomComponent -export type OnForwardComponent = { position: "onForward" } & CustomComponent // 一番前、pointer-events: noneのため触りたい場合は該当部分だけautoにしておくこと +/** 一番前、pointer-events: noneのため触りたい場合は該当部分だけautoにしておくこと */ +export type OnForwardComponent = { position: "onForward" } & CustomComponent export type ComponentWithPosition = | OnBackgroundComponent @@ -96,6 +102,12 @@ export type PluginInRendererArgs = { mirakurunVersionSelector: Recoil.RecoilValueReadOnly mirakurunServicesSelector: Recoil.RecoilValueReadOnly } + constants: { + recoil: { + sharedKey: string + storedKey: string + } + } } export type InitPluginInRenderer = ( @@ -140,7 +152,7 @@ export type AtomFamily = { export type DefineAtom = Atom | AtomFamily export type PluginMeta = { - // 推奨 id フォーマット: `plugins.${authorNamespace}.${pluginNamespace}` or `io.github.c..`(java 形式) + /** 推奨 id フォーマット: `plugins.${authorNamespace}.${pluginNamespace}` or `io.github.c..`(java 形式) */ id: string name: string version: string @@ -148,7 +160,8 @@ export type PluginMeta = { authorUrl?: string description: string url?: string - destroy: () => void | Promise // 現時点で正しく実行される保証はない、セットアップが正常に終了していなくても呼ばれる点に注意 + /** 現時点で正しく実行される保証はない、セットアップが正常に終了していなくても呼ばれる点に注意 */ + destroy: () => void | Promise } export type PluginDefineInRenderer = PluginMeta & { @@ -157,17 +170,23 @@ export type PluginDefineInRenderer = PluginMeta & { }: { plugins: PluginDefineInRenderer[] }) => void | Promise - // 重要: atom の key は `plugins.${authorNamespace}.${pluginNamespace}.` から開始、大きくルールに反する atom (`plugins.`から開始しない)を露出したプラグインはロードされない - exposedAtoms: DefineAtom[] // 他のプラグインと連携するとか - sharedAtoms: DefineAtom[] // ウィンドウ間で共有する(シリアライズ可能にすること) - storedAtoms: DefineAtom[] // 保存する(シリアライズ可能にすること) - // コンポーネントとウィンドウは shadowRoot に展開されるので、各自独自に CSS をバンドルしないとスタイリングが初期化される点に注意する + /** 他のプラグインと連携するとか + * 重要: atom の key は `plugins.${authorNamespace}.${pluginNamespace}.` から開始、大きくルールに反する atom (`plugins.`から開始しない)を露出したプラグインはロードされない */ + exposedAtoms: DefineAtom[] + /** ウィンドウ間で共有する Atom(シリアライズ可能にすること) + * @deprecated 後方互換性のためのフィールドです、同時に Atom に syncEffect も設定してください */ + sharedAtoms: DefineAtom[] + /** 保存する Atom(シリアライズ可能にすること)。 + * @deprecated 後方互換性のためのフィールドです、同時に Atom に syncEffect も設定してください */ + storedAtoms: DefineAtom[] + /** コンポーネントとウィンドウは shadowRoot に展開されるので、各自独自に CSS をバンドルしないとスタイリングが初期化される点に注意する */ components: ComponentWithPosition[] windows: { - [key: string]: React.VFC<{}> // カスタム画面、hash を key に + /** カスタム画面、hash を key に */ + [key: string]: React.VFC<{}> } _experimental_feature__service?: { - // テレビサービス(構想中) + /** テレビサービス(構想中) */ contentType: string restoreByKey: ( arg: unknown diff --git a/src/types/struct.ts b/src/types/struct.ts index 792c1fd..4d30a16 100644 --- a/src/types/struct.ts +++ b/src/types/struct.ts @@ -1,6 +1,6 @@ import { ROUTES } from "../constants/routes" -export type ObjectLiteral = { [key: string]: T } +export type ObjectLiteral = Record export type Routes = keyof typeof ROUTES | (string & {}) diff --git a/src/utils/plugin.ts b/src/utils/plugin.ts index 1e3c353..c3240d1 100644 --- a/src/utils/plugin.ts +++ b/src/utils/plugin.ts @@ -11,8 +11,6 @@ export const pluginValidator = $.object({ setup: $.function(), destroy: $.function(), exposedAtoms: $.array($.any()), - sharedAtoms: $.array($.any()), - storedAtoms: $.array($.any()), components: $.array($.any()), windows: $.object({}), contextMenu: $.object({}).optional(), diff --git a/src/utils/recoil.ts b/src/utils/recoil.ts index f38bcc3..7b565bd 100644 --- a/src/utils/recoil.ts +++ b/src/utils/recoil.ts @@ -1,66 +1,13 @@ import { useEffect, useRef } from "react" import { useRecoilValue } from "recoil" import type { MutableSnapshot, RecoilState } from "recoil" -import { ALL_ATOMS, ALL_FAMILIES } from "../atoms" -import { - globalFontsAtom, - globalSharedAtomsAtom, - globalStoredAtomsAtom, -} from "../atoms/global" -import { AtomFamily } from "../types/plugin" +import { globalFontsAtom } from "../atoms/global" import { ObjectLiteral } from "../types/struct" export const initializeState = - ({ - states, - storedAtoms, - sharedAtoms, - fonts, - }: { - states: ObjectLiteral - storedAtoms: string[] - sharedAtoms: string[] - fonts: string[] - }) => + ({ fonts }: { states: ObjectLiteral; fonts: string[] }) => (mutableSnapShot: MutableSnapshot) => { - mutableSnapShot.set(globalSharedAtomsAtom, sharedAtoms) - mutableSnapShot.set(globalStoredAtomsAtom, storedAtoms) mutableSnapShot.set(globalFontsAtom, fonts) - storedAtoms.forEach((key) => { - const savedValue = window.Preload.store.get(key) - const atom = - ALL_ATOMS.find((atom) => "key" in atom && atom.key === key) || - window.atoms?.find((atom) => "key" in atom && atom.key === key) - if (savedValue !== undefined && savedValue !== null && atom) { - mutableSnapShot.set(atom as never, savedValue) - } else { - console.warn("[Recoil] ignored in initialize:", key, savedValue) - } - }) - Object.entries(states).map(([key, value]) => { - let arg: unknown - try { - arg = JSON.parse(key.split("__").pop() || "") - } catch (error) { - arg = null - } - const atom = - ALL_ATOMS.find((atom) => "key" in atom && atom.key === key) || - window.atoms?.find((atom) => "key" in atom && atom.key === key) || - (arg && - ALL_FAMILIES.find((family) => family(arg).key === key)?.(arg)) || - window.atoms - ?.find( - (atom): atom is AtomFamily => - atom.type === "family" && atom.atom(arg).key === key - ) - ?.atom?.(arg) - if (atom) { - mutableSnapShot.set(atom as never, value) - } else { - console.warn("[Recoil] ignored in initialize:", key) - } - }) } export const useRecoilValueRef = (s: RecoilState) => { diff --git a/yarn.lock b/yarn.lock index fbcbeab..d98258b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10302,7 +10302,8 @@ __metadata: react-resize-detector: ^7.0.0 react-spring-carousel-js: ^1.9.32 react-use: ^17.3.2 - recoil: ^0.7.2 + recoil: ^0.7.3-alpha.2 + recoil-sync: ^0.0.1-alpha.2 sass: ^1.50.0 sass-loader: ^12.6.0 stream-json: ^1.7.4 @@ -10311,7 +10312,7 @@ __metadata: tailwindcss-textshadow: ^2.1.3 ts-node: ^10.7.0 type-fest: ^2.12.2 - typescript: ^4.6.3 + typescript: ^4.7.2 webchimera.js: "patch:webchimera.js@^0.5.2#patches/webchimera.js+0.5.2.patch" webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -11873,9 +11874,20 @@ __metadata: languageName: node linkType: hard -"recoil@npm:^0.7.2": - version: 0.7.2 - resolution: "recoil@npm:0.7.2" +"recoil-sync@npm:^0.0.1-alpha.2": + version: 0.0.1-alpha.2 + resolution: "recoil-sync@npm:0.0.1-alpha.2" + dependencies: + transit-js: ^0.8.874 + peerDependencies: + recoil: ">=0.7.0" + checksum: f718f5b2de4a77100d3b778f2aef71f29c506cd1cc216cddc49920940fbe16093643264ec095cd03a3f8e3468ef6ec6f907965e41c30a8c88a4c7794a8f8f549 + languageName: node + linkType: hard + +"recoil@npm:^0.7.3-alpha.2": + version: 0.7.3-alpha.2 + resolution: "recoil@npm:0.7.3-alpha.2" dependencies: hamt_plus: 1.0.2 peerDependencies: @@ -11885,7 +11897,7 @@ __metadata: optional: true react-native: optional: true - checksum: 2ad0c2052fd33ee358e3b448fd36ea6d4b37e69ace65ff44124605171c4794e4f167215209731aac3d45d42ea9f4c67e9c5a0704ff04ab94b64bcca96d5f59a3 + checksum: 3af4b8ef88f0602515bd2730a8dba12f9aa866db5ce1310e5dc8a431b2e5bd4b5e16b9bc9b2e496b0024b78ece80ebfd61797eb4e64b6cbc62e4e23833d02c6a languageName: node linkType: hard @@ -13597,6 +13609,13 @@ __metadata: languageName: node linkType: hard +"transit-js@npm:^0.8.874": + version: 0.8.874 + resolution: "transit-js@npm:0.8.874" + checksum: a1d3a78a0ce926320ba32cdd59a74104e1b440855497753b91f8e71831302b23adfb21416cdba30153305cf41cf96b75421a607a1799b676572fe6072ee04798 + languageName: node + linkType: hard + "traverse@npm:>=0.3.0 <0.4": version: 0.3.9 resolution: "traverse@npm:0.3.9" @@ -13828,13 +13847,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.6.3": - version: 4.6.3 - resolution: "typescript@npm:4.6.3" +"typescript@npm:^4.7.2": + version: 4.7.2 + resolution: "typescript@npm:4.7.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 255bb26c8cb846ca689dd1c3a56587af4f69055907aa2c154796ea28ee0dea871535b1c78f85a6212c77f2657843a269c3a742d09d81495b97b914bf7920415b + checksum: 5163585e6b56410f77d5483b698d9489bbee8902c99029eb70cf6d21525a186530ce19a00951af84eefd4a131cc51d0959f5118e25e70ab61f45ac4057dbd1ef languageName: node linkType: hard @@ -13848,13 +13867,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^4.6.3#~builtin": - version: 4.6.3 - resolution: "typescript@patch:typescript@npm%3A4.6.3#~builtin::version=4.6.3&hash=493e53" +"typescript@patch:typescript@^4.7.2#~builtin": + version: 4.7.2 + resolution: "typescript@patch:typescript@npm%3A4.7.2#~builtin::version=4.7.2&hash=493e53" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: fe6bdc1afb2f145ddb7b0a3a31f96352209f6a5704d97f038414ea22ff9d8dd42f32cfb6652e30458d7d958d2d4e85de2df11c574899c6f750a6b3c0e90a3a76 + checksum: 09d93fc0983d38eadd9b0427f790b49b4437f45002a87d447be3fbe53120880e87a91dd03e1d900498f99205d6e0b7c9784fe41fca11d56f4bbce371f74bb160 languageName: node linkType: hard