@stackedapp/utils 1.15.5 → 1.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apply_delta.d.ts +60 -0
- package/dist/apply_delta.js +294 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PlayerDelta, StackedSnapshot, StackedOffer } from '@stackedapp/types';
|
|
2
|
+
/**
|
|
3
|
+
* Apply a PlayerDelta to a target object (StackedSnapshot or StackedOffer).
|
|
4
|
+
* Mutates the target object in place and returns it.
|
|
5
|
+
*
|
|
6
|
+
* @param target - The object to apply the delta to
|
|
7
|
+
* @param delta - The delta operations to apply
|
|
8
|
+
* @returns The mutated target object
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Apply snapshot delta
|
|
13
|
+
* const snapshot = { currencies: { gold: { balance: 100 } } };
|
|
14
|
+
* applyDelta(snapshot, {
|
|
15
|
+
* target: 'snapshot',
|
|
16
|
+
* inc: { 'currencies.gold.balance': 50 }
|
|
17
|
+
* });
|
|
18
|
+
* // snapshot.currencies.gold.balance is now 150
|
|
19
|
+
*
|
|
20
|
+
* // Apply offer delta
|
|
21
|
+
* const offer = { completionTrackers: { buyItem: 5 } };
|
|
22
|
+
* applyDelta(offer, {
|
|
23
|
+
* target: 'offers',
|
|
24
|
+
* offers: ['offer-123'],
|
|
25
|
+
* inc: { 'completionTrackers.buyItem': 3 }
|
|
26
|
+
* });
|
|
27
|
+
* // offer.completionTrackers.buyItem is now 8
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyDelta<T extends StackedSnapshot | StackedOffer>(target: T, delta: PlayerDelta): T;
|
|
31
|
+
/**
|
|
32
|
+
* Apply a PlayerDelta to a StackedSnapshot.
|
|
33
|
+
* Convenience wrapper with correct typing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applySnapshotDelta(snapshot: StackedSnapshot, delta: PlayerDelta): StackedSnapshot;
|
|
36
|
+
/**
|
|
37
|
+
* Apply a PlayerDelta to a StackedOffer.
|
|
38
|
+
* Convenience wrapper with correct typing.
|
|
39
|
+
*
|
|
40
|
+
* Note: For offer deltas, check delta.offers to see if this offer is affected.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyOfferDelta(offer: StackedOffer, delta: PlayerDelta): StackedOffer;
|
|
43
|
+
/**
|
|
44
|
+
* Check if an offer is affected by a delta.
|
|
45
|
+
* Use this to filter which offers need updating.
|
|
46
|
+
*
|
|
47
|
+
* @param offerId - The offer's instanceId to check
|
|
48
|
+
* @param delta - The delta to check against
|
|
49
|
+
* @returns true if this offer is in the delta's affected offers list
|
|
50
|
+
*/
|
|
51
|
+
export declare function isOfferAffectedByDelta(offerId: string, delta: PlayerDelta): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Apply a delta to multiple offers, only updating affected ones.
|
|
54
|
+
*
|
|
55
|
+
* @param offers - Map of offerId to StackedOffer
|
|
56
|
+
* @param delta - The delta to apply
|
|
57
|
+
* @returns Array of offerIds that were updated
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyDeltaToOffers(offers: Map<string, StackedOffer> | Record<string, StackedOffer>, delta: PlayerDelta): string[];
|
|
60
|
+
//# sourceMappingURL=apply_delta.d.ts.map
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyDelta = applyDelta;
|
|
4
|
+
exports.applySnapshotDelta = applySnapshotDelta;
|
|
5
|
+
exports.applyOfferDelta = applyOfferDelta;
|
|
6
|
+
exports.isOfferAffectedByDelta = isOfferAffectedByDelta;
|
|
7
|
+
exports.applyDeltaToOffers = applyDeltaToOffers;
|
|
8
|
+
/**
|
|
9
|
+
* Get a nested value from an object using a dot-separated path.
|
|
10
|
+
* e.g., getNestedValue(obj, 'currencies.gold.balance')
|
|
11
|
+
*/
|
|
12
|
+
function getNestedValue(obj, path) {
|
|
13
|
+
const keys = path.split('.');
|
|
14
|
+
let current = obj;
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
if (current == null)
|
|
17
|
+
return undefined;
|
|
18
|
+
current = current[key];
|
|
19
|
+
}
|
|
20
|
+
return current;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Set a nested value in an object using a dot-separated path.
|
|
24
|
+
* Creates intermediate objects as needed.
|
|
25
|
+
* e.g., setNestedValue(obj, 'currencies.gold.balance', 100)
|
|
26
|
+
*/
|
|
27
|
+
function setNestedValue(obj, path, value) {
|
|
28
|
+
const keys = path.split('.');
|
|
29
|
+
let current = obj;
|
|
30
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
31
|
+
const key = keys[i];
|
|
32
|
+
if (current[key] == null || typeof current[key] !== 'object') {
|
|
33
|
+
current[key] = {};
|
|
34
|
+
}
|
|
35
|
+
current = current[key];
|
|
36
|
+
}
|
|
37
|
+
current[keys[keys.length - 1]] = value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Delete a nested value from an object using a dot-separated path.
|
|
41
|
+
* e.g., deleteNestedValue(obj, 'currencies.gold')
|
|
42
|
+
*/
|
|
43
|
+
function deleteNestedValue(obj, path) {
|
|
44
|
+
const keys = path.split('.');
|
|
45
|
+
let current = obj;
|
|
46
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
47
|
+
const key = keys[i];
|
|
48
|
+
if (current[key] == null)
|
|
49
|
+
return;
|
|
50
|
+
current = current[key];
|
|
51
|
+
}
|
|
52
|
+
delete current[keys[keys.length - 1]];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Determine the unique key field for array item matching.
|
|
56
|
+
* Used for addToSet and pull operations.
|
|
57
|
+
*/
|
|
58
|
+
function getArrayItemKey(fieldPath) {
|
|
59
|
+
const field = fieldPath.split('.').pop();
|
|
60
|
+
switch (field) {
|
|
61
|
+
case 'cryptoWallets':
|
|
62
|
+
return 'address';
|
|
63
|
+
case 'identifiers':
|
|
64
|
+
return 'identifier';
|
|
65
|
+
case 'entityLinks':
|
|
66
|
+
return 'playerId';
|
|
67
|
+
case 'tags':
|
|
68
|
+
return null; // Simple string array - match by value directly
|
|
69
|
+
default:
|
|
70
|
+
// For unknown arrays, try common keys
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if two items match based on criteria.
|
|
76
|
+
* For objects: match if all criteria fields match.
|
|
77
|
+
* For primitives: match if equal.
|
|
78
|
+
*/
|
|
79
|
+
function itemMatchesCriteria(item, criteria) {
|
|
80
|
+
if (typeof criteria !== 'object' || criteria === null) {
|
|
81
|
+
// Primitive comparison (e.g., tags array)
|
|
82
|
+
return item === criteria;
|
|
83
|
+
}
|
|
84
|
+
if (typeof item !== 'object' || item === null) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// Object comparison - all criteria fields must match
|
|
88
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
89
|
+
if (item[key] !== value) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check if an item already exists in an array (for addToSet deduplication).
|
|
97
|
+
*/
|
|
98
|
+
function itemExistsInArray(array, newItem, keyField) {
|
|
99
|
+
if (keyField === null) {
|
|
100
|
+
// Simple value comparison (e.g., tags)
|
|
101
|
+
return array.includes(newItem);
|
|
102
|
+
}
|
|
103
|
+
// Object comparison by key field
|
|
104
|
+
if (typeof newItem !== 'object' || newItem === null) {
|
|
105
|
+
return array.includes(newItem);
|
|
106
|
+
}
|
|
107
|
+
const newKeyValue = newItem[keyField];
|
|
108
|
+
return array.some((existing) => {
|
|
109
|
+
if (typeof existing !== 'object' || existing === null)
|
|
110
|
+
return false;
|
|
111
|
+
return existing[keyField] === newKeyValue;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Apply a PlayerDelta to a target object (StackedSnapshot or StackedOffer).
|
|
116
|
+
* Mutates the target object in place and returns it.
|
|
117
|
+
*
|
|
118
|
+
* @param target - The object to apply the delta to
|
|
119
|
+
* @param delta - The delta operations to apply
|
|
120
|
+
* @returns The mutated target object
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* // Apply snapshot delta
|
|
125
|
+
* const snapshot = { currencies: { gold: { balance: 100 } } };
|
|
126
|
+
* applyDelta(snapshot, {
|
|
127
|
+
* target: 'snapshot',
|
|
128
|
+
* inc: { 'currencies.gold.balance': 50 }
|
|
129
|
+
* });
|
|
130
|
+
* // snapshot.currencies.gold.balance is now 150
|
|
131
|
+
*
|
|
132
|
+
* // Apply offer delta
|
|
133
|
+
* const offer = { completionTrackers: { buyItem: 5 } };
|
|
134
|
+
* applyDelta(offer, {
|
|
135
|
+
* target: 'offers',
|
|
136
|
+
* offers: ['offer-123'],
|
|
137
|
+
* inc: { 'completionTrackers.buyItem': 3 }
|
|
138
|
+
* });
|
|
139
|
+
* // offer.completionTrackers.buyItem is now 8
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
function applyDelta(target, delta) {
|
|
143
|
+
// Apply $set operations
|
|
144
|
+
if (delta.set) {
|
|
145
|
+
for (const [path, value] of Object.entries(delta.set)) {
|
|
146
|
+
setNestedValue(target, path, value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Apply $inc operations
|
|
150
|
+
if (delta.inc) {
|
|
151
|
+
for (const [path, amount] of Object.entries(delta.inc)) {
|
|
152
|
+
const current = getNestedValue(target, path);
|
|
153
|
+
const currentNum = typeof current === 'number' ? current : 0;
|
|
154
|
+
setNestedValue(target, path, currentNum + amount);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Apply $min operations
|
|
158
|
+
if (delta.min) {
|
|
159
|
+
for (const [path, value] of Object.entries(delta.min)) {
|
|
160
|
+
const current = getNestedValue(target, path);
|
|
161
|
+
if (typeof current !== 'number') {
|
|
162
|
+
setNestedValue(target, path, value);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
setNestedValue(target, path, Math.min(current, value));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Apply $max operations
|
|
170
|
+
if (delta.max) {
|
|
171
|
+
for (const [path, value] of Object.entries(delta.max)) {
|
|
172
|
+
const current = getNestedValue(target, path);
|
|
173
|
+
if (typeof current !== 'number') {
|
|
174
|
+
setNestedValue(target, path, value);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
setNestedValue(target, path, Math.max(current, value));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Apply $unset operations
|
|
182
|
+
if (delta.unset) {
|
|
183
|
+
for (const path of delta.unset) {
|
|
184
|
+
deleteNestedValue(target, path);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Apply $pull operations BEFORE addToSet/push
|
|
188
|
+
// This is critical for the identifier_link pattern where we:
|
|
189
|
+
// 1. Remove existing item (pull)
|
|
190
|
+
// 2. Add updated item (addToSet)
|
|
191
|
+
// If addToSet ran first, it would see the duplicate and not add.
|
|
192
|
+
if (delta.pull) {
|
|
193
|
+
for (const [path, criteriaList] of Object.entries(delta.pull)) {
|
|
194
|
+
const current = getNestedValue(target, path);
|
|
195
|
+
if (!Array.isArray(current)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const array = current;
|
|
199
|
+
// Filter out items that match ANY of the criteria
|
|
200
|
+
const filtered = array.filter((item) => !criteriaList.some((criteria) => itemMatchesCriteria(item, criteria)));
|
|
201
|
+
setNestedValue(target, path, filtered);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Apply $addToSet operations (add unique items to array)
|
|
205
|
+
if (delta.addToSet) {
|
|
206
|
+
for (const [path, items] of Object.entries(delta.addToSet)) {
|
|
207
|
+
const keyField = getArrayItemKey(path);
|
|
208
|
+
let current = getNestedValue(target, path);
|
|
209
|
+
if (!Array.isArray(current)) {
|
|
210
|
+
current = [];
|
|
211
|
+
setNestedValue(target, path, current);
|
|
212
|
+
}
|
|
213
|
+
const array = current;
|
|
214
|
+
for (const item of items) {
|
|
215
|
+
if (!itemExistsInArray(array, item, keyField)) {
|
|
216
|
+
array.push(item);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Apply $push operations (append to array, allows duplicates)
|
|
222
|
+
if (delta.push) {
|
|
223
|
+
for (const [path, items] of Object.entries(delta.push)) {
|
|
224
|
+
let current = getNestedValue(target, path);
|
|
225
|
+
if (!Array.isArray(current)) {
|
|
226
|
+
current = [];
|
|
227
|
+
setNestedValue(target, path, current);
|
|
228
|
+
}
|
|
229
|
+
const array = current;
|
|
230
|
+
array.push(...items);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return target;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Apply a PlayerDelta to a StackedSnapshot.
|
|
237
|
+
* Convenience wrapper with correct typing.
|
|
238
|
+
*/
|
|
239
|
+
function applySnapshotDelta(snapshot, delta) {
|
|
240
|
+
if (delta.target !== 'snapshot') {
|
|
241
|
+
console.warn('applySnapshotDelta called with non-snapshot delta target:', delta.target);
|
|
242
|
+
}
|
|
243
|
+
return applyDelta(snapshot, delta);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Apply a PlayerDelta to a StackedOffer.
|
|
247
|
+
* Convenience wrapper with correct typing.
|
|
248
|
+
*
|
|
249
|
+
* Note: For offer deltas, check delta.offers to see if this offer is affected.
|
|
250
|
+
*/
|
|
251
|
+
function applyOfferDelta(offer, delta) {
|
|
252
|
+
if (delta.target !== 'offers') {
|
|
253
|
+
console.warn('applyOfferDelta called with non-offers delta target:', delta.target);
|
|
254
|
+
}
|
|
255
|
+
return applyDelta(offer, delta);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Check if an offer is affected by a delta.
|
|
259
|
+
* Use this to filter which offers need updating.
|
|
260
|
+
*
|
|
261
|
+
* @param offerId - The offer's instanceId to check
|
|
262
|
+
* @param delta - The delta to check against
|
|
263
|
+
* @returns true if this offer is in the delta's affected offers list
|
|
264
|
+
*/
|
|
265
|
+
function isOfferAffectedByDelta(offerId, delta) {
|
|
266
|
+
if (delta.target !== 'offers')
|
|
267
|
+
return false;
|
|
268
|
+
if (!delta.offers)
|
|
269
|
+
return false;
|
|
270
|
+
return delta.offers.includes(offerId);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Apply a delta to multiple offers, only updating affected ones.
|
|
274
|
+
*
|
|
275
|
+
* @param offers - Map of offerId to StackedOffer
|
|
276
|
+
* @param delta - The delta to apply
|
|
277
|
+
* @returns Array of offerIds that were updated
|
|
278
|
+
*/
|
|
279
|
+
function applyDeltaToOffers(offers, delta) {
|
|
280
|
+
if (delta.target !== 'offers' || !delta.offers) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const updated = [];
|
|
284
|
+
const offerMap = offers instanceof Map ? offers : new Map(Object.entries(offers));
|
|
285
|
+
for (const offerId of delta.offers) {
|
|
286
|
+
const offer = offerMap.get(offerId);
|
|
287
|
+
if (offer) {
|
|
288
|
+
applyDelta(offer, delta);
|
|
289
|
+
updated.push(offerId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return updated;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=apply_delta.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -26,4 +26,5 @@ var currency_1 = require("./currency");
|
|
|
26
26
|
Object.defineProperty(exports, "currency", { enumerable: true, get: function () { return __importDefault(currency_1).default; } });
|
|
27
27
|
__exportStar(require("./player_data"), exports);
|
|
28
28
|
__exportStar(require("./player_snapshot"), exports);
|
|
29
|
+
__exportStar(require("./apply_delta"), exports);
|
|
29
30
|
//# sourceMappingURL=index.js.map
|