@stamprally/core 0.16.0 → 0.17.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/README.md +6 -6
- package/dist/index.cjs +1121 -960
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -3
- package/dist/index.d.ts +43 -3
- package/dist/index.js +1121 -961
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -169,6 +169,12 @@ function mergeStampRecords(serverRecords, localRecords) {
|
|
|
169
169
|
}
|
|
170
170
|
function resolveRallyStateConflict(serverState, localState, options = { policy: "merge" }) {
|
|
171
171
|
if (options.policy === "server_wins") return serverState;
|
|
172
|
+
if (options.policy === "authoritative_replay")
|
|
173
|
+
return {
|
|
174
|
+
...serverState,
|
|
175
|
+
records: serverState.records.map((record) => ({ ...record })),
|
|
176
|
+
rewards: serverState.rewards.map((reward2) => ({ ...reward2 }))
|
|
177
|
+
};
|
|
172
178
|
return {
|
|
173
179
|
...serverState,
|
|
174
180
|
records: mergeStampRecords(serverState.records, localState.records),
|
|
@@ -1056,1079 +1062,1217 @@ var IndexedDBAdapter = class {
|
|
|
1056
1062
|
}
|
|
1057
1063
|
};
|
|
1058
1064
|
|
|
1059
|
-
// src/client/
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const item = value;
|
|
1070
|
-
for (const key of ["token", "code", "passcode", "value", "tagId"])
|
|
1071
|
-
if (typeof item[key] === "string") return item[key];
|
|
1072
|
-
}
|
|
1073
|
-
return "";
|
|
1074
|
-
}
|
|
1075
|
-
function matches(condition2, value) {
|
|
1076
|
-
if (condition2.type === "gps") {
|
|
1077
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1078
|
-
const item = value;
|
|
1079
|
-
const latitude = item.latitude;
|
|
1080
|
-
const longitude = item.longitude;
|
|
1081
|
-
if (typeof latitude !== "number" || typeof longitude !== "number") return false;
|
|
1082
|
-
const radians = (v) => v * Math.PI / 180;
|
|
1083
|
-
const dLat = radians(latitude - condition2.latitude);
|
|
1084
|
-
const dLon = radians(longitude - condition2.longitude);
|
|
1085
|
-
const h = Math.sin(dLat / 2) ** 2 + Math.cos(radians(condition2.latitude)) * Math.cos(radians(latitude)) * Math.sin(dLon / 2) ** 2;
|
|
1086
|
-
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, h))) <= condition2.radiusMeters;
|
|
1065
|
+
// src/client/offlineQueue.ts
|
|
1066
|
+
function rollbackOptimisticOperation(state, operation) {
|
|
1067
|
+
const previous = operation.request.state;
|
|
1068
|
+
if (operation.kind === "checkIn") {
|
|
1069
|
+
const records = state.records.filter(
|
|
1070
|
+
(record) => record.stampId !== operation.request.spotId || record.acquiredAt !== operation.request.now
|
|
1071
|
+
);
|
|
1072
|
+
const previousRewards = new Map(previous.rewards.map((reward2) => [reward2.rewardId, reward2]));
|
|
1073
|
+
const rewards2 = state.rewards.map((reward2) => previousRewards.get(reward2.rewardId) ?? reward2);
|
|
1074
|
+
return { ...cloneState(state), records, rewards: rewards2, updatedAt: previous.updatedAt };
|
|
1087
1075
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1076
|
+
const previousReward = previous.rewards.find(
|
|
1077
|
+
(reward2) => reward2.rewardId === operation.request.rewardId
|
|
1078
|
+
);
|
|
1079
|
+
const rewards = state.rewards.filter(
|
|
1080
|
+
(reward2) => reward2.rewardId !== operation.request.rewardId || previousReward !== void 0
|
|
1081
|
+
).map(
|
|
1082
|
+
(reward2) => reward2.rewardId === operation.request.rewardId && previousReward !== void 0 ? { ...previousReward } : reward2
|
|
1083
|
+
);
|
|
1084
|
+
const cloned = cloneState(state);
|
|
1085
|
+
const { inventory: _inventory, ...stateWithoutInventory } = cloned;
|
|
1086
|
+
const previousInventory = previous.inventory;
|
|
1087
|
+
return previousInventory === void 0 ? { ...stateWithoutInventory, rewards, updatedAt: previous.updatedAt } : {
|
|
1088
|
+
...stateWithoutInventory,
|
|
1089
|
+
rewards,
|
|
1090
|
+
inventory: {
|
|
1091
|
+
...previousInventory,
|
|
1092
|
+
...previousInventory.rewardRemaining === void 0 ? {} : { rewardRemaining: { ...previousInventory.rewardRemaining } }
|
|
1093
|
+
},
|
|
1094
|
+
updatedAt: previous.updatedAt
|
|
1097
1095
|
};
|
|
1098
1096
|
}
|
|
1099
|
-
var
|
|
1100
|
-
#
|
|
1101
|
-
#
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
#config;
|
|
1105
|
-
#offlineQueue;
|
|
1106
|
-
#userId;
|
|
1107
|
-
#anonymousSessionId;
|
|
1108
|
-
#state = null;
|
|
1109
|
-
#initialization = null;
|
|
1110
|
-
#queue = Promise.resolve();
|
|
1111
|
-
constructor(config, storageOrOptions = {}) {
|
|
1112
|
-
this.#config = config;
|
|
1113
|
-
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
|
|
1114
|
-
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1115
|
-
this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
|
|
1116
|
-
this.#userId = this.#options.userId ?? this.#anonymousSessionId;
|
|
1117
|
-
this.#offlineQueue = this.#options.offlineQueue;
|
|
1118
|
-
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1119
|
-
}
|
|
1120
|
-
getConfig() {
|
|
1121
|
-
return this.#config;
|
|
1097
|
+
var MemoryQueueStorage = class {
|
|
1098
|
+
#values = /* @__PURE__ */ new Map();
|
|
1099
|
+
#rejected = /* @__PURE__ */ new Map();
|
|
1100
|
+
async load(key) {
|
|
1101
|
+
return this.#values.get(key) ?? [];
|
|
1122
1102
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1103
|
+
async save(key, operations) {
|
|
1104
|
+
this.#values.set(key, structuredClone(operations));
|
|
1125
1105
|
}
|
|
1126
|
-
|
|
1127
|
-
return this.#
|
|
1106
|
+
async loadRejectedHistory(key) {
|
|
1107
|
+
return this.#rejected.get(key) ?? [];
|
|
1128
1108
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1109
|
+
async saveRejectedHistory(key, history) {
|
|
1110
|
+
this.#rejected.set(key, structuredClone(history));
|
|
1131
1111
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1112
|
+
};
|
|
1113
|
+
var LocalStorageQueueStorage = class {
|
|
1114
|
+
constructor(storage) {
|
|
1115
|
+
this.storage = storage;
|
|
1134
1116
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1117
|
+
storage;
|
|
1118
|
+
async load(key) {
|
|
1119
|
+
const value = this.storage.getItem(key);
|
|
1120
|
+
if (value === null) return [];
|
|
1121
|
+
try {
|
|
1122
|
+
const parsed = JSON.parse(value);
|
|
1123
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1124
|
+
} catch {
|
|
1125
|
+
return [];
|
|
1126
|
+
}
|
|
1137
1127
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1128
|
+
async save(key, operations) {
|
|
1129
|
+
this.storage.setItem(key, JSON.stringify(operations));
|
|
1140
1130
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1131
|
+
async loadRejectedHistory(key) {
|
|
1132
|
+
const value = this.storage.getItem(`${key}:rejected-history`);
|
|
1133
|
+
if (value === null) return [];
|
|
1134
|
+
try {
|
|
1135
|
+
const parsed = JSON.parse(value);
|
|
1136
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1137
|
+
} catch {
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1143
1140
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1141
|
+
async saveRejectedHistory(key, history) {
|
|
1142
|
+
this.storage.setItem(`${key}:rejected-history`, JSON.stringify(history));
|
|
1146
1143
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1144
|
+
};
|
|
1145
|
+
var IndexedDBOfflineQueueStorage = class {
|
|
1146
|
+
#providedFactory;
|
|
1147
|
+
#databaseName;
|
|
1148
|
+
#databasePromise = null;
|
|
1149
|
+
constructor(options = {}) {
|
|
1150
|
+
this.#providedFactory = options.indexedDB;
|
|
1151
|
+
this.#databaseName = options.databaseName ?? "stamprally-offline-queue";
|
|
1149
1152
|
}
|
|
1150
|
-
|
|
1151
|
-
this.#
|
|
1152
|
-
return () =>
|
|
1153
|
+
async load(key) {
|
|
1154
|
+
const database = await this.#open();
|
|
1155
|
+
return new Promise((resolve, reject) => {
|
|
1156
|
+
const request = database.transaction("operations", "readonly").objectStore("operations").get(key);
|
|
1157
|
+
request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
|
|
1158
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read offline queue."));
|
|
1159
|
+
});
|
|
1153
1160
|
}
|
|
1154
|
-
|
|
1155
|
-
this.#
|
|
1156
|
-
return () =>
|
|
1161
|
+
async save(key, operations) {
|
|
1162
|
+
const database = await this.#open();
|
|
1163
|
+
return new Promise((resolve, reject) => {
|
|
1164
|
+
const transaction = database.transaction("operations", "readwrite");
|
|
1165
|
+
transaction.objectStore("operations").put(structuredClone(operations), key);
|
|
1166
|
+
transaction.oncomplete = () => resolve();
|
|
1167
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save offline queue."));
|
|
1168
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
|
|
1169
|
+
});
|
|
1157
1170
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1171
|
+
async loadRejectedHistory(key) {
|
|
1172
|
+
const database = await this.#open();
|
|
1173
|
+
return new Promise((resolve, reject) => {
|
|
1174
|
+
const request = database.transaction("operations", "readonly").objectStore("operations").get(`${key}:rejected-history`);
|
|
1175
|
+
request.onsuccess = () => resolve(
|
|
1176
|
+
Array.isArray(request.result) ? request.result : []
|
|
1177
|
+
);
|
|
1178
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read rejected operation history."));
|
|
1179
|
+
});
|
|
1160
1180
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
this.#emit(next);
|
|
1171
|
-
return next;
|
|
1172
|
-
}).catch((error) => {
|
|
1173
|
-
this.#initialization = null;
|
|
1174
|
-
throw error;
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
return this.#initialization;
|
|
1181
|
+
async saveRejectedHistory(key, history) {
|
|
1182
|
+
const database = await this.#open();
|
|
1183
|
+
return new Promise((resolve, reject) => {
|
|
1184
|
+
const transaction = database.transaction("operations", "readwrite");
|
|
1185
|
+
transaction.objectStore("operations").put(structuredClone(history), `${key}:rejected-history`);
|
|
1186
|
+
transaction.oncomplete = () => resolve();
|
|
1187
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to save rejected operation history."));
|
|
1188
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("Rejected operation history write aborted."));
|
|
1189
|
+
});
|
|
1178
1190
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
this
|
|
1186
|
-
|
|
1187
|
-
|
|
1191
|
+
#open() {
|
|
1192
|
+
if (this.#databasePromise !== null) return this.#databasePromise;
|
|
1193
|
+
let factory = this.#providedFactory;
|
|
1194
|
+
if (factory === void 0)
|
|
1195
|
+
factory = globalThis.indexedDB;
|
|
1196
|
+
if (factory === void 0 || factory === null)
|
|
1197
|
+
return Promise.reject(new Error("IndexedDB is unavailable in this environment."));
|
|
1198
|
+
this.#databasePromise = new Promise((resolve, reject) => {
|
|
1199
|
+
const request = factory.open(this.#databaseName, 1);
|
|
1200
|
+
request.onupgradeneeded = () => {
|
|
1201
|
+
if (!request.result.objectStoreNames.contains("operations"))
|
|
1202
|
+
request.result.createObjectStore("operations");
|
|
1203
|
+
};
|
|
1204
|
+
request.onsuccess = () => resolve(request.result);
|
|
1205
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to open offline queue."));
|
|
1206
|
+
}).catch((error) => {
|
|
1207
|
+
this.#databasePromise = null;
|
|
1208
|
+
throw error;
|
|
1188
1209
|
});
|
|
1210
|
+
return this.#databasePromise;
|
|
1189
1211
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1212
|
+
};
|
|
1213
|
+
function availableLocalStorage() {
|
|
1214
|
+
try {
|
|
1215
|
+
const storage = globalThis.localStorage;
|
|
1216
|
+
return storage ?? null;
|
|
1217
|
+
} catch {
|
|
1218
|
+
return null;
|
|
1192
1219
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1220
|
+
}
|
|
1221
|
+
function defaultStorage(databaseName) {
|
|
1222
|
+
try {
|
|
1223
|
+
const indexedDB = globalThis.indexedDB;
|
|
1224
|
+
if (indexedDB !== void 0)
|
|
1225
|
+
return {
|
|
1226
|
+
storage: new IndexedDBOfflineQueueStorage({
|
|
1227
|
+
indexedDB,
|
|
1228
|
+
...databaseName === void 0 ? {} : { databaseName }
|
|
1229
|
+
}),
|
|
1230
|
+
capability: "indexeddb"
|
|
1231
|
+
};
|
|
1232
|
+
const storage = availableLocalStorage();
|
|
1233
|
+
if (storage !== void 0 && storage !== null)
|
|
1234
|
+
return { storage: new LocalStorageQueueStorage(storage), capability: "localstorage" };
|
|
1235
|
+
} catch {
|
|
1200
1236
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
message: "Prerequisite spots are not complete."
|
|
1219
|
-
});
|
|
1220
|
-
for (const condition2 of spot2.conditions) {
|
|
1221
|
-
if (condition2.type === "custom") {
|
|
1222
|
-
const validator = this.#options.customValidators?.[condition2.validatorName] ?? this.#options.customValidator;
|
|
1223
|
-
if (validator === void 0)
|
|
1224
|
-
return this.#fail({
|
|
1225
|
-
code: "CUSTOM_VALIDATION_FAILED",
|
|
1226
|
-
spotId,
|
|
1227
|
-
message: "No custom validator is registered."
|
|
1228
|
-
});
|
|
1229
|
-
const context = {
|
|
1230
|
-
rallyId: this.#config.id,
|
|
1231
|
-
spotId,
|
|
1232
|
-
proofData,
|
|
1233
|
-
condition: { type: "custom", validatorName: condition2.validatorName },
|
|
1234
|
-
userState: current
|
|
1235
|
-
};
|
|
1236
|
-
const result = typeof validator === "function" ? await validator(context) : await validator.validate(context);
|
|
1237
|
-
if (result === false || typeof result === "object" && !result.valid)
|
|
1238
|
-
return this.#fail({
|
|
1239
|
-
code: "CUSTOM_VALIDATION_FAILED",
|
|
1240
|
-
spotId,
|
|
1241
|
-
message: typeof result === "object" && result.message !== void 0 ? result.message : "Custom validation failed."
|
|
1242
|
-
});
|
|
1243
|
-
} else if (!matches(condition2, proofData))
|
|
1244
|
-
return this.#fail({ code: "INVALID_PROOF", spotId, message: "Verification failed." });
|
|
1245
|
-
}
|
|
1246
|
-
const now = options.now ?? this.#now();
|
|
1247
|
-
const request = {
|
|
1248
|
-
rallyId: this.#config.id,
|
|
1249
|
-
userId: this.#userId,
|
|
1250
|
-
spotId,
|
|
1251
|
-
proofData,
|
|
1252
|
-
idempotencyKey: options.idempotencyKey ?? id("check-in"),
|
|
1253
|
-
now,
|
|
1254
|
-
state: current,
|
|
1255
|
-
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
1256
|
-
};
|
|
1257
|
-
const remote = this.#options.syncAdapter?.checkIn;
|
|
1258
|
-
if (options.sync !== false && remote !== void 0) {
|
|
1259
|
-
try {
|
|
1260
|
-
const result = await remote(request);
|
|
1261
|
-
return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
|
|
1262
|
-
} catch (error) {
|
|
1263
|
-
if (this.#offlineQueue === void 0) throw error;
|
|
1264
|
-
await this.#offlineQueue.enqueueCheckIn(request);
|
|
1265
|
-
const record2 = { stampId: spotId, acquiredAt: now };
|
|
1266
|
-
const next2 = this.#reconcile({
|
|
1267
|
-
...current,
|
|
1268
|
-
records: [...current.records, record2],
|
|
1269
|
-
updatedAt: now
|
|
1270
|
-
});
|
|
1271
|
-
await this.#storage.save(next2);
|
|
1272
|
-
return this.#commitCheckIn({ ok: true, value: { state: next2, record: record2 } });
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
const record = { stampId: spotId, acquiredAt: now };
|
|
1276
|
-
const next = this.#reconcile({
|
|
1277
|
-
...current,
|
|
1278
|
-
records: [...current.records, record],
|
|
1279
|
-
updatedAt: now
|
|
1280
|
-
});
|
|
1281
|
-
await this.#storage.save(next);
|
|
1282
|
-
return this.#commitCheckIn({ ok: true, value: { state: next, record } });
|
|
1283
|
-
});
|
|
1237
|
+
return { storage: new MemoryQueueStorage(), capability: "memory" };
|
|
1238
|
+
}
|
|
1239
|
+
function offlineOperationId(operation) {
|
|
1240
|
+
const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
|
|
1241
|
+
return operation.kind === "checkIn" ? `checkIn:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}` : `claimReward:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}`;
|
|
1242
|
+
}
|
|
1243
|
+
function requestScope(operation) {
|
|
1244
|
+
return {
|
|
1245
|
+
rallyId: operation.request.rallyId,
|
|
1246
|
+
userId: operation.request.userId
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function errorValue(value, fallbackCode) {
|
|
1250
|
+
if (typeof value === "object" && value !== null) {
|
|
1251
|
+
const candidate = value;
|
|
1252
|
+
if (typeof candidate.code === "string" && typeof candidate.message === "string")
|
|
1253
|
+
return { ...candidate, code: candidate.code, message: candidate.message };
|
|
1284
1254
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1255
|
+
if (value instanceof Error) return { code: fallbackCode, message: value.message };
|
|
1256
|
+
if (typeof value === "string") return { code: fallbackCode, message: value };
|
|
1257
|
+
return { code: fallbackCode, message: "Offline operation was rejected." };
|
|
1258
|
+
}
|
|
1259
|
+
var syncLocks = /* @__PURE__ */ new Map();
|
|
1260
|
+
var SYNC_LOCK_TTL_MS = 3e4;
|
|
1261
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
1262
|
+
maxRetries: 0,
|
|
1263
|
+
initialIntervalMs: 250,
|
|
1264
|
+
backoffMultiplier: 2
|
|
1265
|
+
};
|
|
1266
|
+
function randomId() {
|
|
1267
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1268
|
+
}
|
|
1269
|
+
var OfflineQueue = class {
|
|
1270
|
+
#storage;
|
|
1271
|
+
#queueCapability;
|
|
1272
|
+
#configuredKey;
|
|
1273
|
+
#rallyId;
|
|
1274
|
+
#userId;
|
|
1275
|
+
#conflictPolicy;
|
|
1276
|
+
#onSyncConflict;
|
|
1277
|
+
#operations = [];
|
|
1278
|
+
#rejectedHistory = [];
|
|
1279
|
+
#loaded = false;
|
|
1280
|
+
#state = "idle";
|
|
1281
|
+
#error = null;
|
|
1282
|
+
#sender;
|
|
1283
|
+
#syncPromise = null;
|
|
1284
|
+
#syncResultListener;
|
|
1285
|
+
#changeListener;
|
|
1286
|
+
#synchronizeInstances;
|
|
1287
|
+
#retryOptions;
|
|
1288
|
+
#instanceId = randomId();
|
|
1289
|
+
#lockStorage;
|
|
1290
|
+
#observedLocks = /* @__PURE__ */ new Map();
|
|
1291
|
+
#warnedMemoryLock = false;
|
|
1292
|
+
#storageListener;
|
|
1293
|
+
#channel = null;
|
|
1294
|
+
constructor(options = {}) {
|
|
1295
|
+
if (options.storage !== void 0) {
|
|
1296
|
+
this.#storage = options.storage;
|
|
1297
|
+
this.#queueCapability = "custom";
|
|
1298
|
+
} else if (options.storageLike !== void 0 && options.storageLike !== null) {
|
|
1299
|
+
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1300
|
+
this.#queueCapability = "localstorage";
|
|
1301
|
+
} else {
|
|
1302
|
+
const selected = defaultStorage(options.databaseName);
|
|
1303
|
+
this.#storage = selected.storage;
|
|
1304
|
+
this.#queueCapability = selected.capability;
|
|
1305
|
+
}
|
|
1306
|
+
this.#configuredKey = options.key;
|
|
1307
|
+
this.#rallyId = options.rallyId;
|
|
1308
|
+
this.#userId = options.userId ?? null;
|
|
1309
|
+
this.#conflictPolicy = options.conflictPolicy ?? "authoritative_replay";
|
|
1310
|
+
this.#onSyncConflict = options.onSyncConflict;
|
|
1311
|
+
this.#synchronizeInstances = options.synchronizeInstances ?? true;
|
|
1312
|
+
const retryOptions = {
|
|
1313
|
+
...DEFAULT_RETRY_OPTIONS,
|
|
1314
|
+
...options.retryOptions ?? options.syncRetryOptions ?? options.retry ?? {}
|
|
1315
|
+
};
|
|
1316
|
+
this.#retryOptions = {
|
|
1317
|
+
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
1318
|
+
initialIntervalMs: Math.max(0, retryOptions.initialIntervalMs),
|
|
1319
|
+
backoffMultiplier: Math.max(1, retryOptions.backoffMultiplier)
|
|
1320
|
+
};
|
|
1321
|
+
this.#lockStorage = options.storageLike ?? availableLocalStorage();
|
|
1322
|
+
if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
|
|
1341
1323
|
}
|
|
1342
|
-
|
|
1343
|
-
return this.#
|
|
1344
|
-
const current = await this.initialize();
|
|
1345
|
-
if (this.#offlineQueue !== void 0 && adapter !== void 0) {
|
|
1346
|
-
await this.#offlineQueue.sync(async (operation) => {
|
|
1347
|
-
if (operation.kind === "checkIn") {
|
|
1348
|
-
if (adapter.checkIn === void 0)
|
|
1349
|
-
throw new Error("No check-in sync adapter is configured.");
|
|
1350
|
-
return adapter.checkIn(operation.request);
|
|
1351
|
-
}
|
|
1352
|
-
if (adapter.claimReward === void 0)
|
|
1353
|
-
throw new Error("No reward sync adapter is configured.");
|
|
1354
|
-
return adapter.claimReward(operation.request);
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
if (adapter?.sync === void 0) {
|
|
1358
|
-
this.#emitEvent({ type: "sync", state: this.#state ?? current });
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
const serverState = await adapter.sync({
|
|
1362
|
-
rallyId: this.#config.id,
|
|
1363
|
-
userId: this.#userId,
|
|
1364
|
-
state: this.#state ?? current,
|
|
1365
|
-
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
1366
|
-
});
|
|
1367
|
-
const next = this.#reconcile(serverState);
|
|
1368
|
-
await this.#storage.save(next);
|
|
1369
|
-
this.#state = next;
|
|
1370
|
-
this.#emit(next);
|
|
1371
|
-
this.#emitEvent({ type: "sync", state: next });
|
|
1372
|
-
});
|
|
1324
|
+
get syncState() {
|
|
1325
|
+
return this.#state;
|
|
1373
1326
|
}
|
|
1374
|
-
|
|
1375
|
-
return this.
|
|
1327
|
+
get pendingCount() {
|
|
1328
|
+
return this.#operations.length;
|
|
1376
1329
|
}
|
|
1377
|
-
|
|
1378
|
-
return this.#
|
|
1379
|
-
await this.#storage.remove(this.#config.id, this.#userId);
|
|
1380
|
-
return this.#initializeFresh();
|
|
1381
|
-
});
|
|
1330
|
+
get queueCapability() {
|
|
1331
|
+
return this.#queueCapability;
|
|
1382
1332
|
}
|
|
1383
|
-
|
|
1384
|
-
return this.#
|
|
1385
|
-
if (state.rallyId !== this.#config.id || state.userId !== this.#userId)
|
|
1386
|
-
throw new Error("State belongs to another rally or user.");
|
|
1387
|
-
const next = this.#reconcile(state);
|
|
1388
|
-
await this.#storage.save(next);
|
|
1389
|
-
this.#state = next;
|
|
1390
|
-
this.#initialization = Promise.resolve(next);
|
|
1391
|
-
this.#emit(next);
|
|
1392
|
-
return next;
|
|
1393
|
-
});
|
|
1333
|
+
get rejectedHistory() {
|
|
1334
|
+
return this.#rejectedHistory;
|
|
1394
1335
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
this.#queue = next.then(
|
|
1398
|
-
() => void 0,
|
|
1399
|
-
() => void 0
|
|
1400
|
-
);
|
|
1401
|
-
return next;
|
|
1336
|
+
get error() {
|
|
1337
|
+
return this.#error;
|
|
1402
1338
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
this.#state = next;
|
|
1406
|
-
this.#initialization = Promise.resolve(next);
|
|
1407
|
-
this.#emit(next);
|
|
1408
|
-
return Promise.resolve(next);
|
|
1339
|
+
get operations() {
|
|
1340
|
+
return this.#operations;
|
|
1409
1341
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const records = state.records.filter(
|
|
1413
|
-
(record, index, all) => ids.has(record.stampId) && all.findIndex((candidate) => candidate.stampId === record.stampId) === index
|
|
1414
|
-
);
|
|
1415
|
-
return {
|
|
1416
|
-
...cloneState(state),
|
|
1417
|
-
userId: this.#userId,
|
|
1418
|
-
records,
|
|
1419
|
-
rewards: reconcileRewardStates(
|
|
1420
|
-
this.#config.rewards,
|
|
1421
|
-
state.rewards,
|
|
1422
|
-
records.length,
|
|
1423
|
-
state.updatedAt
|
|
1424
|
-
)
|
|
1425
|
-
};
|
|
1342
|
+
get conflictPolicy() {
|
|
1343
|
+
return this.#conflictPolicy;
|
|
1426
1344
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const next = this.#reconcile(event.state);
|
|
1430
|
-
await this.#storage.save(next);
|
|
1431
|
-
this.#state = next;
|
|
1432
|
-
this.#emit(next);
|
|
1433
|
-
}
|
|
1434
|
-
if (event.error !== void 0) this.#emitEvent({ type: "error", error: event.error });
|
|
1435
|
-
else if (event.result !== void 0 && "ok" in event.result && !event.result.ok)
|
|
1436
|
-
this.#emitEvent({ type: "error", error: event.result.error });
|
|
1345
|
+
get storageKey() {
|
|
1346
|
+
return this.#storageKey();
|
|
1437
1347
|
}
|
|
1438
|
-
|
|
1439
|
-
return this.#
|
|
1348
|
+
get rallyId() {
|
|
1349
|
+
return this.#rallyId;
|
|
1440
1350
|
}
|
|
1441
|
-
|
|
1442
|
-
this.#
|
|
1443
|
-
return { ok: false, error };
|
|
1351
|
+
get userId() {
|
|
1352
|
+
return this.#userId;
|
|
1444
1353
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
this.#state = result.value.state;
|
|
1448
|
-
this.#emit(this.#state);
|
|
1449
|
-
}
|
|
1450
|
-
this.#emitEvent({ type: "checkIn", result });
|
|
1451
|
-
return result;
|
|
1354
|
+
setSyncResultListener(listener) {
|
|
1355
|
+
this.#syncResultListener = listener;
|
|
1452
1356
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1357
|
+
setChangeListener(listener) {
|
|
1358
|
+
this.#changeListener = listener;
|
|
1359
|
+
}
|
|
1360
|
+
async initialize() {
|
|
1361
|
+
if (this.#loaded) return;
|
|
1362
|
+
try {
|
|
1363
|
+
const key = this.#storageKey();
|
|
1364
|
+
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
1365
|
+
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? [];
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
if (this.#queueCapability === "memory") throw error;
|
|
1368
|
+
this.#storage = new MemoryQueueStorage();
|
|
1369
|
+
this.#queueCapability = "memory";
|
|
1370
|
+
this.#operations = [];
|
|
1371
|
+
this.#rejectedHistory = [];
|
|
1372
|
+
this.#warnMemoryLock(
|
|
1373
|
+
`Offline queue persistence is unavailable; queued data can be lost after reload (${String(error)}).`
|
|
1374
|
+
);
|
|
1457
1375
|
}
|
|
1458
|
-
this.#
|
|
1459
|
-
return result;
|
|
1376
|
+
this.#loaded = true;
|
|
1460
1377
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1378
|
+
/** Releases browser listeners when the queue is no longer used. */
|
|
1379
|
+
dispose() {
|
|
1380
|
+
const windowLike = globalThis.window;
|
|
1381
|
+
if (windowLike !== void 0 && this.#storageListener !== void 0)
|
|
1382
|
+
windowLike.removeEventListener("storage", this.#storageListener);
|
|
1383
|
+
this.#storageListener = void 0;
|
|
1384
|
+
this.#channel?.close();
|
|
1385
|
+
this.#channel = null;
|
|
1386
|
+
this.#releaseSyncLock();
|
|
1463
1387
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1388
|
+
/** Selects a rally/user queue scope and loads its pending operations. */
|
|
1389
|
+
async setScope(rallyId, userId) {
|
|
1390
|
+
if (this.#configuredKey !== void 0) {
|
|
1391
|
+
this.#rallyId = rallyId;
|
|
1392
|
+
this.#userId = userId;
|
|
1393
|
+
return this.initialize();
|
|
1394
|
+
}
|
|
1395
|
+
if (this.#rallyId === rallyId && this.#userId === userId && this.#loaded) return;
|
|
1396
|
+
this.#rallyId = rallyId;
|
|
1397
|
+
this.#userId = userId;
|
|
1398
|
+
this.#operations = [];
|
|
1399
|
+
this.#loaded = false;
|
|
1400
|
+
await this.initialize();
|
|
1466
1401
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
#values = /* @__PURE__ */ new Map();
|
|
1472
|
-
#rejected = /* @__PURE__ */ new Map();
|
|
1473
|
-
async load(key) {
|
|
1474
|
-
return this.#values.get(key) ?? [];
|
|
1402
|
+
async switchUser(newUserId) {
|
|
1403
|
+
if (this.#rallyId === void 0)
|
|
1404
|
+
throw new Error("OfflineQueue.switchUser requires a rally scope.");
|
|
1405
|
+
await this.setScope(this.#rallyId, newUserId);
|
|
1475
1406
|
}
|
|
1476
|
-
|
|
1477
|
-
this.#
|
|
1407
|
+
setSender(sender) {
|
|
1408
|
+
this.#sender = sender;
|
|
1478
1409
|
}
|
|
1479
|
-
async
|
|
1480
|
-
|
|
1410
|
+
async enqueue(operation) {
|
|
1411
|
+
if (this.#configuredKey === void 0) {
|
|
1412
|
+
const scope = requestScope(operation);
|
|
1413
|
+
if (this.#rallyId === void 0) await this.setScope(scope.rallyId, scope.userId);
|
|
1414
|
+
if (this.#rallyId !== scope.rallyId || this.#userId !== scope.userId)
|
|
1415
|
+
throw new Error("Offline operation belongs to another rally or user queue.");
|
|
1416
|
+
}
|
|
1417
|
+
await this.initialize();
|
|
1418
|
+
const id2 = offlineOperationId(operation);
|
|
1419
|
+
if (this.#operations.some((item) => offlineOperationId(item) === id2)) return;
|
|
1420
|
+
this.#operations = [...this.#operations, { ...operation, status: "PENDING", attempts: 0 }];
|
|
1421
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1422
|
+
this.#announceChange();
|
|
1481
1423
|
}
|
|
1482
|
-
async
|
|
1483
|
-
this
|
|
1424
|
+
async enqueueCheckIn(request) {
|
|
1425
|
+
return this.enqueue({ kind: "checkIn", request });
|
|
1484
1426
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
constructor(storage) {
|
|
1488
|
-
this.storage = storage;
|
|
1427
|
+
async enqueueClaimReward(request) {
|
|
1428
|
+
return this.enqueue({ kind: "claimReward", request });
|
|
1489
1429
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
const parsed = JSON.parse(value);
|
|
1496
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1497
|
-
} catch {
|
|
1498
|
-
return [];
|
|
1499
|
-
}
|
|
1430
|
+
async clear() {
|
|
1431
|
+
await this.initialize();
|
|
1432
|
+
this.#operations = [];
|
|
1433
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1434
|
+
this.#announceChange();
|
|
1500
1435
|
}
|
|
1501
|
-
async
|
|
1502
|
-
this.
|
|
1436
|
+
async discardRejected(operationId) {
|
|
1437
|
+
await this.initialize();
|
|
1438
|
+
const next = this.#rejectedHistory.filter(
|
|
1439
|
+
(entry) => offlineOperationId(entry.operation) !== operationId
|
|
1440
|
+
);
|
|
1441
|
+
if (next.length === this.#rejectedHistory.length) return false;
|
|
1442
|
+
this.#rejectedHistory = next;
|
|
1443
|
+
await this.#saveRejectedHistory();
|
|
1444
|
+
this.#announceChange();
|
|
1445
|
+
return true;
|
|
1503
1446
|
}
|
|
1504
|
-
async
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1447
|
+
async retryRejected(operationId) {
|
|
1448
|
+
await this.initialize();
|
|
1449
|
+
const entry = this.#rejectedHistory.find(
|
|
1450
|
+
(candidate) => offlineOperationId(candidate.operation) === operationId
|
|
1451
|
+
);
|
|
1452
|
+
if (entry === void 0) return false;
|
|
1453
|
+
if (!this.#operations.some((operation) => offlineOperationId(operation) === operationId))
|
|
1454
|
+
this.#operations = [
|
|
1455
|
+
...this.#operations,
|
|
1456
|
+
{ ...entry.operation, status: "PENDING", attempts: 0 }
|
|
1457
|
+
];
|
|
1458
|
+
this.#rejectedHistory = this.#rejectedHistory.filter((candidate) => candidate !== entry);
|
|
1459
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1460
|
+
await this.#saveRejectedHistory();
|
|
1461
|
+
this.#announceChange();
|
|
1462
|
+
return true;
|
|
1513
1463
|
}
|
|
1514
|
-
async
|
|
1515
|
-
this.
|
|
1464
|
+
async discardRejectedOperation(operationId) {
|
|
1465
|
+
return this.discardRejected(operationId);
|
|
1516
1466
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
#providedFactory;
|
|
1520
|
-
#databaseName;
|
|
1521
|
-
#databasePromise = null;
|
|
1522
|
-
constructor(options = {}) {
|
|
1523
|
-
this.#providedFactory = options.indexedDB;
|
|
1524
|
-
this.#databaseName = options.databaseName ?? "stamprally-offline-queue";
|
|
1467
|
+
async retryRejectedOperation(operationId) {
|
|
1468
|
+
return this.retryRejected(operationId);
|
|
1525
1469
|
}
|
|
1526
|
-
async
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
});
|
|
1470
|
+
async clearRejectedHistory() {
|
|
1471
|
+
await this.initialize();
|
|
1472
|
+
if (this.#rejectedHistory.length === 0) return;
|
|
1473
|
+
this.#rejectedHistory = [];
|
|
1474
|
+
await this.#saveRejectedHistory();
|
|
1475
|
+
this.#announceChange();
|
|
1533
1476
|
}
|
|
1534
|
-
async
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
transaction.onabort = () => reject(transaction.error ?? new Error("Offline queue write aborted."));
|
|
1477
|
+
async sync(sender = this.#sender) {
|
|
1478
|
+
await this.initialize();
|
|
1479
|
+
if (sender === void 0) throw new Error("OfflineQueue.sync requires a sender.");
|
|
1480
|
+
if (this.#syncPromise !== null) return this.#syncPromise;
|
|
1481
|
+
this.#sender = sender;
|
|
1482
|
+
this.#syncPromise = this.#run(sender).finally(() => {
|
|
1483
|
+
this.#syncPromise = null;
|
|
1542
1484
|
});
|
|
1485
|
+
return this.#syncPromise;
|
|
1543
1486
|
}
|
|
1544
|
-
async
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1487
|
+
async retrySync(sender = this.#sender) {
|
|
1488
|
+
return this.sync(sender);
|
|
1489
|
+
}
|
|
1490
|
+
async #run(sender) {
|
|
1491
|
+
const locks = globalThis.navigator?.locks;
|
|
1492
|
+
if (locks !== void 0 && typeof locks.request === "function") {
|
|
1493
|
+
let callbackStarted = false;
|
|
1494
|
+
try {
|
|
1495
|
+
const acquired = await locks.request(
|
|
1496
|
+
`stamprally:${this.#storageKey()}:sync`,
|
|
1497
|
+
{ ifAvailable: true },
|
|
1498
|
+
async (lock) => {
|
|
1499
|
+
if (lock === null) {
|
|
1500
|
+
await this.#reloadFromStorage();
|
|
1501
|
+
this.#state = "idle";
|
|
1502
|
+
this.#changeListener?.();
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
callbackStarted = true;
|
|
1506
|
+
await this.#runWithStorageLock(sender);
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
);
|
|
1510
|
+
if (!acquired) return;
|
|
1511
|
+
return;
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
if (callbackStarted) throw error;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (this.#lockStorage === null)
|
|
1517
|
+
this.#warnMemoryLock(
|
|
1518
|
+
"No cross-tab storage lock is available; offline sync is single-tab only."
|
|
1550
1519
|
);
|
|
1551
|
-
|
|
1552
|
-
});
|
|
1520
|
+
await this.#runWithStorageLock(sender);
|
|
1553
1521
|
}
|
|
1554
|
-
async
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1522
|
+
async #runWithStorageLock(sender) {
|
|
1523
|
+
this.#state = "syncing";
|
|
1524
|
+
this.#error = null;
|
|
1525
|
+
this.#changeListener?.();
|
|
1526
|
+
if (!this.#acquireSyncLock()) {
|
|
1527
|
+
await this.#reloadFromStorage();
|
|
1528
|
+
this.#state = "idle";
|
|
1529
|
+
this.#changeListener?.();
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
try {
|
|
1533
|
+
while (this.#operations.length > 0) {
|
|
1534
|
+
const operation = this.#operations[0];
|
|
1535
|
+
if (operation === void 0) break;
|
|
1536
|
+
let attempt = 0;
|
|
1537
|
+
let response;
|
|
1538
|
+
while (true) {
|
|
1539
|
+
await this.#updateOperationStatus("IN_FLIGHT", attempt);
|
|
1540
|
+
try {
|
|
1541
|
+
response = this.#normalizeResponse(await sender(operation));
|
|
1542
|
+
} catch (cause) {
|
|
1543
|
+
response = {
|
|
1544
|
+
status: "RETRYABLE_ERROR",
|
|
1545
|
+
error: errorValue(cause, "RETRYABLE_ERROR")
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
if (response.status !== "RETRYABLE_ERROR") break;
|
|
1549
|
+
const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
|
|
1550
|
+
await this.#updateOperationStatus("PENDING", attempt + 1);
|
|
1551
|
+
await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
|
|
1552
|
+
if (attempt >= this.#retryOptions.maxRetries) {
|
|
1553
|
+
await this.#updateOperationStatus("FAILED_RETRYABLE", attempt + 1);
|
|
1554
|
+
throw new Error(error2.message);
|
|
1555
|
+
}
|
|
1556
|
+
const interval = this.#retryOptions.initialIntervalMs * this.#retryOptions.backoffMultiplier ** attempt;
|
|
1557
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, interval)));
|
|
1558
|
+
attempt += 1;
|
|
1559
|
+
}
|
|
1560
|
+
const result = response.result;
|
|
1561
|
+
const state = response.state ?? (result !== void 0 && "conflict" in result && result.conflict === true ? result.serverState : result !== void 0 && "ok" in result && result.ok ? result.value.state : void 0);
|
|
1562
|
+
const error = response.status === "REJECTED_PERMANENT" ? errorValue(
|
|
1563
|
+
response.error ?? response.reason ?? (result !== void 0 && "ok" in result && !result.ok ? result.error : void 0),
|
|
1564
|
+
"REJECTED_PERMANENT"
|
|
1565
|
+
) : void 0;
|
|
1566
|
+
await this.#updateOperationStatus(
|
|
1567
|
+
response.status === "ACCEPTED" ? "ACCEPTED" : "REJECTED_PERMANENT",
|
|
1568
|
+
attempt + 1
|
|
1569
|
+
);
|
|
1570
|
+
const eventState = state;
|
|
1571
|
+
if (response.status === "REJECTED_PERMANENT" && error !== void 0) {
|
|
1572
|
+
this.#rejectedHistory = [
|
|
1573
|
+
...this.#rejectedHistory,
|
|
1574
|
+
{
|
|
1575
|
+
operation: { ...operation, status: "REJECTED_PERMANENT", attempts: attempt + 1 },
|
|
1576
|
+
reason: error,
|
|
1577
|
+
errorCode: error.code,
|
|
1578
|
+
rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1579
|
+
attempts: attempt + 1
|
|
1580
|
+
}
|
|
1581
|
+
];
|
|
1582
|
+
await this.#saveRejectedHistory();
|
|
1583
|
+
}
|
|
1584
|
+
this.#operations = this.#operations.slice(1);
|
|
1585
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1586
|
+
this.#announceChange();
|
|
1587
|
+
await this.#syncResultListener?.({
|
|
1588
|
+
operation,
|
|
1589
|
+
...result === void 0 ? {} : { result },
|
|
1590
|
+
status: response.status,
|
|
1591
|
+
...error === void 0 ? {} : { error },
|
|
1592
|
+
...eventState === void 0 ? {} : { state: eventState }
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
this.#state = "idle";
|
|
1596
|
+
this.#changeListener?.();
|
|
1597
|
+
} catch (cause) {
|
|
1598
|
+
this.#state = "error";
|
|
1599
|
+
this.#error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1600
|
+
this.#changeListener?.();
|
|
1601
|
+
throw this.#error;
|
|
1602
|
+
} finally {
|
|
1603
|
+
this.#releaseSyncLock();
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async #updateOperationStatus(status, attempts) {
|
|
1607
|
+
const operation = this.#operations[0];
|
|
1608
|
+
if (operation === void 0) return;
|
|
1609
|
+
this.#operations = [{ ...operation, status, attempts }, ...this.#operations.slice(1)];
|
|
1610
|
+
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1611
|
+
this.#announceChange();
|
|
1563
1612
|
}
|
|
1564
|
-
#
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
return Promise.reject(new Error("IndexedDB is unavailable in this environment."));
|
|
1571
|
-
this.#databasePromise = new Promise((resolve, reject) => {
|
|
1572
|
-
const request = factory.open(this.#databaseName, 1);
|
|
1573
|
-
request.onupgradeneeded = () => {
|
|
1574
|
-
if (!request.result.objectStoreNames.contains("operations"))
|
|
1575
|
-
request.result.createObjectStore("operations");
|
|
1613
|
+
#subscribeToExternalChanges() {
|
|
1614
|
+
const windowLike = globalThis.window;
|
|
1615
|
+
if (windowLike !== void 0) {
|
|
1616
|
+
this.#storageListener = (event) => {
|
|
1617
|
+
if (event.key === this.#storageKey() || event.key === `${this.#storageKey()}:rejected-history`)
|
|
1618
|
+
void this.#reloadFromStorage();
|
|
1576
1619
|
};
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1620
|
+
windowLike.addEventListener("storage", this.#storageListener);
|
|
1621
|
+
}
|
|
1622
|
+
const Channel = globalThis.BroadcastChannel;
|
|
1623
|
+
if (Channel !== void 0) {
|
|
1624
|
+
try {
|
|
1625
|
+
this.#channel = new Channel("stamprally:queue-sync");
|
|
1626
|
+
this.#channel.addEventListener("message", (event) => {
|
|
1627
|
+
if (typeof event.data === "object" && event.data !== null) {
|
|
1628
|
+
const data = event.data;
|
|
1629
|
+
if (data.type === "lock" && data.lockKey === this.#lockKey()) {
|
|
1630
|
+
this.#observedLocks.set(data.lockKey, {
|
|
1631
|
+
owner: typeof data.owner === "string" ? data.owner : "unknown",
|
|
1632
|
+
expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : 0
|
|
1633
|
+
});
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (data.type === "unlock" && data.lockKey === this.#lockKey()) {
|
|
1637
|
+
this.#observedLocks.delete(data.lockKey);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (typeof event.data === "object" && event.data !== null && "key" in event.data && event.data.key === this.#storageKey())
|
|
1642
|
+
void this.#reloadFromStorage();
|
|
1643
|
+
});
|
|
1644
|
+
} catch {
|
|
1645
|
+
this.#channel = null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
#announceChange() {
|
|
1650
|
+
this.#changeListener?.();
|
|
1651
|
+
this.#channel?.postMessage({
|
|
1652
|
+
type: "change",
|
|
1653
|
+
key: this.#storageKey(),
|
|
1654
|
+
owner: this.#instanceId
|
|
1582
1655
|
});
|
|
1583
|
-
return this.#databasePromise;
|
|
1584
1656
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1657
|
+
async #reloadFromStorage() {
|
|
1658
|
+
if (this.#state === "syncing") return;
|
|
1659
|
+
try {
|
|
1660
|
+
const key = this.#storageKey();
|
|
1661
|
+
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
1662
|
+
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? this.#rejectedHistory;
|
|
1663
|
+
this.#loaded = true;
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1592
1666
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
try {
|
|
1596
|
-
const indexedDB = globalThis.indexedDB;
|
|
1597
|
-
if (indexedDB !== void 0)
|
|
1598
|
-
return {
|
|
1599
|
-
storage: new IndexedDBOfflineQueueStorage({
|
|
1600
|
-
indexedDB,
|
|
1601
|
-
...databaseName === void 0 ? {} : { databaseName }
|
|
1602
|
-
}),
|
|
1603
|
-
capability: "indexeddb"
|
|
1604
|
-
};
|
|
1605
|
-
const storage = availableLocalStorage();
|
|
1606
|
-
if (storage !== void 0 && storage !== null)
|
|
1607
|
-
return { storage: new LocalStorageQueueStorage(storage), capability: "localstorage" };
|
|
1608
|
-
} catch {
|
|
1667
|
+
#lockKey() {
|
|
1668
|
+
return `${this.#storageKey()}:sync-lock`;
|
|
1609
1669
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1670
|
+
#acquireSyncLock() {
|
|
1671
|
+
const key = this.#lockKey();
|
|
1672
|
+
const now = Date.now();
|
|
1673
|
+
const local = syncLocks.get(key);
|
|
1674
|
+
if (local !== void 0 && local.expiresAt > now && local.owner !== this.#instanceId)
|
|
1675
|
+
return false;
|
|
1676
|
+
const observed = this.#observedLocks.get(key);
|
|
1677
|
+
if (observed !== void 0 && observed.expiresAt > now && observed.owner !== this.#instanceId)
|
|
1678
|
+
return false;
|
|
1679
|
+
if (this.#lockStorage !== null) {
|
|
1680
|
+
try {
|
|
1681
|
+
const existing = this.#lockStorage.getItem(key);
|
|
1682
|
+
if (existing !== null) {
|
|
1683
|
+
const parsed = JSON.parse(existing);
|
|
1684
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
this.#lockStorage.setItem(
|
|
1688
|
+
key,
|
|
1689
|
+
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
1690
|
+
);
|
|
1691
|
+
this.#channel?.postMessage({
|
|
1692
|
+
type: "lock",
|
|
1693
|
+
lockKey: key,
|
|
1694
|
+
owner: this.#instanceId,
|
|
1695
|
+
expiresAt: now + SYNC_LOCK_TTL_MS
|
|
1696
|
+
});
|
|
1697
|
+
} catch {
|
|
1698
|
+
this.#warnMemoryLock("Persistent lock access failed; offline sync is single-tab only.");
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
1702
|
+
return true;
|
|
1627
1703
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1641
|
-
}
|
|
1642
|
-
var OfflineQueue = class {
|
|
1643
|
-
#storage;
|
|
1644
|
-
#queueCapability;
|
|
1645
|
-
#configuredKey;
|
|
1646
|
-
#rallyId;
|
|
1647
|
-
#userId;
|
|
1648
|
-
#conflictPolicy;
|
|
1649
|
-
#onSyncConflict;
|
|
1650
|
-
#operations = [];
|
|
1651
|
-
#rejectedHistory = [];
|
|
1652
|
-
#loaded = false;
|
|
1653
|
-
#state = "idle";
|
|
1654
|
-
#error = null;
|
|
1655
|
-
#sender;
|
|
1656
|
-
#syncPromise = null;
|
|
1657
|
-
#syncResultListener;
|
|
1658
|
-
#synchronizeInstances;
|
|
1659
|
-
#retryOptions;
|
|
1660
|
-
#instanceId = randomId();
|
|
1661
|
-
#lockStorage;
|
|
1662
|
-
#observedLocks = /* @__PURE__ */ new Map();
|
|
1663
|
-
#warnedMemoryLock = false;
|
|
1664
|
-
#storageListener;
|
|
1665
|
-
#channel = null;
|
|
1666
|
-
constructor(options = {}) {
|
|
1667
|
-
if (options.storage !== void 0) {
|
|
1668
|
-
this.#storage = options.storage;
|
|
1669
|
-
this.#queueCapability = "custom";
|
|
1670
|
-
} else if (options.storageLike !== void 0 && options.storageLike !== null) {
|
|
1671
|
-
this.#storage = new LocalStorageQueueStorage(options.storageLike);
|
|
1672
|
-
this.#queueCapability = "localstorage";
|
|
1673
|
-
} else {
|
|
1674
|
-
const selected = defaultStorage(options.databaseName);
|
|
1675
|
-
this.#storage = selected.storage;
|
|
1676
|
-
this.#queueCapability = selected.capability;
|
|
1704
|
+
#releaseSyncLock() {
|
|
1705
|
+
const key = this.#lockKey();
|
|
1706
|
+
const current = syncLocks.get(key);
|
|
1707
|
+
if (current?.owner === this.#instanceId) syncLocks.delete(key);
|
|
1708
|
+
if (this.#lockStorage !== null) {
|
|
1709
|
+
try {
|
|
1710
|
+
const value = this.#lockStorage.getItem(key);
|
|
1711
|
+
if (value !== null && JSON.parse(value).owner === this.#instanceId)
|
|
1712
|
+
this.#lockStorage.removeItem?.(key);
|
|
1713
|
+
this.#channel?.postMessage({ type: "unlock", lockKey: key, owner: this.#instanceId });
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1677
1716
|
}
|
|
1678
|
-
this.#configuredKey = options.key;
|
|
1679
|
-
this.#rallyId = options.rallyId;
|
|
1680
|
-
this.#userId = options.userId ?? null;
|
|
1681
|
-
this.#conflictPolicy = options.conflictPolicy ?? "server_wins";
|
|
1682
|
-
this.#onSyncConflict = options.onSyncConflict;
|
|
1683
|
-
this.#synchronizeInstances = options.synchronizeInstances ?? true;
|
|
1684
|
-
const retryOptions = {
|
|
1685
|
-
...DEFAULT_RETRY_OPTIONS,
|
|
1686
|
-
...options.retryOptions ?? options.syncRetryOptions ?? options.retry ?? {}
|
|
1687
|
-
};
|
|
1688
|
-
this.#retryOptions = {
|
|
1689
|
-
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
1690
|
-
initialIntervalMs: Math.max(0, retryOptions.initialIntervalMs),
|
|
1691
|
-
backoffMultiplier: Math.max(1, retryOptions.backoffMultiplier)
|
|
1692
|
-
};
|
|
1693
|
-
this.#lockStorage = options.storageLike ?? availableLocalStorage();
|
|
1694
|
-
if (this.#synchronizeInstances) this.#subscribeToExternalChanges();
|
|
1695
1717
|
}
|
|
1696
|
-
|
|
1697
|
-
return this.#
|
|
1718
|
+
#storageKey() {
|
|
1719
|
+
if (this.#configuredKey !== void 0) return this.#configuredKey;
|
|
1720
|
+
return `stamprally:queue:${this.#rallyId ?? "unscoped"}:${this.#userId ?? "anonymous"}`;
|
|
1698
1721
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1722
|
+
#normalizeResponse(value) {
|
|
1723
|
+
if ("ok" in value) {
|
|
1724
|
+
if (value.ok === false) {
|
|
1725
|
+
if ("status" in value && value.status === "RETRYABLE_ERROR")
|
|
1726
|
+
return { status: "RETRYABLE_ERROR", error: errorValue(value.error, "RETRYABLE_ERROR") };
|
|
1727
|
+
return { status: "REJECTED_PERMANENT", result: value };
|
|
1728
|
+
}
|
|
1729
|
+
return { status: "ACCEPTED", result: value };
|
|
1730
|
+
}
|
|
1731
|
+
if ("status" in value) {
|
|
1732
|
+
if (value.status === "ACCEPTED") return value;
|
|
1733
|
+
return value;
|
|
1734
|
+
}
|
|
1735
|
+
return { status: "ACCEPTED", result: value };
|
|
1701
1736
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1737
|
+
async resolveConflict(operation, localState, serverState) {
|
|
1738
|
+
const configured = this.#onSyncConflict;
|
|
1739
|
+
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
1740
|
+
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
1704
1741
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1742
|
+
async #saveRejectedHistory() {
|
|
1743
|
+
await this.#storage.saveRejectedHistory?.(this.#storageKey(), this.#rejectedHistory);
|
|
1744
|
+
}
|
|
1745
|
+
#warnMemoryLock(message) {
|
|
1746
|
+
if (this.#warnedMemoryLock) return;
|
|
1747
|
+
this.#warnedMemoryLock = true;
|
|
1748
|
+
console.warn(`[@stamprally/core] ${message}`);
|
|
1707
1749
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1750
|
+
};
|
|
1751
|
+
function normalizeOperation(operation) {
|
|
1752
|
+
const status = operation.status;
|
|
1753
|
+
return {
|
|
1754
|
+
...operation,
|
|
1755
|
+
status: status === "IN_FLIGHT" || status === "REJECTED" ? "PENDING" : status === "RETRYABLE_ERROR" ? "FAILED_RETRYABLE" : operation.status ?? "PENDING",
|
|
1756
|
+
attempts: operation.attempts ?? 0
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function normalizeRejectedHistory(entry) {
|
|
1760
|
+
return {
|
|
1761
|
+
...entry,
|
|
1762
|
+
operation: normalizeOperation(entry.operation),
|
|
1763
|
+
reason: errorValue(entry.reason, "REJECTED_PERMANENT"),
|
|
1764
|
+
errorCode: entry.errorCode || entry.reason.code,
|
|
1765
|
+
rejectedAt: entry.rejectedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1766
|
+
attempts: entry.attempts ?? entry.operation.attempts ?? 0
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/client/client.ts
|
|
1771
|
+
function isStorage(value) {
|
|
1772
|
+
return "load" in value && "save" in value && "remove" in value;
|
|
1773
|
+
}
|
|
1774
|
+
function id(prefix) {
|
|
1775
|
+
return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
|
|
1776
|
+
}
|
|
1777
|
+
function proof(value) {
|
|
1778
|
+
if (typeof value === "string") return value;
|
|
1779
|
+
if (typeof value === "object" && value !== null) {
|
|
1780
|
+
const item = value;
|
|
1781
|
+
for (const key of ["token", "code", "passcode", "value", "tagId"])
|
|
1782
|
+
if (typeof item[key] === "string") return item[key];
|
|
1710
1783
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1784
|
+
return "";
|
|
1785
|
+
}
|
|
1786
|
+
function matches(condition2, value) {
|
|
1787
|
+
if (condition2.type === "gps") {
|
|
1788
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1789
|
+
const item = value;
|
|
1790
|
+
const latitude = item.latitude;
|
|
1791
|
+
const longitude = item.longitude;
|
|
1792
|
+
if (typeof latitude !== "number" || typeof longitude !== "number") return false;
|
|
1793
|
+
const radians = (v) => v * Math.PI / 180;
|
|
1794
|
+
const dLat = radians(latitude - condition2.latitude);
|
|
1795
|
+
const dLon = radians(longitude - condition2.longitude);
|
|
1796
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(radians(condition2.latitude)) * Math.cos(radians(latitude)) * Math.sin(dLon / 2) ** 2;
|
|
1797
|
+
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, h))) <= condition2.radiusMeters;
|
|
1713
1798
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1799
|
+
return proof(value).trim() !== "";
|
|
1800
|
+
}
|
|
1801
|
+
function emptyState(config, userId, now) {
|
|
1802
|
+
return {
|
|
1803
|
+
rallyId: config.id,
|
|
1804
|
+
userId,
|
|
1805
|
+
records: [],
|
|
1806
|
+
rewards: reconcileRewardStates(config.rewards, [], 0, now),
|
|
1807
|
+
updatedAt: now
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function errorMessage(error, fallback) {
|
|
1811
|
+
return error !== void 0 && "message" in error && typeof error.message === "string" ? error.message : fallback;
|
|
1812
|
+
}
|
|
1813
|
+
var StampRallyClient = class {
|
|
1814
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
1815
|
+
#eventListeners = /* @__PURE__ */ new Set();
|
|
1816
|
+
#syncEventListeners = /* @__PURE__ */ new Set();
|
|
1817
|
+
#storage;
|
|
1818
|
+
#options;
|
|
1819
|
+
#config;
|
|
1820
|
+
#offlineQueue;
|
|
1821
|
+
#userId;
|
|
1822
|
+
#anonymousSessionId;
|
|
1823
|
+
#state = null;
|
|
1824
|
+
#initialization = null;
|
|
1825
|
+
#queue = Promise.resolve();
|
|
1826
|
+
#syncRevision = 0;
|
|
1827
|
+
#syncMetrics = null;
|
|
1828
|
+
constructor(config, storageOrOptions = {}) {
|
|
1829
|
+
this.#config = config;
|
|
1830
|
+
this.#options = isStorage(storageOrOptions) ? { storage: storageOrOptions } : storageOrOptions;
|
|
1831
|
+
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1832
|
+
this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
|
|
1833
|
+
this.#userId = this.#options.userId ?? this.#anonymousSessionId;
|
|
1834
|
+
this.#offlineQueue = this.#options.offlineQueue;
|
|
1835
|
+
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1836
|
+
this.#offlineQueue?.setChangeListener(() => {
|
|
1837
|
+
this.#syncRevision += 1;
|
|
1838
|
+
if (this.#state !== null) this.#emit(this.#state);
|
|
1839
|
+
});
|
|
1716
1840
|
}
|
|
1717
|
-
|
|
1718
|
-
return this.#
|
|
1841
|
+
getConfig() {
|
|
1842
|
+
return this.#config;
|
|
1719
1843
|
}
|
|
1720
|
-
|
|
1721
|
-
return this.#
|
|
1844
|
+
getState() {
|
|
1845
|
+
return this.#state;
|
|
1722
1846
|
}
|
|
1723
|
-
|
|
1847
|
+
getUserId() {
|
|
1724
1848
|
return this.#userId;
|
|
1725
1849
|
}
|
|
1726
|
-
|
|
1727
|
-
this.#
|
|
1850
|
+
getAnonymousSessionId() {
|
|
1851
|
+
return this.#anonymousSessionId;
|
|
1728
1852
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
try {
|
|
1732
|
-
const key = this.#storageKey();
|
|
1733
|
-
this.#operations = (await this.#storage.load(key)).map(normalizeOperation);
|
|
1734
|
-
this.#rejectedHistory = (await this.#storage.loadRejectedHistory?.(key))?.map(normalizeRejectedHistory) ?? [];
|
|
1735
|
-
} catch (error) {
|
|
1736
|
-
if (this.#queueCapability === "memory") throw error;
|
|
1737
|
-
this.#storage = new MemoryQueueStorage();
|
|
1738
|
-
this.#queueCapability = "memory";
|
|
1739
|
-
this.#operations = [];
|
|
1740
|
-
this.#rejectedHistory = [];
|
|
1741
|
-
this.#warnMemoryLock(
|
|
1742
|
-
`Offline queue persistence is unavailable; queued data can be lost after reload (${String(error)}).`
|
|
1743
|
-
);
|
|
1744
|
-
}
|
|
1745
|
-
this.#loaded = true;
|
|
1853
|
+
get syncState() {
|
|
1854
|
+
return this.#offlineQueue?.syncState ?? "idle";
|
|
1746
1855
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const windowLike = globalThis.window;
|
|
1750
|
-
if (windowLike !== void 0 && this.#storageListener !== void 0)
|
|
1751
|
-
windowLike.removeEventListener("storage", this.#storageListener);
|
|
1752
|
-
this.#storageListener = void 0;
|
|
1753
|
-
this.#channel?.close();
|
|
1754
|
-
this.#channel = null;
|
|
1755
|
-
this.#releaseSyncLock();
|
|
1856
|
+
get pendingCount() {
|
|
1857
|
+
return this.#offlineQueue?.pendingCount ?? 0;
|
|
1756
1858
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
if (this.#configuredKey !== void 0) {
|
|
1760
|
-
this.#rallyId = rallyId;
|
|
1761
|
-
this.#userId = userId;
|
|
1762
|
-
return this.initialize();
|
|
1763
|
-
}
|
|
1764
|
-
if (this.#rallyId === rallyId && this.#userId === userId && this.#loaded) return;
|
|
1765
|
-
this.#rallyId = rallyId;
|
|
1766
|
-
this.#userId = userId;
|
|
1767
|
-
this.#operations = [];
|
|
1768
|
-
this.#loaded = false;
|
|
1769
|
-
await this.initialize();
|
|
1859
|
+
get rejectedHistory() {
|
|
1860
|
+
return this.#offlineQueue?.rejectedHistory ?? [];
|
|
1770
1861
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
throw new Error("OfflineQueue.switchUser requires a rally scope.");
|
|
1774
|
-
await this.setScope(this.#rallyId, newUserId);
|
|
1862
|
+
getSyncRevision() {
|
|
1863
|
+
return this.#syncRevision;
|
|
1775
1864
|
}
|
|
1776
|
-
|
|
1777
|
-
this.#
|
|
1865
|
+
get queueCapability() {
|
|
1866
|
+
return this.#offlineQueue?.queueCapability ?? "custom";
|
|
1778
1867
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const scope = requestScope(operation);
|
|
1782
|
-
if (this.#rallyId === void 0) await this.setScope(scope.rallyId, scope.userId);
|
|
1783
|
-
if (this.#rallyId !== scope.rallyId || this.#userId !== scope.userId)
|
|
1784
|
-
throw new Error("Offline operation belongs to another rally or user queue.");
|
|
1785
|
-
}
|
|
1786
|
-
await this.initialize();
|
|
1787
|
-
const id2 = offlineOperationId(operation);
|
|
1788
|
-
if (this.#operations.some((item) => offlineOperationId(item) === id2)) return;
|
|
1789
|
-
this.#operations = [...this.#operations, { ...operation, status: "PENDING", attempts: 0 }];
|
|
1790
|
-
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1791
|
-
this.#announceChange();
|
|
1868
|
+
discardRejected(operationId) {
|
|
1869
|
+
return this.#offlineQueue?.discardRejected(operationId) ?? Promise.resolve(false);
|
|
1792
1870
|
}
|
|
1793
|
-
|
|
1794
|
-
return this
|
|
1871
|
+
retryRejected(operationId) {
|
|
1872
|
+
return this.#offlineQueue?.retryRejected(operationId) ?? Promise.resolve(false);
|
|
1795
1873
|
}
|
|
1796
|
-
|
|
1797
|
-
return this.
|
|
1874
|
+
dismissRejectedOperation(operationId) {
|
|
1875
|
+
return this.discardRejected(operationId);
|
|
1798
1876
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
this.#operations = [];
|
|
1802
|
-
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1803
|
-
this.#announceChange();
|
|
1877
|
+
retryOperation(operationId) {
|
|
1878
|
+
return this.retryRejected(operationId);
|
|
1804
1879
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
(entry) => offlineOperationId(entry.operation) !== operationId
|
|
1809
|
-
);
|
|
1810
|
-
if (next.length === this.#rejectedHistory.length) return false;
|
|
1811
|
-
this.#rejectedHistory = next;
|
|
1812
|
-
await this.#saveRejectedHistory();
|
|
1813
|
-
this.#announceChange();
|
|
1814
|
-
return true;
|
|
1880
|
+
subscribe(listener) {
|
|
1881
|
+
this.#listeners.add(listener);
|
|
1882
|
+
return () => this.#listeners.delete(listener);
|
|
1815
1883
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
(candidate) => offlineOperationId(candidate.operation) === operationId
|
|
1820
|
-
);
|
|
1821
|
-
if (entry === void 0) return false;
|
|
1822
|
-
if (!this.#operations.some((operation) => offlineOperationId(operation) === operationId))
|
|
1823
|
-
this.#operations = [
|
|
1824
|
-
...this.#operations,
|
|
1825
|
-
{ ...entry.operation, status: "PENDING", attempts: 0 }
|
|
1826
|
-
];
|
|
1827
|
-
this.#rejectedHistory = this.#rejectedHistory.filter((candidate) => candidate !== entry);
|
|
1828
|
-
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1829
|
-
await this.#saveRejectedHistory();
|
|
1830
|
-
this.#announceChange();
|
|
1831
|
-
return true;
|
|
1884
|
+
subscribeEvents(listener) {
|
|
1885
|
+
this.#eventListeners.add(listener);
|
|
1886
|
+
return () => this.#eventListeners.delete(listener);
|
|
1832
1887
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1888
|
+
subscribeSyncEvents(listener) {
|
|
1889
|
+
this.#syncEventListeners.add(listener);
|
|
1890
|
+
return () => this.#syncEventListeners.delete(listener);
|
|
1835
1891
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1892
|
+
subscribeSyncState(listener) {
|
|
1893
|
+
const wrapped = () => listener();
|
|
1894
|
+
this.#listeners.add(wrapped);
|
|
1895
|
+
return () => this.#listeners.delete(wrapped);
|
|
1838
1896
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
if (this.#rejectedHistory.length === 0) return;
|
|
1842
|
-
this.#rejectedHistory = [];
|
|
1843
|
-
await this.#saveRejectedHistory();
|
|
1844
|
-
this.#announceChange();
|
|
1897
|
+
init() {
|
|
1898
|
+
return this.initialize();
|
|
1845
1899
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
if (
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1900
|
+
initialize() {
|
|
1901
|
+
if (this.#state !== null) return Promise.resolve(this.#state);
|
|
1902
|
+
if (this.#initialization === null) {
|
|
1903
|
+
this.#initialization = (async () => {
|
|
1904
|
+
await this.#offlineQueue?.setScope(this.#config.id, this.#userId);
|
|
1905
|
+
return this.#storage.load(this.#config.id, this.#userId);
|
|
1906
|
+
})().then((state) => {
|
|
1907
|
+
const next = state === null ? emptyState(this.#config, this.#userId, this.#now()) : this.#reconcile(state);
|
|
1908
|
+
this.#state = next;
|
|
1909
|
+
this.#emit(next);
|
|
1910
|
+
return next;
|
|
1911
|
+
}).catch((error) => {
|
|
1912
|
+
this.#initialization = null;
|
|
1913
|
+
throw error;
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
return this.#initialization;
|
|
1917
|
+
}
|
|
1918
|
+
switchUser(newUserId) {
|
|
1919
|
+
return this.#enqueue(async () => {
|
|
1920
|
+
const nextUserId = newUserId ?? this.#anonymousSessionId;
|
|
1921
|
+
if (this.#userId === nextUserId && this.#state !== null) return this.#state;
|
|
1922
|
+
this.#userId = nextUserId;
|
|
1923
|
+
this.#state = null;
|
|
1924
|
+
this.#initialization = null;
|
|
1925
|
+
await this.#offlineQueue?.switchUser(nextUserId);
|
|
1926
|
+
return this.initialize();
|
|
1853
1927
|
});
|
|
1854
|
-
return this.#syncPromise;
|
|
1855
1928
|
}
|
|
1856
|
-
async
|
|
1857
|
-
return this.
|
|
1929
|
+
async getUserState(rallyId, userId) {
|
|
1930
|
+
return this.#storage.load(rallyId, userId);
|
|
1858
1931
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
const acquired = await locks.request(
|
|
1865
|
-
`stamprally:${this.#storageKey()}:sync`,
|
|
1866
|
-
{ ifAvailable: true },
|
|
1867
|
-
async (lock) => {
|
|
1868
|
-
if (lock === null) {
|
|
1869
|
-
await this.#reloadFromStorage();
|
|
1870
|
-
this.#state = "idle";
|
|
1871
|
-
return false;
|
|
1872
|
-
}
|
|
1873
|
-
callbackStarted = true;
|
|
1874
|
-
await this.#runWithStorageLock(sender);
|
|
1875
|
-
return true;
|
|
1876
|
-
}
|
|
1877
|
-
);
|
|
1878
|
-
if (!acquired) return;
|
|
1879
|
-
return;
|
|
1880
|
-
} catch (error) {
|
|
1881
|
-
if (callbackStarted) throw error;
|
|
1932
|
+
clearUserState(userId = this.#userId) {
|
|
1933
|
+
return this.#enqueue(async () => {
|
|
1934
|
+
await this.#storage.remove(this.#config.id, userId);
|
|
1935
|
+
if (userId === this.#userId) {
|
|
1936
|
+
await this.#initializeFresh();
|
|
1882
1937
|
}
|
|
1883
|
-
}
|
|
1884
|
-
if (this.#lockStorage === null)
|
|
1885
|
-
this.#warnMemoryLock(
|
|
1886
|
-
"No cross-tab storage lock is available; offline sync is single-tab only."
|
|
1887
|
-
);
|
|
1888
|
-
await this.#runWithStorageLock(sender);
|
|
1938
|
+
});
|
|
1889
1939
|
}
|
|
1890
|
-
|
|
1891
|
-
this.#
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
} catch (cause) {
|
|
1909
|
-
response = {
|
|
1910
|
-
status: "RETRYABLE_ERROR",
|
|
1911
|
-
error: errorValue(cause, "RETRYABLE_ERROR")
|
|
1912
|
-
};
|
|
1913
|
-
}
|
|
1914
|
-
if (response.status !== "RETRYABLE_ERROR") break;
|
|
1915
|
-
const error2 = errorValue(response.error ?? response.reason, "RETRYABLE_ERROR");
|
|
1916
|
-
await this.#updateOperationStatus("PENDING", attempt + 1);
|
|
1917
|
-
await this.#syncResultListener?.({ operation, status: response.status, error: error2 });
|
|
1918
|
-
if (attempt >= this.#retryOptions.maxRetries) {
|
|
1919
|
-
await this.#updateOperationStatus("FAILED_RETRYABLE", attempt + 1);
|
|
1920
|
-
throw new Error(error2.message);
|
|
1921
|
-
}
|
|
1922
|
-
const interval = this.#retryOptions.initialIntervalMs * this.#retryOptions.backoffMultiplier ** attempt;
|
|
1923
|
-
await new Promise((resolve) => setTimeout(resolve, Math.max(0, interval)));
|
|
1924
|
-
attempt += 1;
|
|
1925
|
-
}
|
|
1926
|
-
const result = response.result;
|
|
1927
|
-
const state = response.state ?? (result !== void 0 && "conflict" in result && result.conflict === true ? result.serverState : result !== void 0 && "ok" in result && result.ok ? result.value.state : void 0);
|
|
1928
|
-
const error = response.status === "REJECTED_PERMANENT" ? errorValue(
|
|
1929
|
-
response.error ?? response.reason ?? (result !== void 0 && "ok" in result && !result.ok ? result.error : void 0),
|
|
1930
|
-
"REJECTED_PERMANENT"
|
|
1931
|
-
) : void 0;
|
|
1932
|
-
await this.#updateOperationStatus(
|
|
1933
|
-
response.status === "ACCEPTED" ? "ACCEPTED" : "REJECTED_PERMANENT",
|
|
1934
|
-
attempt + 1
|
|
1935
|
-
);
|
|
1936
|
-
const eventState = state;
|
|
1937
|
-
if (response.status === "REJECTED_PERMANENT" && error !== void 0) {
|
|
1938
|
-
this.#rejectedHistory = [
|
|
1939
|
-
...this.#rejectedHistory,
|
|
1940
|
-
{
|
|
1941
|
-
operation: { ...operation, status: "REJECTED_PERMANENT", attempts: attempt + 1 },
|
|
1942
|
-
reason: error,
|
|
1943
|
-
errorCode: error.code,
|
|
1944
|
-
rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1945
|
-
attempts: attempt + 1
|
|
1946
|
-
}
|
|
1947
|
-
];
|
|
1948
|
-
await this.#saveRejectedHistory();
|
|
1949
|
-
}
|
|
1950
|
-
this.#operations = this.#operations.slice(1);
|
|
1951
|
-
await this.#storage.save(this.#storageKey(), this.#operations);
|
|
1952
|
-
this.#announceChange();
|
|
1953
|
-
await this.#syncResultListener?.({
|
|
1954
|
-
operation,
|
|
1955
|
-
...result === void 0 ? {} : { result },
|
|
1956
|
-
status: response.status,
|
|
1957
|
-
...error === void 0 ? {} : { error },
|
|
1958
|
-
...eventState === void 0 ? {} : { state: eventState }
|
|
1940
|
+
checkIn(spotId, proofData, options = {}) {
|
|
1941
|
+
return this.#enqueue(async () => {
|
|
1942
|
+
const current = await this.initialize();
|
|
1943
|
+
const spot2 = this.#config.spots.find((item) => item.id === spotId);
|
|
1944
|
+
if (spot2 === void 0)
|
|
1945
|
+
return this.#fail({ code: "SPOT_NOT_FOUND", spotId, message: "Spot was not found." });
|
|
1946
|
+
if (current.records.some((record2) => record2.stampId === spotId))
|
|
1947
|
+
return this.#fail({
|
|
1948
|
+
code: "STAMP_ALREADY_ACQUIRED",
|
|
1949
|
+
spotId,
|
|
1950
|
+
message: "Spot was already claimed."
|
|
1951
|
+
});
|
|
1952
|
+
const acquired = new Set(current.records.map((record2) => record2.stampId));
|
|
1953
|
+
if (spot2.prerequisites?.some((id2) => !acquired.has(id2)))
|
|
1954
|
+
return this.#fail({
|
|
1955
|
+
code: "PREREQUISITES_NOT_MET",
|
|
1956
|
+
spotId,
|
|
1957
|
+
message: "Prerequisite spots are not complete."
|
|
1959
1958
|
});
|
|
1959
|
+
for (const condition2 of spot2.conditions) {
|
|
1960
|
+
if (condition2.type === "custom") {
|
|
1961
|
+
const validator = this.#options.customValidators?.[condition2.validatorName] ?? this.#options.customValidator;
|
|
1962
|
+
if (validator === void 0)
|
|
1963
|
+
return this.#fail({
|
|
1964
|
+
code: "CUSTOM_VALIDATION_FAILED",
|
|
1965
|
+
spotId,
|
|
1966
|
+
message: "No custom validator is registered."
|
|
1967
|
+
});
|
|
1968
|
+
const context = {
|
|
1969
|
+
rallyId: this.#config.id,
|
|
1970
|
+
spotId,
|
|
1971
|
+
proofData,
|
|
1972
|
+
condition: { type: "custom", validatorName: condition2.validatorName },
|
|
1973
|
+
userState: current
|
|
1974
|
+
};
|
|
1975
|
+
const result = typeof validator === "function" ? await validator(context) : await validator.validate(context);
|
|
1976
|
+
if (result === false || typeof result === "object" && !result.valid)
|
|
1977
|
+
return this.#fail({
|
|
1978
|
+
code: "CUSTOM_VALIDATION_FAILED",
|
|
1979
|
+
spotId,
|
|
1980
|
+
message: typeof result === "object" && result.message !== void 0 ? result.message : "Custom validation failed."
|
|
1981
|
+
});
|
|
1982
|
+
} else if (!matches(condition2, proofData))
|
|
1983
|
+
return this.#fail({ code: "INVALID_PROOF", spotId, message: "Verification failed." });
|
|
1960
1984
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1985
|
+
const now = options.now ?? this.#now();
|
|
1986
|
+
const request = {
|
|
1987
|
+
rallyId: this.#config.id,
|
|
1988
|
+
userId: this.#userId,
|
|
1989
|
+
spotId,
|
|
1990
|
+
proofData,
|
|
1991
|
+
idempotencyKey: options.idempotencyKey ?? id("check-in"),
|
|
1992
|
+
now,
|
|
1993
|
+
state: current,
|
|
1994
|
+
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
1995
|
+
};
|
|
1996
|
+
const remote = this.#options.syncAdapter?.checkIn;
|
|
1997
|
+
if (options.sync !== false && remote !== void 0) {
|
|
1998
|
+
try {
|
|
1999
|
+
const result = await remote(request);
|
|
2000
|
+
return result.ok ? this.#commitCheckIn(result) : this.#fail(result.error);
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (this.#offlineQueue === void 0) throw error;
|
|
2003
|
+
await this.#offlineQueue.enqueueCheckIn(request);
|
|
2004
|
+
const record2 = { stampId: spotId, acquiredAt: now };
|
|
2005
|
+
const next2 = this.#reconcile({
|
|
2006
|
+
...current,
|
|
2007
|
+
records: [...current.records, record2],
|
|
2008
|
+
updatedAt: now
|
|
2009
|
+
});
|
|
2010
|
+
await this.#storage.save(next2);
|
|
2011
|
+
return this.#commitCheckIn({ ok: true, value: { state: next2, record: record2 } });
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
const record = { stampId: spotId, acquiredAt: now };
|
|
2015
|
+
const next = this.#reconcile({
|
|
2016
|
+
...current,
|
|
2017
|
+
records: [...current.records, record],
|
|
2018
|
+
updatedAt: now
|
|
2019
|
+
});
|
|
2020
|
+
await this.#storage.save(next);
|
|
2021
|
+
return this.#commitCheckIn({ ok: true, value: { state: next, record } });
|
|
2022
|
+
});
|
|
1976
2023
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2024
|
+
claimReward(rewardId, options = {}) {
|
|
2025
|
+
return this.#enqueue(async () => {
|
|
2026
|
+
const current = await this.initialize();
|
|
2027
|
+
const configured = this.#config.rewards.find((item) => item.id === rewardId);
|
|
2028
|
+
if (configured === void 0)
|
|
2029
|
+
return this.#fail({ code: "REWARD_NOT_FOUND", rewardId, message: "Reward was not found." });
|
|
2030
|
+
const now = options.now ?? this.#now();
|
|
2031
|
+
const state = current.rewards.find((item) => item.rewardId === rewardId) ?? {
|
|
2032
|
+
rewardId,
|
|
2033
|
+
status: "LOCKED"
|
|
2034
|
+
};
|
|
2035
|
+
const local = consumeReward({
|
|
2036
|
+
reward: configured,
|
|
2037
|
+
currentState: state,
|
|
2038
|
+
now,
|
|
2039
|
+
...options.staffPasscode === void 0 ? {} : { inputPasscode: options.staffPasscode },
|
|
2040
|
+
...options.staffId === void 0 ? {} : { staffId: options.staffId }
|
|
2041
|
+
});
|
|
2042
|
+
if (!local.ok) return this.#fail(local.error);
|
|
2043
|
+
const request = {
|
|
2044
|
+
rallyId: this.#config.id,
|
|
2045
|
+
userId: this.#userId,
|
|
2046
|
+
rewardId,
|
|
2047
|
+
idempotencyKey: options.idempotencyKey ?? id("claim"),
|
|
2048
|
+
now,
|
|
2049
|
+
options,
|
|
2050
|
+
state: current,
|
|
2051
|
+
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
2052
|
+
};
|
|
2053
|
+
const remote = this.#options.syncAdapter?.claimReward;
|
|
2054
|
+
if (options.sync !== false && remote !== void 0) {
|
|
2055
|
+
try {
|
|
2056
|
+
const result = await remote(request);
|
|
2057
|
+
return result.ok ? this.#commitClaim(result) : this.#fail(result.error);
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
if (this.#offlineQueue === void 0) throw error;
|
|
2060
|
+
await this.#offlineQueue.enqueueClaimReward(request);
|
|
2061
|
+
const next2 = {
|
|
2062
|
+
...current,
|
|
2063
|
+
rewards: current.rewards.map(
|
|
2064
|
+
(item) => item.rewardId === rewardId ? local.value : item
|
|
2065
|
+
),
|
|
2066
|
+
updatedAt: now
|
|
2067
|
+
};
|
|
2068
|
+
await this.#storage.save(next2);
|
|
2069
|
+
return this.#commitClaim({ ok: true, value: { state: next2, reward: local.value } });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const next = {
|
|
2073
|
+
...current,
|
|
2074
|
+
rewards: current.rewards.map((item) => item.rewardId === rewardId ? local.value : item),
|
|
2075
|
+
updatedAt: now
|
|
1983
2076
|
};
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2077
|
+
await this.#storage.save(next);
|
|
2078
|
+
return this.#commitClaim({ ok: true, value: { state: next, reward: local.value } });
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
sync(adapter = this.#options.syncAdapter) {
|
|
2082
|
+
return this.#enqueue(async () => {
|
|
2083
|
+
this.#emitSyncEvent({ type: "SYNC_STARTED" });
|
|
2084
|
+
this.#syncMetrics = { processed: 0, failed: 0 };
|
|
1988
2085
|
try {
|
|
1989
|
-
|
|
1990
|
-
this.#
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : 0
|
|
1997
|
-
});
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
if (data.type === "unlock" && data.lockKey === this.#lockKey()) {
|
|
2001
|
-
this.#observedLocks.delete(data.lockKey);
|
|
2002
|
-
return;
|
|
2086
|
+
const current = await this.initialize();
|
|
2087
|
+
if (this.#offlineQueue !== void 0 && adapter !== void 0) {
|
|
2088
|
+
await this.#offlineQueue.sync(async (operation) => {
|
|
2089
|
+
if (operation.kind === "checkIn") {
|
|
2090
|
+
if (adapter.checkIn === void 0)
|
|
2091
|
+
throw new Error("No check-in sync adapter is configured.");
|
|
2092
|
+
return adapter.checkIn(operation.request);
|
|
2003
2093
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2094
|
+
if (adapter.claimReward === void 0)
|
|
2095
|
+
throw new Error("No reward sync adapter is configured.");
|
|
2096
|
+
return adapter.claimReward(operation.request);
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
if (adapter?.sync === void 0) {
|
|
2100
|
+
this.#emitEvent({ type: "sync", state: this.#state ?? current });
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const localState = this.#state ?? current;
|
|
2104
|
+
const serverState = await adapter.sync({
|
|
2105
|
+
rallyId: this.#config.id,
|
|
2106
|
+
userId: this.#userId,
|
|
2107
|
+
state: localState,
|
|
2108
|
+
...this.#userId === this.#anonymousSessionId ? { anonymousSessionId: this.#anonymousSessionId } : {}
|
|
2109
|
+
});
|
|
2110
|
+
const resolved = resolveRallyStateConflict(serverState, localState, {
|
|
2111
|
+
policy: this.#options.conflictResolutionPolicy ?? this.#options.conflictPolicy ?? "authoritative_replay"
|
|
2112
|
+
});
|
|
2113
|
+
const next = this.#reconcile(resolved);
|
|
2114
|
+
await this.#storage.save(next);
|
|
2115
|
+
this.#state = next;
|
|
2116
|
+
this.#emit(next);
|
|
2117
|
+
this.#emitEvent({ type: "sync", state: next });
|
|
2118
|
+
} finally {
|
|
2119
|
+
const metrics = this.#syncMetrics ?? { processed: 0, failed: 0 };
|
|
2120
|
+
this.#syncMetrics = null;
|
|
2121
|
+
this.#emitSyncEvent({
|
|
2122
|
+
type: "SYNC_COMPLETED",
|
|
2123
|
+
totalProcessed: metrics.processed,
|
|
2124
|
+
failedCount: metrics.failed
|
|
2007
2125
|
});
|
|
2008
|
-
} catch {
|
|
2009
|
-
this.#channel = null;
|
|
2010
2126
|
}
|
|
2011
|
-
}
|
|
2127
|
+
});
|
|
2012
2128
|
}
|
|
2013
|
-
|
|
2014
|
-
this
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2129
|
+
retrySync() {
|
|
2130
|
+
return this.sync();
|
|
2131
|
+
}
|
|
2132
|
+
reset() {
|
|
2133
|
+
return this.#enqueue(async () => {
|
|
2134
|
+
await this.#storage.remove(this.#config.id, this.#userId);
|
|
2135
|
+
return this.#initializeFresh();
|
|
2018
2136
|
});
|
|
2019
2137
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
this.#
|
|
2027
|
-
|
|
2028
|
-
|
|
2138
|
+
restore(state) {
|
|
2139
|
+
return this.#enqueue(async () => {
|
|
2140
|
+
if (state.rallyId !== this.#config.id || state.userId !== this.#userId)
|
|
2141
|
+
throw new Error("State belongs to another rally or user.");
|
|
2142
|
+
const next = this.#reconcile(state);
|
|
2143
|
+
await this.#storage.save(next);
|
|
2144
|
+
this.#state = next;
|
|
2145
|
+
this.#initialization = Promise.resolve(next);
|
|
2146
|
+
this.#emit(next);
|
|
2147
|
+
return next;
|
|
2148
|
+
});
|
|
2029
2149
|
}
|
|
2030
|
-
#
|
|
2031
|
-
|
|
2150
|
+
#enqueue(operation) {
|
|
2151
|
+
const next = this.#queue.then(operation, operation);
|
|
2152
|
+
this.#queue = next.then(
|
|
2153
|
+
() => void 0,
|
|
2154
|
+
() => void 0
|
|
2155
|
+
);
|
|
2156
|
+
return next;
|
|
2032
2157
|
}
|
|
2033
|
-
#
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
const observed = this.#observedLocks.get(key);
|
|
2040
|
-
if (observed !== void 0 && observed.expiresAt > now && observed.owner !== this.#instanceId)
|
|
2041
|
-
return false;
|
|
2042
|
-
if (this.#lockStorage !== null) {
|
|
2043
|
-
try {
|
|
2044
|
-
const existing = this.#lockStorage.getItem(key);
|
|
2045
|
-
if (existing !== null) {
|
|
2046
|
-
const parsed = JSON.parse(existing);
|
|
2047
|
-
if (typeof parsed === "object" && parsed !== null && typeof parsed.expiresAt === "number" && parsed.expiresAt > now && parsed.owner !== this.#instanceId)
|
|
2048
|
-
return false;
|
|
2049
|
-
}
|
|
2050
|
-
this.#lockStorage.setItem(
|
|
2051
|
-
key,
|
|
2052
|
-
JSON.stringify({ owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS })
|
|
2053
|
-
);
|
|
2054
|
-
this.#channel?.postMessage({
|
|
2055
|
-
type: "lock",
|
|
2056
|
-
lockKey: key,
|
|
2057
|
-
owner: this.#instanceId,
|
|
2058
|
-
expiresAt: now + SYNC_LOCK_TTL_MS
|
|
2059
|
-
});
|
|
2060
|
-
} catch {
|
|
2061
|
-
this.#warnMemoryLock("Persistent lock access failed; offline sync is single-tab only.");
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
syncLocks.set(key, { owner: this.#instanceId, expiresAt: now + SYNC_LOCK_TTL_MS });
|
|
2065
|
-
return true;
|
|
2158
|
+
#initializeFresh() {
|
|
2159
|
+
const next = emptyState(this.#config, this.#userId, this.#now());
|
|
2160
|
+
this.#state = next;
|
|
2161
|
+
this.#initialization = Promise.resolve(next);
|
|
2162
|
+
this.#emit(next);
|
|
2163
|
+
return Promise.resolve(next);
|
|
2066
2164
|
}
|
|
2067
|
-
#
|
|
2068
|
-
const
|
|
2069
|
-
const
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2165
|
+
#reconcile(state) {
|
|
2166
|
+
const ids = new Set(this.#config.spots.map((spot2) => spot2.id));
|
|
2167
|
+
const records = state.records.filter(
|
|
2168
|
+
(record, index, all) => ids.has(record.stampId) && all.findIndex((candidate) => candidate.stampId === record.stampId) === index
|
|
2169
|
+
);
|
|
2170
|
+
return {
|
|
2171
|
+
...cloneState(state),
|
|
2172
|
+
userId: this.#userId,
|
|
2173
|
+
records,
|
|
2174
|
+
rewards: reconcileRewardStates(
|
|
2175
|
+
this.#config.rewards,
|
|
2176
|
+
state.rewards,
|
|
2177
|
+
records.length,
|
|
2178
|
+
state.updatedAt
|
|
2179
|
+
)
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
async #handleOfflineSyncResult(event) {
|
|
2183
|
+
if (event.status === "ACCEPTED") {
|
|
2184
|
+
if (this.#syncMetrics !== null) this.#syncMetrics.processed += 1;
|
|
2185
|
+
if (event.state !== void 0) {
|
|
2186
|
+
const next = this.#reconcile(event.state);
|
|
2187
|
+
await this.#storage.save(next);
|
|
2188
|
+
this.#state = next;
|
|
2189
|
+
this.#emit(next);
|
|
2190
|
+
}
|
|
2191
|
+
this.#emitSyncEvent({
|
|
2192
|
+
type: "OPERATION_ACCEPTED",
|
|
2193
|
+
operationId: this.#operationId(event.operation),
|
|
2194
|
+
resourceId: this.#resourceId(event.operation)
|
|
2195
|
+
});
|
|
2196
|
+
} else if (event.status === "REJECTED_PERMANENT") {
|
|
2197
|
+
const error = event.error ?? {
|
|
2198
|
+
code: "REJECTED_PERMANENT",
|
|
2199
|
+
message: "Offline operation was rejected."
|
|
2200
|
+
};
|
|
2201
|
+
if (this.#syncMetrics !== null) {
|
|
2202
|
+
this.#syncMetrics.processed += 1;
|
|
2203
|
+
this.#syncMetrics.failed += 1;
|
|
2078
2204
|
}
|
|
2205
|
+
const base = event.state ?? this.#state ?? event.operation.request.state;
|
|
2206
|
+
const next = this.#reconcile(rollbackOptimisticOperation(base, event.operation));
|
|
2207
|
+
await this.#storage.save(next);
|
|
2208
|
+
this.#state = next;
|
|
2209
|
+
this.#emit(next);
|
|
2210
|
+
this.#emitSyncEvent({
|
|
2211
|
+
type: "OPERATION_ROLLED_BACK",
|
|
2212
|
+
operationId: this.#operationId(event.operation),
|
|
2213
|
+
resourceId: this.#resourceId(event.operation),
|
|
2214
|
+
reason: errorMessage(error, "Offline operation was rejected."),
|
|
2215
|
+
errorCode: error.code
|
|
2216
|
+
});
|
|
2217
|
+
} else if (event.status === "RETRYABLE_ERROR") {
|
|
2218
|
+
if (this.#syncMetrics !== null) this.#syncMetrics.failed += 1;
|
|
2219
|
+
this.#emitSyncEvent({
|
|
2220
|
+
type: "OPERATION_RETRYABLE_ERROR",
|
|
2221
|
+
operationId: this.#operationId(event.operation),
|
|
2222
|
+
error: errorMessage(event.error, "Retryable sync error.")
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
if (event.state !== void 0 && event.status === void 0) {
|
|
2226
|
+
const next = this.#reconcile(event.state);
|
|
2227
|
+
await this.#storage.save(next);
|
|
2228
|
+
this.#state = next;
|
|
2229
|
+
this.#emit(next);
|
|
2079
2230
|
}
|
|
2231
|
+
if (event.error !== void 0) this.#emitEvent({ type: "error", error: event.error });
|
|
2232
|
+
else if (event.result !== void 0 && "ok" in event.result && !event.result.ok)
|
|
2233
|
+
this.#emitEvent({ type: "error", error: event.result.error });
|
|
2080
2234
|
}
|
|
2081
|
-
#
|
|
2082
|
-
|
|
2083
|
-
return `stamprally:queue:${this.#rallyId ?? "unscoped"}:${this.#userId ?? "anonymous"}`;
|
|
2235
|
+
#now() {
|
|
2236
|
+
return this.#options.clock?.() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2084
2237
|
}
|
|
2085
|
-
#
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2238
|
+
#fail(error) {
|
|
2239
|
+
this.#emitEvent({ type: "error", error });
|
|
2240
|
+
return { ok: false, error };
|
|
2241
|
+
}
|
|
2242
|
+
#commitCheckIn(result) {
|
|
2243
|
+
if (result.ok) {
|
|
2244
|
+
this.#state = result.value.state;
|
|
2245
|
+
this.#emit(this.#state);
|
|
2093
2246
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2247
|
+
this.#emitEvent({ type: "checkIn", result });
|
|
2248
|
+
return result;
|
|
2249
|
+
}
|
|
2250
|
+
#commitClaim(result) {
|
|
2251
|
+
if (result.ok) {
|
|
2252
|
+
this.#state = result.value.state;
|
|
2253
|
+
this.#emit(this.#state);
|
|
2097
2254
|
}
|
|
2098
|
-
|
|
2255
|
+
this.#emitEvent({ type: "rewardClaimed", result });
|
|
2256
|
+
return result;
|
|
2099
2257
|
}
|
|
2100
|
-
|
|
2101
|
-
const
|
|
2102
|
-
const policy = (typeof configured === "function" ? await configured({ operation, localState, serverState }) : configured) ?? this.#conflictPolicy;
|
|
2103
|
-
return resolveRallyStateConflict(serverState, localState, { policy });
|
|
2258
|
+
#emit(state) {
|
|
2259
|
+
for (const listener of this.#listeners) listener(state);
|
|
2104
2260
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2261
|
+
#emitEvent(event) {
|
|
2262
|
+
for (const listener of this.#eventListeners) listener(event);
|
|
2107
2263
|
}
|
|
2108
|
-
#
|
|
2109
|
-
|
|
2110
|
-
this.#
|
|
2111
|
-
|
|
2264
|
+
#emitSyncEvent(event) {
|
|
2265
|
+
for (const listener of this.#syncEventListeners) listener(event);
|
|
2266
|
+
this.#emitEvent({ type: "syncLifecycle", event });
|
|
2267
|
+
}
|
|
2268
|
+
#operationId(operation) {
|
|
2269
|
+
const identity = operation.request.userId ?? operation.request.anonymousSessionId ?? "anonymous";
|
|
2270
|
+
return `${operation.kind}:${operation.request.rallyId}:${identity}:${operation.request.idempotencyKey}`;
|
|
2271
|
+
}
|
|
2272
|
+
#resourceId(operation) {
|
|
2273
|
+
return operation.kind === "checkIn" ? operation.request.spotId : operation.request.rewardId;
|
|
2112
2274
|
}
|
|
2113
2275
|
};
|
|
2114
|
-
function normalizeOperation(operation) {
|
|
2115
|
-
const status = operation.status;
|
|
2116
|
-
return {
|
|
2117
|
-
...operation,
|
|
2118
|
-
status: status === "IN_FLIGHT" || status === "REJECTED" ? "PENDING" : status === "RETRYABLE_ERROR" ? "FAILED_RETRYABLE" : operation.status ?? "PENDING",
|
|
2119
|
-
attempts: operation.attempts ?? 0
|
|
2120
|
-
};
|
|
2121
|
-
}
|
|
2122
|
-
function normalizeRejectedHistory(entry) {
|
|
2123
|
-
return {
|
|
2124
|
-
...entry,
|
|
2125
|
-
operation: normalizeOperation(entry.operation),
|
|
2126
|
-
reason: errorValue(entry.reason, "REJECTED_PERMANENT"),
|
|
2127
|
-
errorCode: entry.errorCode || entry.reason.code,
|
|
2128
|
-
rejectedAt: entry.rejectedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2129
|
-
attempts: entry.attempts ?? entry.operation.attempts ?? 0
|
|
2130
|
-
};
|
|
2131
|
-
}
|
|
2132
2276
|
|
|
2133
2277
|
// src/crypto/token.ts
|
|
2134
2278
|
var encoder = new TextEncoder();
|
|
@@ -2622,6 +2766,18 @@ function finiteNumber(value, key, path, errors, minimum) {
|
|
|
2622
2766
|
);
|
|
2623
2767
|
}
|
|
2624
2768
|
}
|
|
2769
|
+
function nonNegativeInteger(value, key, path, errors) {
|
|
2770
|
+
const item = value[key];
|
|
2771
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
2772
|
+
add(errors, `${path}.${key}`, "Expected a finite number.", "invalid_number");
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (!Number.isInteger(item)) {
|
|
2776
|
+
add(errors, `${path}.${key}`, "Expected a non-negative integer.", "invalid_integer");
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (item < 0) add(errors, `${path}.${key}`, "Expected a non-negative integer.", "out_of_range");
|
|
2780
|
+
}
|
|
2625
2781
|
function localizedText(value, path, errors) {
|
|
2626
2782
|
if (typeof value === "string") return;
|
|
2627
2783
|
if (!isRecord3(value)) {
|
|
@@ -2816,9 +2972,7 @@ function reward(value, path, errors, isPublic) {
|
|
|
2816
2972
|
finiteNumber(value, "requiredStampCount", path, errors, 0);
|
|
2817
2973
|
for (const key of ["stockLimit", "userClaimLimit"]) {
|
|
2818
2974
|
if (hasOwn(value, key) && value[key] !== void 0) {
|
|
2819
|
-
|
|
2820
|
-
if (typeof value[key] === "number" && !Number.isInteger(value[key]))
|
|
2821
|
-
add(errors, `${path}.${key}`, "Expected an integer.", "invalid_integer");
|
|
2975
|
+
nonNegativeInteger(value, key, path, errors);
|
|
2822
2976
|
}
|
|
2823
2977
|
}
|
|
2824
2978
|
optionalString(value, "validUntil", path, errors);
|
|
@@ -2864,8 +3018,14 @@ function validate(value, isPublic) {
|
|
|
2864
3018
|
});
|
|
2865
3019
|
if (!isPublic) {
|
|
2866
3020
|
optionalString(value, "staffPasscode", "$", errors);
|
|
2867
|
-
if (hasOwn(value, "inventory") && value.inventory !== void 0
|
|
2868
|
-
|
|
3021
|
+
if (hasOwn(value, "inventory") && value.inventory !== void 0) {
|
|
3022
|
+
if (!isRecord3(value.inventory))
|
|
3023
|
+
add(errors, "$.inventory", "Expected an object.", "invalid_type");
|
|
3024
|
+
else {
|
|
3025
|
+
for (const [key, item] of Object.entries(value.inventory))
|
|
3026
|
+
if (item !== void 0) nonNegativeInteger(value.inventory, key, "$.inventory", errors);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
2869
3029
|
if (hasOwn(value, "inventoryMode") && value.inventoryMode !== void 0 && value.inventoryMode !== "shared" && value.inventoryMode !== "per_reward")
|
|
2870
3030
|
add(errors, "$.inventoryMode", "Expected shared or per_reward.", "invalid_enum");
|
|
2871
3031
|
if (hasOwn(value, "serverMetadata") && value.serverMetadata !== void 0 && !isRecord3(value.serverMetadata))
|
|
@@ -3109,6 +3269,6 @@ async function verifySnapshotToken(token, secretKey, now = Date.now()) {
|
|
|
3109
3269
|
}
|
|
3110
3270
|
}
|
|
3111
3271
|
|
|
3112
|
-
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createAnonymousSessionId, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, evaluateSpotStatus, exportProgressToken, getCurrentGeoContext, getOrderedSpots, getSpotStatus, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, offlineOperationId, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, resolveRallyStateConflict, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
3272
|
+
export { ConfigValidationError, DEFAULT_SHEET_THEME, MemoryQueueStorage as InMemoryOfflineQueueStorage, InMemoryStorage, IndexedDBAdapter, IndexedDBOfflineQueueStorage, LocalStorageAdapter, OfflineQueue, StampRallyClient, StorageAdapterError, THEME_PRESETS, assertPublicConfig, calculateDistanceMeters, calculateProgress, consumeReward, createAnonymousSessionId, createClaimTicketNumber, createSecureToken, createSignedSnapshotToken, createUniqueClaimTicketNumber, evaluateCondition, evaluateConditionDetailed, evaluateSpotStatus, exportProgressToken, getCurrentGeoContext, getOrderedSpots, getSpotStatus, importProgressToken, isGeolocationSupported, isNfcSupported, isPublicConfig, isQrSupported, isRewardState, isStampRallyState, issueClaimTicketNumber, normalizePasscode, offlineOperationId, parseAdminConfig, parsePublicConfig, processStamp, readNfcContext, readQrContext, reconcileRewardStates, resolveLocalizedText, resolveRallyStateConflict, rollbackOptimisticOperation, safeParseAdminConfig, safeParsePublicConfig, sanitizeAdminConfig, storageKey, toLocalizedString, toPublicConfig, updateLocalizedField, validatePublicConfigSafety, validateRallyConfigRelations, verifyPasscode, verifySecureToken, verifySnapshotToken };
|
|
3113
3273
|
//# sourceMappingURL=index.js.map
|
|
3114
3274
|
//# sourceMappingURL=index.js.map
|