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
@@ -1,20 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Timestamp = void 0;
4
+ const NexaError_1 = require("../errors/NexaError");
5
+ const MIN_SECONDS = -62135596800; // 0001-01-01T00:00:00Z
6
+ const MAX_SECONDS = 253402300799; // 9999-12-31T23:59:59Z
4
7
  class Timestamp {
5
8
  constructor(seconds, nanoseconds) {
9
+ if (!Number.isSafeInteger(seconds) || seconds < MIN_SECONDS || seconds > MAX_SECONDS) {
10
+ throw new NexaError_1.NexaError('invalid-argument', `Timestamp seconds must be a safe integer between ${MIN_SECONDS} and ${MAX_SECONDS}, but got ${seconds}.`);
11
+ }
12
+ if (!Number.isSafeInteger(nanoseconds) || nanoseconds < 0 || nanoseconds > 999999999) {
13
+ throw new NexaError_1.NexaError('invalid-argument', `Timestamp nanoseconds must be a non-negative safe integer less than 1,000,000,000, but got ${nanoseconds}.`);
14
+ }
6
15
  this.seconds = seconds;
7
16
  this.nanoseconds = nanoseconds;
17
+ Object.freeze(this);
8
18
  }
9
19
  static now() {
10
20
  return Timestamp.fromMillis(Date.now());
11
21
  }
12
22
  static fromDate(date) {
23
+ if (!(date instanceof Date) || isNaN(date.getTime())) {
24
+ throw new NexaError_1.NexaError('invalid-argument', 'Timestamp.fromDate() expects a valid Date object.');
25
+ }
13
26
  return Timestamp.fromMillis(date.getTime());
14
27
  }
15
28
  static fromMillis(milliseconds) {
16
- const seconds = Math.floor(milliseconds / 1000);
17
- const nanoseconds = Math.floor((milliseconds % 1000) * 1e6);
29
+ if (typeof milliseconds !== 'number' || !Number.isFinite(milliseconds)) {
30
+ throw new NexaError_1.NexaError('invalid-argument', 'Timestamp.fromMillis() expects a finite number.');
31
+ }
32
+ let seconds = Math.floor(milliseconds / 1000);
33
+ const remainderMs = milliseconds - seconds * 1000;
34
+ let nanoseconds = Math.round(remainderMs * 1e6);
35
+ if (nanoseconds >= 1e9) {
36
+ seconds += 1;
37
+ nanoseconds = 0;
38
+ }
18
39
  return new Timestamp(seconds, nanoseconds);
19
40
  }
20
41
  toDate() {
@@ -24,8 +45,13 @@ class Timestamp {
24
45
  return this.seconds * 1000 + Math.floor(this.nanoseconds / 1e6);
25
46
  }
26
47
  isEqual(other) {
48
+ if (!(other instanceof Timestamp))
49
+ return false;
27
50
  return this.seconds === other.seconds && this.nanoseconds === other.nanoseconds;
28
51
  }
52
+ valueOf() {
53
+ return this.toString();
54
+ }
29
55
  toString() {
30
56
  return `Timestamp(seconds=${this.seconds}, nanoseconds=${this.nanoseconds})`;
31
57
  }
@@ -3,32 +3,90 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeBatch = void 0;
4
4
  const helpers_1 = require("../utils/helpers");
5
5
  const NexaError_1 = require("../errors/NexaError");
6
+ const QueryUtils_1 = require("./QueryUtils");
7
+ const pathValidation_1 = require("./pathValidation");
6
8
  const writeBatch = (db) => {
9
+ if (!db) {
10
+ throw new NexaError_1.NexaError('invalid-argument', 'writeBatch requires a valid NexaApp database instance.');
11
+ }
7
12
  const operations = [];
13
+ let committed = false;
8
14
  const batchObj = {
9
- set: (docRef, data) => {
10
- operations.push({ type: 'set', path: docRef.path, data });
15
+ set: (docRef, data, options) => {
16
+ if (committed) {
17
+ throw new NexaError_1.NexaError('failed-precondition', 'A WriteBatch cannot be modified after commit() has been called.');
18
+ }
19
+ if (!docRef || !docRef.path) {
20
+ throw new NexaError_1.NexaError('invalid-argument', 'WriteBatch.set() requires a valid DocumentReference.');
21
+ }
22
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
23
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
24
+ throw new NexaError_1.NexaError('invalid-argument', 'WriteBatch.set() data must be a plain object.');
25
+ }
26
+ if (operations.length >= 500) {
27
+ throw new NexaError_1.NexaError('invalid-argument', 'A WriteBatch cannot contain more than 500 operations.');
28
+ }
29
+ operations.push({
30
+ type: 'set',
31
+ path: docRef.path,
32
+ data: (0, QueryUtils_1.cloneNexaData)(data),
33
+ options
34
+ });
11
35
  return batchObj;
12
36
  },
13
37
  update: (docRef, data) => {
14
- operations.push({ type: 'update', path: docRef.path, data });
38
+ if (committed) {
39
+ throw new NexaError_1.NexaError('failed-precondition', 'A WriteBatch cannot be modified after commit() has been called.');
40
+ }
41
+ if (!docRef || !docRef.path) {
42
+ throw new NexaError_1.NexaError('invalid-argument', 'WriteBatch.update() requires a valid DocumentReference.');
43
+ }
44
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
45
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
46
+ throw new NexaError_1.NexaError('invalid-argument', 'WriteBatch.update() data must be a plain object.');
47
+ }
48
+ if (operations.length >= 500) {
49
+ throw new NexaError_1.NexaError('invalid-argument', 'A WriteBatch cannot contain more than 500 operations.');
50
+ }
51
+ operations.push({
52
+ type: 'update',
53
+ path: docRef.path,
54
+ data: (0, QueryUtils_1.cloneNexaData)(data)
55
+ });
15
56
  return batchObj;
16
57
  },
17
58
  patch: (docRef, data) => {
18
- operations.push({ type: 'update', path: docRef.path, data });
19
- return batchObj;
59
+ return batchObj.update(docRef, data);
20
60
  },
21
61
  delete: (docRef) => {
22
- operations.push({ type: 'delete', path: docRef.path });
62
+ if (committed) {
63
+ throw new NexaError_1.NexaError('failed-precondition', 'A WriteBatch cannot be modified after commit() has been called.');
64
+ }
65
+ if (!docRef || !docRef.path) {
66
+ throw new NexaError_1.NexaError('invalid-argument', 'WriteBatch.delete() requires a valid DocumentReference.');
67
+ }
68
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
69
+ if (operations.length >= 500) {
70
+ throw new NexaError_1.NexaError('invalid-argument', 'A WriteBatch cannot contain more than 500 operations.');
71
+ }
72
+ operations.push({
73
+ type: 'delete',
74
+ path: docRef.path
75
+ });
23
76
  return batchObj;
24
77
  },
25
78
  commit: async () => {
79
+ if (committed) {
80
+ throw new NexaError_1.NexaError('failed-precondition', 'A WriteBatch cannot be committed more than once.');
81
+ }
82
+ committed = true;
26
83
  if (db._initPromise)
27
84
  await db._initPromise;
28
85
  if (operations.length === 0)
29
86
  return;
30
87
  const idempotencyKey = (0, helpers_1.generateIdempotencyKey)();
31
88
  const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
89
+ // 1. Optimistic Local Commit
32
90
  for (const op of operations) {
33
91
  if (op.type === 'set' || op.type === 'update') {
34
92
  const newData = (0, helpers_1.applyDataTransforms)(db._firestoreCache[op.path] || {}, op.data);
@@ -42,39 +100,34 @@ const writeBatch = (db) => {
42
100
  db._notifySnapshotCallbacks(op.path, 'document_deleted', null);
43
101
  }
44
102
  }
103
+ // 2. Offline Queue Persistence
104
+ for (const op of operations) {
105
+ await db._addOfflineJob({
106
+ id: `${idempotencyKey}_${op.path.replace(/\//g, '_')}`,
107
+ type: op.type === 'update' ? 'patch' : op.type,
108
+ path: op.path,
109
+ data: op.data,
110
+ idempotencyKey,
111
+ merge: op.options?.merge,
112
+ state: 'pending'
113
+ });
114
+ }
45
115
  if (!isOnline) {
46
- for (const op of operations) {
47
- await db._addOfflineJob({
48
- id: Math.random().toString(36).substring(2, 9),
49
- type: op.type,
50
- path: op.path,
51
- data: op.data,
52
- idempotencyKey,
53
- state: 'pending'
54
- });
55
- }
56
- return { success: true, message: 'Offline: Batch operations queued in IndexedDB' };
116
+ return { success: true, message: 'Offline: Batch operations queued for sync' };
57
117
  }
58
- else {
59
- try {
60
- return (await db.client.post(`/api/firestore/${db.projectId}/batch`, { operations, idempotencyKey }, { headers: { 'X-Idempotency-Key': idempotencyKey } })).data;
61
- }
62
- catch (error) {
63
- if (error && error.response && error.response.status >= 400 && error.response.status < 500) {
64
- throw (0, NexaError_1.toNexaError)(error);
65
- }
66
- for (const op of operations) {
67
- await db._addOfflineJob({
68
- id: Math.random().toString(36).substring(2, 9),
69
- type: op.type,
70
- path: op.path,
71
- data: op.data,
72
- idempotencyKey,
73
- state: 'pending'
74
- });
75
- }
76
- return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline' };
118
+ // 3. Online Server Sync
119
+ try {
120
+ const response = await db.client.post(`/api/firestore/${db.projectId}/batch`, { operations, idempotencyKey }, { headers: { 'X-Idempotency-Key': idempotencyKey } });
121
+ return response.data;
122
+ }
123
+ catch (error) {
124
+ const status = error?.response?.status;
125
+ const isClientError = status && status >= 400 && status < 500 && status !== 408 && status !== 429;
126
+ if (isClientError) {
127
+ throw (0, NexaError_1.toNexaError)(error);
77
128
  }
129
+ // Retryable/network error: keep in offline queue
130
+ return { success: true, message: 'Network disrupted: batch queued offline' };
78
131
  }
79
132
  }
80
133
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Validates raw path string and extracts clean segments.
3
+ * Throws NexaError('invalid-argument') on any validation failure.
4
+ */
5
+ export declare function validatePathSegments(path: string): string[];
6
+ /**
7
+ * Validates that a path points to a valid collection (odd number of segments).
8
+ */
9
+ export declare function validateCollectionPath(path: string): string;
10
+ /**
11
+ * Validates that a path points to a valid document (even number of segments).
12
+ */
13
+ export declare function validateDocumentPath(path: string): string;
14
+ /**
15
+ * Generates a 20-character alphanumeric auto-ID matching Firestore specifications.
16
+ */
17
+ export declare function generateAutoId(): string;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validatePathSegments = validatePathSegments;
4
+ exports.validateCollectionPath = validateCollectionPath;
5
+ exports.validateDocumentPath = validateDocumentPath;
6
+ exports.generateAutoId = generateAutoId;
7
+ const NexaError_1 = require("../errors/NexaError");
8
+ const CONTROL_CHARS_REGEX = /[\x00-\x1F\x7F]/;
9
+ /**
10
+ * Validates raw path string and extracts clean segments.
11
+ * Throws NexaError('invalid-argument') on any validation failure.
12
+ */
13
+ function validatePathSegments(path) {
14
+ if (typeof path !== 'string' || path.length === 0) {
15
+ throw new NexaError_1.NexaError('invalid-argument', 'Path must be a non-empty string.');
16
+ }
17
+ if (CONTROL_CHARS_REGEX.test(path)) {
18
+ throw new NexaError_1.NexaError('invalid-argument', 'Path contains invalid control characters.');
19
+ }
20
+ if (path.startsWith('/') || path.endsWith('/') || path.includes('//')) {
21
+ throw new NexaError_1.NexaError('invalid-argument', 'Path must not contain empty segments or leading/trailing slashes.');
22
+ }
23
+ const segments = path.split('/');
24
+ for (const seg of segments) {
25
+ if (!seg || seg.trim().length === 0) {
26
+ throw new NexaError_1.NexaError('invalid-argument', 'Path segment cannot be empty or whitespace-only.');
27
+ }
28
+ }
29
+ return segments;
30
+ }
31
+ /**
32
+ * Validates that a path points to a valid collection (odd number of segments).
33
+ */
34
+ function validateCollectionPath(path) {
35
+ const segments = validatePathSegments(path);
36
+ if (segments.length % 2 === 0) {
37
+ throw new NexaError_1.NexaError('invalid-argument', `Invalid collection path (${path}). Collection paths must have an odd number of segments.`);
38
+ }
39
+ return segments.join('/');
40
+ }
41
+ /**
42
+ * Validates that a path points to a valid document (even number of segments).
43
+ */
44
+ function validateDocumentPath(path) {
45
+ const segments = validatePathSegments(path);
46
+ if (segments.length % 2 !== 0) {
47
+ throw new NexaError_1.NexaError('invalid-argument', `Invalid document path (${path}). Document paths must have an even number of segments.`);
48
+ }
49
+ return segments.join('/');
50
+ }
51
+ /**
52
+ * Generates a 20-character alphanumeric auto-ID matching Firestore specifications.
53
+ */
54
+ function generateAutoId() {
55
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
56
+ let autoId = '';
57
+ for (let i = 0; i < 20; i++) {
58
+ autoId += chars.charAt(Math.floor(Math.random() * chars.length));
59
+ }
60
+ return autoId;
61
+ }
@@ -3,7 +3,7 @@ import { QueryConstraint, WhereFilterOp } from '../types/index';
3
3
  export declare const where: (fieldPath: string | FieldPath, opStr: WhereFilterOp | string, value: any) => QueryConstraint;
4
4
  export declare const orderBy: (fieldPath: string | FieldPath, directionStr?: "asc" | "desc") => QueryConstraint;
5
5
  export declare const limit: (limitValue: number) => QueryConstraint;
6
- export declare const startAt: (docSnapshot: any) => QueryConstraint;
7
- export declare const startAfter: (docSnapshot: any) => QueryConstraint;
8
- export declare const endAt: (docSnapshot: any) => QueryConstraint;
9
- export declare const endBefore: (docSnapshot: any) => QueryConstraint;
6
+ export declare const startAt: (...docSnapshotOrValues: any[]) => QueryConstraint;
7
+ export declare const startAfter: (...docSnapshotOrValues: any[]) => QueryConstraint;
8
+ export declare const endAt: (...docSnapshotOrValues: any[]) => QueryConstraint;
9
+ export declare const endBefore: (...docSnapshotOrValues: any[]) => QueryConstraint;
@@ -1,31 +1,92 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.endBefore = exports.endAt = exports.startAfter = exports.startAt = exports.limit = exports.orderBy = exports.where = void 0;
4
+ const FieldPath_1 = require("./FieldPath");
5
+ const NexaError_1 = require("../errors/NexaError");
6
+ const VALID_WHERE_OPS = new Set([
7
+ '<',
8
+ '<=',
9
+ '==',
10
+ '!=',
11
+ '>=',
12
+ '>',
13
+ 'array-contains',
14
+ 'array-contains-any',
15
+ 'in',
16
+ 'not-in'
17
+ ]);
18
+ function validateFieldPath(fieldPath) {
19
+ if (!fieldPath) {
20
+ throw new NexaError_1.NexaError('invalid-argument', 'Field path cannot be empty.');
21
+ }
22
+ if (typeof fieldPath !== 'string' && !(fieldPath instanceof FieldPath_1.FieldPath)) {
23
+ throw new NexaError_1.NexaError('invalid-argument', 'Field path must be a non-empty string or FieldPath instance.');
24
+ }
25
+ if (typeof fieldPath === 'string' && fieldPath.trim().length === 0) {
26
+ throw new NexaError_1.NexaError('invalid-argument', 'Field path string cannot be empty or whitespace.');
27
+ }
28
+ }
4
29
  const where = (fieldPath, opStr, value) => {
30
+ validateFieldPath(fieldPath);
31
+ if (!VALID_WHERE_OPS.has(opStr)) {
32
+ throw new NexaError_1.NexaError('invalid-argument', `Invalid query operator '${opStr}'. Valid operators are: ${Array.from(VALID_WHERE_OPS).join(', ')}`);
33
+ }
34
+ if (opStr === 'in' || opStr === 'not-in' || opStr === 'array-contains-any') {
35
+ if (!Array.isArray(value)) {
36
+ throw new NexaError_1.NexaError('invalid-argument', `Query operator '${opStr}' requires an array value, but got ${typeof value}.`);
37
+ }
38
+ if (value.length === 0) {
39
+ throw new NexaError_1.NexaError('invalid-argument', `Query operator '${opStr}' requires a non-empty array.`);
40
+ }
41
+ if (value.length > 30) {
42
+ throw new NexaError_1.NexaError('invalid-argument', `Query operator '${opStr}' supports at most 30 elements in array filter.`);
43
+ }
44
+ }
5
45
  return { type: 'where', fieldPath, opStr, value };
6
46
  };
7
47
  exports.where = where;
8
48
  const orderBy = (fieldPath, directionStr = 'asc') => {
49
+ validateFieldPath(fieldPath);
50
+ if (directionStr !== 'asc' && directionStr !== 'desc') {
51
+ throw new NexaError_1.NexaError('invalid-argument', `Invalid orderBy direction '${directionStr}'. Must be 'asc' or 'desc'.`);
52
+ }
9
53
  return { type: 'orderBy', fieldPath, directionStr };
10
54
  };
11
55
  exports.orderBy = orderBy;
12
56
  const limit = (limitValue) => {
57
+ if (typeof limitValue !== 'number' || !Number.isSafeInteger(limitValue) || limitValue <= 0) {
58
+ throw new NexaError_1.NexaError('invalid-argument', `limit() value must be a positive safe integer, but got ${limitValue}.`);
59
+ }
13
60
  return { type: 'limit', limitValue };
14
61
  };
15
62
  exports.limit = limit;
16
- const startAt = (docSnapshot) => {
17
- return { type: 'startAt', docSnapshot };
63
+ const createCursorConstraint = (type, docSnapshotOrValues) => {
64
+ if (!docSnapshotOrValues || docSnapshotOrValues.length === 0) {
65
+ throw new NexaError_1.NexaError('invalid-argument', `${type}() requires at least one argument (DocumentSnapshot or field values).`);
66
+ }
67
+ const firstArg = docSnapshotOrValues[0];
68
+ const isSnapshot = firstArg &&
69
+ typeof firstArg === 'object' &&
70
+ typeof firstArg.id === 'string' &&
71
+ (typeof firstArg.data === 'function' || typeof firstArg.exists === 'function');
72
+ if (isSnapshot && docSnapshotOrValues.length === 1) {
73
+ return { type, docSnapshot: firstArg };
74
+ }
75
+ return { type, cursorValues: docSnapshotOrValues };
76
+ };
77
+ const startAt = (...docSnapshotOrValues) => {
78
+ return createCursorConstraint('startAt', docSnapshotOrValues);
18
79
  };
19
80
  exports.startAt = startAt;
20
- const startAfter = (docSnapshot) => {
21
- return { type: 'startAfter', docSnapshot };
81
+ const startAfter = (...docSnapshotOrValues) => {
82
+ return createCursorConstraint('startAfter', docSnapshotOrValues);
22
83
  };
23
84
  exports.startAfter = startAfter;
24
- const endAt = (docSnapshot) => {
25
- return { type: 'endAt', docSnapshot };
85
+ const endAt = (...docSnapshotOrValues) => {
86
+ return createCursorConstraint('endAt', docSnapshotOrValues);
26
87
  };
27
88
  exports.endAt = endAt;
28
- const endBefore = (docSnapshot) => {
29
- return { type: 'endBefore', docSnapshot };
89
+ const endBefore = (...docSnapshotOrValues) => {
90
+ return createCursorConstraint('endBefore', docSnapshotOrValues);
30
91
  };
31
92
  exports.endBefore = endBefore;
@@ -5,56 +5,110 @@ const SnapshotManager_1 = require("./SnapshotManager");
5
5
  const ConflictResolver_1 = require("../sync/ConflictResolver");
6
6
  const NexaError_1 = require("../errors/NexaError");
7
7
  const helpers_1 = require("../utils/helpers");
8
+ const QueryUtils_1 = require("./QueryUtils");
9
+ const pathValidation_1 = require("./pathValidation");
8
10
  const runTransaction = async (db, updateFunction, options) => {
11
+ if (!db) {
12
+ throw new NexaError_1.NexaError('invalid-argument', 'runTransaction requires a valid NexaApp database instance.');
13
+ }
14
+ if (typeof updateFunction !== 'function') {
15
+ throw new NexaError_1.NexaError('invalid-argument', 'updateFunction must be a valid function.');
16
+ }
9
17
  if (db._initPromise)
10
18
  await db._initPromise;
19
+ const maxAttempts = options?.maxAttempts ?? 5;
11
20
  return ConflictResolver_1.ConflictResolver.resolveWithRetry(async () => {
12
21
  const reads = [];
13
22
  const operations = [];
23
+ let writesStarted = false;
14
24
  const idempotencyKey = (0, helpers_1.generateIdempotencyKey)();
15
25
  const transaction = {
16
26
  get: async (docRef) => {
17
- let res;
27
+ if (writesStarted) {
28
+ throw new NexaError_1.NexaError('failed-precondition', 'Firestore transactions require all reads to be executed before all writes.');
29
+ }
30
+ if (!docRef || !docRef.path) {
31
+ throw new NexaError_1.NexaError('invalid-argument', 'Transaction.get() requires a valid DocumentReference.');
32
+ }
33
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
34
+ if (typeof navigator !== 'undefined' && !navigator.onLine) {
35
+ throw new NexaError_1.NexaError('unavailable', 'Transaction failed: Client is offline. Transactions require an active server connection.');
36
+ }
18
37
  try {
19
- if (typeof navigator !== 'undefined' && !navigator.onLine) {
20
- throw Object.assign(new Error('NEXA_ERR_TRANSACTION_OFFLINE'), { code: 'NEXA_ERR_TRANSACTION_OFFLINE' });
21
- }
22
- res = await db.client.get(`/api/firestore/${db.projectId}/documentWithMetadata?docPath=${encodeURIComponent(docRef.path)}`);
38
+ const res = await db.client.get(`/api/firestore/${db.projectId}/documentWithMetadata?docPath=${encodeURIComponent(docRef.path)}`);
39
+ const { data, updatedAt } = res.data;
40
+ reads.push({ path: docRef.path, readUpdatedAt: updatedAt ?? null });
41
+ return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, (data ?? null), false, db.hasPendingWrites(docRef.path));
23
42
  }
24
43
  catch (err) {
25
- if (err?.code === 'NEXA_ERR_TRANSACTION_OFFLINE' || err?.message === 'NEXA_ERR_TRANSACTION_OFFLINE') {
26
- throw Object.assign(new Error('Transaction failed: Perangkat sedang offline atau jaringan terputus. Transaksi memerlukan koneksi aktif. Gunakan writeBatch atau runOfflineSafeTransaction untuk operasi offline.'), { code: 'NEXA_ERR_TRANSACTION_OFFLINE' });
44
+ if (err?.response?.status === 404) {
45
+ reads.push({ path: docRef.path, readUpdatedAt: null });
46
+ return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, null, false, false);
27
47
  }
28
48
  throw (0, NexaError_1.toNexaError)(err);
29
49
  }
30
- const { data, updatedAt } = res.data;
31
- reads.push({ path: docRef.path, readUpdatedAt: updatedAt });
32
- return SnapshotManager_1.SnapshotManager.createDocumentSnapshot(docRef, data, false, db.hasPendingWrites(docRef.path));
33
50
  },
34
51
  set: (docRef, data) => {
35
- operations.push({ type: 'set', path: docRef.path, data });
52
+ if (!docRef || !docRef.path) {
53
+ throw new NexaError_1.NexaError('invalid-argument', 'Transaction.set() requires a valid DocumentReference.');
54
+ }
55
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
56
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
57
+ throw new NexaError_1.NexaError('invalid-argument', 'Transaction.set() data must be a plain object.');
58
+ }
59
+ writesStarted = true;
60
+ operations.push({
61
+ type: 'set',
62
+ path: docRef.path,
63
+ data: (0, QueryUtils_1.cloneNexaData)(data)
64
+ });
36
65
  return transaction;
37
66
  },
38
67
  update: (docRef, data) => {
39
- operations.push({ type: 'update', path: docRef.path, data });
68
+ if (!docRef || !docRef.path) {
69
+ throw new NexaError_1.NexaError('invalid-argument', 'Transaction.update() requires a valid DocumentReference.');
70
+ }
71
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
72
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
73
+ throw new NexaError_1.NexaError('invalid-argument', 'Transaction.update() data must be a plain object.');
74
+ }
75
+ writesStarted = true;
76
+ operations.push({
77
+ type: 'update',
78
+ path: docRef.path,
79
+ data: (0, QueryUtils_1.cloneNexaData)(data)
80
+ });
40
81
  return transaction;
41
82
  },
42
83
  patch: (docRef, data) => {
43
- operations.push({ type: 'update', path: docRef.path, data });
44
- return transaction;
84
+ return transaction.update(docRef, data);
45
85
  },
46
86
  delete: (docRef) => {
47
- operations.push({ type: 'delete', path: docRef.path });
87
+ if (!docRef || !docRef.path) {
88
+ throw new NexaError_1.NexaError('invalid-argument', 'Transaction.delete() requires a valid DocumentReference.');
89
+ }
90
+ (0, pathValidation_1.validateDocumentPath)(docRef.path);
91
+ writesStarted = true;
92
+ operations.push({
93
+ type: 'delete',
94
+ path: docRef.path
95
+ });
48
96
  return transaction;
49
97
  }
50
98
  };
51
99
  const result = await updateFunction(transaction);
52
100
  if (operations.length > 0 || reads.length > 0) {
53
101
  if (typeof navigator !== 'undefined' && !navigator.onLine) {
54
- throw Object.assign(new Error('Transaction failed: Perangkat sedang offline atau jaringan terputus. Transaksi memerlukan koneksi aktif. Gunakan writeBatch atau runOfflineSafeTransaction untuk operasi offline.'), { code: 'NEXA_ERR_TRANSACTION_OFFLINE' });
102
+ throw new NexaError_1.NexaError('unavailable', 'Transaction failed: Client is offline. Transactions require an active server connection.');
103
+ }
104
+ try {
105
+ await db.client.post(`/api/firestore/${db.projectId}/transaction`, { reads, operations, idempotencyKey }, { headers: { 'X-Idempotency-Key': idempotencyKey } });
106
+ }
107
+ catch (err) {
108
+ throw (0, NexaError_1.toNexaError)(err);
55
109
  }
56
- await db.client.post(`/api/firestore/${db.projectId}/transaction`, { reads, operations, idempotencyKey }, { headers: { 'X-Idempotency-Key': idempotencyKey } });
57
110
  }
111
+ // Atomic local commit after successful server transaction
58
112
  for (const op of operations) {
59
113
  if (op.type === 'set' || op.type === 'update') {
60
114
  const newData = (0, helpers_1.applyDataTransforms)(db._firestoreCache[op.path] || {}, op.data);
@@ -69,6 +123,6 @@ const runTransaction = async (db, updateFunction, options) => {
69
123
  }
70
124
  }
71
125
  return result;
72
- }, options?.maxAttempts ?? 5);
126
+ }, maxAttempts);
73
127
  };
74
128
  exports.runTransaction = runTransaction;
@@ -1,4 +1,4 @@
1
- import { NexaApp } from "../app/NexaApp";
1
+ import { NexaApp } from '../app/NexaApp';
2
2
  import { DocumentReference } from '../types/index';
3
3
  export { FieldValue } from './FieldValue';
4
4
  export type WriteResult<T = unknown> = {