nexabase-console 2.1.8 → 2.2.0

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.
@@ -2,10 +2,16 @@ export declare const FIELD_VALUE_BRAND: unique symbol;
2
2
  export type FieldValueMethod = 'serverTimestamp' | 'increment' | 'arrayUnion' | 'arrayRemove' | 'deleteField';
3
3
  export declare class FieldValue {
4
4
  readonly [FIELD_VALUE_BRAND] = true;
5
+ readonly _isFieldValue: boolean;
5
6
  readonly method: FieldValueMethod;
6
7
  readonly value?: any;
7
8
  constructor(method: FieldValueMethod, value?: any);
8
9
  static isFieldValue(val: any): val is FieldValue;
10
+ toJSON(): {
11
+ _isFieldValue: boolean;
12
+ method: FieldValueMethod;
13
+ value: any;
14
+ };
9
15
  isEqual(other: any): boolean;
10
16
  }
11
17
  export declare const serverTimestamp: () => FieldValue;
@@ -7,14 +7,27 @@ exports.FIELD_VALUE_BRAND = Symbol.for('nexabase.fieldValue');
7
7
  class FieldValue {
8
8
  constructor(method, value) {
9
9
  this[_a] = true;
10
+ this._isFieldValue = true;
10
11
  this.method = method;
11
12
  this.value = value;
13
+ this._isFieldValue = true;
12
14
  Object.freeze(this);
13
15
  }
14
16
  static isFieldValue(val) {
15
17
  return (val !== null &&
16
18
  typeof val === 'object' &&
17
- (val instanceof FieldValue || val[exports.FIELD_VALUE_BRAND] === true));
19
+ (val instanceof FieldValue ||
20
+ val[exports.FIELD_VALUE_BRAND] === true ||
21
+ val._isFieldValue === true ||
22
+ (typeof val.method === 'string' &&
23
+ ['serverTimestamp', 'increment', 'arrayUnion', 'arrayRemove', 'deleteField'].includes(val.method))));
24
+ }
25
+ toJSON() {
26
+ return {
27
+ _isFieldValue: true,
28
+ method: this.method,
29
+ value: this.value !== undefined ? this.value : undefined
30
+ };
18
31
  }
