Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
*.d.ts
dist
node_modules
*.config.js
62 changes: 51 additions & 11 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
module.exports = {
extends: ['expensify', 'prettier'],
rules: {
// Overwriting this for now because web-e will conflict with this
'react/jsx-filename-extension': [1, {extensions: ['.js']}],
'rulesdir/no-multiple-onyx-in-file': 'off',
},
parser: '@typescript-eslint/parser',
env: {
jest: true,
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.native.js', '.web.js'],
overrides: [
{
files: ['*.js', '*.jsx'],
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.website.js', '.desktop.js', '.native.js', '.ios.js', '.android.js', '.config.js', '.ts', '.tsx'],
},
},
},
rules: {
// Overwriting this for now because web-e will conflict with this
'react/jsx-filename-extension': [1, {extensions: ['.js']}],
'rulesdir/no-multiple-onyx-in-file': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
},
},
ignorePatterns: 'dist',
{
files: ['*.ts', '*.tsx'],
extends: ['expensify', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/stylistic', 'plugin:import/typescript', 'prettier', 'plugin:prettier/recommended'],
plugins: ['react', 'react-native', 'import', '@typescript-eslint'],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'rulesdir/prefer-underscore-method': 'off',
'react/jsx-props-no-spreading': 'off',
'react/require-default-props': 'off',
'react/jsx-filename-extension': ['error', {extensions: ['.tsx', '.jsx']}],
'import/no-unresolved': 'error',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
'@typescript-eslint/consistent-type-imports': ['error', {prefer: 'type-imports'}],
'@typescript-eslint/consistent-type-exports': ['error', {fixMixedExportsWithInlineTypeSpecifier: false}],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/array-type': ['error', {default: 'array-simple'}],
'@typescript-eslint/consistent-type-definitions': 'off',
'rulesdir/no-multiple-onyx-in-file': 'off',
},
},
],
};
9 changes: 9 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ jobs:
- run: npm run lint
env:
CI: true

- name: Verify there's no Prettier diff
run: |
npm run prettier -- --loglevel silent
if ! git diff --name-only --exit-code; then
# shellcheck disable=SC2016
echo 'Error: Prettier diff detected! Please run `npm run prettier` and commit the changes.'
exit 1
fi
25 changes: 25 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: TypeScript Checks

on:
pull_request:
types: [opened, synchronize]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: npm
cache-dependency-path: package-lock.json

- run: npm ci

- name: Type check with TypeScript
run: npm run typecheck
env:
CI: true
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
dist/**/*.js
dist
package.json
package-lock.json
*.html
Expand Down
105 changes: 83 additions & 22 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
## Functions

