@stamprally/core 0.20.1 → 0.22.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 +12 -5
- package/dist/index.cjs +44 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -7
- package/dist/index.d.ts +25 -7
- package/dist/index.js +43 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @stamprally/core v0.
|
|
1
|
+
# @stamprally/core v0.22.0
|
|
2
2
|
|
|
3
3
|
Dependency-free domain models, immutable state transitions, storage adapters, browser detectors, and safe configuration parsers.
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ import { InMemoryStorage, StampRallyClient, type PublicRallyConfig } from "@stam
|
|
|
7
7
|
|
|
8
8
|
const config: PublicRallyConfig = {
|
|
9
9
|
id: "city-tour",
|
|
10
|
-
version: "0.
|
|
10
|
+
version: "0.22.0",
|
|
11
11
|
title: "City Tour",
|
|
12
12
|
spots: [{ id: "station", orderIndex: 0, name: "Central Station", conditions: [{ type: "passcode" }] }],
|
|
13
13
|
rewards: [],
|
|
@@ -23,7 +23,7 @@ The browser detectors `getCurrentGeoContext`, `readNfcContext`, and `readQrConte
|
|
|
23
23
|
|
|
24
24
|
`InMemoryStorage`, `LocalStorageAdapter`, and `IndexedDBAdapter` implement `StampStorage`. `updateLocalizedField` updates one locale without dropping existing translations.
|
|
25
25
|
|
|
26
|
-
## v0.
|
|
26
|
+
## v0.22.0 offline synchronization
|
|
27
27
|
|
|
28
28
|
Configure `OfflineQueue` with `rallyId` and `userId` to persist pending work under
|
|
29
29
|
`stamprally:queue:<rallyId>:<userId-or-anonymous>`. `switchUser` loads the other
|
|
@@ -35,7 +35,7 @@ When no authenticated user is supplied, the client creates a persistent UUID v4
|
|
|
35
35
|
`anonymousSessionId` and includes it in sync requests. Queue replay uses Web Locks
|
|
36
36
|
when available and supports bounded exponential backoff through `retryOptions`.
|
|
37
37
|
|
|
38
|
-
### v0.
|
|
38
|
+
### v0.22.0 batch sync, queue capability, and storage capability
|
|
39
39
|
|
|
40
40
|
An adapter can return `SyncProgressResponse` from `sync` when it sends queued
|
|
41
41
|
operations to a server-side `syncProgress` endpoint. The adapter passes queued
|
|
@@ -56,12 +56,19 @@ results are removed from the queue; permanent reasons are added to
|
|
|
56
56
|
permanent prerequisite failure does not prevent later independent operations from
|
|
57
57
|
being evaluated.
|
|
58
58
|
|
|
59
|
-
`client.
|
|
59
|
+
`client.queueCapabilities.multiTabSync` is `"supported_web_locks"` when the Web
|
|
60
60
|
Locks API is available. Otherwise it is `"disabled_unsafe_environment"`:
|
|
61
61
|
cross-tab automatic synchronization is disabled, the foreground tab must trigger
|
|
62
62
|
sync explicitly, and a `storageCapabilityWarning` event is emitted. localStorage
|
|
63
63
|
may still be used for durable queue data, but it is never used as a lock.
|
|
64
64
|
|
|
65
|
+
`client.queueCapability` remains the legacy storage string (`"indexeddb"`,
|
|
66
|
+
`"localstorage"`, `"memory"`, `"custom"`, or `"disabled"`) so string comparisons
|
|
67
|
+
such as `client.queueCapability === "indexeddb"` remain valid. New code can use
|
|
68
|
+
`client.queueCapabilities`, which provides `storageType`, `isPersistent`, and
|
|
69
|
+
`multiTabSync`. An omitted or `false` `offlineQueue` is reported as
|
|
70
|
+
`disabled` with `storageType: "none"`.
|
|
71
|
+
|
|
65
72
|
`evaluateSpotStatus` derives `UNCLAIMED`, `CLAIMED`, `LOCKED`, or `VERIFYING`
|
|
66
73
|
without mutating state. A spot with incomplete `prerequisites` is `LOCKED` and
|
|
67
74
|
must not be verified by a client or viewer.
|
package/dist/index.cjs
CHANGED
|
@@ -457,6 +457,12 @@ async function readQrContext(videoElement, options = {}) {
|
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
// src/engine/transition.ts
|
|
460
|
+
function sortOperationsDeterministically(operations) {
|
|
461
|
+
return [...operations].sort((left, right) => {
|
|
462
|
+
if (left.timestamp !== right.timestamp) return left.timestamp - right.timestamp;
|
|
463
|
+
return left.operationId.localeCompare(right.operationId);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
460
466
|
function reconcileRewardStates(rewards, currentStates, acquiredStampCount, now) {
|
|
461
467
|
const states = new Map(currentStates.map((state) => [state.rewardId, state]));
|
|
462
468
|
return rewards.map((reward2) => {
|
|
@@ -1007,6 +1013,11 @@ var IndexedDBAdapter = class {
|
|
|
1007
1013
|
};
|
|
1008
1014
|
|
|
1009
1015
|
// src/client/offlineQueue.ts
|
|
1016
|
+
function getLegacyQueueCapability(capability) {
|
|
1017
|
+
if (typeof capability === "string")
|
|
1018
|
+
return capability === "memory" || capability === "disabled" ? "volatile" : "persistent";
|
|
1019
|
+
return capability.isPersistent ? "persistent" : "volatile";
|
|
1020
|
+
}
|
|
1010
1021
|
function rollbackOptimisticOperation(state, operation) {
|
|
1011
1022
|
const previous = operation.request.state;
|
|
1012
1023
|
if (operation.kind === "checkIn") {
|
|
@@ -1265,8 +1276,14 @@ var OfflineQueue = class {
|
|
|
1265
1276
|
return this.#operations.length;
|
|
1266
1277
|
}
|
|
1267
1278
|
get queueCapability() {
|
|
1279
|
+
return this.#queueCapability;
|
|
1280
|
+
}
|
|
1281
|
+
get queueCapabilities() {
|
|
1282
|
+
const storageType = this.#queueCapability;
|
|
1283
|
+
const isPersistent = storageType !== "memory";
|
|
1268
1284
|
return {
|
|
1269
|
-
|
|
1285
|
+
storageType,
|
|
1286
|
+
isPersistent,
|
|
1270
1287
|
multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment"
|
|
1271
1288
|
};
|
|
1272
1289
|
}
|
|
@@ -1325,7 +1342,7 @@ var OfflineQueue = class {
|
|
|
1325
1342
|
}
|
|
1326
1343
|
if (this.#queueCapability === "memory")
|
|
1327
1344
|
this.#warnCapability("Offline queue persistence is unavailable; queued data is memory-only.");
|
|
1328
|
-
if (this.
|
|
1345
|
+
if (this.queueCapabilities.multiTabSync === "disabled_unsafe_environment")
|
|
1329
1346
|
this.#warnCapability(
|
|
1330
1347
|
"Web Locks is unavailable; automatic cross-tab synchronization is disabled. Sync must be triggered by the foreground tab."
|
|
1331
1348
|
);
|
|
@@ -1763,8 +1780,11 @@ function isReplayable(operation) {
|
|
|
1763
1780
|
return operation.status === void 0 || operation.status === "ACCEPTED" || operation.status === "PENDING" || operation.status === "IN_FLIGHT" || operation.status === "FAILED_RETRYABLE";
|
|
1764
1781
|
}
|
|
1765
1782
|
function operationId(operation) {
|
|
1766
|
-
|
|
1767
|
-
|
|
1783
|
+
return offlineOperationId(operation);
|
|
1784
|
+
}
|
|
1785
|
+
function operationTimestamp(operation) {
|
|
1786
|
+
const parsed = Date.parse(operation.request.now);
|
|
1787
|
+
return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
|
|
1768
1788
|
}
|
|
1769
1789
|
function applyInventoryDelta(state, previous, optimistic) {
|
|
1770
1790
|
const previousInventory = previous.inventory;
|
|
@@ -1840,12 +1860,19 @@ function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configAr
|
|
|
1840
1860
|
function rebuildUserStateLog(baseline, operations, config) {
|
|
1841
1861
|
let state = cloneState(baseline);
|
|
1842
1862
|
const rejectedOperationIds = [];
|
|
1863
|
+
const sortedOperations = sortOperationsDeterministically(
|
|
1864
|
+
operations.map((operation) => ({
|
|
1865
|
+
operation,
|
|
1866
|
+
operationId: offlineOperationId(operation),
|
|
1867
|
+
timestamp: operationTimestamp(operation)
|
|
1868
|
+
}))
|
|
1869
|
+
).map(({ operation }) => operation);
|
|
1843
1870
|
const rejectedCheckIns = new Set(
|
|
1844
|
-
|
|
1871
|
+
sortedOperations.filter(
|
|
1845
1872
|
(operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
|
|
1846
1873
|
).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
|
|
1847
1874
|
);
|
|
1848
|
-
for (const operation of
|
|
1875
|
+
for (const operation of sortedOperations) {
|
|
1849
1876
|
if (operation.status === "REJECTED_PERMANENT") {
|
|
1850
1877
|
if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
|
|
1851
1878
|
continue;
|
|
@@ -1953,7 +1980,7 @@ var StampRallyClient = class {
|
|
|
1953
1980
|
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1954
1981
|
this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
|
|
1955
1982
|
this.#userId = this.#options.userId ?? this.#anonymousSessionId;
|
|
1956
|
-
this.#offlineQueue = this.#options.offlineQueue;
|
|
1983
|
+
this.#offlineQueue = this.#options.offlineQueue === false ? void 0 : this.#options.offlineQueue;
|
|
1957
1984
|
this.#offlineQueue?.setReplayConfig(config);
|
|
1958
1985
|
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1959
1986
|
this.#offlineQueue?.setCapabilityWarningListener(
|
|
@@ -1989,16 +2016,20 @@ var StampRallyClient = class {
|
|
|
1989
2016
|
return this.#syncRevision;
|
|
1990
2017
|
}
|
|
1991
2018
|
get queueCapability() {
|
|
1992
|
-
return this.#offlineQueue?.queueCapability ??
|
|
1993
|
-
|
|
2019
|
+
return this.#offlineQueue?.queueCapability ?? "disabled";
|
|
2020
|
+
}
|
|
2021
|
+
get queueCapabilities() {
|
|
2022
|
+
return this.#offlineQueue?.queueCapabilities ?? {
|
|
2023
|
+
storageType: "none",
|
|
2024
|
+
isPersistent: false,
|
|
1994
2025
|
multiTabSync: "disabled_unsafe_environment"
|
|
1995
2026
|
};
|
|
1996
2027
|
}
|
|
1997
2028
|
get storageCapability() {
|
|
1998
|
-
return this.#offlineQueue?.storageCapability ?? "
|
|
2029
|
+
return this.#offlineQueue?.storageCapability ?? "disabled";
|
|
1999
2030
|
}
|
|
2000
2031
|
get isStoragePersistent() {
|
|
2001
|
-
return this.#offlineQueue?.isStoragePersistent ??
|
|
2032
|
+
return this.#offlineQueue?.isStoragePersistent ?? false;
|
|
2002
2033
|
}
|
|
2003
2034
|
discardRejected(operationId2) {
|
|
2004
2035
|
return this.#offlineQueue?.discardRejected(operationId2) ?? Promise.resolve(false);
|
|
@@ -3481,6 +3512,7 @@ exports.evaluateConditionDetailed = evaluateConditionDetailed;
|
|
|
3481
3512
|
exports.evaluateSpotStatus = evaluateSpotStatus;
|
|
3482
3513
|
exports.exportProgressToken = exportProgressToken;
|
|
3483
3514
|
exports.getCurrentGeoContext = getCurrentGeoContext;
|
|
3515
|
+
exports.getLegacyQueueCapability = getLegacyQueueCapability;
|
|
3484
3516
|
exports.getOrderedSpots = getOrderedSpots;
|
|
3485
3517
|
exports.getSpotStatus = getSpotStatus;
|
|
3486
3518
|
exports.importProgressToken = importProgressToken;
|
|
@@ -3507,6 +3539,7 @@ exports.rollbackOptimisticOperation = rollbackOptimisticOperation;
|
|
|
3507
3539
|
exports.safeParseAdminConfig = safeParseAdminConfig;
|
|
3508
3540
|
exports.safeParsePublicConfig = safeParsePublicConfig;
|
|
3509
3541
|
exports.sanitizeAdminConfig = sanitizeAdminConfig;
|
|
3542
|
+
exports.sortOperationsDeterministically = sortOperationsDeterministically;
|
|
3510
3543
|
exports.storageKey = storageKey;
|
|
3511
3544
|
exports.toLocalizedString = toLocalizedString;
|
|
3512
3545
|
exports.toPublicConfig = toPublicConfig;
|