19
32
  isEqual(other) {
20
33
  if (!FieldValue.isFieldValue(other))
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export * from './persistence/LocalStore';
28
28
  export * from './persistence/MutationQueue';
29
29
  export * from './sync/SyncEngine';
30
30
  export * from './sync/ConflictResolver';
31
+ export * from './sync/CrossTabSync';
31
32
  export * from './transport/HttpClient';
32
33
  export * from './transport/SSEClient';
33
34
  export * from './transport/WebSocketClient';
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ __exportStar(require("./persistence/MutationQueue"), exports);
52
52
  // Sync
53
53
  __exportStar(require("./sync/SyncEngine"), exports);
54
54
  __exportStar(require("./sync/ConflictResolver"), exports);
55
+ __exportStar(require("./sync/CrossTabSync"), exports);
55
56
  // Transport
56
57
  __exportStar(require("./transport/HttpClient"), exports);
57
58
  __exportStar(require("./transport/SSEClient"), exports);
@@ -0,0 +1,83 @@
1
+ export interface CrossTabMessage<T = any> {
2
+ id: string;
3
+ senderTabId: string;
4
+ projectId?: string;
5
+ type: string;
6
+ timestamp: number;
7
+ payload: T;
8
+ }
9
+ export interface LocalMutationPayload {
10
+ path: string;
11
+ action: 'set' | 'patch' | 'update' | 'delete' | string;
12
+ data?: any;
13
+ metadata?: {
14
+ mutationId?: string;
15
+ source?: 'local' | 'server' | string;
16
+ state?: 'pending' | 'acknowledged' | string;
17
+ hasPendingWrites?: boolean;
18
+ [key: string]: any;
19
+ };
20
+ }
21
+ export type CrossTabMessageHandler<T = any> = (payload: T, message: CrossTabMessage<T>) => void;
22
+ export type MutationSyncHandler = (path: string, action: string, data?: any, metadata?: LocalMutationPayload['metadata']) => void;
23
+ /**
24
+ * Serializes rich JavaScript objects (Timestamp, GeoPoint, Date) into structured JSON-safe
25
+ * objects with explicit type tags so they can cross BroadcastChannel or localStorage boundaries safely.
26
+ */
27
+ export declare function serializeCrossTabData(data: any, seen?: WeakSet<object>): any;
28
+ /**
29
+ * Deserializes structured JSON-safe objects back into rich JavaScript types (Timestamp, GeoPoint, etc.).
30
+ */
31
+ export declare function deserializeCrossTabData(data: any): any;
32
+ /**
33
+ * Checks if native BroadcastChannel is supported in the current environment.
34
+ */
35
+ export declare function isBroadcastChannelSupported(): boolean;
36
+ /**
37
+ * CrossTabSyncChannel provides ultra-fast, local cross-tab state synchronization
38
+ * with native BroadcastChannel support and automatic fallback to localStorage storage events.
39
+ */
40
+ export declare class CrossTabSyncChannel {
41
+ readonly channelName: string;
42
+ readonly tabId: string;
43
+ private broadcastChannel;
44
+ private listeners;
45
+ private wildcardListeners;
46
+ private storageListener;
47
+ private isClosed;
48
+ constructor(channelName: string);
49
+ private initTransport;
50
+ private handleIncomingRawMessage;
51
+ /**
52
+ * Broadcasts a typed message across all other browser tabs.
53
+ */
54
+ broadcast<T = any>(type: string, payload: T, projectId?: string): void;
55
+ /**
56
+ * Broadcasts a local mutation event (write, update, delete) to keep caches across tabs in sync instantly.
57
+ */
58
+ broadcastMutation(path: string, action: string, data?: any, metadata?: LocalMutationPayload['metadata'], projectId?: string): void;
59
+ /**
60
+ * Subscribes to a specific event type. Returns an unsubscribe callback.
61
+ */
62
+ on<T = any>(type: string, handler: CrossTabMessageHandler<T>): () => void;
63
+ /**
64
+ * Subscribes to all incoming messages regardless of type.
65
+ */
66
+ onAny(handler: CrossTabMessageHandler): () => void;
67
+ /**
68
+ * Helper specifically for listening to local mutations across tabs.
69
+ */
70
+ onMutation(handler: MutationSyncHandler): () => void;
71
+ /**
72
+ * Closes the channel and removes all event listeners.
73
+ */
74
+ close(): void;
75
+ }
76
+ /**
77
+ * Returns a singleton instance of CrossTabSyncChannel for the given channel name.
78
+ */
79
+ export declare function getCrossTabChannel(channelName: string): CrossTabSyncChannel;
80
+ /**
81
+ * Creates or gets a project-scoped cross-tab synchronization channel.
82
+ */
83
+ export declare function getProjectCrossTabChannel(projectId: string): CrossTabSyncChannel;
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CrossTabSyncChannel = void 0;
4
+ exports.serializeCrossTabData = serializeCrossTabData;
5
+ exports.deserializeCrossTabData = deserializeCrossTabData;
6
+ exports.isBroadcastChannelSupported = isBroadcastChannelSupported;
7
+ exports.getCrossTabChannel = getCrossTabChannel;
8
+ exports.getProjectCrossTabChannel = getProjectCrossTabChannel;
9
+ const Timestamp_1 = require("../firestore/Timestamp");
10
+ const GeoPoint_1 = require("../firestore/GeoPoint");
11
+ const QueryUtils_1 = require("../firestore/QueryUtils");
12
+ /**
13
+ * Generates a unique Tab ID for distinguishing cross-tab messages from current tab.
14
+ */
15
+ function createTabId() {
16
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
17
+ return crypto.randomUUID();
18
+ }
19
+ return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
20
+ }
21
+ /**
22
+ * Serializes rich JavaScript objects (Timestamp, GeoPoint, Date) into structured JSON-safe
23
+ * objects with explicit type tags so they can cross BroadcastChannel or localStorage boundaries safely.
24
+ */
25
+ function serializeCrossTabData(data, seen = new WeakSet()) {
26
+ if (data === null || data === undefined)
27
+ return data;
28
+ const type = typeof data;
29
+ if (type === 'boolean' || type === 'number' || type === 'string')
30
+ return data;
31
+ if (data instanceof Date) {
32
+ return {
33
+ __nexa_type__: 'timestamp',
34
+ seconds: Math.floor(data.getTime() / 1000),
35
+ nanoseconds: (data.getTime() % 1000) * 1000000
36
+ };
37
+ }
38
+ if (data instanceof Timestamp_1.Timestamp) {
39
+ return {
40
+ __nexa_type__: 'timestamp',
41
+ seconds: data.seconds,
42
+ nanoseconds: data.nanoseconds
43
+ };
44
+ }
45
+ // Handle Timestamp-like objects (e.g. if prototype was stripped)
46
+ if (typeof data === 'object' &&
47
+ 'seconds' in data &&
48
+ 'nanoseconds' in data &&
49
+ typeof data.seconds === 'number' &&
50
+ typeof data.nanoseconds === 'number') {
51
+ return {
52
+ __nexa_type__: 'timestamp',
53
+ seconds: data.seconds,
54
+ nanoseconds: data.nanoseconds
55
+ };
56
+ }
57
+ if (data instanceof GeoPoint_1.GeoPoint) {
58
+ return {
59
+ __nexa_type__: 'geopoint',
60
+ latitude: data.latitude,
61
+ longitude: data.longitude
62
+ };
63
+ }
64
+ if (typeof data === 'object' &&
65
+ 'latitude' in data &&
66
+ 'longitude' in data &&
67
+ typeof data.latitude === 'number' &&
68
+ typeof data.longitude === 'number') {
69
+ return {
70
+ __nexa_type__: 'geopoint',
71
+ latitude: data.latitude,
72
+ longitude: data.longitude
73
+ };
74
+ }
75
+ if (Array.isArray(data)) {
76
+ if (seen.has(data))
77
+ return null;
78
+ seen.add(data);
79
+ const res = data.map((item) => serializeCrossTabData(item, seen));
80
+ seen.delete(data);
81
+ return res;
82
+ }
83
+ if (typeof data === 'object') {
84
+ if (seen.has(data))
85
+ return null;
86
+ seen.add(data);
87
+ const res = {};
88
+ for (const key of Object.keys(data)) {
89
+ res[key] = serializeCrossTabData(data[key], seen);
90
+ }
91
+ seen.delete(data);
92
+ return res;
93
+ }
94
+ return data;
95
+ }
96
+ /**
97
+ * Deserializes structured JSON-safe objects back into rich JavaScript types (Timestamp, GeoPoint, etc.).
98
+ */
99
+ function deserializeCrossTabData(data) {
100
+ return (0, QueryUtils_1.reviveNexaData)(data);
101
+ }
102
+ /**
103
+ * Checks if native BroadcastChannel is supported in the current environment.
104
+ */
105
+ function isBroadcastChannelSupported() {
106
+ return typeof window !== 'undefined' && typeof window.BroadcastChannel !== 'undefined';
107
+ }
108
+ /**
109
+ * CrossTabSyncChannel provides ultra-fast, local cross-tab state synchronization
110
+ * with native BroadcastChannel support and automatic fallback to localStorage storage events.
111
+ */
112
+ class CrossTabSyncChannel {
113
+ constructor(channelName) {
114
+ this.broadcastChannel = null;
115
+ this.listeners = new Map();
116
+ this.wildcardListeners = new Set();
117
+ this.storageListener = null;
118
+ this.isClosed = false;
119
+ this.channelName = channelName;
120
+ this.tabId = createTabId();
121
+ this.initTransport();
122
+ }
123
+ initTransport() {
124
+ if (typeof window === 'undefined')
125
+ return;
126
+ if (isBroadcastChannelSupported()) {
127
+ try {
128
+ this.broadcastChannel = new window.BroadcastChannel(this.channelName);
129
+ this.broadcastChannel.onmessage = (event) => {
130
+ this.handleIncomingRawMessage(event.data);
131
+ };
132
+ return;
133
+ }
134
+ catch (err) {
135
+ console.warn(`[CrossTabSyncChannel] Native BroadcastChannel failed for "${this.channelName}", falling back to StorageEvent:`, err);
136
+ }
137
+ }
138
+ // Fallback: localStorage storage events
139
+ if (typeof window.addEventListener === 'function' && typeof window.localStorage !== 'undefined') {
140
+ const storageKey = `__nexa_crosstab_${this.channelName}`;
141
+ this.storageListener = (e) => {
142
+ if (e.key === storageKey && e.newValue) {
143
+ try {
144
+ const raw = JSON.parse(e.newValue);
145
+ this.handleIncomingRawMessage(raw);
146
+ }
147
+ catch (parseErr) {
148
+ // Ignore parse errors from concurrent writes
149
+ }
150
+ }
151
+ };
152
+ window.addEventListener('storage', this.storageListener);
153
+ }
154
+ }
155
+ handleIncomingRawMessage(raw) {
156
+ if (!raw || typeof raw !== 'object')
157
+ return;
158
+ const msg = raw;
159
+ // Discard messages originating from the current tab
160
+ if (msg.senderTabId === this.tabId)
161
+ return;
162
+ // Reconstruct rich objects (Timestamp, GeoPoint)
163
+ const revivedPayload = deserializeCrossTabData(msg.payload);
164
+ const message = {
165
+ ...msg,
166
+ payload: revivedPayload
167
+ };
168
+ // Notify specific type listeners
169
+ if (msg.type && this.listeners.has(msg.type)) {
170
+ const typeSet = this.listeners.get(msg.type);
171
+ typeSet.forEach((fn) => {
172
+ try {
173
+ fn(revivedPayload, message);
174
+ }
175
+ catch (e) {
176
+ console.error(`[CrossTabSyncChannel] Error in listener for type "${msg.type}":`, e);
177
+ }
178
+ });
179
+ }
180
+ // Notify wildcard listeners
181
+ this.wildcardListeners.forEach((fn) => {
182
+ try {
183
+ fn(revivedPayload, message);
184
+ }
185
+ catch (e) {
186
+ console.error(`[CrossTabSyncChannel] Error in wildcard listener:`, e);
187
+ }
188
+ });
189
+ }
190
+ /**
191
+ * Broadcasts a typed message across all other browser tabs.
192
+ */
193
+ broadcast(type, payload, projectId) {
194
+ if (this.isClosed)
195
+ return;
196
+ const serializedPayload = serializeCrossTabData(payload);
197
+ const message = {
198
+ id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
199
+ senderTabId: this.tabId,
200
+ projectId,
201
+ type,
202
+ timestamp: Date.now(),
203
+ payload: serializedPayload
204
+ };
205
+ // 1. Native BroadcastChannel
206
+ if (this.broadcastChannel) {
207
+ try {
208
+ this.broadcastChannel.postMessage(message);
209
+ return;
210
+ }
211
+ catch (e) {
212
+ console.warn('[CrossTabSyncChannel] BroadcastChannel postMessage failed:', e);
213
+ }
214
+ }
215
+ // 2. StorageEvent Fallback
216
+ if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
217
+ try {
218
+ const storageKey = `__nexa_crosstab_${this.channelName}`;
219
+ window.localStorage.setItem(storageKey, JSON.stringify(message));
220
+ }
221
+ catch (e) {
222
+ // Storage quota or restriction
223
+ }
224
+ }
225
+ }
226
+ /**
227
+ * Broadcasts a local mutation event (write, update, delete) to keep caches across tabs in sync instantly.
228
+ */
229
+ broadcastMutation(path, action, data, metadata, projectId) {
230
+ const payload = {
231
+ path,
232
+ action,
233
+ data,
234
+ metadata
235
+ };
236
+ this.broadcast('LOCAL_MUTATION', payload, projectId);
237
+ }
238
+ /**
239
+ * Subscribes to a specific event type. Returns an unsubscribe callback.
240
+ */
241
+ on(type, handler) {
242
+ if (!this.listeners.has(type)) {
243
+ this.listeners.set(type, new Set());
244
+ }
245
+ this.listeners.get(type).add(handler);
246
+ return () => {
247
+ const set = this.listeners.get(type);
248
+ if (set) {
249
+ set.delete(handler);
250
+ if (set.size === 0) {
251
+ this.listeners.delete(type);
252
+ }
253
+ }
254
+ };
255
+ }
256
+ /**
257
+ * Subscribes to all incoming messages regardless of type.
258
+ */
259
+ onAny(handler) {
260
+ this.wildcardListeners.add(handler);
261
+ return () => {
262
+ this.wildcardListeners.delete(handler);
263
+ };
264
+ }
265
+ /**
266
+ * Helper specifically for listening to local mutations across tabs.
267
+ */
268
+ onMutation(handler) {
269
+ return this.on('LOCAL_MUTATION', (payload) => {
270
+ if (payload && payload.path && payload.action) {
271
+ handler(payload.path, payload.action, payload.data, payload.metadata);
272
+ }
273
+ });
274
+ }
275
+ /**
276
+ * Closes the channel and removes all event listeners.
277
+ */
278
+ close() {
279
+ this.isClosed = true;
280
+ this.listeners.clear();
281
+ this.wildcardListeners.clear();
282
+ if (this.broadcastChannel) {
283
+ try {
284
+ this.broadcastChannel.close();
285
+ }
286
+ catch (e) { }
287
+ this.broadcastChannel = null;
288
+ }
289
+ if (this.storageListener && typeof window !== 'undefined') {
290
+ try {
291
+ window.removeEventListener('storage', this.storageListener);
292
+ }
293
+ catch (e) { }
294
+ this.storageListener = null;
295
+ }
296
+ }
297
+ }
298
+ exports.CrossTabSyncChannel = CrossTabSyncChannel;
299
+ // Global registry for CrossTab channels by name
300
+ const channelsRegistry = new Map();
301
+ /**
302
+ * Returns a singleton instance of CrossTabSyncChannel for the given channel name.
303
+ */
304
+ function getCrossTabChannel(channelName) {
305
+ let channel = channelsRegistry.get(channelName);
306
+ if (!channel) {
307
+ channel = new CrossTabSyncChannel(channelName);
308
+ channelsRegistry.set(channelName, channel);
309
+ }
310
+ return channel;
311
+ }
312
+ /**
313
+ * Creates or gets a project-scoped cross-tab synchronization channel.
314
+ */
315
+ function getProjectCrossTabChannel(projectId) {
316
+ return getCrossTabChannel(`nexabase_tab_sync_${projectId}`);
317
+ }
@@ -1,14 +1,14 @@
1
1
  import { MutationQueue } from '../persistence/MutationQueue';
