@stamprally/server 0.9.0 → 0.11.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 +10 -18
- package/dist/index.cjs +187 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -14
- package/dist/index.d.ts +40 -14
- package/dist/index.js +187 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -2,20 +2,36 @@ import * as _stamprally_core from '@stamprally/core';
|
|
|
2
2
|
import { UserRallyState, AdminRallyConfig } from '@stamprally/core';
|
|
3
3
|
export { UserRallyState } from '@stamprally/core';
|
|
4
4
|
|
|
5
|
+
interface UserClaimRecord {
|
|
6
|
+
readonly rallyId: string;
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
readonly rewardId: string;
|
|
9
|
+
readonly ticketNumber: string;
|
|
10
|
+
readonly timestamp: number;
|
|
11
|
+
}
|
|
5
12
|
interface ServerPersistenceAdapter {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
/** Runs the callback in a storage-native transaction when supported. */
|
|
14
|
+
runTransaction?<T>(rallyId: string, operation: (transaction: ServerPersistenceAdapter) => Promise<T>): Promise<T>;
|
|
15
|
+
acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
|
|
16
|
+
releaseLock(rallyId: string, lockKey: string): Promise<void>;
|
|
17
|
+
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
18
|
+
decrementRewardStock(rallyId: string, rewardId: string): Promise<{
|
|
9
19
|
readonly success: boolean;
|
|
10
20
|
readonly remainingStock: number;
|
|
11
21
|
}>;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
restoreRewardStock?(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
23
|
+
rollbackUserState?(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
24
|
+
rollbackUserClaim?(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
|
|
25
|
+
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
26
|
+
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
15
27
|
getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
|
|
28
|
+
recordUserClaim(params: UserClaimRecord): Promise<void>;
|
|
29
|
+
recordClaim?(params: UserClaimRecord): Promise<void>;
|
|
30
|
+
incrementUserClaimCount?(rallyId: string, userId: string, rewardId: string): Promise<void>;
|
|
16
31
|
getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
|
|
17
32
|
saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
|
|
18
33
|
recordAuditLog(log: AuditLog): Promise<void>;
|
|
34
|
+
removeAuditLog?(auditId: string): Promise<void>;
|
|
19
35
|
}
|
|
20
36
|
interface InMemoryServerPersistenceOptions {
|
|
21
37
|
readonly stocks?: Readonly<Record<string, number>>;
|
|
@@ -23,21 +39,31 @@ interface InMemoryServerPersistenceOptions {
|
|
|
23
39
|
declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {
|
|
24
40
|
#private;
|
|
25
41
|
constructor(options?: InMemoryServerPersistenceOptions);
|
|
26
|
-
acquireLock(key: string, ttlMs: number): Promise<boolean>;
|
|
27
|
-
releaseLock(key: string): Promise<void>;
|
|
28
|
-
|
|
42
|
+
acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean>;
|
|
43
|
+
releaseLock(rallyId: string, key: string): Promise<void>;
|
|
44
|
+
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
45
|
+
decrementRewardStock(rallyId: string, rewardId: string): Promise<{
|
|
29
46
|
success: boolean;
|
|
30
47
|
remainingStock: number;
|
|
31
48
|
}>;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
restoreRewardStock(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
50
|
+
rollbackUserState(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
51
|
+
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
52
|
+
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
35
53
|
getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
|
|
36
54
|
getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
|
|
37
55
|
saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
|
|
38
56
|
recordAuditLog(log: AuditLog): Promise<void>;
|
|
57
|
+
removeAuditLog(auditId: string): Promise<void>;
|
|
39
58
|
getAuditLogs(): ReadonlyArray<AuditLog>;
|
|
40
|
-
|
|
59
|
+
recordUserClaim(params: UserClaimRecord): Promise<void>;
|
|
60
|
+
rollbackUserClaim(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
|
|
61
|
+
incrementUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<void>;
|
|
62
|
+
getClaimRecords(): ReadonlyArray<UserClaimRecord>;
|
|
63
|
+
/** @deprecated Use recordUserClaim with a ticket number and timestamp. */
|
|
64
|
+
recordClaim(params: UserClaimRecord): Promise<void>;
|
|
65
|
+
recordClaim(rallyId: string, userId: string, rewardId: string): Promise<void>;
|
|
66
|
+
runTransaction<T>(_rallyId: string, operation: (transaction: ServerPersistenceAdapter) => Promise<T>): Promise<T>;
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
type ErrorResponse = {
|
|
@@ -110,4 +136,4 @@ interface ServerOptions {
|
|
|
110
136
|
readonly now?: () => string;
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer };
|
|
139
|
+
export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer, type UserClaimRecord };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,20 +2,36 @@ import * as _stamprally_core from '@stamprally/core';
|
|
|
2
2
|
import { UserRallyState, AdminRallyConfig } from '@stamprally/core';
|
|
3
3
|
export { UserRallyState } from '@stamprally/core';
|
|
4
4
|
|
|
5
|
+
interface UserClaimRecord {
|
|
6
|
+
readonly rallyId: string;
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
readonly rewardId: string;
|
|
9
|
+
readonly ticketNumber: string;
|
|
10
|
+
readonly timestamp: number;
|
|
11
|
+
}
|
|
5
12
|
interface ServerPersistenceAdapter {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
/** Runs the callback in a storage-native transaction when supported. */
|
|
14
|
+
runTransaction?<T>(rallyId: string, operation: (transaction: ServerPersistenceAdapter) => Promise<T>): Promise<T>;
|
|
15
|
+
acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
|
|
16
|
+
releaseLock(rallyId: string, lockKey: string): Promise<void>;
|
|
17
|
+
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
18
|
+
decrementRewardStock(rallyId: string, rewardId: string): Promise<{
|
|
9
19
|
readonly success: boolean;
|
|
10
20
|
readonly remainingStock: number;
|
|
11
21
|
}>;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
restoreRewardStock?(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
23
|
+
rollbackUserState?(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
24
|
+
rollbackUserClaim?(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
|
|
25
|
+
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
26
|
+
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
15
27
|
getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
|
|
28
|
+
recordUserClaim(params: UserClaimRecord): Promise<void>;
|
|
29
|
+
recordClaim?(params: UserClaimRecord): Promise<void>;
|
|
30
|
+
incrementUserClaimCount?(rallyId: string, userId: string, rewardId: string): Promise<void>;
|
|
16
31
|
getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
|
|
17
32
|
saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
|
|
18
33
|
recordAuditLog(log: AuditLog): Promise<void>;
|
|
34
|
+
removeAuditLog?(auditId: string): Promise<void>;
|
|
19
35
|
}
|
|
20
36
|
interface InMemoryServerPersistenceOptions {
|
|
21
37
|
readonly stocks?: Readonly<Record<string, number>>;
|
|
@@ -23,21 +39,31 @@ interface InMemoryServerPersistenceOptions {
|
|
|
23
39
|
declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapter {
|
|
24
40
|
#private;
|
|
25
41
|
constructor(options?: InMemoryServerPersistenceOptions);
|
|
26
|
-
acquireLock(key: string, ttlMs: number): Promise<boolean>;
|
|
27
|
-
releaseLock(key: string): Promise<void>;
|
|
28
|
-
|
|
42
|
+
acquireLock(rallyId: string, key: string, ttlMs: number): Promise<boolean>;
|
|
43
|
+
releaseLock(rallyId: string, key: string): Promise<void>;
|
|
44
|
+
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
45
|
+
decrementRewardStock(rallyId: string, rewardId: string): Promise<{
|
|
29
46
|
success: boolean;
|
|
30
47
|
remainingStock: number;
|
|
31
48
|
}>;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
restoreRewardStock(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
50
|
+
rollbackUserState(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
51
|
+
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
52
|
+
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
35
53
|
getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
|
|
36
54
|
getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
|
|
37
55
|
saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
|
|
38
56
|
recordAuditLog(log: AuditLog): Promise<void>;
|
|
57
|
+
removeAuditLog(auditId: string): Promise<void>;
|
|
39
58
|
getAuditLogs(): ReadonlyArray<AuditLog>;
|
|
40
|
-
|
|
59
|
+
recordUserClaim(params: UserClaimRecord): Promise<void>;
|
|
60
|
+
rollbackUserClaim(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
|
|
61
|
+
incrementUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<void>;
|
|
62
|
+
getClaimRecords(): ReadonlyArray<UserClaimRecord>;
|
|
63
|
+
/** @deprecated Use recordUserClaim with a ticket number and timestamp. */
|
|
64
|
+
recordClaim(params: UserClaimRecord): Promise<void>;
|
|
65
|
+
recordClaim(rallyId: string, userId: string, rewardId: string): Promise<void>;
|
|
66
|
+
runTransaction<T>(_rallyId: string, operation: (transaction: ServerPersistenceAdapter) => Promise<T>): Promise<T>;
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
type ErrorResponse = {
|
|
@@ -110,4 +136,4 @@ interface ServerOptions {
|
|
|
110
136
|
readonly now?: () => string;
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer };
|
|
139
|
+
export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer, type UserClaimRecord };
|
package/dist/index.js
CHANGED
|
@@ -6,42 +6,73 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
6
6
|
#idempotent = /* @__PURE__ */ new Map();
|
|
7
7
|
#states = /* @__PURE__ */ new Map();
|
|
8
8
|
#stocks;
|
|
9
|
+
#stockDefaults;
|
|
9
10
|
#claims = /* @__PURE__ */ new Map();
|
|
11
|
+
#claimRecords = [];
|
|
10
12
|
#auditLogs = [];
|
|
11
13
|
constructor(options = {}) {
|
|
12
|
-
this.#stocks = new Map(
|
|
14
|
+
this.#stocks = /* @__PURE__ */ new Map();
|
|
15
|
+
this.#stockDefaults = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const [key, stock] of Object.entries(options.stocks ?? {})) {
|
|
17
|
+
if (key.includes(":")) this.#stocks.set(key, stock);
|
|
18
|
+
else this.#stockDefaults.set(key, stock);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
#key(rallyId, key) {
|
|
22
|
+
return `${rallyId}:${key}`;
|
|
23
|
+
}
|
|
24
|
+
#stockKey(rallyId, rewardId) {
|
|
25
|
+
const scopedKey = `${rallyId}:${rewardId}`;
|
|
26
|
+
if (this.#stocks.has(scopedKey)) return scopedKey;
|
|
27
|
+
const defaultStock = this.#stockDefaults.get(rewardId);
|
|
28
|
+
if (defaultStock !== void 0) {
|
|
29
|
+
this.#stocks.set(scopedKey, defaultStock);
|
|
30
|
+
return scopedKey;
|
|
31
|
+
}
|
|
32
|
+
return scopedKey;
|
|
13
33
|
}
|
|
14
|
-
async acquireLock(key, ttlMs) {
|
|
34
|
+
async acquireLock(rallyId, key, ttlMs) {
|
|
35
|
+
const scopedKey = this.#key(rallyId, key);
|
|
15
36
|
const now2 = Date.now();
|
|
16
|
-
const until = this.#locks.get(
|
|
37
|
+
const until = this.#locks.get(scopedKey);
|
|
17
38
|
if (until !== void 0 && until > now2) return false;
|
|
18
|
-
this.#locks.set(
|
|
39
|
+
this.#locks.set(scopedKey, now2 + Math.max(1, ttlMs));
|
|
19
40
|
return true;
|
|
20
41
|
}
|
|
21
|
-
async releaseLock(key) {
|
|
22
|
-
this.#locks.delete(key);
|
|
42
|
+
async releaseLock(rallyId, key) {
|
|
43
|
+
this.#locks.delete(this.#key(rallyId, key));
|
|
44
|
+
}
|
|
45
|
+
async getRewardStock(rallyId, rewardId) {
|
|
46
|
+
return this.#stocks.get(this.#stockKey(rallyId, rewardId)) ?? null;
|
|
23
47
|
}
|
|
24
|
-
async decrementRewardStock(rewardId) {
|
|
25
|
-
const
|
|
48
|
+
async decrementRewardStock(rallyId, rewardId) {
|
|
49
|
+
const key = this.#stockKey(rallyId, rewardId);
|
|
50
|
+
const current = this.#stocks.get(key);
|
|
26
51
|
if (current === void 0) return { success: true, remainingStock: Number.POSITIVE_INFINITY };
|
|
27
52
|
if (current <= 0) return { success: false, remainingStock: 0 };
|
|
28
|
-
this.#stocks.set(
|
|
53
|
+
this.#stocks.set(key, current - 1);
|
|
29
54
|
return { success: true, remainingStock: current - 1 };
|
|
30
55
|
}
|
|
31
|
-
async
|
|
32
|
-
const
|
|
33
|
-
|
|
56
|
+
async restoreRewardStock(rallyId, rewardId, count = 1) {
|
|
57
|
+
const key = this.#stockKey(rallyId, rewardId);
|
|
58
|
+
const current = this.#stocks.get(key);
|
|
59
|
+
if (current !== void 0) this.#stocks.set(key, current + Math.max(0, count));
|
|
60
|
+
}
|
|
61
|
+
async rollbackUserState(rallyId, userId, previousState) {
|
|
62
|
+
const key = `${rallyId}:${userId}`;
|
|
63
|
+
if (previousState === null) this.#states.delete(key);
|
|
64
|
+
else this.#states.set(key, structuredClone(previousState));
|
|
34
65
|
}
|
|
35
|
-
async getIdempotentResult(key) {
|
|
36
|
-
const value = this.#idempotent.get(key);
|
|
66
|
+
async getIdempotentResult(rallyId, key) {
|
|
67
|
+
const value = this.#idempotent.get(this.#key(rallyId, key));
|
|
37
68
|
if (value === void 0 || value.expiresAt <= Date.now()) {
|
|
38
|
-
this.#idempotent.delete(key);
|
|
69
|
+
this.#idempotent.delete(this.#key(rallyId, key));
|
|
39
70
|
return null;
|
|
40
71
|
}
|
|
41
72
|
return structuredClone(value.value);
|
|
42
73
|
}
|
|
43
|
-
async saveIdempotentResult(key, result, ttlMs) {
|
|
44
|
-
this.#idempotent.set(key, {
|
|
74
|
+
async saveIdempotentResult(rallyId, key, result, ttlMs) {
|
|
75
|
+
this.#idempotent.set(this.#key(rallyId, key), {
|
|
45
76
|
value: structuredClone(result),
|
|
46
77
|
expiresAt: Date.now() + Math.max(1, ttlMs)
|
|
47
78
|
});
|
|
@@ -59,13 +90,72 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
59
90
|
async recordAuditLog(log) {
|
|
60
91
|
this.#auditLogs.push(structuredClone(log));
|
|
61
92
|
}
|
|
93
|
+
async removeAuditLog(auditId) {
|
|
94
|
+
const index = this.#auditLogs.findIndex((log) => log.id === auditId);
|
|
95
|
+
if (index >= 0) this.#auditLogs.splice(index, 1);
|
|
96
|
+
}
|
|
62
97
|
getAuditLogs() {
|
|
63
98
|
return structuredClone(this.#auditLogs);
|
|
64
99
|
}
|
|
65
|
-
|
|
100
|
+
async recordUserClaim(params) {
|
|
101
|
+
const { rallyId, userId, rewardId } = params;
|
|
102
|
+
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
103
|
+
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
104
|
+
this.#claimRecords.push(structuredClone(params));
|
|
105
|
+
}
|
|
106
|
+
async rollbackUserClaim(rallyId, userId, rewardId, ticketNumber) {
|
|
107
|
+
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
108
|
+
const count = this.#claims.get(key) ?? 0;
|
|
109
|
+
if (count <= 1) this.#claims.delete(key);
|
|
110
|
+
else this.#claims.set(key, count - 1);
|
|
111
|
+
const index = this.#claimRecords.findIndex(
|
|
112
|
+
(record) => record.rallyId === rallyId && record.userId === userId && record.rewardId === rewardId && record.ticketNumber === ticketNumber
|
|
113
|
+
);
|
|
114
|
+
if (index >= 0) this.#claimRecords.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
async incrementUserClaimCount(rallyId, userId, rewardId) {
|
|
66
117
|
const key = `${rallyId}:${userId}:${rewardId}`;
|
|
67
118
|
this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
|
|
68
119
|
}
|
|
120
|
+
getClaimRecords() {
|
|
121
|
+
return structuredClone(this.#claimRecords);
|
|
122
|
+
}
|
|
123
|
+
recordClaim(paramsOrRallyId, userId, rewardId) {
|
|
124
|
+
const params = typeof paramsOrRallyId === "string" ? {
|
|
125
|
+
rallyId: paramsOrRallyId,
|
|
126
|
+
userId: userId ?? "",
|
|
127
|
+
rewardId: rewardId ?? "",
|
|
128
|
+
ticketNumber: "",
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
} : paramsOrRallyId;
|
|
131
|
+
return this.recordUserClaim(params);
|
|
132
|
+
}
|
|
133
|
+
async runTransaction(_rallyId, operation) {
|
|
134
|
+
const snapshot = {
|
|
135
|
+
stocks: new Map(this.#stocks),
|
|
136
|
+
idempotent: new Map(this.#idempotent),
|
|
137
|
+
states: new Map(this.#states),
|
|
138
|
+
claims: new Map(this.#claims),
|
|
139
|
+
claimRecords: structuredClone(this.#claimRecords),
|
|
140
|
+
auditLogs: structuredClone(this.#auditLogs)
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
return await operation(this);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.#stocks.clear();
|
|
146
|
+
for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
|
|
147
|
+
this.#idempotent.clear();
|
|
148
|
+
for (const [key, value] of snapshot.idempotent)
|
|
149
|
+
this.#idempotent.set(key, structuredClone(value));
|
|
150
|
+
this.#states.clear();
|
|
151
|
+
for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));
|
|
152
|
+
this.#claims.clear();
|
|
153
|
+
for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
|
|
154
|
+
this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
|
|
155
|
+
this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
69
159
|
};
|
|
70
160
|
function json(body, status = 200) {
|
|
71
161
|
return new Response(JSON.stringify(body), {
|
|
@@ -177,10 +267,17 @@ var StampRallyServer = class {
|
|
|
177
267
|
}
|
|
178
268
|
async checkIn(request) {
|
|
179
269
|
const key = `check-in:${request.rallyId}:${request.userId}:${request.idempotencyKey}`;
|
|
180
|
-
const previous = await this.#persistence.getIdempotentResult(
|
|
270
|
+
const previous = await this.#persistence.getIdempotentResult(
|
|
271
|
+
request.rallyId,
|
|
272
|
+
key
|
|
273
|
+
);
|
|
181
274
|
if (previous !== null) return previous;
|
|
182
275
|
const lockKey = `state:${request.rallyId}:${request.userId}`;
|
|
183
|
-
if (!await this.#persistence.acquireLock(
|
|
276
|
+
if (!await this.#persistence.acquireLock(
|
|
277
|
+
request.rallyId,
|
|
278
|
+
lockKey,
|
|
279
|
+
this.#options.lockTtlMs ?? 5e3
|
|
280
|
+
))
|
|
184
281
|
return { ok: false, code: "CONFLICT", message: "The user state is being updated." };
|
|
185
282
|
const timestamp = now(this.#options, request.now);
|
|
186
283
|
try {
|
|
@@ -264,12 +361,20 @@ var StampRallyServer = class {
|
|
|
264
361
|
timestamp
|
|
265
362
|
);
|
|
266
363
|
} finally {
|
|
267
|
-
await this.#persistence.releaseLock(lockKey);
|
|
364
|
+
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
268
365
|
}
|
|
269
366
|
}
|
|
270
367
|
async claimReward(request) {
|
|
368
|
+
if (this.#persistence.runTransaction !== void 0)
|
|
369
|
+
return this.#persistence.runTransaction(
|
|
370
|
+
request.rallyId,
|
|
371
|
+
(transaction) => this.#claimRewardMutation(request, transaction)
|
|
372
|
+
);
|
|
373
|
+
return this.#claimRewardMutation(request, this.#persistence);
|
|
374
|
+
}
|
|
375
|
+
async #claimRewardMutation(request, persistence) {
|
|
271
376
|
const key = `claim:${request.rallyId}:${request.userId}:${request.rewardId}:${request.idempotencyKey}`;
|
|
272
|
-
const previous = await
|
|
377
|
+
const previous = await persistence.getIdempotentResult(request.rallyId, key);
|
|
273
378
|
if (previous !== null) return previous;
|
|
274
379
|
const reward = this.#config.rewards.find((item) => item.id === request.rewardId);
|
|
275
380
|
if (reward === void 0)
|
|
@@ -277,18 +382,24 @@ var StampRallyServer = class {
|
|
|
277
382
|
key,
|
|
278
383
|
{ ok: false, code: "REWARD_NOT_FOUND", message: "Reward was not found." },
|
|
279
384
|
request,
|
|
280
|
-
now(this.#options, request.now)
|
|
385
|
+
now(this.#options, request.now),
|
|
386
|
+
persistence
|
|
281
387
|
);
|
|
282
388
|
const lockKey = `reward:${request.rallyId}:${reward.id}`;
|
|
283
|
-
if (!await
|
|
389
|
+
if (!await persistence.acquireLock(request.rallyId, lockKey, this.#options.lockTtlMs ?? 5e3))
|
|
284
390
|
return { ok: false, code: "CONFLICT", message: "The reward is being claimed." };
|
|
285
391
|
const timestamp = now(this.#options, request.now);
|
|
286
392
|
let decremented = false;
|
|
393
|
+
let previousState = null;
|
|
394
|
+
let recordedTicket = null;
|
|
395
|
+
let successAuditId = null;
|
|
287
396
|
try {
|
|
288
|
-
const checked = await
|
|
397
|
+
const checked = await persistence.getIdempotentResult(request.rallyId, key);
|
|
289
398
|
if (checked !== null) return checked;
|
|
290
|
-
const
|
|
291
|
-
|
|
399
|
+
const storedState = await persistence.getUserState(request.rallyId, request.userId);
|
|
400
|
+
previousState = storedState;
|
|
401
|
+
const current = storedState ?? initialState(this.#config, request.userId, timestamp);
|
|
402
|
+
const claimCount = await persistence.getUserClaimCount(
|
|
292
403
|
request.rallyId,
|
|
293
404
|
request.userId,
|
|
294
405
|
reward.id
|
|
@@ -311,15 +422,17 @@ var StampRallyServer = class {
|
|
|
311
422
|
key,
|
|
312
423
|
{ ok: false, code: local.error.code, message: "Reward cannot be claimed." },
|
|
313
424
|
request,
|
|
314
|
-
timestamp
|
|
425
|
+
timestamp,
|
|
426
|
+
persistence
|
|
315
427
|
);
|
|
316
|
-
const stock = await
|
|
428
|
+
const stock = await persistence.decrementRewardStock(request.rallyId, reward.id);
|
|
317
429
|
if (!stock.success)
|
|
318
430
|
return this.#rememberClaim(
|
|
319
431
|
key,
|
|
320
432
|
{ ok: false, code: "OUT_OF_STOCK", message: "Reward is out of stock." },
|
|
321
433
|
request,
|
|
322
|
-
timestamp
|
|
434
|
+
timestamp,
|
|
435
|
+
persistence
|
|
323
436
|
);
|
|
324
437
|
decremented = true;
|
|
325
438
|
const next = {
|
|
@@ -328,41 +441,59 @@ var StampRallyServer = class {
|
|
|
328
441
|
updatedAt: timestamp
|
|
329
442
|
};
|
|
330
443
|
try {
|
|
331
|
-
await
|
|
444
|
+
await persistence.saveUserState(request.rallyId, request.userId, next);
|
|
332
445
|
const response = local.value.claimTicketNumber === void 0 ? { ok: true, state: next } : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
446
|
+
recordedTicket = local.value.claimTicketNumber ?? "";
|
|
447
|
+
await persistence.recordUserClaim({
|
|
448
|
+
rallyId: request.rallyId,
|
|
449
|
+
userId: request.userId,
|
|
450
|
+
rewardId: reward.id,
|
|
451
|
+
ticketNumber: recordedTicket,
|
|
452
|
+
timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp)
|
|
453
|
+
});
|
|
454
|
+
const successAudit = audit(
|
|
455
|
+
request.rallyId,
|
|
456
|
+
request.userId,
|
|
457
|
+
"CLAIM_REWARD",
|
|
458
|
+
reward.id,
|
|
459
|
+
request.idempotencyKey,
|
|
460
|
+
"SUCCESS",
|
|
461
|
+
timestamp
|
|
343
462
|
);
|
|
344
|
-
|
|
463
|
+
successAuditId = successAudit.id;
|
|
464
|
+
await persistence.recordAuditLog(successAudit);
|
|
465
|
+
await persistence.saveIdempotentResult(
|
|
466
|
+
request.rallyId,
|
|
345
467
|
key,
|
|
346
468
|
response,
|
|
347
469
|
this.#options.idempotencyTtlMs ?? 864e5
|
|
348
470
|
);
|
|
349
|
-
if (this.#persistence instanceof Object && "recordClaim" in this.#persistence && typeof this.#persistence.recordClaim === "function")
|
|
350
|
-
this.#persistence.recordClaim(request.rallyId, request.userId, reward.id);
|
|
351
471
|
return response;
|
|
352
472
|
} catch (error) {
|
|
353
|
-
|
|
473
|
+
if (recordedTicket !== null && persistence.rollbackUserClaim !== void 0)
|
|
474
|
+
await persistence.rollbackUserClaim(
|
|
475
|
+
request.rallyId,
|
|
476
|
+
request.userId,
|
|
477
|
+
reward.id,
|
|
478
|
+
recordedTicket
|
|
479
|
+
);
|
|
480
|
+
if (persistence.rollbackUserState !== void 0)
|
|
481
|
+
await persistence.rollbackUserState(request.rallyId, request.userId, previousState);
|
|
482
|
+
if (successAuditId !== null && persistence.removeAuditLog !== void 0)
|
|
483
|
+
await persistence.removeAuditLog(successAuditId);
|
|
484
|
+
await this.#restoreRewardStock(persistence, request.rallyId, reward.id);
|
|
354
485
|
decremented = false;
|
|
355
486
|
throw error;
|
|
356
487
|
}
|
|
357
488
|
} catch (error) {
|
|
358
|
-
if (decremented) await this.#persistence.
|
|
489
|
+
if (decremented) await this.#restoreRewardStock(persistence, request.rallyId, reward.id);
|
|
359
490
|
return {
|
|
360
491
|
ok: false,
|
|
361
492
|
code: "PERSISTENCE_FAILED",
|
|
362
493
|
message: error instanceof Error ? error.message : "Reward claim failed."
|
|
363
494
|
};
|
|
364
495
|
} finally {
|
|
365
|
-
await
|
|
496
|
+
await persistence.releaseLock(request.rallyId, lockKey);
|
|
366
497
|
}
|
|
367
498
|
}
|
|
368
499
|
async sync(rallyId, userId) {
|
|
@@ -395,14 +526,15 @@ var StampRallyServer = class {
|
|
|
395
526
|
)
|
|
396
527
|
);
|
|
397
528
|
await this.#persistence.saveIdempotentResult(
|
|
529
|
+
rallyId,
|
|
398
530
|
key,
|
|
399
531
|
result,
|
|
400
532
|
this.#options.idempotencyTtlMs ?? 864e5
|
|
401
533
|
);
|
|
402
534
|
return result;
|
|
403
535
|
}
|
|
404
|
-
async #rememberClaim(key, result, request, timestamp) {
|
|
405
|
-
await
|
|
536
|
+
async #rememberClaim(key, result, request, timestamp, persistence = this.#persistence) {
|
|
537
|
+
await persistence.recordAuditLog(
|
|
406
538
|
audit(
|
|
407
539
|
request.rallyId,
|
|
408
540
|
request.userId,
|
|
@@ -414,13 +546,18 @@ var StampRallyServer = class {
|
|
|
414
546
|
result.ok ? void 0 : result.code
|
|
415
547
|
)
|
|
416
548
|
);
|
|
417
|
-
await
|
|
549
|
+
await persistence.saveIdempotentResult(
|
|
550
|
+
request.rallyId,
|
|
418
551
|
key,
|
|
419
552
|
result,
|
|
420
553
|
this.#options.idempotencyTtlMs ?? 864e5
|
|
421
554
|
);
|
|
422
555
|
return result;
|
|
423
556
|
}
|
|
557
|
+
async #restoreRewardStock(persistence, rallyId, rewardId) {
|
|
558
|
+
if (persistence.restoreRewardStock !== void 0)
|
|
559
|
+
await persistence.restoreRewardStock(rallyId, rewardId);
|
|
560
|
+
}
|
|
424
561
|
};
|
|
425
562
|
|
|
426
563
|
export { InMemoryServerPersistenceAdapter, StampRallyServer };
|