@stamprally/core 0.11.0 → 0.12.0

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.cjs CHANGED
@@ -99,6 +99,47 @@ function issueClaimTicketNumber(reward2, currentState, options = {}) {
99
99
  return { ...currentState, claimTicketNumber };
100
100
  }
101
101
 
102
+ // src/engine/sync.ts
103
+ function latestTimestamp(serverTimestamp, localTimestamp) {
104
+ const serverTime = Date.parse(serverTimestamp);
105
+ const localTime = Date.parse(localTimestamp);
106
+ if (!Number.isNaN(serverTime) && !Number.isNaN(localTime))
107
+ return serverTime >= localTime ? serverTimestamp : localTimestamp;
108
+ if (!Number.isNaN(serverTime)) return serverTimestamp;
109
+ if (!Number.isNaN(localTime)) return localTimestamp;
110
+ return serverTimestamp >= localTimestamp ? serverTimestamp : localTimestamp;
111
+ }
112
+ function mergeRewardStates(serverRewards, localRewards) {
113
+ const merged = new Map(serverRewards.map((reward2) => [reward2.rewardId, reward2]));
114
+ for (const localReward of localRewards) {
115
+ const serverReward = merged.get(localReward.rewardId);
116
+ if (serverReward === void 0) {
117
+ merged.set(localReward.rewardId, localReward);
118
+ continue;
119
+ }
120
+ if (localReward.status === "CONSUMED" && serverReward.status !== "CONSUMED")
121
+ merged.set(localReward.rewardId, localReward);
122
+ }
123
+ return [...merged.values()];
124
+ }
125
+ function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
126
+ if (options.policy === "server_wins") return serverState;
127
+ const records = [...serverState.records];
128
+ const knownStamps = new Set(records.map((record) => record.stampId));
129
+ for (const record of localState.records) {
130
+ if (!knownStamps.has(record.stampId)) {
131
+ records.push(record);
132
+ knownStamps.add(record.stampId);
133
+ }
134
+ }
135
+ return {
136
+ ...serverState,
137
+ records,
138
+ rewards: mergeRewardStates(serverState.rewards, localState.rewards),
139
+ updatedAt: latestTimestamp(serverState.updatedAt, localState.updatedAt)
140
+ };
141
+ }
142
+
102
143
  // src/detectors/types.ts
103
144
  function createDetectorError(detector, code, message, cause) {
104
145
  return cause === void 0 ? { detector, code, message } : { detector, code, message, cause };
@@ -1000,6 +1041,7 @@ var StampRallyClient = class {
1000
1041
  this.#storage = this.#options.storage ?? new InMemoryStorage();
1001
1042
  this.#userId = this.#options.userId ?? null;
1002
1043
  this.#offlineQueue = this.#options.offlineQueue;
1044
+ this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
1003
1045
  }
1004
1046
  getConfig() {
1005
1047
  return this.#config;
@@ -1217,12 +1259,19 @@ var StampRallyClient = class {
1217
1259
  });
1218
1260
  }
1219
1261
  if (adapter?.sync === void 0) {
1220
- this.#emitEvent({ type: "sync", state: current });
1262
+ this.#emitEvent({ type: "sync", state: this.#state ?? current });
1221
1263
  return;
1222
1264
  }
1223
- const next = this.#reconcile(
1224
- await adapter.sync({ rallyId: this.#config.id, userId: this.#userId, state: current })
1225
- );
1265
+ const serverState = await adapter.sync({
1266
+ rallyId: this.#config.id,
1267
+ userId: this.#userId,
1268
+ state: this.#state ?? current
1269
+ });
1270
+ const localState = this.#state ?? current;
1271
+ const merged = this.#offlineQueue === void 0 ? serverState : resolveRallyStateConflict(serverState, localState, {
1272
+ policy: this.#offlineQueue.conflictPolicy
1273
+ });
1274
+ const next = this.#reconcile(merged);
1226
1275
  await this.#storage.save(next);
1227
1276
  this.#state = next;
1228
1277
  this.#emit(next);
@@ -1282,6 +1331,16 @@ var StampRallyClient = class {
1282
1331
  )
1283
1332
  };
1284
1333
  }
1334
+ async #handleOfflineSyncResult(event) {
1335
+ if (event.state !== void 0) {
1336
+ const next = this.#reconcile(event.state);
1337
+ await this.#storage.save(next);
1338
+ this.#state = next;
1339
+ this.#emit(next);
1340
+ }
1341
+ if ("ok" in event.result && !event.result.ok)
1342
+ this.#emitEvent({ type: "error", error: event.result.error });
1343
+ }
1285
1344
  #now() {
1286
1345
  return this.#options.clock?.() ?? (/* @__PURE__ */ new Date()).toISOString();
1287
1346
  }
@@ -1418,6 +1477,7 @@ var OfflineQueue = class {
1418
1477
  #error = null;
1419
1478
  #sender;
1420
1479
  #syncPromise = null;
1480
+ #syncResultListener;
1421
1481
  constructor(options = {}) {
1422
1482
  if (options.storage !== void 0) this.#storage = options.storage;
1423
1483
  else if (options.storageLike !== void 0 && options.storageLike !== null)
@@ -1439,6 +1499,12 @@ var OfflineQueue = class {
1439
1499
  get operations() {
1440
1500
  return this.#operations;
1441
1501
  }
1502
+ get conflictPolicy() {
1503
+ return this.#conflictPolicy;
1504
+ }
1505
+ setSyncResultListener(listener) {
1506
+ this.#syncResultListener = listener;
1507
+ }
1442
1508
  async initialize() {
1443
1509
  if (this.#loaded) return;
1444
1510
  this.#operations = [...await this.#storage.load(this.#key)];
@@ -1491,8 +1557,12 @@ var OfflineQueue = class {
1491
1557
  } catch (cause) {
1492
1558
  throw cause instanceof Error ? cause : new Error(String(cause));
1493
1559
  }
1494
- if ("conflict" in result && result.conflict === true)
1495
- await this.resolveConflict(operation, result.localState, result.serverState);
1560
+ const state = "conflict" in result && result.conflict === true ? await this.resolveConflict(operation, result.localState, result.serverState) : "ok" in result && result.ok ? result.value.state : void 0;
1561
+ await this.#syncResultListener?.({
1562
+ operation,
1563
+ result,
1564
+ ...state === void 0 ? {} : { state }
1565
+ });
1496
1566
  this.#operations = this.#operations.slice(1);
1497
1567
  await this.#storage.save(this.#key, this.#operations);
1498
1568
  }
@@ -1506,9 +1576,7 @@ var OfflineQueue = class {
1506
1576
  async resolveConflict(operation, localState, serverState) {
1507
1577
  const configured = this.#onSyncConflict;
1508
1578
  const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
1509
- if (policy === "merge") {
1510
- return;
1511
- }
1579
+ return resolveRallyStateConflict(serverState, localState, { policy });
1512
1580
  }
1513
1581
  };
1514
1582
 
@@ -2437,6 +2505,7 @@ exports.readNfcContext = readNfcContext;
2437
2505
  exports.readQrContext = readQrContext;
2438
2506
  exports.reconcileRewardStates = reconcileRewardStates;
2439
2507
  exports.resolveLocalizedText = resolveLocalizedText;
2508
+ exports.resolveRallyStateConflict = resolveRallyStateConflict;
2440
2509
  exports.safeParseAdminConfig = safeParseAdminConfig;
2441
2510
  exports.safeParsePublicConfig = safeParsePublicConfig;
2442
2511
  exports.sanitizeAdminConfig = sanitizeAdminConfig;