nexabase-console 2.1.2 → 2.1.3

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.
Files changed (35) hide show
  1. package/dist/app/NexaApp.d.ts +3 -0
  2. package/dist/app/NexaApp.js +38 -11
  3. package/dist/firestore/CollectionReference.d.ts +2 -2
  4. package/dist/firestore/CollectionReference.js +41 -2
  5. package/dist/firestore/DocumentReference.d.ts +1 -1
  6. package/dist/firestore/DocumentReference.js +41 -11
  7. package/dist/firestore/FieldPath.d.ts +2 -1
  8. package/dist/firestore/FieldPath.js +31 -4
  9. package/dist/firestore/FieldValue.d.ts +8 -4
  10. package/dist/firestore/FieldValue.js +59 -7
  11. package/dist/firestore/Firestore.js +16 -4
  12. package/dist/firestore/GeoPoint.d.ts +1 -0
  13. package/dist/firestore/GeoPoint.js +13 -6
  14. package/dist/firestore/Query.d.ts +5 -0
  15. package/dist/firestore/Query.js +190 -56
  16. package/dist/firestore/QueryUtils.d.ts +19 -2
  17. package/dist/firestore/QueryUtils.js +296 -58
  18. package/dist/firestore/Snapshot.js +62 -17
  19. package/dist/firestore/SnapshotManager.js +27 -12
  20. package/dist/firestore/Timestamp.d.ts +1 -0
  21. package/dist/firestore/Timestamp.js +28 -2
  22. package/dist/firestore/batch.js +89 -36
  23. package/dist/firestore/pathValidation.d.ts +17 -0
  24. package/dist/firestore/pathValidation.js +61 -0
  25. package/dist/firestore/queryConstraints.d.ts +4 -4
  26. package/dist/firestore/queryConstraints.js +69 -8
  27. package/dist/firestore/transaction.js +72 -18
  28. package/dist/firestore/writes.d.ts +1 -1
  29. package/dist/firestore/writes.js +32 -14
  30. package/dist/transport/WebSocketClient.d.ts +341 -9
  31. package/dist/transport/WebSocketClient.js +1895 -131
  32. package/dist/transport/WebSocketClient.test.d.ts +1 -0
  33. package/dist/transport/WebSocketClient.test.js +454 -0
  34. package/dist/types/index.d.ts +1 -0
  35. package/package.json +1 -1
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getPendingWritesCount = exports.deleteDoc = exports.updateDoc = exports.patchDoc = exports.setDoc = exports.FieldValue = void 0;
4
4
  const helpers_1 = require("../utils/helpers");
5
+ const NexaError_1 = require("../errors/NexaError");
6
+ const QueryUtils_1 = require("./QueryUtils");
7
+ const pathValidation_1 = require("./pathValidation");
5
8
  var FieldValue_1 = require("./FieldValue");
6
9
  Object.defineProperty(exports, "FieldValue", { enumerable: true, get: function () { return FieldValue_1.FieldValue; } });