<dl>
<dt><a href="#sendActionToDevTools">sendActionToDevTools(method, key, value, mergedValue)</a></dt>
<dd><p>Sends an action to DevTools extension</p>
</dd>
<dt><a href="#maybeFlushBatchUpdates">maybeFlushBatchUpdates()</a> ⇒ <code>Promise</code></dt>
<dd><p>We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
update operations. Instead of calling the subscribers for each update operation, we batch them together which will
cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.</p>
</dd>
<dt><a href="#getSubsetOfData">getSubsetOfData(sourceData, selector, [withOnyxInstanceState])</a> ⇒ <code>Mixed</code></dt>
<dd><p>Uses a selector function to return a simplified version of sourceData</p>
</dd>
Expand All @@ -25,12 +34,6 @@ If the requested key is a collection, it will return an object with all the coll
<dt><a href="#disconnect">disconnect(connectionID, [keyToRemoveFromEvictionBlocklist])</a></dt>
<dd><p>Remove the listener for a react component</p>
</dd>
<dt><a href="#maybeFlushBatchUpdates">maybeFlushBatchUpdates()</a> ⇒ <code>Promise</code></dt>
<dd><p>We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
update operations. Instead of calling the subscribers for each update operation, we batch them together which will
cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.</p>
</dd>
<dt><a href="#scheduleSubscriberUpdate">scheduleSubscriberUpdate(key, value, [canUpdateSubscriber])</a> ⇒ <code>Promise</code></dt>
<dd><p>Schedules an update that will be appended to the macro task queue (so it doesn&#39;t update the subscribers immediately).</p>
</dd>
Expand All @@ -39,11 +42,15 @@ cause react to schedule the updates at once instead of after each other. This is
so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
subscriber callbacks receive the data in a different format than they normally expect and it breaks code.</p>
</dd>
<dt><a href="#broadcastUpdate">broadcastUpdate(key, value, hasChanged, method)</a> ⇒ <code>Promise</code></dt>
<dt><a href="#broadcastUpdate">broadcastUpdate(key, value, method, hasChanged, wasRemoved)</a> ⇒ <code>Promise</code></dt>
<dd><p>Notifys subscribers and writes current value to cache</p>
</dd>
<dt><a href="#hasPendingMergeForKey">hasPendingMergeForKey(key)</a> ⇒ <code>Boolean</code></dt>
<dd></dd>
<dt><a href="#removeNullValues">removeNullValues(key, value)</a> ⇒ <code>Mixed</code></dt>
<dd><p>Removes a key from storage if the value is null.
Otherwise removes all nested null values in objects and returns the object</p>
</dd>
<dt><a href="#set">set(key, value)</a> ⇒ <code>Promise</code></dt>
<dd><p>Write a value to our store with the given key</p>
</dd>
Expand Down Expand Up @@ -83,11 +90,41 @@ value will be saved to storage after the default value.</p>
<dt><a href="#setMemoryOnlyKeys">setMemoryOnlyKeys(keyList)</a></dt>
<dd><p>When set these keys will not be persisted to storage</p>
</dd>
<dt><a href="#onClear">onClear(callback)</a></dt>
<dd><p>Sets the callback to be called when the clear finishes executing.</p>
</dd>
<dt><a href="#subscribeToEvents">subscribeToEvents()</a></dt>
<dd><p>Subscribes to the Broadcast channel and executes actions based on the
types of events.</p>
</dd>
<dt><a href="#init">init([options])</a></dt>
<dd><p>Initialize the store with actions and listening for storage events</p>
</dd>
</dl>

<a name="sendActionToDevTools"></a>

## sendActionToDevTools(method, key, value, mergedValue)
Sends an action to DevTools extension

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| method | <code>string</code> | Onyx method from METHOD |
| key | <code>string</code> | Onyx key that was changed |
| value | <code>any</code> | contains the change that was made by the method |
| mergedValue | <code>any</code> | (optional) value that was written in the storage after a merge method was executed. |

<a name="maybeFlushBatchUpdates"></a>

## maybeFlushBatchUpdates() ⇒ <code>Promise</code>
We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
update operations. Instead of calling the subscribers for each update operation, we batch them together which will
cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.

**Kind**: global function
<a name="getSubsetOfData"></a>

## getSubsetOfData(sourceData, selector, [withOnyxInstanceState]) ⇒ <code>Mixed</code>
Expand Down Expand Up @@ -182,15 +219,6 @@ Remove the listener for a react component
```js
Onyx.disconnect(connectionID);
```
<a name="maybeFlushBatchUpdates"></a>

## maybeFlushBatchUpdates() ⇒ <code>Promise</code>
We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
update operations. Instead of calling the subscribers for each update operation, we batch them together which will
cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.

**Kind**: global function
<a name="scheduleSubscriberUpdate"></a>

## scheduleSubscriberUpdate(key, value, [canUpdateSubscriber]) ⇒ <code>Promise</code>
Expand Down Expand Up @@ -224,26 +252,41 @@ subscriber callbacks receive the data in a different format than they normally e

<a name="broadcastUpdate"></a>

## broadcastUpdate(key, value, hasChanged, method) ⇒ <code>Promise</code>
## broadcastUpdate(key, value, method, hasChanged, wasRemoved) ⇒ <code>Promise</code>
Notifys subscribers and writes current value to cache

**Kind**: global function

| Param | Type | Default |
| --- | --- | --- |
| key | <code>String</code> | |
| value | <code>\*</code> | |
| method | <code>String</code> | |
| hasChanged | <code>Boolean</code> | |
| wasRemoved | <code>Boolean</code> | <code>false</code> |

<a name="hasPendingMergeForKey"></a>

## hasPendingMergeForKey(key) ⇒ <code>Boolean</code>
**Kind**: global function

| Param | Type |
| --- | --- |
| key | <code>String</code> |
| value | <code>\*</code> |
| hasChanged | <code>Boolean</code> |
| method | <code>String</code> |

<a name="hasPendingMergeForKey"></a>
<a name="removeNullValues"></a>

## removeNullValues(key, value) ⇒ <code>Mixed</code>
Removes a key from storage if the value is null.
Otherwise removes all nested null values in objects and returns the object

## hasPendingMergeForKey(key) ⇒ <code>Boolean</code>
**Kind**: global function
**Returns**: <code>Mixed</code> - The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely

| Param | Type |
| --- | --- |
| key | <code>String</code> |
| value | <code>Mixed</code> |

<a name="set"></a>

Expand Down Expand Up @@ -367,6 +410,24 @@ When set these keys will not be persisted to storage
| --- | --- |
| keyList | <code>Array.&lt;string&gt;</code> |

<a name="onClear"></a>

## onClear(callback)
Sets the callback to be called when the clear finishes executing.

**Kind**: global function

| Param | Type |
| --- | --- |
| callback | <code>function</code> |

<a name="subscribeToEvents"></a>

## subscribeToEvents()
Subscribes to the Broadcast channel and executes actions based on the
types of events.

**Kind**: global function
<a name="init"></a>

## init([options])
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,13 @@ function signOut() {
`Onyx.get`, `Onyx.set`, and the rest of the API accesses the underlying storage
differently depending on the platform

Under the hood storage access calls are delegated to a [`StorageProvider`](lib/storage/index.web.js)
Under the hood storage access calls are delegated to a [`StorageProvider`](lib/storage/index.js)
Some platforms (like web and desktop) might use the same storage provider

If a platform needs to use a separate library (like using MMVK for react-native) it should be added in the following way:
1. Create a `StorageProvider.js` at [lib/storage/providers](lib/storage/providers)
Reference an existing [StorageProvider](lib/storage/providers/AsyncStorage.js) for the interface that has to be implemented
2. Update the factory at [lib/storage/index.web.js](lib/storage/index.web.js) and [lib/storage/index.native.js](lib/storage/index.native.js) to return the newly created Provider for the desired Platform(s)
2. Update the factory at [lib/storage/index.js](lib/storage/index.js) and [lib/storage/index.native.js](lib/storage/index.native.js) to return the newly created Provider for the desired Platform(s)

# API Reference

Expand Down Expand Up @@ -433,9 +433,7 @@ The action logs use this naming convention:
`react-native` bundles source using the `metro` bundler. `metro` does not follow symlinks, so we can't use `npm link` to
link a local version of Onyx during development

To quickly test small changes you can directly go to `node_modules/react-native-onyx` in the parent project and:
- tweak original source if you're testing over a react-native project
- tweak `dist/web.development.js` for non react-native-projects
To quickly test small changes you can directly go to `node_modules/react-native-onyx` in the parent project and tweak original source code.

To continuously work on Onyx we have to set up a task that copies content to parent project's `node_modules/react-native-onyx`:
1. Work on Onyx feature or a fix
Expand Down
16 changes: 2 additions & 14 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
module.exports = (api) => {
const isWeb = api.caller((caller) => caller && caller.target === 'web');
if (isWeb) {
return {
// Default browser list is a reasonable preset covering a wide list of non-dead browsers
// https://github.com/browserslist/browserslist#best-practices
targets: 'defaults',
presets: ['@babel/preset-env', '@babel/preset-react'],
};
}

return {
presets: ['module:metro-react-native-babel-preset'],
};
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};
15 changes: 15 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
preset: 'react-native',
transform: {
'\\.[jt]sx?$': 'babel-jest',
},
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/tests/unit/mocks/', '<rootDir>/tests/e2e/'],
testMatch: ['**/tests/unit/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
globals: {
__DEV__: true,
WebSocket: {},
},
timers: 'fake',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', './jestSetup.js'],
};
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Onyx, {OnyxUpdate, ConnectOptions} from './Onyx';
import {CustomTypeOptions, OnyxCollection, OnyxEntry} from './types';
import {CustomTypeOptions, OnyxCollection, OnyxEntry, NullishDeep, KeyValueMapping, OnyxKey, Selector} from './types';
import withOnyx from './withOnyx';

export default Onyx;
export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions};
export {CustomTypeOptions, OnyxCollection, OnyxEntry, OnyxUpdate, withOnyx, ConnectOptions, NullishDeep, KeyValueMapping, OnyxKey, Selector};
File renamed without changes.
File renamed without changes.
11 changes: 0 additions & 11 deletions native.js

This file was deleted.

Loading