2
2
  import { SSEClient } from '../transport/SSEClient';
3
+ import { CrossTabSyncChannel, MutationSyncHandler } from './CrossTabSync';
3
4
  export declare class SyncEngine {
4
5
  private projectId;
5
6
  private mutationQueue;
6
7
  private sseClient;
7
- private broadcastChannel;
8
+ private crossTabChannel;
8
9
  constructor(projectId: string, mutationQueue: MutationQueue, sseClient: SSEClient);
9
- private initCrossTabSync;
10
- private _serializeForBroadcast;
11
- broadcastLocalMutation(path: string, action: string, data?: any): void;
12
- listenCrossTab(onMutation: (path: string, action: string, data?: any) => void): () => void;
10
+ getCrossTabChannel(): CrossTabSyncChannel;
11
+ broadcastLocalMutation(path: string, action: string, data?: any, metadata?: any): void;
12
+ listenCrossTab(onMutation: MutationSyncHandler): () => void;
13
13
  syncOfflineMutations(): Promise<void>;
14
14
  }
@@ -1,82 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SyncEngine = void 0;
4
+ const CrossTabSync_1 = require("./CrossTabSync");
4
5
  class SyncEngine {
5
6
  constructor(projectId, mutationQueue, sseClient) {
6
- this.broadcastChannel = null;
7
7
  this.projectId = projectId;
8
8
  this.mutationQueue = mutationQueue;
9
9
  this.sseClient = sseClient;
10
- this.initCrossTabSync();
10
+ this.crossTabChannel = (0, CrossTabSync_1.getProjectCrossTabChannel)(this.projectId);
11
11
  }
12
- initCrossTabSync() {
13
- if (typeof window !== 'undefined' && 'BroadcastChannel' in window && !this.broadcastChannel) {
14
- try {
15
- this.broadcastChannel = new BroadcastChannel(`nexabase_tab_sync_${this.projectId}`);
16
- }
17
- catch (e) {
18
- console.warn('[SyncEngine] BroadcastChannel initialization failed:', e);
19
- }
20
- }
12
+ getCrossTabChannel() {
13
+ return this.crossTabChannel;
21
14
  }
22
- _serializeForBroadcast(data, seen = new WeakSet()) {
23
- if (data === null || data === undefined)
24
- return data;
25
- if (typeof data === 'boolean' || typeof data === 'number' || typeof data === 'string')
26
- return data;
27
- if (data instanceof Date) {
28
- return { __nexa_type__: 'timestamp', seconds: Math.floor(data.getTime() / 1000), nanoseconds: (data.getTime() % 1000) * 1000000 };
29
- }
30
- // Check if it's a Timestamp object (even without prototype due to potential prior cloning)
31
- if (typeof data === 'object' && 'seconds' in data && 'nanoseconds' in data && typeof data.seconds === 'number' && typeof data.nanoseconds === 'number') {
32
- return { __nexa_type__: 'timestamp', seconds: data.seconds, nanoseconds: data.nanoseconds };
33
- }
34
- // Check for GeoPoint
35
- if (typeof data === 'object' && 'latitude' in data && 'longitude' in data && typeof data.latitude === 'number' && typeof data.longitude === 'number') {
36
- return { __nexa_type__: 'geopoint', latitude: data.latitude, longitude: data.longitude };
37
- }
38
- if (Array.isArray(data)) {
39
- if (seen.has(data))
40
- return null;
41
- seen.add(data);
42
- const res = data.map(item => this._serializeForBroadcast(item, seen));
43
- seen.delete(data);
44
- return res;
45
- }
46
- if (typeof data === 'object') {
47
- if (seen.has(data))
48
- return null;
49
- seen.add(data);
50
- const res = {};
51
- for (const key of Object.keys(data)) {
52
- res[key] = this._serializeForBroadcast(data[key], seen);
53
- }
54
- seen.delete(data);
55
- return res;
56
- }
57
- return data;
58
- }
59
- broadcastLocalMutation(path, action, data) {
60
- if (this.broadcastChannel) {
61
- try {
62
- const serialized = this._serializeForBroadcast(data);
63
- this.broadcastChannel.postMessage({ type: 'LOCAL_MUTATION', path, action, data: serialized });
64
- }
65
- catch (e) { }
66
- }
15
+ broadcastLocalMutation(path, action, data, metadata) {
16
+ this.crossTabChannel.broadcastMutation(path, action, data, metadata, this.projectId);
67
17
  }
68
18
  listenCrossTab(onMutation) {
69
- if (!this.broadcastChannel)
70
- return () => { };
71
- const handler = (event) => {
72
- if (event.data && event.data.type === 'LOCAL_MUTATION') {
73
- onMutation(event.data.path, event.data.action, event.data.data);
74
- }
75
- };
76
- this.broadcastChannel.addEventListener('message', handler);
77
- return () => {
78
- this.broadcastChannel.removeEventListener('message', handler);
79
- };
19
+ return this.crossTabChannel.onMutation(onMutation);
80
20
  }
81
21
  async syncOfflineMutations() {
82
22
  await this.mutationQueue.syncQueue();
@@ -5,6 +5,7 @@ exports.deepEqual = deepEqual;
5
5
  exports.applyDataTransforms = applyDataTransforms;
6
6
  exports.calculateDocChanges = calculateDocChanges;
7
7
  exports.generateIdempotencyKey = generateIdempotencyKey;
8
+ const FieldValue_1 = require("../firestore/FieldValue");
8
9
  const Timestamp_1 = require("../firestore/Timestamp");
9
10
  function getNestedValue(obj, path, docId) {
10
11
  if (!obj || !path)
@@ -46,34 +47,61 @@ function deepEqual(a, b) {
46
47
  return true;
47
48
  }
48
49
  function applyDataTransforms(existingData, updateData) {
49
- const result = { ...existingData };
50
+ if (!updateData || typeof updateData !== 'object')
51
+ return existingData || {};
52
+ const result = { ...(existingData || {}) };
50
53
  const processNode = (obj, keyPath, value) => {
54
+ if (!obj || typeof obj !== 'object')
55
+ return;
51
56
  const current = keyPath[0];
52
57
  if (keyPath.length === 1) {
53
- if (value && typeof value === 'object' && value._isFieldValue) {
54
- const fv = value;
55
- if (fv.method === 'serverTimestamp') {
58
+ if (FieldValue_1.FieldValue.isFieldValue(value) ||
59
+ (value &&
60
+ typeof value === 'object' &&
61
+ (value._isFieldValue ||
62
+ ['deleteField', 'serverTimestamp', 'increment', 'arrayUnion', 'arrayRemove'].includes(value.method)))) {
63
+ const method = value.method;
64
+ const val = value.value;
65
+ if (method === 'serverTimestamp') {
56
66
  obj[current] = Timestamp_1.Timestamp.now();
57
67
  }
58
- else if (fv.method === 'increment') {
59
- obj[current] = (typeof obj[current] === 'number' ? obj[current] : 0) + (fv.value || 1);
68
+ else if (method === 'increment') {
69
+ obj[current] = (typeof obj[current] === 'number' ? obj[current] : 0) + (typeof val === 'number' ? val : 1);
60
70
  }
61
- else if (fv.method === 'arrayUnion') {
71
+ else if (method === 'arrayUnion') {
62
72
  const arr = Array.isArray(obj[current]) ? obj[current] : [];
63
- obj[current] = [...new Set([...arr, ...(fv.value || [])])];
73
+ const toAdd = Array.isArray(val) ? val : (val !== undefined ? [val] : []);
74
+ const existingSet = new Set(arr.map((item) => (typeof item === 'object' ? JSON.stringify(item) : item)));
75
+ for (const item of toAdd) {
76
+ const itemKey = typeof item === 'object' ? JSON.stringify(item) : item;
77
+ if (!existingSet.has(itemKey)) {
78
+ existingSet.add(itemKey);
79
+ arr.push(item);
80
+ }
81
+ }
82
+ obj[current] = arr;
64
83
  }
65
- else if (fv.method === 'arrayRemove') {
84
+ else if (method === 'arrayRemove') {
66
85
  const arr = Array.isArray(obj[current]) ? obj[current] : [];
67
- const toRemove = new Set(fv.value || []);
68
- obj[current] = arr.filter((x) => !toRemove.has(x));
86
+ const toRemove = Array.isArray(val) ? val : (val !== undefined ? [val] : []);
87
+ const removeSet = new Set(toRemove.map((item) => (typeof item === 'object' ? JSON.stringify(item) : item)));
88
+ obj[current] = arr.filter((x) => {
89
+ const itemKey = typeof x === 'object' ? JSON.stringify(x) : x;
90
+ return !removeSet.has(itemKey);
91
+ });
69
92
  }
70
- else if (fv.method === 'deleteField') {
93
+ else if (method === 'deleteField') {
71
94
  delete obj[current];
72
95
  }
73
96
  }
74
- else if (value && typeof value === 'object' && !Array.isArray(value) && typeof value.isEqual !== 'function') {
75
- if (!obj[current] || typeof obj[current] !== 'object')
97
+ else if (value &&
98
+ typeof value === 'object' &&
99
+ !Array.isArray(value) &&
100
+ typeof value.isEqual !== 'function' &&
101
+ !(value instanceof Timestamp_1.Timestamp)) {
102
+ if (!obj[current] || typeof obj[current] !== 'object' || Array.isArray(obj[current])) {
76
103
  obj[current] = {};
104
+ }
77
105
  for (const [k, v] of Object.entries(value)) {
78
106
  processNode(obj[current], [k], v);
79
107
  }
@@ -83,8 +111,13 @@ function applyDataTransforms(existingData, updateData) {
83
111
  }
84
112
  }
85
113
  else {
86
- if (!obj[current] || typeof obj[current] !== 'object')
114
+ if (!obj[current] || typeof obj[current] !== 'object' || Array.isArray(obj[current])) {
115
+ const isDelete = FieldValue_1.FieldValue.isFieldValue(value) ||
116
+ (value && typeof value === 'object' && value.method === 'deleteField');
117
+ if (isDelete)
118
+ return;
87
119
  obj[current] = {};
120
+ }
88
121
  processNode(obj[current], keyPath.slice(1), value);
89
122
  }
90
123
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexabase-console",
3
- "version": "2.1.8",
3
+ "version": "2.2.0",
4
4
  "description": "SDK Client resmi untuk NexaBase: Platform Sinkronisasi NoSQL, Realtime, File Storage, & Autentikasi Offline-First.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",