nexabase-console 2.0.6 → 2.0.8

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,155 +2,209 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  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
5
  var FieldValue_1 = require("./FieldValue");
7
6
  Object.defineProperty(exports, "FieldValue", { enumerable: true, get: function () { return FieldValue_1.FieldValue; } });
8
- const setDoc = async (docRef, data, options) => {
7
+ const normalizeError = (error) => {
8
+ const status = error?.response?.status;
9
+ let code = error?.code || 'internal';
10
+ let retryable = false;
11
+ if (code === 'ERR_NETWORK')
12
+ code = 'network-error';
13
+ if (code === 'ECONNABORTED' || code === 'ETIMEDOUT')
14
+ code = 'timeout';
15
+ if (code === 'ERR_CANCELED')
16
+ code = 'aborted';
17
+ if (status) {
18
+ if ([408, 425, 429, 500, 502, 503, 504].includes(status))
19
+ retryable = true;
20
+ }
21
+ else if (['network-error', 'timeout', 'unavailable', 'aborted', 'resource-exhausted'].includes(code)) {
22
+ retryable = true;
23
+ }
24
+ if (['invalid-argument', 'permission-denied', 'unauthenticated', 'not-found', 'failed-precondition', 'already-exists'].includes(code)) {
25
+ retryable = false;
26
+ }
27
+ return {
28
+ code,
29
+ status,
30
+ message: error?.message || 'Unknown error',
31
+ retryable,
32
+ source: error?.isAxiosError ? 'http' : (error?.source || 'internal')
33
+ };
34
+ };
35
+ const safelyNotifyMutation = (db, path, action, data, metadata) => {
36
+ try {
37
+ db._broadcastLocalMutation(path, action, data, metadata);
38
+ }
39
+ catch (e) {
40
+ console.error('[NexaBase] Listener error:', e);
41
+ }
42
+ try {
43
+ db._notifySnapshotCallbacks(path, action === 'delete' ? 'document_deleted' : 'document_written', data, metadata);
44
+ }
45
+ catch (e) {
46
+ console.error('[NexaBase] Listener error:', e);
47
+ }
48
+ };
49
+ 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' });
52
+ }
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' });
55
+ }
9
56
  const db = docRef.db;
10
57
  if (db._initPromise)
11
58
  await db._initPromise;
12
- const existingData = options?.merge ? db._firestoreCache[docRef.path] || {} : {};
13
- const finalData = (0, helpers_1.applyDataTransforms)(existingData, data);
14
- await db._setCache(docRef.path, finalData);
15
- db._broadcastLocalMutation(docRef.path, 'set', finalData);
16
- db._notifySnapshotCallbacks(docRef.path, 'document_written', finalData);
17
59
  const key = options?.idempotencyKey || (0, helpers_1.generateIdempotencyKey)();
18
- const jobId = Math.random().toString(36).substring(2, 9);
60
+ const mutationId = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
61
+ ? crypto.randomUUID()
62
+ : (0, helpers_1.generateIdempotencyKey)();
63
+ const job = {
64
+ id: mutationId,
65
+ type: action,
66
+ path: docRef.path,
67
+ data: data,
68
+ idempotencyKey: key,
69
+ merge: options?.merge,
70
+ state: 'pending',
71
+ timestamp: Date.now(),
72
+ sequenceNumber: await db.localStore.nextMutationSequence(),
73
+ leaseOwner: 'foreground',
74
+ leaseExpiresAt: Date.now() + 30000
75
+ };
76
+ if (options?.preconditionExists) {
77
+ job.precondition = { exists: true };
78
+ }
79
+ db.markInflight(docRef.path, mutationId);
80
+ // Phase 1: Local Commit
81
+ let localView;
82
+ try {
83
+ localView = await db._commitAtomicMutation(docRef.path, action, job);
84
+ }
85
+ catch (error) {
86
+ db.unmarkInflight(docRef.path, mutationId);
87
+ throw Object.assign(new Error('Local persistence failed'), { code: 'internal', cause: error });
88
+ }
89
+ safelyNotifyMutation(db, docRef.path, action, localView.data, {
90
+ mutationId,
91
+ source: 'local',
92
+ state: 'pending',
93
+ hasPendingWrites: localView.hasPendingWrites
94
+ });
19
95
  const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
