@stamprally/core 0.21.0 → 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 +11 -9
- package/dist/index.cjs +41 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -24
- package/dist/index.d.ts +22 -24
- package/dist/index.js +41 -20
- 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,16 +56,18 @@ 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`
|
|
66
|
-
`"
|
|
67
|
-
|
|
68
|
-
`
|
|
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"`.
|
|
69
71
|
|
|
70
72
|
`evaluateSpotStatus` derives `UNCLAIMED`, `CLAIMED`, `LOCKED`, or `VERIFYING`
|
|
71
73
|
without mutating state. A spot with incomplete `prerequisites` is `LOCKED` and
|
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) => {
|
|
@@ -1008,6 +1014,8 @@ var IndexedDBAdapter = class {
|
|
|
1008
1014
|
|
|
1009
1015
|
// src/client/offlineQueue.ts
|
|
1010
1016
|
function getLegacyQueueCapability(capability) {
|
|
1017
|
+
if (typeof capability === "string")
|
|
1018
|
+
return capability === "memory" || capability === "disabled" ? "volatile" : "persistent";
|
|
1011
1019
|
return capability.isPersistent ? "persistent" : "volatile";
|
|
1012
1020
|
}
|
|
1013
1021
|
function rollbackOptimisticOperation(state, operation) {
|
|
@@ -1268,13 +1276,15 @@ var OfflineQueue = class {
|
|
|
1268
1276
|
return this.#operations.length;
|
|
1269
1277
|
}
|
|
1270
1278
|
get queueCapability() {
|
|
1271
|
-
|
|
1279
|
+
return this.#queueCapability;
|
|
1280
|
+
}
|
|
1281
|
+
get queueCapabilities() {
|
|
1282
|
+
const storageType = this.#queueCapability;
|
|
1283
|
+
const isPersistent = storageType !== "memory";
|
|
1272
1284
|
return {
|
|
1273
|
-
|
|
1274
|
-
mode: isPersistent ? "persistent" : "volatile_memory",
|
|
1275
|
-
multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment",
|
|
1285
|
+
storageType,
|
|
1276
1286
|
isPersistent,
|
|
1277
|
-
|
|
1287
|
+
multiTabSync: this.#hasWebLocks() ? "supported_web_locks" : "disabled_unsafe_environment"
|
|
1278
1288
|
};
|
|
1279
1289
|
}
|
|
1280
1290
|
get storageCapability() {
|
|
@@ -1332,7 +1342,7 @@ var OfflineQueue = class {
|
|
|
1332
1342
|
}
|
|
1333
1343
|
if (this.#queueCapability === "memory")
|
|
1334
1344
|
this.#warnCapability("Offline queue persistence is unavailable; queued data is memory-only.");
|
|
1335
|
-
if (this.
|
|
1345
|
+
if (this.queueCapabilities.multiTabSync === "disabled_unsafe_environment")
|
|
1336
1346
|
this.#warnCapability(
|
|
1337
1347
|
"Web Locks is unavailable; automatic cross-tab synchronization is disabled. Sync must be triggered by the foreground tab."
|
|
1338
1348
|
);
|
|
@@ -1770,8 +1780,11 @@ function isReplayable(operation) {
|
|
|
1770
1780
|
return operation.status === void 0 || operation.status === "ACCEPTED" || operation.status === "PENDING" || operation.status === "IN_FLIGHT" || operation.status === "FAILED_RETRYABLE";
|
|
1771
1781
|
}
|
|
1772
1782
|
function operationId(operation) {
|
|
1773
|
-
|
|
1774
|
-
|
|
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;
|
|
1775
1788
|
}
|
|
1776
1789
|
function applyInventoryDelta(state, previous, optimistic) {
|
|
1777
1790
|
const previousInventory = previous.inventory;
|
|
@@ -1847,12 +1860,19 @@ function rebuildUserStateFromLog(baselineOrOptions, operationsArgument, configAr
|
|
|
1847
1860
|
function rebuildUserStateLog(baseline, operations, config) {
|
|
1848
1861
|
let state = cloneState(baseline);
|
|
1849
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);
|
|
1850
1870
|
const rejectedCheckIns = new Set(
|
|
1851
|
-
|
|
1871
|
+
sortedOperations.filter(
|
|
1852
1872
|
(operation) => operation.status === "REJECTED_PERMANENT" && operation.kind === "checkIn"
|
|
1853
1873
|
).map((operation) => operation.kind === "checkIn" ? operation.request.spotId : void 0).filter((spotId) => spotId !== void 0)
|
|
1854
1874
|
);
|
|
1855
|
-
for (const operation of
|
|
1875
|
+
for (const operation of sortedOperations) {
|
|
1856
1876
|
if (operation.status === "REJECTED_PERMANENT") {
|
|
1857
1877
|
if (operation.kind === "checkIn") rejectedCheckIns.add(operation.request.spotId);
|
|
1858
1878
|
continue;
|
|
@@ -1960,7 +1980,7 @@ var StampRallyClient = class {
|
|
|
1960
1980
|
this.#storage = this.#options.storage ?? new InMemoryStorage();
|
|
1961
1981
|
this.#anonymousSessionId = this.#options.anonymousSessionId ?? createAnonymousSessionId();
|
|
1962
1982
|
this.#userId = this.#options.userId ?? this.#anonymousSessionId;
|
|
1963
|
-
this.#offlineQueue = this.#options.offlineQueue;
|
|
1983
|
+
this.#offlineQueue = this.#options.offlineQueue === false ? void 0 : this.#options.offlineQueue;
|
|
1964
1984
|
this.#offlineQueue?.setReplayConfig(config);
|
|
1965
1985
|
this.#offlineQueue?.setSyncResultListener((event) => this.#handleOfflineSyncResult(event));
|
|
1966
1986
|
this.#offlineQueue?.setCapabilityWarningListener(
|
|
@@ -1996,19 +2016,20 @@ var StampRallyClient = class {
|
|
|
1996
2016
|
return this.#syncRevision;
|
|
1997
2017
|
}
|
|
1998
2018
|
get queueCapability() {
|
|
1999
|
-
return this.#offlineQueue?.queueCapability ??
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2019
|
+
return this.#offlineQueue?.queueCapability ?? "disabled";
|
|
2020
|
+
}
|
|
2021
|
+
get queueCapabilities() {
|
|
2022
|
+
return this.#offlineQueue?.queueCapabilities ?? {
|
|
2023
|
+
storageType: "none",
|
|
2024
|
+
isPersistent: false,
|
|
2025
|
+
multiTabSync: "disabled_unsafe_environment"
|
|
2005
2026
|
};
|
|
2006
2027
|
}
|
|
2007
2028
|
get storageCapability() {
|
|
2008
|
-
return this.#offlineQueue?.storageCapability ?? "
|
|
2029
|
+
return this.#offlineQueue?.storageCapability ?? "disabled";
|
|
2009
2030
|
}
|
|
2010
2031
|
get isStoragePersistent() {
|
|
2011
|
-
return this.#offlineQueue?.isStoragePersistent ??
|
|
2032
|
+
return this.#offlineQueue?.isStoragePersistent ?? false;
|
|
2012
2033
|
}
|
|
2013
2034
|
discardRejected(operationId2) {
|
|
2014
2035
|
return this.#offlineQueue?.discardRejected(operationId2) ?? Promise.resolve(false);
|
|
@@ -3518,6 +3539,7 @@ exports.rollbackOptimisticOperation = rollbackOptimisticOperation;
|
|
|
3518
3539
|
exports.safeParseAdminConfig = safeParseAdminConfig;
|
|
3519
3540
|
exports.safeParsePublicConfig = safeParsePublicConfig;
|
|
3520
3541
|
exports.sanitizeAdminConfig = sanitizeAdminConfig;
|
|
3542
|
+
exports.sortOperationsDeterministically = sortOperationsDeterministically;
|
|
3521
3543
|
exports.storageKey = storageKey;
|
|
3522
3544
|
exports.toLocalizedString = toLocalizedString;
|
|
3523
3545
|
exports.toPublicConfig = toPublicConfig;
|