react-native-onyx 3.0.95 → 3.0.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Onyx.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as Logger from './Logger';
2
- import type { CollectionKeyBase, ConnectOptions, InitOptions, OnyxKey, OnyxMergeCollectionInput, OnyxSetCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetInput, OnyxUpdate, SetOptions } from './types';
2
+ import type { CollectionKeyBase, ConnectOptions, InitOptions, OnyxKey, OnyxMergeCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetCollectionInput, OnyxSetInput, OnyxUpdate, SetOptions } from './types';
3
3
  import type { Connection } from './OnyxConnectionManager';
4
4
  /** Initialize the store with actions and listening for storage events */
5
5
  declare function init({ keys, initialKeyStates, evictableKeys, shouldSyncMultipleInstances, enableDevTools, skippableCollectionMemberIDs, ramOnlyKeys, snapshotMergeKeys, }: InitOptions): void;
package/dist/Onyx.js CHANGED
@@ -55,18 +55,49 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
55
55
  OnyxUtils_1.default.setSnapshotMergeKeys(new Set(snapshotMergeKeys));
56
56
  OnyxKeys_1.default.setRamOnlyKeys(new Set(ramOnlyKeys));
57
57
  if (shouldSyncMultipleInstances) {
58
- (_a = storage_1.default.keepInstancesSync) === null || _a === void 0 ? void 0 : _a.call(storage_1.default, (key, value) => {
59
- // RAM-only keys should never sync from storage as they may have stale persisted data
60
- // from before the key was migrated to RAM-only.
61
- if (OnyxKeys_1.default.isRamOnlyKey(key)) {
62
- return;
58
+ // Cross-tab sync (InstanceSync) hands us the full batch of key/value pairs that changed together in
59
+ // a single write. We process it synchronously, grouping collection members so each affected
60
+ // collection is notified once (mirroring the local mergeCollection batching) instead of
61
+ // re-delivering the whole collection per member.
62
+ (_a = storage_1.default.keepInstancesSync) === null || _a === void 0 ? void 0 : _a.call(storage_1.default, (pairs) => {
63
+ const individual = [];
64
+ const collectionBatches = new Map();
65
+ for (const [key, value] of pairs) {
66
+ // RAM-only keys should never sync from storage as they may have stale persisted data
67
+ // from before the key was migrated to RAM-only.
68
+ if (OnyxKeys_1.default.isRamOnlyKey(key)) {
69
+ continue;
70
+ }
71
+ const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
72
+ const isCollectionMember = !!collectionKey && OnyxKeys_1.default.isCollectionMemberKey(collectionKey, key);
73
+ // Capture the previous cached value BEFORE cache.set() so keysChanged() can diff old vs new per member.
74
+ const previousValue = isCollectionMember ? OnyxCache_1.default.get(key) : undefined;
75
+ OnyxCache_1.default.set(key, value);
76
+ if (isCollectionMember && collectionKey) {
77
+ let batch = collectionBatches.get(collectionKey);
78
+ if (!batch) {
79
+ batch = { partial: {}, previous: {} };
80
+ collectionBatches.set(collectionKey, batch);
81
+ }
82
+ batch.partial[key] = value;
83
+ // Keep the earliest previous value in case the same member appears twice in one batch.
84
+ if (!(key in batch.previous)) {
85
+ batch.previous[key] = previousValue;
86
+ }
87
+ }
88
+ else {
89
+ individual.push([key, value]);
90
+ }
91
+ }
92
+ // Non-collection keys: notify individually, matching keyChanged() semantics for exact keys.
93
+ for (const [key, value] of individual) {
94
+ OnyxUtils_1.default.keyChanged(key, value);
95
+ }
96
+ // One keysChanged() per collection notifies the collection-root subscriber once and lets
97
+ // keysChanged() decide which individual member subscribers actually changed.
98
+ for (const [collectionKey, { partial, previous }] of collectionBatches) {
99
+ OnyxUtils_1.default.keysChanged(collectionKey, partial, previous);
63
100
  }
64
- OnyxCache_1.default.set(key, value);
65
- // Check if this is a collection member key to prevent duplicate callbacks
66
- // When a collection is updated, individual members sync separately to other tabs
67
- // Setting isProcessingCollectionUpdate=true prevents triggering collection callbacks for each individual update
68
- const isKeyCollectionMember = OnyxKeys_1.default.isCollectionMember(key);
69
- OnyxUtils_1.default.keyChanged(key, value, undefined, isKeyCollectionMember);
70
101
  });
71
102
  }
72
103
  OnyxUtils_1.default.initStoreValues(keys, initialKeyStates, evictableKeys);
@@ -1,23 +1,29 @@
1
- /**
2
- * The InstancesSync object provides data-changed events like the ones that exist
3
- * when using LocalStorage APIs in the browser. These events are great because multiple tabs can listen for when
4
- * data changes and then stay up-to-date with everything happening in Onyx.
5
- */
6
1
  import type { OnyxKey } from '../../types';
7
- import type { StorageKeyList, OnStorageKeyChanged } from '../providers/types';
2
+ import type { StorageKeyList, OnStorageKeysChanged } from '../providers/types';
8
3
  import type StorageProvider from '../providers/types';
9
4
  /**
10
- * Raise an event through `localStorage` to let other tabs know a value changed
11
- * @param {String} onyxKey
5
+ * Raise cross-tab event(s) for a batch of changed keys. Sending keys together (instead of one event per
6
+ * key) preserves the write's batch boundary across tabs, so the receiving tab notifies collection
7
+ * subscribers once for the whole batch — matching the local mergeCollection behavior — instead of
8
+ * re-delivering the whole collection once per member (O(N^2), which can crash the tab). Large batches are
9
+ * chunked so no single payload approaches the localStorage quota.
12
10
  */
13
- declare function raiseStorageSyncEvent(onyxKey: OnyxKey): void;
14
11
  declare function raiseStorageSyncManyKeysEvent(onyxKeys: StorageKeyList): void;
12
+ /**
13
+ * Raise an event through `localStorage` to let other tabs know a single key changed.
14
+ *
15
+ * This intentionally emits the raw key (the legacy, pre-batching format) rather than a JSON array, so a
16
+ * tab still running the previous bundle during a deploy keeps receiving single-key updates (a new message,
17
+ * a pin, a rename, etc.). Only multi-key writes use the batched JSON-array format; the receiver here
18
+ * understands both. The mixed-version gap is therefore limited to bulk collection writes, which resolve on reload.
19
+ */
20
+ declare function raiseStorageSyncEvent(onyxKey: OnyxKey): void;
15
21
  declare const InstanceSync: {
16
22
  shouldBeUsed: boolean;
17
23
  /**
18
- * @param {Function} onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
24
+ * @param {Function} onStorageKeysChanged Storage synchronization mechanism keeping all opened tabs in sync
19
25
  */
20
- init: (onStorageKeyChanged: OnStorageKeyChanged, store: StorageProvider<unknown>) => void;
26
+ init: (onStorageKeysChanged: OnStorageKeysChanged, store: StorageProvider<unknown>) => void;
21
27
  setItem: typeof raiseStorageSyncEvent;
22
28
  removeItem: typeof raiseStorageSyncEvent;
23
29
  removeItems: typeof raiseStorageSyncManyKeysEvent;
@@ -1,30 +1,130 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ /**
40
+ * The InstancesSync object provides data-changed events like the ones that exist
41
+ * when using LocalStorage APIs in the browser. These events are great because multiple tabs can listen for when
42
+ * data changes and then stay up-to-date with everything happening in Onyx.
43
+ */
44
+ const Logger = __importStar(require("../../Logger"));
6
45
  const NoopProvider_1 = __importDefault(require("../providers/NoopProvider"));
7
46
  const SYNC_ONYX = 'SYNC_ONYX';
47
+ // localStorage stores values as UTF-16 (~2 bytes/char). The per-origin quota isn't fixed by the spec —
48
+ // it's user-agent dependent and commonly ~5MB — so we keep each SYNC_ONYX payload conservatively small
49
+ // (and pair it with a try/catch in emitSyncEvent). This way a large key batch (e.g. Onyx.clear() on a
50
+ // heavy account, or a bulk import) is split across several events instead of throwing QuotaExceededError.
51
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
52
+ const MAX_SYNC_PAYLOAD_LENGTH = 1000000;
8
53
  /**
9
- * Raise an event through `localStorage` to let other tabs know a value changed
10
- * @param {String} onyxKey
54
+ * Parses the SYNC_ONYX storage event value.
55
+ * The payload is a JSON array of the changed keys (a batch). It falls back to treating the raw
56
+ * value as a single key for backwards compatibility with the previous one-key-per-event format.
11
57
  */
12
- function raiseStorageSyncEvent(onyxKey) {
13
- global.localStorage.setItem(SYNC_ONYX, onyxKey);
14
- global.localStorage.removeItem(SYNC_ONYX);
58
+ function parseSyncOnyxStorageEventValue(value) {
59
+ let onyxKeys;
60
+ try {
61
+ const parsed = JSON.parse(value);
62
+ onyxKeys = Array.isArray(parsed) ? parsed : [value];
63
+ }
64
+ catch (_a) {
65
+ onyxKeys = [value];
66
+ }
67
+ return onyxKeys;
15
68
  }
69
+ /**
70
+ * Emit a single SYNC_ONYX storage event. Wrapped so a failed cross-tab signal
71
+ * degrades gracefully — other tabs simply miss this update until their next organic sync/reload — instead
72
+ * of throwing an uncaught rejection in the writing tab.
73
+ */
74
+ function emitSyncEvent(value) {
75
+ try {
76
+ global.localStorage.setItem(SYNC_ONYX, value);
77
+ global.localStorage.removeItem(SYNC_ONYX);
78
+ }
79
+ catch (error) {
80
+ Logger.logAlert(`[InstanceSync] Failed to raise storage sync event: ${error}`);
81
+ }
82
+ }
83
+ /**
84
+ * Raise cross-tab event(s) for a batch of changed keys. Sending keys together (instead of one event per
85
+ * key) preserves the write's batch boundary across tabs, so the receiving tab notifies collection
86
+ * subscribers once for the whole batch — matching the local mergeCollection behavior — instead of
87
+ * re-delivering the whole collection once per member (O(N^2), which can crash the tab). Large batches are
88
+ * chunked so no single payload approaches the localStorage quota.
89
+ */
16
90
  function raiseStorageSyncManyKeysEvent(onyxKeys) {
91
+ if (onyxKeys.length === 0) {
92
+ return;
93
+ }
94
+ let chunk = [];
95
+ let chunkLength = 2; // accounts for the surrounding `[]`
17
96
  for (const onyxKey of onyxKeys) {
18
- raiseStorageSyncEvent(onyxKey);
97
+ const keyLength = onyxKey.length + 3; // quotes + comma separator
98
+ if (chunk.length > 0 && chunkLength + keyLength > MAX_SYNC_PAYLOAD_LENGTH) {
99
+ emitSyncEvent(JSON.stringify(chunk));
100
+ chunk = [];
101
+ chunkLength = 2;
102
+ }
103
+ chunk.push(onyxKey);
104
+ chunkLength += keyLength;
105
+ }
106
+ if (chunk.length > 0) {
107
+ emitSyncEvent(JSON.stringify(chunk));
19
108
  }
20
109
  }
110
+ /**
111
+ * Raise an event through `localStorage` to let other tabs know a single key changed.
112
+ *
113
+ * This intentionally emits the raw key (the legacy, pre-batching format) rather than a JSON array, so a
114
+ * tab still running the previous bundle during a deploy keeps receiving single-key updates (a new message,
115
+ * a pin, a rename, etc.). Only multi-key writes use the batched JSON-array format; the receiver here
116
+ * understands both. The mixed-version gap is therefore limited to bulk collection writes, which resolve on reload.
117
+ */
118
+ function raiseStorageSyncEvent(onyxKey) {
119
+ emitSyncEvent(onyxKey);
120
+ }
21
121
  let storage = NoopProvider_1.default;
22
122
  const InstanceSync = {
23
123
  shouldBeUsed: true,
24
124
  /**
25
- * @param {Function} onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
125
+ * @param {Function} onStorageKeysChanged Storage synchronization mechanism keeping all opened tabs in sync
26
126
  */
27
- init: (onStorageKeyChanged, store) => {
127
+ init: (onStorageKeysChanged, store) => {
28
128
  storage = store;
29
129
  // This listener will only be triggered by events coming from other tabs
30
130
  global.addEventListener('storage', (event) => {
@@ -32,8 +132,8 @@ const InstanceSync = {
32
132
  if (event.key !== SYNC_ONYX || !event.newValue) {
33
133
  return;
34
134
  }
35
- const onyxKey = event.newValue;
36
- storage.getItem(onyxKey).then((value) => onStorageKeyChanged(onyxKey, value));
135
+ const onyxKeys = parseSyncOnyxStorageEventValue(event.newValue);
136
+ storage.multiGet(onyxKeys).then((pairs) => onStorageKeysChanged(pairs));
37
137
  });
38
138
  },
39
139
  setItem: raiseStorageSyncEvent,
@@ -181,14 +181,14 @@ const storage = {
181
181
  */
182
182
  getDatabaseSize: () => tryOrDegradePerformance(() => provider.getDatabaseSize()),
183
183
  /**
184
- * @param onStorageKeyChanged - Storage synchronization mechanism keeping all opened tabs in sync (web only)
184
+ * @param onStorageKeysChanged - Storage synchronization mechanism keeping all opened tabs in sync (web only)
185
185
  */
186
- keepInstancesSync(onStorageKeyChanged) {
186
+ keepInstancesSync(onStorageKeysChanged) {
187
187
  // If InstanceSync shouldn't be used, it means we're on a native platform and we don't need to keep instances in sync
188
188
  if (!InstanceSync_1.default.shouldBeUsed)
189
189
  return;
190
190
  shouldKeepInstancesSync = true;
191
- InstanceSync_1.default.init(onStorageKeyChanged, this);
191
+ InstanceSync_1.default.init(onStorageKeysChanged, this);
192
192
  },
193
193
  };
194
194
  exports.default = storage;
@@ -9,7 +9,8 @@ type DatabaseSize = {
9
9
  bytesRemaining: number;
10
10
  usageDetails?: Record<string, number>;
11
11
  };
12
- type OnStorageKeyChanged = <TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>) => void;
12
+ /** Called with the full batch of key/value pairs that changed together in a single cross-tab sync event. */
13
+ type OnStorageKeysChanged = (pairs: StorageKeyValuePair[]) => void;
13
14
  type StorageProvider<TStore> = {
14
15
  store: TStore;
15
16
  /**
@@ -77,9 +78,9 @@ type StorageProvider<TStore> = {
77
78
  */
78
79
  classifyError: (error: unknown) => ValueOf<typeof StorageErrorClass>;
79
80
  /**
80
- * @param onStorageKeyChanged Storage synchronization mechanism keeping all opened tabs in sync
81
+ * @param onStorageKeysChanged Storage synchronization mechanism keeping all opened tabs in sync
81
82
  */
82
- keepInstancesSync?: (onStorageKeyChanged: OnStorageKeyChanged) => void;
83
+ keepInstancesSync?: (onStorageKeysChanged: OnStorageKeysChanged) => void;
83
84
  };
84
85
  export default StorageProvider;
85
- export type { StorageKeyList, StorageKeyValuePair, OnStorageKeyChanged };
86
+ export type { StorageKeyList, StorageKeyValuePair, OnStorageKeysChanged };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.95",
3
+ "version": "3.0.96",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",