7
10
  const normalizeError = (error) => {
@@ -17,6 +20,18 @@ const normalizeError = (error) => {
17
20
  if (status) {
18
21
  if ([408, 425, 429, 500, 502, 503, 504].includes(status))
19
22
  retryable = true;
23
+ if (status === 401)
24
+ code = 'unauthenticated';
25
+ if (status === 403)
26
+ code = 'permission-denied';
27
+ if (status === 404)
28
+ code = 'not-found';
29
+ if (status === 409)
30
+ code = 'already-exists';
31
+ if (status === 400)
32
+ code = 'invalid-argument';
33
+ if (status === 412)
34
+ code = 'failed-precondition';
20
35
  }
21
36
  else if (['network-error', 'timeout', 'unavailable', 'aborted', 'resource-exhausted'].includes(code)) {
22
37
  retryable = true;
@@ -27,7 +42,7 @@ const normalizeError = (error) => {
27
42
  return {
28
43
  code,
29
44
  status,
30
- message: error?.message || 'Unknown error',
45
+ message: error?.message || 'Write operation failed',
31
46
  retryable,
32
47
  source: error?.isAxiosError ? 'http' : (error?.source || 'internal')
33
48
  };
@@ -47,12 +62,16 @@ const safelyNotifyMutation = (db, path, action, data, metadata) => {
47
62
  }
48
63
  };
49
64
  const executeWriteMutation = async (docRef, action, data, options) => {
50
- if (!docRef || !docRef.path || !docRef.db || typeof docRef.path !== 'string' || docRef.path.split('/').length % 2 !== 0) {
51
- throw Object.assign(new Error('Invalid DocumentReference'), { code: 'invalid-argument' });
65
+ if (!docRef || !docRef.path || !docRef.db) {
66
+ throw new NexaError_1.NexaError('invalid-argument', 'Invalid DocumentReference provided to write operation.');
52
67
  }
53
- if (action !== 'delete' && (!data || typeof data !== 'object' || Array.isArray(data))) {
54
- throw Object.assign(new Error('Data must be a valid plain object'), { code: 'invalid-argument' });
68
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
69
+ if (action !== 'delete') {
70
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
71
+ throw new NexaError_1.NexaError('invalid-argument', 'Data for write operation must be a plain object.');
72
+ }
55
73
  }
74
+ const clonedData = action !== 'delete' ? (0, QueryUtils_1.cloneNexaData)(data) : null;
56
75
  const db = docRef.db;
57
76
  if (db._initPromise)
58
77
  await db._initPromise;
@@ -64,7 +83,7 @@ const executeWriteMutation = async (docRef, action, data, options) => {
64
83
  id: mutationId,
65
84
  type: action,
66
85
  path: docRef.path,
67
- data: data,
86
+ data: clonedData,
68
87
  idempotencyKey: key,
69
88
  merge: options?.merge,
70
89
  state: 'pending',
@@ -84,7 +103,7 @@ const executeWriteMutation = async (docRef, action, data, options) => {
84
103
  }
85
104
  catch (error) {
86
105
  db.unmarkInflight(docRef.path, mutationId);
87
- throw Object.assign(new Error('Local persistence failed'), { code: 'internal', cause: error });
106
+ throw new NexaError_1.NexaError('internal', 'Local persistence failed', error);
88
107
  }
89
108
  safelyNotifyMutation(db, docRef.path, action, localView.data, {
90
109
  mutationId,
@@ -106,7 +125,7 @@ const executeWriteMutation = async (docRef, action, data, options) => {
106
125
  action: action === 'patch' ? 'update' : action,
107
126
  docPath: docRef.path,
108
127
  projectId: db.projectId,
109
- data,
128
+ data: clonedData,
110
129
  merge: options?.merge,
111
130
  idempotencyKey: key,
112
131
  preconditionExists: options?.preconditionExists
@@ -115,10 +134,10 @@ const executeWriteMutation = async (docRef, action, data, options) => {
115
134
  else {
116
135
  const headers = { 'X-Idempotency-Key': key };
117
136
  if (action === 'set') {
118
- res = (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key, merge: options?.merge }, { headers })).data;
137
+ res = (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data: clonedData, idempotencyKey: key, merge: options?.merge }, { headers })).data;
119
138
  }
120
139
  else if (action === 'patch') {
121
- res = (await db.client.patch(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key, preconditionExists: options?.preconditionExists }, { headers })).data;
140
+ res = (await db.client.patch(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data: clonedData, idempotencyKey: key, preconditionExists: options?.preconditionExists }, { headers })).data;
122
141
  }
123
142
  else {
124
143
  res = (await db.client.delete(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`, { headers })).data;
@@ -146,7 +165,7 @@ const executeWriteMutation = async (docRef, action, data, options) => {
146
165
  }
147
166
  catch (rejectionError) {
148
167
  db.unmarkInflight(docRef.path, mutationId);
149
- throw Object.assign(new Error('Server rejected mutation, but local rollback failed'), { code: normErr.code, cause: rejectionError, originalError: networkError });
168
+ throw new NexaError_1.NexaError(normErr.code, `Server rejected mutation, but local rollback failed: ${normErr.message}`, rejectionError);
150
169
  }
151
170
  db.unmarkInflight(docRef.path, mutationId);
152
171
  safelyNotifyMutation(db, docRef.path, rebasedView.data ? (action === 'delete' ? 'set' : action) : 'delete', rebasedView.data, {
@@ -155,13 +174,12 @@ const executeWriteMutation = async (docRef, action, data, options) => {
155
174
  state: 'rejected',
156
175
  hasPendingWrites: rebasedView.hasPendingWrites
157
176
  });
158
- throw Object.assign(new Error(normErr.message), { code: normErr.code, cause: networkError });
177
+ throw new NexaError_1.NexaError(normErr.code, normErr.message, networkError);
159
178
  }
160
179
  }
161
180
  // Phase 4: ACK Validation
162
181
  const isValidAck = res && res.success;
163
182
  if (!isValidAck) {
164
- console.error('[NexaBase] Protocol error: Invalid server ACK:', res);
165
183
  db.unmarkInflight(docRef.path, mutationId);
166
184
  return { status: 'pending', committed: false, writtenLocally: true, mutationId };
167
185
  }
@@ -174,7 +192,7 @@ const executeWriteMutation = async (docRef, action, data, options) => {
174
192
  action,
175
193
  serverDocument: res.document,
176
194
  updateTime: res.document?.updateTime,
177
- mutationData: data,
195
+ mutationData: clonedData,
178
196
  merge: options?.merge
179
197
  });
180
198
  }
@@ -1,24 +1,356 @@
1
+ import { Socket } from 'socket.io-client';
2
+ /**
3
+ * Client operational lifecycle states.
4
+ */
5
+ export type ClientState = 'idle' | 'electing' | 'follower' | 'leader' | 'disconnecting' | 'disposed';
6
+ /**
7
+ * Coordination mode across browser tabs.
8
+ */
9
+ export type CoordinationMode = 'shared' | 'independent';
10
+ /**
11
+ * Standardized typed error codes for WebSocket and coordination failures.
12
+ */
13
+ export type WebSocketErrorCode = 'invalid-argument' | 'connection-timeout' | 'timeout' | 'broadcast-failure' | 'leader-changed' | 'unauthenticated' | 'disconnected' | 'disposed' | 'ambiguous-outcome' | 'unavailable' | 'cancelled';
14
+ /**
15
+ * Serialized error format passed across leader/follower boundaries and to consumers.
16
+ */
17
+ export interface SerializedError {
18
+ name: string;
19
+ message: string;
20
+ code: WebSocketErrorCode;
21
+ retryable: boolean;
22
+ source: 'client' | 'leader' | 'follower' | 'server';
23
+ isAmbiguous: boolean;
24
+ details?: unknown;
25
+ }
26
+ /**
27
+ * Production error class preserving structured metadata across tab boundaries.
28
+ */
29
+ export declare class NexaWebSocketError extends Error implements SerializedError {
30
+ readonly code: WebSocketErrorCode;
31
+ readonly retryable: boolean;
32
+ readonly source: 'client' | 'leader' | 'follower' | 'server';
33
+ readonly isAmbiguous: boolean;
34
+ readonly details?: unknown;
35
+ constructor(options: {
36
+ message: string;
37
+ code?: WebSocketErrorCode;
38
+ retryable?: boolean;
39
+ source?: 'client' | 'leader' | 'follower' | 'server';
40
+ isAmbiguous?: boolean;
41
+ details?: unknown;
42
+ cause?: unknown;
43
+ });
44
+ toJSON(): SerializedError;
45
+ static from(err: unknown, defaultSource?: 'client' | 'leader' | 'follower' | 'server'): NexaWebSocketError;
46
+ }
47
+ /**
48
+ * Tri-state path extraction outcome to prevent invalid paths from becoming global events.
49
+ */
50
+ export type PathExtractionResult = {
51
+ status: 'none';
52
+ } | {
53
+ status: 'valid';
54
+ path: string;
55
+ } | {
56
+ status: 'invalid';
57
+ raw: unknown;
58
+ };
59
+ /**
60
+ * Options for configuring WebSocketClient.
61
+ */
62
+ export interface WebSocketClientOptions {
63
+ sessionScope?: string;
64
+ authPayload?: unknown;
65
+ getAuthPayload?: () => Promise<unknown> | unknown;
66
+ defaultTimeoutMs?: number;
67
+ coordinationMode?: CoordinationMode;
68
+ followerTtlMs?: number;
69
+ heartbeatIntervalMs?: number;
70
+ pruneIntervalMs?: number;
71
+ leaderLeaseIntervalMs?: number;
72
+ socketFactory?: (url: string, opts: any) => Socket;
73
+ broadcastChannelFactory?: (name: string) => BroadcastChannel;
74
+ locks?: LockManager;
75
+ }
76
+ /**
77
+ * Valid broadcast message types.
78
+ */
79
+ export type BroadcastMessageType = 'follower_subscribe' | 'follower_unsubscribe' | 'follower_sync_request' | 'follower_announce' | 'follower_heartbeat' | 'follower_disconnect' | 'follower_send' | 'leader_elected' | 'leader_state' | 'leader_event' | 'leader_send_response';
80
+ export interface BaseBroadcastMessage {
81
+ type: BroadcastMessageType;
82
+ senderId: string;
83
+ timestamp: number;
84
+ }
85
+ export interface FollowerSubscribeMessage extends BaseBroadcastMessage {
86
+ type: 'follower_subscribe';
87
+ payload: {
88
+ path: string;
89
+ };
90
+ }
91
+ export interface FollowerUnsubscribeMessage extends BaseBroadcastMessage {
92
+ type: 'follower_unsubscribe';
93
+ payload: {
94
+ path: string;
95
+ };
96
+ }
97
+ export interface FollowerSyncRequestMessage extends BaseBroadcastMessage {
98
+ type: 'follower_sync_request';
99
+ payload?: Record<string, never>;
100
+ }
101
+ export interface FollowerAnnounceMessage extends BaseBroadcastMessage {
102
+ type: 'follower_announce';
103
+ payload: {
104
+ paths: string[];
105
+ globalListenersCount: number;
106
+ };
107
+ }
108
+ export interface FollowerHeartbeatMessage extends BaseBroadcastMessage {
109
+ type: 'follower_heartbeat';
110
+ payload: {
111
+ globalListenersCount: number;
112
+ paths?: string[];
113
+ };
114
+ }
115
+ export interface FollowerDisconnectMessage extends BaseBroadcastMessage {
116
+ type: 'follower_disconnect';
117
+ payload?: {
118
+ tabId?: string;
119
+ };
120
+ }
121
+ export interface FollowerSendMessage extends BaseBroadcastMessage {
122
+ type: 'follower_send';
123
+ payload: {
124
+ requestId: string;
125
+ expectedLeaderId: string;
126
+ expectedElectionId: string;
127
+ event: string;
128
+ data: unknown;
129
+ targetTabId: string;
130
+ deadline: number;
131
+ idempotencyKey?: string;
132
+ };
133
+ }
134
+ export interface LeaderElectedMessage extends BaseBroadcastMessage {
135
+ type: 'leader_elected';
136
+ leaderId: string;
137
+ electionId: string;
138
+ termSeq: number;
139
+ payload: {
140
+ leaderId: string;
141
+ electionId: string;
142
+ isConnected: boolean;
143
+ termSeq: number;
144
+ };
145
+ }
146
+ export interface LeaderStateMessage extends BaseBroadcastMessage {
147
+ type: 'leader_state';
148
+ leaderId: string;
149
+ electionId: string;
150
+ termSeq: number;
151
+ payload: {
152
+ leaderId: string;
153
+ electionId: string;
154
+ isConnected: boolean;
155
+ termSeq: number;
156
+ };
157
+ }
158
+ export interface LeaderEventMessage extends BaseBroadcastMessage {
159
+ type: 'leader_event';
160
+ leaderId: string;
161
+ electionId: string;
162
+ payload: {
163
+ event: string;
164
+ data: unknown;
165
+ targetTabIds?: string[];
166
+ };
167
+ }
168
+ export interface LeaderSendResponseMessage extends BaseBroadcastMessage {
169
+ type: 'leader_send_response';
170
+ leaderId: string;
171
+ electionId: string;
172
+ payload: {
173
+ requestId: string;
174
+ targetTabId: string;
175
+ response?: unknown;
176
+ error?: SerializedError;
177
+ };
178
+ }
179
+ export type BroadcastMessage = FollowerSubscribeMessage | FollowerUnsubscribeMessage | FollowerSyncRequestMessage | FollowerAnnounceMessage | FollowerHeartbeatMessage | FollowerDisconnectMessage | FollowerSendMessage | LeaderElectedMessage | LeaderStateMessage | LeaderEventMessage | LeaderSendResponseMessage;
180
+ /**
181
+ * Production-hardened WebSocketClient with distributed coordination,
182
+ * term-fenced follower forwarding, BFCache/background resilience, and strict invariants.
183
+ */
1
184
  export declare class WebSocketClient {
2
- private projectId;
3
- private serverUrl;
4
- private socket;
5
- isConnected: boolean;
185
+ private readonly _projectId;
186
+ private readonly _serverUrl;
187
+ private readonly _sessionScope;
188
+ private readonly _tabId;
189
+ private readonly _options;
190
+ private readonly _coordinationKey;
191
+ private _coordinationMode;
192
+ private _socket;
193
+ private _isConnected;
194
+ private _lastReportedConnected;
195
+ private _state;
196
+ private _isSuspended;
197
+ private _authPayload;
198
+ private _electionId;
199
+ private _currentLeaderId;
200
+ private _currentElectionId;
201
+ private _currentTermSeq;
202
+ private _isLeaderSocketConnected;
6
203
  private listeners;
7
- private subscribedPaths;
204
+ private localSubscribedPaths;
205
+ private activeLocalSendsCount;
8
206
  private broadcastChannel;
9
- private isLeader;
10
- private hasInitialized;
11
- constructor(projectId: string, serverUrl: string);
207
+ private lockAbortController;
208
+ private resolveLeaderLock;
209
+ private followerSubscriptions;
210
+ private followerSubscriptionsByTab;
211
+ private followerGlobalNeeds;
212
+ private processedFollowerRequests;
213
+ private pendingSendRequests;
214
+ private activeLeaderSends;
215
+ private readinessWaiters;
216
+ private heartbeatTimer;
217
+ private pruneTimer;
218
+ private leaseTimer;
219
+ private reconnectTimer;
220
+ private pageLifecycleCleanup;
221
+ private readonly followerTtlMs;
222
+ private readonly heartbeatIntervalMs;
223
+ private readonly pruneIntervalMs;
224
+ private readonly leaderLeaseIntervalMs;
225
+ private readonly defaultTimeoutMs;
226
+ private static readonly DEFAULT_FOLLOWER_TTL;
227
+ private static readonly DEFAULT_HEARTBEAT_INTERVAL;
228
+ private static readonly DEFAULT_PRUNE_INTERVAL;
229
+ private static readonly DEFAULT_LEADER_LEASE_INTERVAL;
230
+ private static readonly DEFAULT_TIMEOUT_MS;
231
+ private static readonly MAX_DEDUP_CACHE_SIZE;
232
+ private static readonly DEDUP_CACHE_TTL_MS;
233
+ private static readonly RESERVED_SEND_EVENTS;
234
+ constructor(projectId: string, serverUrl: string, options?: WebSocketClientOptions);
235
+ get projectId(): string;
236
+ get serverUrl(): string;
237
+ get tabId(): string;
238
+ get isConnected(): boolean;
239
+ get isLeader(): boolean;
240
+ get state(): ClientState;
241
+ get electionId(): string | null;
242
+ get currentLeaderId(): string | null;
243
+ get currentElectionId(): string | null;
244
+ get isIndependent(): boolean;
245
+ get coordinationMode(): CoordinationMode;
246
+ get isSuspended(): boolean;
247
+ /**
248
+ * Unique ID generation with Web Crypto fallback.
249
+ */
250
+ static generateId(): string;
251
+ /**
252
+ * Stable deterministic hashing function.
253
+ */
254
+ static hashScope(scope: string): string;
255
+ /**
256
+ * Normalizes server URL and creates a unique coordination key.
257
+ */
258
+ static deriveCoordinationKey(projectId: string, serverUrl: string, sessionScope: string | null): string;
259
+ /**
260
+ * Sanitizes, segments, and bounds path strings.
261
+ */
262
+ static cleanAndValidatePath(path: unknown): string | null;
263
+ static validatePathOrThrow(path: unknown): string;
264
+ /**
265
+ * Segment-aware path matcher: checks exact match or segment hierarchy.
266
+ */
267
+ static isPathMatch(eventPath: string, subPath: string): boolean;
268
+ /**
269
+ * Tri-state path extraction distinguishing: none vs valid vs invalid.
270
+ */
271
+ static extractPayloadPathTriState(payload: unknown): PathExtractionResult;
272
+ /**
273
+ * Backward-compatible extractor returning valid path or null.
274
+ */
275
+ static extractPayloadPath(payload: unknown): string | null;
276
+ /**
277
+ * Dynamic authentication updater.
278
+ */
279
+ updateAuth(authPayload: unknown): void;
280
+ private getFreshAuthPayload;
281
+ private transitionTo;
282
+ /**
283
+ * Atomically downgrades from shared coordination to independent socket mode.
284
+ */
285
+ private switchToIndependentMode;
286
+ /**
287
+ * Safe non-throwing broadcast poster returning a delivery indicator boolean.
288
+ */
289
+ private postBroadcast;
290
+ /**
291
+ * Strictly validates incoming broadcast payload schema, types, bounds, and string sizes.
292
+ */
293
+ validateBroadcastMessage(raw: unknown): BroadcastMessage | null;
12
294
  private handleBroadcastMessage;
295
+ private handleMessageAsLeader;
296
+ private pruneDedupCache;
297
+ private handleFollowerSendRequest;
298
+ private handleMessageAsFollower;
299
+ private updateFollowerConnectionState;
300
+ private handleFollowerAnnounce;
301
+ private announceNeedsToLeader;
302
+ private announceGlobalListenersChange;
303
+ private getGlobalListenersCount;
304
+ private addFollowerPathSubscription;
305
+ private removeFollowerPathSubscription;
306
+ private pruneFollower;
307
+ private startFollowerHeartbeat;
308
+ private stopFollowerHeartbeat;
309
+ private startLeaderPruneTimer;
310
+ private stopLeaderPruneTimer;
311
+ private startLeaderLeaseTimer;
312
+ private stopLeaderLeaseTimer;
313
+ /**
314
+ * Subscribes an event listener callback with reference counting and connection management.
315
+ */
13
316
  addListener(event: string, callback: (data: any) => void): () => void;
317
+ /**
318
+ * Reference-counted path subscription.
319
+ */
14
320
  subscribe(path: string): void;
321
+ /**
322
+ * Decrements reference-counted path subscription.
323
+ */
15
324
  unsubscribe(path: string): void;
16
325
  private broadcastToFollowers;
326
+ /**
327
+ * Connects socket directly or coordinates election via Web Locks.
328
+ */
17
329
  ensureConnected(): void;
18
330
  private startSocketConnection;
19
- send(event: string, data: any): Promise<any>;
331
+ private isLocalSubscribedToPath;
332
+ private handleSocketDataEvent;
333
+ private executeSendAsLeader;
334
+ private checkReadinessWaiters;
335
+ private waitForReady;
336
+ private static isMutatingEvent;
337
+ /**
338
+ * Production-hardened request execution with invariant counter handling in a single finally block.
339
+ */
340
+ send(event: string, data: any, timeoutMs?: number): Promise<any>;
20
341
  checkCloseConnection(): void;
342
+ hasNetworkNeeds(): boolean;
343
+ private hasLocalNeeds;
21
344
  hasListeners(): boolean;
345
+ /**
346
+ * Dispatches events using a defensive snapshot of listeners to avoid mutation hazards.
347
+ */
22
348
  dispatch(event: string, data: any): void;
349
+ private releaseLeaderLock;
350
+ private disconnectSocket;
351
+ private setupPageLifecycleListeners;
352
+ /**
353
+ * Idempotent client teardown releasing locks, clearing queues, and sending best-effort disconnect notice.
354
+ */
23
355
  close(): void;
24
356
  }