@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.
@@ -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
@@ -5,4 +5,5 @@ export * from './blockchain_utils';
5
5
  export { default as currency } from './currency';
6
6
  export * from './player_data';
7
7
  export * from './player_snapshot';
8
+ export * from './apply_delta';
8
9
  //# sourceMappingURL=index.d.ts.map
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackedapp/utils",
3
- "version": "1.15.5",
3
+ "version": "1.15.7",
4
4
  "description": "Public utilities for Stacked platform SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",