nexabase-console 2.1.7 → 2.1.9

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.
@@ -5,6 +5,7 @@ export declare class GeoPoint {
5
5
  isEqual(other: GeoPoint): boolean;
6
6
  toString(): string;
7
7
  toJSON(): {
8
+ __nexa_type__: string;
8
9
  latitude: number;
9
10
  longitude: number;
10
11
  };
@@ -23,7 +23,7 @@ class GeoPoint {
23
23
  return `GeoPoint(latitude=${this.latitude}, longitude=${this.longitude})`;
24
24
  }
25
25
  toJSON() {
26
- return { latitude: this.latitude, longitude: this.longitude };
26
+ return { __nexa_type__: 'geopoint', latitude: this.latitude, longitude: this.longitude };
27
27
  }
28
28
  }
29
29
  exports.GeoPoint = GeoPoint;
@@ -11,6 +11,7 @@ export declare class Timestamp {
11
11
  valueOf(): string;
12
12
  toString(): string;
13
13
  toJSON(): {
14
+ __nexa_type__: string;
14
15
  seconds: number;
15
16
  nanoseconds: number;
16
17
  };
@@ -56,7 +56,7 @@ class Timestamp {
56
56
  return `Timestamp(seconds=${this.seconds}, nanoseconds=${this.nanoseconds})`;
57
57
  }
58
58
  toJSON() {
59
- return { seconds: this.seconds, nanoseconds: this.nanoseconds };
59
+ return { __nexa_type__: 'timestamp', seconds: this.seconds, nanoseconds: this.nanoseconds };
60
60
  }
61
61
  }
62
62
  exports.Timestamp = Timestamp;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexabase-console",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
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",