Split flipper-plugin package
Summary: flipper-server-companion depends on flipper-plugin. flipper-plugin includes dependencies that run only in a browser. Splitting flipper-plugin into core and browser packages helps to avoid including browser-only dependencies into flipper-server bundle. As a result, bundle size could be cut in half. Subsequently, RSS usage drops as there is twice as less code to process for V8. Note: it currently breaks external flipper-data-source package. It will be restored in subsequent diffs Reviewed By: lblasa Differential Revision: D38658285 fbshipit-source-id: 751b11fa9f3a2d938ce166687b8310ba8b059dee
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2090120cda
commit
97b8b8a1c4
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {createState} from '../atom';
|
||||
|
||||
test('can subscribe to atom state changes', () => {
|
||||
const state = createState('abc');
|
||||
let receivedValue: string | undefined;
|
||||
let receivedPrevValue: string | undefined;
|
||||
const unsubscribe = state.subscribe((value, prevValue) => {
|
||||
receivedValue = value;
|
||||
receivedPrevValue = prevValue;
|
||||
});
|
||||
try {
|
||||
state.set('def');
|
||||
expect(receivedValue).toBe('def');
|
||||
expect(receivedPrevValue).toBe('abc');
|
||||
state.set('ghi');
|
||||
expect(receivedValue).toBe('ghi');
|
||||
expect(receivedPrevValue).toBe('def');
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
test('can unsubscribe from atom state changes', () => {
|
||||
const state = createState('abc');
|
||||
let receivedValue: string | undefined;
|
||||
let receivedPrevValue: string | undefined;
|
||||
const unsubscribe = state.subscribe((value, prevValue) => {
|
||||
receivedValue = value;
|
||||
receivedPrevValue = prevValue;
|
||||
});
|
||||
try {
|
||||
state.set('def');
|
||||
expect(receivedValue).toBe('def');
|
||||
expect(receivedPrevValue).toBe('abc');
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
state.set('ghi');
|
||||
expect(receivedValue).toBe('def');
|
||||
expect(receivedPrevValue).toBe('abc');
|
||||
});
|
||||
|
||||
test('can unsubscribe from atom state changes using unsubscribe method', () => {
|
||||
const state = createState('abc');
|
||||
let receivedValue: string | undefined;
|
||||
let receivedPrevValue: string | undefined;
|
||||
const listener = (value: string, prevValue: string) => {
|
||||
receivedValue = value;
|
||||
receivedPrevValue = prevValue;
|
||||
};
|
||||
state.subscribe(listener);
|
||||
try {
|
||||
state.set('def');
|
||||
expect(receivedValue).toBe('def');
|
||||
expect(receivedPrevValue).toBe('abc');
|
||||
} finally {
|
||||
state.unsubscribe(listener);
|
||||
}
|
||||
state.set('ghi');
|
||||
expect(receivedValue).toBe('def');
|
||||
expect(receivedPrevValue).toBe('abc');
|
||||
});
|
||||
141
desktop/flipper-plugin-core/src/state/atom.tsx
Normal file
141
desktop/flipper-plugin-core/src/state/atom.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {produce, Draft, enableMapSet} from 'immer';
|
||||
import {
|
||||
getCurrentPluginInstance,
|
||||
Persistable,
|
||||
registerStorageAtom,
|
||||
} from '../plugin/PluginBase';
|
||||
import {
|
||||
deserializeShallowObject,
|
||||
makeShallowSerializable,
|
||||
} from '../utils/shallowSerialization';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
export interface ReadOnlyAtom<T> {
|
||||
get(): T;
|
||||
subscribe(listener: (value: T, prevValue: T) => void): () => void;
|
||||
unsubscribe(listener: (value: T, prevValue: T) => void): void;
|
||||
}
|
||||
|
||||
export interface Atom<T> extends ReadOnlyAtom<T> {
|
||||
set(newValue: T): void;
|
||||
update(recipe: (draft: Draft<T>) => void): void;
|
||||
update<X extends T>(recipe: (draft: X) => void): void;
|
||||
}
|
||||
|
||||
export class AtomValue<T> implements Atom<T>, Persistable {
|
||||
value: T;
|
||||
listeners: ((value: T, prevValue: T) => void)[] = [];
|
||||
|
||||
constructor(initialValue: T) {
|
||||
this.value = initialValue;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set(nextValue: T) {
|
||||
if (nextValue !== this.value) {
|
||||
const prevValue = this.value;
|
||||
this.value = nextValue;
|
||||
this.notifyChanged(prevValue);
|
||||
}
|
||||
}
|
||||
|
||||
deserialize(value: T) {
|
||||
this.set(deserializeShallowObject(value));
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return makeShallowSerializable(this.get());
|
||||
}
|
||||
|
||||
update(recipe: (draft: Draft<T>) => void) {
|
||||
this.set(produce(this.value, recipe));
|
||||
}
|
||||
|
||||
notifyChanged(prevValue: T) {
|
||||
// TODO: add scheduling
|
||||
this.listeners.slice().forEach((l) => l(this.value, prevValue));
|
||||
}
|
||||
|
||||
subscribe(listener: (value: T, prevValue: T) => void) {
|
||||
this.listeners.push(listener);
|
||||
return () => this.unsubscribe(listener);
|
||||
}
|
||||
|
||||
unsubscribe(listener: (value: T, prevValue: T) => void) {
|
||||
const idx = this.listeners.indexOf(listener);
|
||||
if (idx !== -1) {
|
||||
this.listeners.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type StateOptions = {
|
||||
/**
|
||||
* Should this state persist when exporting a plugin?
|
||||
* If set, the atom will be saved / loaded under the key provided
|
||||
*/
|
||||
persist?: string;
|
||||
/**
|
||||
* Store this state in local storage, instead of as part of the plugin import / export.
|
||||
* State stored in local storage is shared between the same plugin
|
||||
* across multiple clients/ devices, but not actively synced.
|
||||
*/
|
||||
persistToLocalStorage?: boolean;
|
||||
};
|
||||
|
||||
export function createState<T>(
|
||||
initialValue: T,
|
||||
options?: StateOptions,
|
||||
): Atom<T>;
|
||||
export function createState<T>(): Atom<T | undefined>;
|
||||
export function createState(
|
||||
initialValue: any = undefined,
|
||||
options: StateOptions = {},
|
||||
): Atom<any> {
|
||||
const atom = new AtomValue(initialValue);
|
||||
if (options?.persistToLocalStorage) {
|
||||
syncAtomWithLocalStorage(options, atom);
|
||||
} else {
|
||||
registerStorageAtom(options.persist, atom);
|
||||
}
|
||||
return atom;
|
||||
}
|
||||
|
||||
function syncAtomWithLocalStorage(options: StateOptions, atom: AtomValue<any>) {
|
||||
if (!options?.persist) {
|
||||
throw new Error(
|
||||
"The 'persist' option should be set when 'persistToLocalStorage' is set",
|
||||
);
|
||||
}
|
||||
const pluginInstance = getCurrentPluginInstance();
|
||||
if (!pluginInstance) {
|
||||
throw new Error(
|
||||
"The 'persistToLocalStorage' option cannot be used outside a plugin definition",
|
||||
);
|
||||
}
|
||||
const storageKey = `flipper:${pluginInstance.definition.id}:atom:${options.persist}`;
|
||||
const storedValue = window.localStorage.getItem(storageKey);
|
||||
if (storedValue != null) {
|
||||
atom.deserialize(JSON.parse(storedValue));
|
||||
}
|
||||
atom.subscribe(() => {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(atom.serialize()));
|
||||
});
|
||||
}
|
||||
|
||||
export function isAtom(value: any): value is Atom<any> {
|
||||
return value instanceof AtomValue;
|
||||
}
|
||||
29
desktop/flipper-plugin-core/src/state/batch.tsx
Normal file
29
desktop/flipper-plugin-core/src/state/batch.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export let batch: (callback: (...args: any[]) => void) => void = (callback) =>
|
||||
callback();
|
||||
|
||||
export const setBatchedUpdateImplementation = (
|
||||
impl: (callback: (...args: any[]) => void) => void,
|
||||
) => {
|
||||
batch = impl;
|
||||
};
|
||||
|
||||
export function batched<T extends Function>(fn: T): T;
|
||||
export function batched(fn: any) {
|
||||
return function (this: any) {
|
||||
let res: any;
|
||||
batch(() => {
|
||||
// eslint-disable-next-line
|
||||
res = fn.apply(this, arguments);
|
||||
});
|
||||
return res;
|
||||
};
|
||||
}
|
||||
43
desktop/flipper-plugin-core/src/state/createDataSource.tsx
Normal file
43
desktop/flipper-plugin-core/src/state/createDataSource.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
DataSource,
|
||||
createDataSource as baseCreateDataSource,
|
||||
DataSourceOptions as BaseDataSourceOptions,
|
||||
DataSourceOptionKey as BaseDataSourceOptionKey,
|
||||
} from '../data-source/DataSource';
|
||||
import {registerStorageAtom} from '../plugin/PluginBase';
|
||||
|
||||
type DataSourceOptions = BaseDataSourceOptions & {
|
||||
/**
|
||||
* Should this state persist when exporting a plugin?
|
||||
* If set, the dataSource will be saved / loaded under the key provided
|
||||
*/
|
||||
persist?: string;
|
||||
};
|
||||
|
||||
export function createDataSource<T, Key extends keyof T>(
|
||||
initialSet: readonly T[],
|
||||
options: DataSourceOptions & BaseDataSourceOptionKey<Key>,
|
||||
): DataSource<T, T[Key] extends string | number ? T[Key] : never>;
|
||||
export function createDataSource<T>(
|
||||
initialSet?: readonly T[],
|
||||
options?: DataSourceOptions,
|
||||
): DataSource<T, never>;
|
||||
export function createDataSource<T, Key extends keyof T>(
|
||||
initialSet: readonly T[] = [],
|
||||
options?: DataSourceOptions & BaseDataSourceOptionKey<Key>,
|
||||
): DataSource<T, T[Key] extends string | number ? T[Key] : never> {
|
||||
const ds = options
|
||||
? baseCreateDataSource(initialSet, options)
|
||||
: baseCreateDataSource(initialSet);
|
||||
registerStorageAtom(options?.persist, ds);
|
||||
return ds;
|
||||
}
|
||||
Reference in New Issue
Block a user