@topgunbuild/client 0.2.0-alpha → 0.2.1

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.
package/dist/index.mjs CHANGED
@@ -232,6 +232,8 @@ var SyncEngine = class {
232
232
  this.waitingForCapacity = [];
233
233
  this.highWaterMarkEmitted = false;
234
234
  this.backpressureListeners = /* @__PURE__ */ new Map();
235
+ // Write Concern state (Phase 5.01)
236
+ this.pendingWriteConcernPromises = /* @__PURE__ */ new Map();
235
237
  this.nodeId = config.nodeId;
236
238
  this.serverUrl = config.serverUrl;
237
239
  this.storageAdapter = config.storageAdapter;
@@ -633,6 +635,22 @@ var SyncEngine = class {
633
635
  }
634
636
  async handleServerMessage(message) {
635
637
  switch (message.type) {
638
+ case "BATCH": {
639
+ const batchData = message.data;
640
+ const view = new DataView(batchData.buffer, batchData.byteOffset, batchData.byteLength);
641
+ let offset = 0;
642
+ const count = view.getUint32(offset, true);
643
+ offset += 4;
644
+ for (let i = 0; i < count; i++) {
645
+ const msgLen = view.getUint32(offset, true);
646
+ offset += 4;
647
+ const msgData = batchData.slice(offset, offset + msgLen);
648
+ offset += msgLen;
649
+ const innerMsg = deserialize(msgData);
650
+ await this.handleServerMessage(innerMsg);
651
+ }
652
+ break;
653
+ }
636
654
  case "AUTH_REQUIRED":
637
655
  this.sendAuth();
638
656
  break;
@@ -664,8 +682,18 @@ var SyncEngine = class {
664
682
  this.authToken = null;
665
683
  break;
666
684
  case "OP_ACK": {
667
- const { lastId } = message.payload;
668
- logger.info({ lastId }, "Received ACK for ops");
685
+ const { lastId, achievedLevel, results } = message.payload;
686
+ logger.info({ lastId, achievedLevel, hasResults: !!results }, "Received ACK for ops");
687
+ if (results && Array.isArray(results)) {
688
+ for (const result of results) {
689
+ const op = this.opLog.find((o) => o.id === result.opId);
690
+ if (op && !op.synced) {
691
+ op.synced = true;
692
+ logger.debug({ opId: result.opId, achievedLevel: result.achievedLevel, success: result.success }, "Op ACK with Write Concern");
693
+ }
694
+ this.resolveWriteConcernPromise(result.opId, result);
695
+ }
696
+ }
669
697
  let maxSyncedId = -1;
670
698
  let ackedCount = 0;
671
699
  this.opLog.forEach((op) => {
@@ -726,18 +754,20 @@ var SyncEngine = class {
726
754
  }
727
755
  case "SERVER_EVENT": {
728
756
  const { mapName, eventType, key, record, orRecord, orTag } = message.payload;
729
- const localMap = this.maps.get(mapName);
730
- if (localMap) {
731
- if (localMap instanceof LWWMap && record) {
732
- localMap.merge(key, record);
733
- await this.storageAdapter.put(`${mapName}:${key}`, record);
734
- } else if (localMap instanceof ORMap) {
735
- if (eventType === "OR_ADD" && orRecord) {
736
- localMap.apply(key, orRecord);
737
- } else if (eventType === "OR_REMOVE" && orTag) {
738
- localMap.applyTombstone(orTag);
739
- }
740
- }
757
+ await this.applyServerEvent(mapName, eventType, key, record, orRecord, orTag);
758
+ break;
759
+ }
760
+ case "SERVER_BATCH_EVENT": {
761
+ const { events } = message.payload;
762
+ for (const event of events) {
763
+ await this.applyServerEvent(
764
+ event.mapName,
765
+ event.eventType,
766
+ event.key,
767
+ event.record,
768
+ event.orRecord,
769
+ event.orTag
770
+ );
741
771
  }
742
772
  break;
743
773
  }
@@ -940,6 +970,25 @@ var SyncEngine = class {
940
970
  getHLC() {
941
971
  return this.hlc;
942
972
  }
973
+ /**
974
+ * Helper method to apply a single server event to the local map.
975
+ * Used by both SERVER_EVENT and SERVER_BATCH_EVENT handlers.
976
+ */
977
+ async applyServerEvent(mapName, eventType, key, record, orRecord, orTag) {
978
+ const localMap = this.maps.get(mapName);
979
+ if (localMap) {
980
+ if (localMap instanceof LWWMap && record) {
981
+ localMap.merge(key, record);
982
+ await this.storageAdapter.put(`${mapName}:${key}`, record);
983
+ } else if (localMap instanceof ORMap) {
984
+ if (eventType === "OR_ADD" && orRecord) {
985
+ localMap.apply(key, orRecord);
986
+ } else if (eventType === "OR_REMOVE" && orTag) {
987
+ localMap.applyTombstone(orTag);
988
+ }
989
+ }
990
+ }
991
+ }
943
992
  /**
944
993
  * Closes the WebSocket connection and cleans up resources.
945
994
  */
@@ -954,6 +1003,7 @@ var SyncEngine = class {
954
1003
  this.websocket.close();
955
1004
  this.websocket = null;
956
1005
  }
1006
+ this.cancelAllWriteConcernPromises(new Error("SyncEngine closed"));
957
1007
  this.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
958
1008
  logger.info("SyncEngine closed");
959
1009
  }
@@ -1274,6 +1324,58 @@ var SyncEngine = class {
1274
1324
  });
1275
1325
  }
1276
1326
  }
1327
+ // ============================================
1328
+ // Write Concern Methods (Phase 5.01)
1329
+ // ============================================
1330
+ /**
1331
+ * Register a pending Write Concern promise for an operation.
1332
+ * The promise will be resolved when the server sends an ACK with the operation result.
1333
+ *
1334
+ * @param opId - Operation ID
1335
+ * @param timeout - Timeout in ms (default: 5000)
1336
+ * @returns Promise that resolves with the Write Concern result
1337
+ */
1338
+ registerWriteConcernPromise(opId, timeout = 5e3) {
1339
+ return new Promise((resolve, reject) => {
1340
+ const timeoutHandle = setTimeout(() => {
1341
+ this.pendingWriteConcernPromises.delete(opId);
1342
+ reject(new Error(`Write Concern timeout for operation ${opId}`));
1343
+ }, timeout);
1344
+ this.pendingWriteConcernPromises.set(opId, {
1345
+ resolve,
1346
+ reject,
1347
+ timeoutHandle
1348
+ });
1349
+ });
1350
+ }
1351
+ /**
1352
+ * Resolve a pending Write Concern promise with the server result.
1353
+ *
1354
+ * @param opId - Operation ID
1355
+ * @param result - Result from server ACK
1356
+ */
1357
+ resolveWriteConcernPromise(opId, result) {
1358
+ const pending = this.pendingWriteConcernPromises.get(opId);
1359
+ if (pending) {
1360
+ if (pending.timeoutHandle) {
1361
+ clearTimeout(pending.timeoutHandle);
1362
+ }
1363
+ pending.resolve(result);
1364
+ this.pendingWriteConcernPromises.delete(opId);
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Cancel all pending Write Concern promises (e.g., on disconnect).
1369
+ */
1370
+ cancelAllWriteConcernPromises(error) {
1371
+ for (const [opId, pending] of this.pendingWriteConcernPromises.entries()) {
1372
+ if (pending.timeoutHandle) {
1373
+ clearTimeout(pending.timeoutHandle);
1374
+ }
1375
+ pending.reject(error);
1376
+ }
1377
+ this.pendingWriteConcernPromises.clear();
1378
+ }
1277
1379
  };
1278
1380
 
1279
1381
  // src/TopGunClient.ts