20
96
  if (!isOnline) {
21
- await db._addOfflineJob({
22
- id: jobId,
23
- type: 'set',
24
- path: docRef.path,
25
- data,
26
- idempotencyKey: key,
27
- merge: options?.merge,
28
- state: 'pending'
29
- });
30
- return { success: true, message: 'Offline: Tersimpan di cache lokal' };
97
+ db.unmarkInflight(docRef.path, mutationId);
98
+ return { status: 'pending', committed: false, writtenLocally: true, mutationId };
31
99
  }
32
- else {
33
- db.markInflight(docRef.path);
34
- try {
35
- const res = (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key, merge: options?.merge }, { headers: { 'X-Idempotency-Key': key } })).data;
36
- return res;
37
- }
38
- catch (error) {
39
- if (error && error.response && error.response.status >= 400 && error.response.status < 500) {
40
- throw (0, NexaError_1.toNexaError)(error);
41
- }
42
- await db._addOfflineJob({
43
- id: jobId,
44
- type: 'set',
45
- path: docRef.path,
100
+ // Phase 2: Network Send
101
+ let res;
102
+ let networkError = null;
103
+ try {
104
+ if (db.wsClient && db.wsClient.isConnected) {
105
+ res = await db.wsClient.send('firestore_write', {
106
+ action: action === 'patch' ? 'update' : action,
107
+ docPath: docRef.path,
46
108
  data,
47
- idempotencyKey: key,
48
109
  merge: options?.merge,
49
- state: 'pending'
110
+ idempotencyKey: key,
111
+ preconditionExists: options?.preconditionExists
50
112
  });
51
- return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline' };
52
113
  }
53
- finally {
54
- db.unmarkInflight(docRef.path);
114
+ else {
115
+ const headers = { 'X-Idempotency-Key': key };
116
+ if (action === 'set') {
117
+ res = (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key, merge: options?.merge }, { headers })).data;
118
+ }
119
+ else if (action === 'patch') {
120
+ res = (await db.client.patch(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key, preconditionExists: options?.preconditionExists }, { headers })).data;
121
+ }
122
+ else {
123
+ res = (await db.client.delete(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`, { headers })).data;
124
+ }
55
125
  }
56
126
  }
57
- };
58
- exports.setDoc = setDoc;
59
- const patchDoc = async (docRef, data, options) => {
60
- const db = docRef.db;
61
- if (db._initPromise)
62
- await db._initPromise;
63
- const existingData = db._firestoreCache[docRef.path] || {};
64
- const finalData = (0, helpers_1.applyDataTransforms)(existingData, data);
65
- await db._setCache(docRef.path, finalData);
66
- db._broadcastLocalMutation(docRef.path, 'patch', finalData);
67
- db._notifySnapshotCallbacks(docRef.path, 'document_written', finalData);
68
- const key = options?.idempotencyKey || (0, helpers_1.generateIdempotencyKey)();
69
- const jobId = Math.random().toString(36).substring(2, 9);
70
- const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
71
- if (!isOnline) {
72
- await db._addOfflineJob({
73
- id: jobId,
74
- type: 'patch',
75
- path: docRef.path,
76
- data,
77
- idempotencyKey: key,
78
- state: 'pending'
79
- });
80
- return { success: true, message: 'Offline: Tersimpan di cache lokal' };
127
+ catch (err) {
128
+ networkError = err;
81
129
  }
82
- else {
83
- db.markInflight(docRef.path);
84
- try {
85
- const res = (await db.client.patch(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data, idempotencyKey: key }, { headers: { 'X-Idempotency-Key': key } })).data;
86
- return res;
130
+ // Phase 3: Server Error Handling
131
+ if (networkError) {
132
+ const normErr = normalizeError(networkError);
133
+ if (normErr.retryable) {
134
+ db.unmarkInflight(docRef.path, mutationId);
135
+ return { status: 'pending', committed: false, writtenLocally: true, mutationId };
87
136
  }
88
- catch (error) {
89
- if (error && error.response && error.response.status >= 400 && error.response.status < 500) {
90
- throw (0, NexaError_1.toNexaError)(error);
137
+ else {
138
+ let rebasedView;
139
+ try {
140
+ rebasedView = await db._rejectMutationAtomically({
141
+ mutationId,
142
+ path: docRef.path,
143
+ error: normErr
144
+ });
91
145
  }
92
- await db._addOfflineJob({
93
- id: jobId,
94
- type: 'patch',
95
- path: docRef.path,
96
- data,
97
- idempotencyKey: key,
98
- state: 'pending'
146
+ catch (rejectionError) {
147
+ db.unmarkInflight(docRef.path, mutationId);
148
+ throw Object.assign(new Error('Server rejected mutation, but local rollback failed'), { code: normErr.code, cause: rejectionError, originalError: networkError });
149
+ }
150
+ db.unmarkInflight(docRef.path, mutationId);
151
+ safelyNotifyMutation(db, docRef.path, rebasedView.data ? (action === 'delete' ? 'set' : action) : 'delete', rebasedView.data, {
152
+ mutationId,
153
+ source: 'local',
154
+ state: 'rejected',
155
+ hasPendingWrites: rebasedView.hasPendingWrites
99
156
  });
100
- return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline' };
101
- }
102
- finally {
103
- db.unmarkInflight(docRef.path);
157
+ throw Object.assign(new Error(normErr.message), { code: normErr.code, cause: networkError });
104
158
  }
105
159
  }
160
+ // Phase 4: ACK Validation
161
+ const isValidAck = res && res.success &&
162
+ (action === 'delete' || (res.document && res.document.fields)) &&
163
+ (action !== 'delete' ? res.document.path === docRef.path : true);
164
+ if (!isValidAck) {
165
+ console.error('[NexaBase] Protocol error: Invalid server ACK:', res);
166
+ db.unmarkInflight(docRef.path, mutationId);
167
+ return { status: 'pending', committed: false, writtenLocally: true, mutationId };
168
+ }
169
+ // Phase 5: Atomic Local ACK
170
+ let ackView;
171
+ try {
172
+ ackView = await db._commitMutationAck({
173
+ mutationId,
174
+ path: docRef.path,
175
+ action,
176
+ serverDocument: res.document,
177
+ updateTime: res.document?.updateTime
178
+ });
179
+ }
180
+ catch (ackError) {
181
+ console.error('[NexaBase] Failed to persist ACK locally, mutation remains pending:', ackError);
182
+ db.unmarkInflight(docRef.path, mutationId);
183
+ return { status: 'pending', committed: false, writtenLocally: true, mutationId };
184
+ }
185
+ // Phase 6: Notification
186
+ db.unmarkInflight(docRef.path, mutationId);
187
+ safelyNotifyMutation(db, docRef.path, action, ackView.data, {
188
+ mutationId,
189
+ source: 'server',
190
+ state: 'acknowledged',
191
+ hasPendingWrites: ackView.hasPendingWrites
192
+ });
193
+ return { status: 'committed', committed: true, writtenLocally: true, mutationId, data: ackView.data };
194
+ };
195
+ const setDoc = async (docRef, data, options) => {
196
+ return executeWriteMutation(docRef, 'set', data, options);
197
+ };
198
+ exports.setDoc = setDoc;
199
+ const patchDoc = async (docRef, data, options) => {
200
+ return executeWriteMutation(docRef, 'patch', data, options);
106
201
  };
107
202
  exports.patchDoc = patchDoc;
108
203
  const updateDoc = async (docRef, data, options) => {
109
- return (0, exports.patchDoc)(docRef, data, options);
204
+ return (0, exports.patchDoc)(docRef, data, { ...options, preconditionExists: true });
110
205
  };
111
206
  exports.updateDoc = updateDoc;
112
207
  const deleteDoc = async (docRef, options) => {
113
- const db = docRef.db;
114
- if (db._initPromise)
115
- await db._initPromise;
116
- await db._deleteCache(docRef.path);
117
- db._broadcastLocalMutation(docRef.path, 'delete');
118
- db._notifySnapshotCallbacks(docRef.path, 'document_deleted', null);
119
- const key = options?.idempotencyKey || (0, helpers_1.generateIdempotencyKey)();
120
- const jobId = Math.random().toString(36).substring(2, 9);
121
- const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
122
- if (!isOnline) {
123
- await db._addOfflineJob({
124
- id: jobId,
125
- type: 'delete',
126
- path: docRef.path,
127
- idempotencyKey: key,
128
- state: 'pending'
129
- });
130
- return { success: true };
131
- }
132
- else {
133
- db.markInflight(docRef.path);
134
- try {
135
- const res = (await db.client.delete(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`, { headers: { 'X-Idempotency-Key': key } })).data;
136
- return res;
137
- }
138
- catch (error) {
139
- if (error && error.response && error.response.status >= 400 && error.response.status < 500) {
140
- throw (0, NexaError_1.toNexaError)(error);
141
- }
142
- await db._addOfflineJob({
143
- id: jobId,
144
- type: 'delete',
145
- path: docRef.path,
146
- idempotencyKey: key,
147
- state: 'pending'
148
- });
149
- return { success: true };
150
- }
151
- finally {
152
- db.unmarkInflight(docRef.path);
153
- }
154
- }
208
+ return executeWriteMutation(docRef, 'delete', null, options);
155
209
  };
156
210
  exports.deleteDoc = deleteDoc;
package/dist/index.d.ts CHANGED
@@ -30,6 +30,10 @@ export * from './sync/SyncEngine';
30
30
  export * from './sync/ConflictResolver';
31
31
  export * from './transport/HttpClient';
32
32
  export * from './transport/SSEClient';
33
+ export * from './transport/WebSocketClient';
33
34
  export * from './errors/NexaError';
34
35
  export * from './types/index';
35
36
  export * from './utils/helpers';
37
+ export * from './storage/types';
38
+ export * from './storage/NexaStorageError';
39
+ export { connectStorageEmulator } from './storage/Storage';
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.connectStorageEmulator = void 0;
17
18
  // App
18
19
  __exportStar(require("./app/initializeApp"), exports);
19
20
  __exportStar(require("./app/NexaApp"), exports);
@@ -54,7 +55,12 @@ __exportStar(require("./sync/ConflictResolver"), exports);
54
55
  // Transport
55
56
  __exportStar(require("./transport/HttpClient"), exports);
56
57
  __exportStar(require("./transport/SSEClient"), exports);
58
+ __exportStar(require("./transport/WebSocketClient"), exports);
57
59
  // Errors & Types
58
60
  __exportStar(require("./errors/NexaError"), exports);
59
61
  __exportStar(require("./types/index"), exports);
60
62
  __exportStar(require("./utils/helpers"), exports);
63
+ __exportStar(require("./storage/types"), exports);
64
+ __exportStar(require("./storage/NexaStorageError"), exports);
65
+ var Storage_1 = require("./storage/Storage");
66
+ Object.defineProperty(exports, "connectStorageEmulator", { enumerable: true, get: function () { return Storage_1.connectStorageEmulator; } });
@@ -2,11 +2,16 @@ import { IndexedDB } from './IndexedDB';
2
2
  export declare class LocalStore {
3
3
  private indexedDB;
4
4
  private inMemoryCache;
5
+ private inMemoryServerBase;
6
+ private currentSequence;
5
7
  constructor(indexedDB: IndexedDB);
8
+ nextMutationSequence(): Promise<number>;
9
+ getServerBaseCache(): Record<string, any>;
6
10
  set(path: string, data: any): Promise<void>;
7
11
  getInMemory(path: string): any;
8
12
  getMemoryCache(): Record<string, any>;
9
13
  get(path: string): Promise<any>;
10
14
  delete(path: string): Promise<void>;
11
15
  loadAll(): Promise<Record<string, any>>;
16
+ garbageCollect(maxItems?: number): Promise<void>;
12
17
  }
@@ -4,8 +4,17 @@ exports.LocalStore = void 0;
4
4
  class LocalStore {
5
5
  constructor(indexedDB) {
6
6
  this.inMemoryCache = {};
7
+ this.inMemoryServerBase = {};
8
+ this.currentSequence = 0;
7
9
  this.indexedDB = indexedDB;
8
10
  }
11
+ async nextMutationSequence() {
12
+ this.currentSequence++;
13
+ return this.currentSequence;
14
+ }
15
+ getServerBaseCache() {
16
+ return this.inMemoryServerBase;
17
+ }
9
18
  async set(path, data) {
10
19
  this.inMemoryCache[path] = data;
11
20
  const db = this.indexedDB.getRawDB();
@@ -14,7 +23,7 @@ class LocalStore {
14
23
  return new Promise((resolve, reject) => {
15
24
  const tx = db.transaction('offline_cache', 'readwrite');
16
25
  const store = tx.objectStore('offline_cache');
17
- const req = store.put({ path, data, timestamp: Date.now() });
26
+ const req = store.put({ path, data, timestamp: Date.now(), serverBase: this.inMemoryServerBase[path] !== undefined ? this.inMemoryServerBase[path] : null });
18
27
  req.onsuccess = () => resolve();
19
28
  req.onerror = () => reject(req.error);
20
29
  });
@@ -39,6 +48,8 @@ class LocalStore {
39
48
  const data = req.result ? req.result.data : null;
40
49
  if (data)
41
50
  this.inMemoryCache[path] = data;
51
+ if (req.result && req.result.serverBase !== undefined)
52
+ this.inMemoryServerBase[path] = req.result.serverBase;
42
53
  resolve(data);
43
54
  };
44
55
  req.onerror = () => reject(req.error);
@@ -46,6 +57,7 @@ class LocalStore {
46
57
  }
47
58
  async delete(path) {
48
59
  delete this.inMemoryCache[path];
60
+ delete this.inMemoryServerBase[path];
49
61
  const db = this.indexedDB.getRawDB();
50
62
  if (!db)
51
63
  return;
@@ -68,11 +80,55 @@ class LocalStore {
68
80
  req.onsuccess = () => {
69
81
  req.result.forEach((item) => {
70
82
  this.inMemoryCache[item.path] = item.data;
83
+ if (item.serverBase !== undefined)
84
+ this.inMemoryServerBase[item.path] = item.serverBase;
71
85
  });
72
86
  resolve(this.inMemoryCache);
73
87
  };
74
88
  req.onerror = () => reject(req.error);
75
89
  });
76
90
  }
91
+ async garbageCollect(maxItems = 5000) {
92
+ const db = this.indexedDB.getRawDB();
93
+ if (!db)
94
+ return;
95
+ return new Promise((resolve, reject) => {
96
+ const tx = db.transaction('offline_cache', 'readwrite');
97
+ const store = tx.objectStore('offline_cache');
98
+ const req = store.getAll();
99
+ req.onsuccess = () => {
100
+ const items = req.result;
101
+ if (items.length > maxItems) {
102
+ items.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
103
+ const itemsToRemove = items.slice(0, items.length - maxItems);
104
+ if (itemsToRemove.length === 0) {
105
+ return resolve();
106
+ }
107
+ let removedCount = 0;
108
+ itemsToRemove.forEach((item) => {
109
+ const delReq = store.delete(item.path);
110
+ delReq.onsuccess = () => {
111
+ delete this.inMemoryCache[item.path];
112
+ delete this.inMemoryServerBase[item.path];
113
+ removedCount++;
114
+ if (removedCount === itemsToRemove.length) {
115
+ resolve();
116
+ }
117
+ };
118
+ delReq.onerror = () => {
119
+ removedCount++;
120
+ if (removedCount === itemsToRemove.length) {
121
+ resolve();
122
+ }
123
+ };
124
+ });
125
+ }
126
+ else {
127
+ resolve();
128
+ }
129
+ };
130
+ req.onerror = () => reject(req.error);
131
+ });
132
+ }
77
133
  }
78
134
  exports.LocalStore = LocalStore;
@@ -13,9 +13,10 @@ export declare class MutationQueue {
13
13
  loadQueue(): Promise<OfflineJob[]>;
14
14
  addJob(job: OfflineJob): Promise<void>;
15
15
  saveQueue(queue: OfflineJob[]): Promise<void>;
16
+ claimJob(mutationId: string, ownerId: string, leaseDurationMs: number): Promise<OfflineJob | null>;
16
17
  removeJob(jobId: string): Promise<void>;
17
- markInflight(path: string): void;
18
- unmarkInflight(path: string): void;
18
+ markInflight(path: string, mutationId?: string): void;
19
+ unmarkInflight(path: string, mutationId?: string): void;
19
20
  hasPendingWrites(path: string): boolean;
20
21
  getQueue(): OfflineJob[];
21
22
  syncQueue(): Promise<void>;
@@ -4,7 +4,7 @@ exports.MutationQueue = void 0;
4
4
  class MutationQueue {
5
5
  constructor(projectId, indexedDB, client) {
6
6
  this.inMemoryQueue = [];
7
- this.inflightWrites = new Set();
7
+ this.inflightWrites = new Map();
8
8
  this.isSyncing = false;
9
9
  this.projectId = projectId;
10
10
  this.indexedDB = indexedDB;
@@ -71,20 +71,45 @@ class MutationQueue {
71
71
  clearReq.onerror = () => reject(clearReq.error);
72
72
  });
73
73
  }
74
+ async claimJob(mutationId, ownerId, leaseDurationMs) {
75
+ const job = this.inMemoryQueue.find(j => j.id === mutationId);
76
+ if (!job)
77
+ return null;
78
+ const now = Date.now();
79
+ if (job.leaseOwner && job.leaseOwner !== ownerId && job.leaseExpiresAt && job.leaseExpiresAt > now) {
80
+ return null;
81
+ }
82
+ job.leaseOwner = ownerId;
83
+ job.leaseExpiresAt = now + leaseDurationMs;
84
+ await this.saveQueue(this.inMemoryQueue);
85
+ return job;
86
+ }
74
87
  async removeJob(jobId) {
75
88
  const updated = this.inMemoryQueue.filter((j) => j.id !== jobId);
76
89
  await this.saveQueue(updated);
77
90
  }
78
- markInflight(path) {
79
- this.inflightWrites.add(path);
91
+ markInflight(path, mutationId) {
92
+ if (!mutationId)
93
+ return;
94
+ if (!this.inflightWrites.has(path))
95
+ this.inflightWrites.set(path, new Set());
96
+ this.inflightWrites.get(path).add(mutationId);
80
97
  }
81
- unmarkInflight(path) {
82
- this.inflightWrites.delete(path);
98
+ unmarkInflight(path, mutationId) {
99
+ if (!mutationId)
100
+ return;
101
+ const set = this.inflightWrites.get(path);
102
+ if (set) {
103
+ set.delete(mutationId);
104
+ if (set.size === 0) {
105
+ this.inflightWrites.delete(path);
106
+ }
107
+ }
83
108
  }
84
109
  hasPendingWrites(path) {
85
110
  if (this.inflightWrites.has(path))
86
111
  return true;
87
- for (const p of this.inflightWrites) {
112
+ for (const p of Array.from(this.inflightWrites.keys())) {
88
113
  if (p.startsWith(path + '/'))
89
114
  return true;
90
115
  }
@@ -112,7 +137,7 @@ class MutationQueue {
112
137
  merge: job.merge
113
138
  }, { headers });
114
139
  }
115
- else if (job.type === 'patch' || job.type === 'update') {
140
+ else if (job.type === 'patch') {
116
141
  await this.client.patch(`/api/firestore/${this.projectId}/document`, {
117
142
  docPath: job.path,
118
143
  data: job.data,
@@ -1,15 +1,16 @@
1
+ import { WebSocketClient } from './WebSocketClient';
1
2
  export declare class SSEClient {
2
- private projectId;
3
- private sseUrl;
4
- private sse;
5
- private isListening;
6
- private listeners;
3
+ private wsClient;
7
4
  constructor(projectId: string, sseUrl: string);
8
5
  addListener(event: string, callback: (data: any) => void): () => void;
6
+ subscribe(path: string): void;
7
+ unsubscribe(path: string): void;
9
8
  ensureConnected(): void;
10
9
  checkCloseConnection(): void;
11
10
  hasListeners(): boolean;
12
11
  dispatch(event: string, data: any): void;
12
+ send(event: string, data: any): Promise<any>;
13
13
  close(): void;
14
14
  }
15
15
  export { SSEClient as SSEManager };
16
+ export { WebSocketClient };