@smartico/public-api 0.0.402 → 0.0.404
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/.claude/skills/api-tsdoc/SKILL.md +675 -0
- package/.claude/skills/api-tsdoc-final/SKILL.md +266 -0
- package/.claude/skills/api-tsdoc-howto/SKILL.md +143 -0
- package/dist/MiniGames/SAWTemplate.d.ts +4 -0
- package/dist/NodeCache.d.ts +1 -0
- package/dist/OCache.d.ts +7 -0
- package/dist/SmarticoGlobal.d.ts +0 -6
- package/dist/WSAPI/WSAPITypes.d.ts +43 -0
- package/dist/WSAPI/WSAPIUser.d.ts +3 -0
- package/dist/index.js +92 -33
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +60 -10
- package/dist/index.modern.mjs.map +1 -1
- package/docs/api/interfaces/TUserProfile.md +136 -0
- package/docs/capabilities/_responses/buyStoreItem.json +4 -0
- package/docs/capabilities/_responses/claimBonus.json +4 -0
- package/docs/capabilities/_responses/gamePickGetActiveRound.json +95 -0
- package/docs/capabilities/_responses/gamePickGetActiveRounds.json +191 -0
- package/docs/capabilities/_responses/gamePickGetBoard.json +47 -0
- package/docs/capabilities/_responses/gamePickGetGameInfo.json +355 -0
- package/docs/capabilities/_responses/gamePickGetHistory.json +191 -0
- package/docs/capabilities/_responses/gamePickGetUserInfo.json +14 -0
- package/docs/capabilities/_responses/getAchCategories.json +12 -0
- package/docs/capabilities/_responses/getActivityLog.json +1 -0
- package/docs/capabilities/_responses/getAvatarPrompts.json +16 -0
- package/docs/capabilities/_responses/getAvatarsCustomized.json +12 -0
- package/docs/capabilities/_responses/getAvatarsList.json +24 -0
- package/docs/capabilities/_responses/getBadges.json +98 -0
- package/docs/capabilities/_responses/getBonuses.json +18 -0
- package/docs/capabilities/_responses/getClanInfo.json +39 -0
- package/docs/capabilities/_responses/getClans.json +35 -0
- package/docs/capabilities/_responses/getCurrentLevel.json +20 -0
- package/docs/capabilities/_responses/getCustomSections.json +25 -0
- package/docs/capabilities/_responses/getInboxMessageBody.json +7 -0
- package/docs/capabilities/_responses/getInboxMessages.json +18 -0
- package/docs/capabilities/_responses/getInboxUnreadCount.json +1 -0
- package/docs/capabilities/_responses/getJackpotEligibleGames.json +20 -0
- package/docs/capabilities/_responses/getJackpotWinners.json +28 -0
- package/docs/capabilities/_responses/getLeaderBoard.json +45 -0
- package/docs/capabilities/_responses/getLevels.json +40 -0
- package/docs/capabilities/_responses/getMiniGames.json +208 -0
- package/docs/capabilities/_responses/getMiniGamesHistory.json +1 -0
- package/docs/capabilities/_responses/getMissions.json +117 -0
- package/docs/capabilities/_responses/getRaffleDrawRun.json +62 -0
- package/docs/capabilities/_responses/getRaffleDrawRunsHistory.json +34 -0
- package/docs/capabilities/_responses/getRaffles.json +140 -0
- package/docs/capabilities/_responses/getStoreCategories.json +12 -0
- package/docs/capabilities/_responses/getStoreItems.json +47 -0
- package/docs/capabilities/_responses/getStorePurchasedItems.json +49 -0
- package/docs/capabilities/_responses/getTournamentInstanceInfo.json +75 -0
- package/docs/capabilities/_responses/getTournamentsList.json +66 -0
- package/docs/capabilities/_responses/getTranslations.json +25 -0
- package/docs/capabilities/_responses/getUserLevelExtraCounters.json +4 -0
- package/docs/capabilities/_responses/getUserProfile.json +34 -0
- package/docs/capabilities/_responses/jackpotGet.json +113 -0
- package/docs/capabilities/_responses/jackpotOptIn.json +9 -0
- package/docs/capabilities/_responses/jackpotOptOut.json +9 -0
- package/docs/capabilities/_responses/markAllInboxMessagesAsRead.json +4 -0
- package/docs/capabilities/_responses/markInboxMessageAsRead.json +4 -0
- package/docs/capabilities/_responses/markUnmarkInboxMessageAsFavorite.json +4 -0
- package/docs/capabilities/_responses/miniGameWinAcknowledgeRequest.json +10 -0
- package/docs/capabilities/_responses/playMiniGame.json +6 -0
- package/docs/capabilities/_responses/playMiniGameBatch.json +9 -0
- package/docs/capabilities/_responses/registerInTournament.json +4 -0
- package/docs/capabilities/_responses/requestMissionOptIn.json +4 -0
- package/docs/capabilities/_responses/setAvatar.json +4 -0
- package/docs/capabilities/avatarsCustomize.md +115 -0
- package/docs/capabilities/buyStoreItem.md +179 -0
- package/docs/capabilities/checkSegmentListMatch.md +69 -0
- package/docs/capabilities/checkSegmentMatch.md +74 -0
- package/docs/capabilities/claimBonus.md +125 -0
- package/docs/capabilities/claimRafflePrize.md +116 -0
- package/docs/capabilities/deleteAllInboxMessages.md +66 -0
- package/docs/capabilities/deleteInboxMessage.md +60 -0
- package/docs/capabilities/gamePickGetActiveRound.md +248 -0
- package/docs/capabilities/gamePickGetActiveRounds.md +259 -0
- package/docs/capabilities/gamePickGetBoard.md +202 -0
- package/docs/capabilities/gamePickGetGameInfo.md +318 -0
- package/docs/capabilities/gamePickGetHistory.md +244 -0
- package/docs/capabilities/gamePickGetRoundInfoForUser.md +189 -0
- package/docs/capabilities/gamePickGetUserInfo.md +91 -0
- package/docs/capabilities/gamePickSubmitSelection.md +222 -0
- package/docs/capabilities/gamePickSubmitSelectionQuiz.md +198 -0
- package/docs/capabilities/getAchCategories.md +119 -0
- package/docs/capabilities/getActivityLog.md +108 -0
- package/docs/capabilities/getAvatarPrompts.md +81 -0
- package/docs/capabilities/getAvatarsCustomized.md +96 -0
- package/docs/capabilities/getAvatarsList.md +101 -0
- package/docs/capabilities/getBadges.md +237 -0
- package/docs/capabilities/getBonuses.md +159 -0
- package/docs/capabilities/getClanInfo.md +164 -0
- package/docs/capabilities/getClanTournamentPlayers.md +112 -0
- package/docs/capabilities/getClans.md +168 -0
- package/docs/capabilities/getCurrentLevel.md +123 -0
- package/docs/capabilities/getCustomSections.md +96 -0
- package/docs/capabilities/getInboxMessageBody.md +87 -0
- package/docs/capabilities/getInboxMessages.md +146 -0
- package/docs/capabilities/getInboxUnreadCount.md +54 -0
- package/docs/capabilities/getJackpotEligibleGames.md +93 -0
- package/docs/capabilities/getJackpotWinners.md +113 -0
- package/docs/capabilities/getLeaderBoard.md +187 -0
- package/docs/capabilities/getLeaderBoards.md +88 -0
- package/docs/capabilities/getLevels.md +118 -0
- package/docs/capabilities/getMiniGames.md +295 -0
- package/docs/capabilities/getMiniGamesHistory.md +183 -0
- package/docs/capabilities/getMissions.md +253 -0
- package/docs/capabilities/getRaffleDrawRun.md +210 -0
- package/docs/capabilities/getRaffleDrawRunsHistory.md +116 -0
- package/docs/capabilities/getRaffleWonPrizes.md +153 -0
- package/docs/capabilities/getRaffles.md +249 -0
- package/docs/capabilities/getRelatedItemsForGame.md +236 -0
- package/docs/capabilities/getStoreCategories.md +128 -0
- package/docs/capabilities/getStoreItems.md +260 -0
- package/docs/capabilities/getStorePurchasedItems.md +280 -0
- package/docs/capabilities/getTournamentInstanceInfo.md +301 -0
- package/docs/capabilities/getTournamentsList.md +234 -0
- package/docs/capabilities/getTranslations.md +95 -0
- package/docs/capabilities/getUserLevelExtraCounters.md +92 -0
- package/docs/capabilities/getUserProfile.md +184 -0
- package/docs/capabilities/jackpotGet.md +173 -0
- package/docs/capabilities/jackpotOptIn.md +94 -0
- package/docs/capabilities/jackpotOptOut.md +92 -0
- package/docs/capabilities/joinClan.md +169 -0
- package/docs/capabilities/markAllInboxMessagesAsRead.md +72 -0
- package/docs/capabilities/markInboxMessageAsRead.md +69 -0
- package/docs/capabilities/markUnmarkInboxMessageAsFavorite.md +75 -0
- package/docs/capabilities/miniGameWinAcknowledgeRequest.md +98 -0
- package/docs/capabilities/playMiniGame.md +217 -0
- package/docs/capabilities/playMiniGameBatch.md +129 -0
- package/docs/capabilities/registerInTournament.md +187 -0
- package/docs/capabilities/reportClickEvent.md +74 -0
- package/docs/capabilities/reportImpressionEvent.md +67 -0
- package/docs/capabilities/requestMissionClaimReward.md +117 -0
- package/docs/capabilities/requestMissionOptIn.md +98 -0
- package/docs/capabilities/requestRaffleOptin.md +108 -0
- package/docs/capabilities/setAvatar.md +132 -0
- package/docs/capabilities/smarticoCallbacks.md +165 -0
- package/docs/capabilities/smarticoGlobal.md +62 -0
- package/package.json +3 -2
- package/readme-smartico-dev.md +208 -0
- package/scripts/capture-responses-browser.mjs +301 -0
- package/scripts/gen-capabilities.js +368 -0
- package/scripts/run-public-api.mjs +105 -0
- package/src/MiniGames/SAWGetTemplatesResponse.ts +2 -0
- package/src/MiniGames/SAWTemplate.ts +5 -0
- package/src/NodeCache.ts +8 -0
- package/src/OCache.ts +17 -0
- package/src/SmarticoGlobal.ts +0 -7
- package/src/WSAPI/WSAPI.ts +3 -3
- package/src/WSAPI/WSAPIJackpots.ts +4 -1
- package/src/WSAPI/WSAPIMiniGames.ts +1 -1
- package/src/WSAPI/WSAPITypes.ts +44 -0
- package/src/WSAPI/WSAPIUser.ts +30 -5
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* gen-capabilities.js — deterministic capability-page generator.
|
|
4
|
+
*
|
|
5
|
+
* Stitches the existing single-source-of-truth doc inputs into one
|
|
6
|
+
* self-contained "API capability page" per public WSAPI method:
|
|
7
|
+
*
|
|
8
|
+
* method TSDoc (summary / @remarks contract / @param / @example / {@link})
|
|
9
|
+
* + the return TYPE's own field comments (annotate the response shape)
|
|
10
|
+
* + captured REAL response (docs/capabilities/_responses/<method>.json)
|
|
11
|
+
* ────────────────────────────────────────────────────────────────────────
|
|
12
|
+
* → docs/capabilities/<method>.md
|
|
13
|
+
*
|
|
14
|
+
* The capture step (run_public_api → _responses/<method>.json) is a SEPARATE
|
|
15
|
+
* skill-driven step; this script is pure, deterministic assembly.
|
|
16
|
+
*
|
|
17
|
+
* node scripts/gen-capabilities.js
|
|
18
|
+
*/
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const ts = require('typescript');
|
|
22
|
+
|
|
23
|
+
const ROOT = path.join(__dirname, '..');
|
|
24
|
+
const SRC = path.join(ROOT, 'src');
|
|
25
|
+
const WSAPI_DIR = path.join(SRC, 'WSAPI');
|
|
26
|
+
const OUT_DIR = path.join(ROOT, 'docs', 'capabilities');
|
|
27
|
+
const RESP_DIR = path.join(OUT_DIR, '_responses');
|
|
28
|
+
|
|
29
|
+
const SKIP_METHODS = new Set(['clearCaches']);
|
|
30
|
+
|
|
31
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function listFiles(dir, ext, acc = []) {
|
|
34
|
+
if (!fs.existsSync(dir)) return acc;
|
|
35
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const p = path.join(dir, e.name);
|
|
37
|
+
if (e.isDirectory()) listFiles(p, ext, acc);
|
|
38
|
+
else if (e.name.endsWith(ext)) acc.push(p);
|
|
39
|
+
}
|
|
40
|
+
return acc;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parse = (file) =>
|
|
44
|
+
ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
45
|
+
|
|
46
|
+
const LINK_KINDS = new Set([
|
|
47
|
+
ts.SyntaxKind.JSDocLink, ts.SyntaxKind.JSDocLinkCode, ts.SyntaxKind.JSDocLinkPlain,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** Stringify a JSDoc comment (string | NodeArray), rendering {@link X} as `X`. */
|
|
51
|
+
function tagText(comment) {
|
|
52
|
+
if (!comment) return '';
|
|
53
|
+
if (typeof comment === 'string') return comment.trim();
|
|
54
|
+
return comment.map(c => {
|
|
55
|
+
if (typeof c === 'string') return c;
|
|
56
|
+
if (LINK_KINDS.has(c.kind)) {
|
|
57
|
+
const nm = c.name ? c.name.getText() : '';
|
|
58
|
+
const tail = (c.text || '').trim();
|
|
59
|
+
return '`' + (nm || tail) + '`' + (nm && tail ? ' ' + tail : '');
|
|
60
|
+
}
|
|
61
|
+
return c.text ?? '';
|
|
62
|
+
}).join('').replace(/[ \t]+/g, ' ').trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Example-response trimming so a method returning a huge map/array (e.g.
|
|
66
|
+
// getTranslations → ~800 keys) doesn't dump a 900-line page. Long strings are
|
|
67
|
+
// clipped; arrays show 2 items; "dictionary-sized" objects (> OBJ_CAP keys, i.e.
|
|
68
|
+
// a key→value map rather than a struct) are truncated to a representative sample.
|
|
69
|
+
// Example-response trimming. NO depth collapse (the real nested shape is the point
|
|
70
|
+
// — e.g. gamePickGetGameInfo.sawTemplate / allRounds). Only bound the pathological
|
|
71
|
+
// cases: long strings, wide arrays, and dictionary-sized maps (getTranslations → 800
|
|
72
|
+
// keys). The full structural definition lives in the type-driven Returns section.
|
|
73
|
+
const EX_ARR = 1, EX_OBJ_CAP = 40, EX_OBJ_KEEP = 12;
|
|
74
|
+
// Noise keys collapsed in the example payload: irrelevant-to-consumer config and
|
|
75
|
+
// keyed duplicates. They're the size driver on the biggest pages and add no signal.
|
|
76
|
+
const NOISE_KEYS = {
|
|
77
|
+
labelInfo: '…(label config — typed `any`, omitted)',
|
|
78
|
+
prizesMap: '…(keyed duplicate of `prizes[]`)',
|
|
79
|
+
};
|
|
80
|
+
function trimVal(v) {
|
|
81
|
+
if (typeof v === 'string') return v.length > 160 ? v.slice(0, 157) + '…' : v;
|
|
82
|
+
if (Array.isArray(v)) return v.slice(0, EX_ARR).map(trimVal);
|
|
83
|
+
if (v && typeof v === 'object') {
|
|
84
|
+
const keys = Object.keys(v);
|
|
85
|
+
const capped = keys.length > EX_OBJ_CAP;
|
|
86
|
+
const o = {};
|
|
87
|
+
for (const k of keys.slice(0, capped ? EX_OBJ_KEEP : keys.length)) {
|
|
88
|
+
o[k] = (NOISE_KEYS[k] && v[k] && typeof v[k] === 'object') ? NOISE_KEYS[k] : trimVal(v[k]);
|
|
89
|
+
}
|
|
90
|
+
if (capped) o['…'] = `(+${keys.length - EX_OBJ_KEEP} more keys)`;
|
|
91
|
+
return o;
|
|
92
|
+
}
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** First capitalized type identifier in a type-text (the base of `TX[]`, `TX | null`, …). */
|
|
97
|
+
const baseTypeName = (txt) =>
|
|
98
|
+
((txt || '').match(/[A-Z][A-Za-z0-9_]+/g) || [])
|
|
99
|
+
.find(n => !['Promise', 'Partial', 'Array', 'Record', 'GamesApiResponse', 'Date'].includes(n)) || '';
|
|
100
|
+
|
|
101
|
+
// ── type registry: name → { ext:[names], fields:{ name:{comment,typeText} } } ─
|
|
102
|
+
|
|
103
|
+
function buildTypeRegistry() {
|
|
104
|
+
const reg = {};
|
|
105
|
+
const trailing = (sf, node) => {
|
|
106
|
+
const tr = ts.getTrailingCommentRanges(sf.text, node.end) || [];
|
|
107
|
+
return tr.length ? sf.text.slice(tr[0].pos, tr[0].end).replace(/^\/\/\s?/, '').trim() : '';
|
|
108
|
+
};
|
|
109
|
+
for (const file of listFiles(SRC, '.ts')) {
|
|
110
|
+
const sf = parse(file);
|
|
111
|
+
const visit = (node) => {
|
|
112
|
+
if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
113
|
+
const name = node.name.text;
|
|
114
|
+
const ext = (node.heritageClauses || [])
|
|
115
|
+
.flatMap(h => h.types.map(t => baseTypeName(t.expression.getText(sf)))).filter(Boolean);
|
|
116
|
+
const fields = {};
|
|
117
|
+
for (const m of node.members) {
|
|
118
|
+
if (!ts.isPropertySignature(m) || !m.name) continue;
|
|
119
|
+
const fname = m.name.getText(sf);
|
|
120
|
+
const comment = ts.getJSDocCommentsAndTags(m).map(d => tagText(d.comment)).filter(Boolean).join(' ')
|
|
121
|
+
|| trailing(sf, m);
|
|
122
|
+
fields[fname] = { comment: comment.replace(/\s+/g, ' ').trim(), typeText: m.type ? m.type.getText(sf) : '' };
|
|
123
|
+
}
|
|
124
|
+
reg[name] = { ext, fields };
|
|
125
|
+
}
|
|
126
|
+
ts.forEachChild(node, visit);
|
|
127
|
+
};
|
|
128
|
+
visit(sf);
|
|
129
|
+
}
|
|
130
|
+
return reg;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Merge a type's own fields over its inherited fields. */
|
|
134
|
+
function resolveFields(name, reg, seen = new Set()) {
|
|
135
|
+
if (!name || !reg[name] || seen.has(name)) return {};
|
|
136
|
+
seen.add(name);
|
|
137
|
+
const acc = {};
|
|
138
|
+
for (const e of reg[name].ext) Object.assign(acc, resolveFields(e, reg, seen));
|
|
139
|
+
Object.assign(acc, reg[name].fields);
|
|
140
|
+
return acc;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── method extraction ───────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function extractMethods() {
|
|
146
|
+
const methods = [];
|
|
147
|
+
for (const file of listFiles(WSAPI_DIR, '.ts')) {
|
|
148
|
+
const base = path.basename(file);
|
|
149
|
+
if (base === 'WSAPITypes.ts' || base === 'WSAPIBase.ts') continue;
|
|
150
|
+
const domain = base.replace(/^WSAPI/, '').replace(/\.ts$/, '').toLowerCase();
|
|
151
|
+
const sf = parse(file);
|
|
152
|
+
const visit = (node) => {
|
|
153
|
+
if (ts.isMethodDeclaration(node) && node.name) {
|
|
154
|
+
const isPublic = !node.modifiers?.some(m =>
|
|
155
|
+
m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
156
|
+
const name = node.name.getText(sf);
|
|
157
|
+
if (isPublic && !SKIP_METHODS.has(name) && !name.startsWith('_')) methods.push({ name, domain, node, sf });
|
|
158
|
+
}
|
|
159
|
+
ts.forEachChild(node, visit);
|
|
160
|
+
};
|
|
161
|
+
visit(sf);
|
|
162
|
+
}
|
|
163
|
+
return methods;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readJSDoc(node, sf) {
|
|
167
|
+
const docs = ts.getJSDocCommentsAndTags(node);
|
|
168
|
+
const out = { description: '', remarks: '', params: [], example: '', returns: '', links: [] };
|
|
169
|
+
for (const d of docs) {
|
|
170
|
+
if (ts.isJSDoc(d)) out.description = tagText(d.comment) || out.description;
|
|
171
|
+
for (const t of d.tags || (ts.isJSDoc(d) ? [] : [d])) {
|
|
172
|
+
const tag = t.tagName?.text, txt = tagText(t.comment);
|
|
173
|
+
if (tag === 'remarks') out.remarks += (out.remarks ? '\n\n' : '') + txt;
|
|
174
|
+
else if (tag === 'param') out.params.push({ name: t.name?.getText(sf) ?? '', text: txt });
|
|
175
|
+
else if (tag === 'example') out.example += (out.example ? '\n\n' : '') + txt;
|
|
176
|
+
else if (tag === 'returns') out.returns = txt;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const raw = docs.map(d => d.getText(sf)).join('\n');
|
|
180
|
+
let m; const re = /\{@link\s+([A-Za-z0-9_.]+)/g;
|
|
181
|
+
while ((m = re.exec(raw))) if (!out.links.includes(m[1])) out.links.push(m[1]);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── page assembly ───────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function splitRemarks(remarks) {
|
|
188
|
+
const m = remarks.match(/\*\*Error codes[\s\S]*?(?=\n\s*\*\*[A-Z]|$)/);
|
|
189
|
+
return m ? { contract: remarks.replace(m[0], '').trim(), errors: m[0].trim() } : { contract: remarks, errors: '' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Recursively render a type's fields from the registry, expanding nested object /
|
|
193
|
+
// array field types (bounded by depth + a per-branch cycle guard) so the full
|
|
194
|
+
// structure is described, not collapsed.
|
|
195
|
+
const RT_MAX_DEPTH = 4;
|
|
196
|
+
function renderType(typeName, reg, seen, depth) {
|
|
197
|
+
const out = [];
|
|
198
|
+
const pad = ' '.repeat(depth);
|
|
199
|
+
for (const [k, info] of Object.entries(resolveFields(typeName, reg))) {
|
|
200
|
+
const tt = info.typeText || '';
|
|
201
|
+
out.push(`${pad}- \`${k}\` (${tt})${info.comment ? ` — ${info.comment}` : ''}`);
|
|
202
|
+
const base = baseTypeName(tt);
|
|
203
|
+
if (base && reg[base] && !seen.has(base) && depth < RT_MAX_DEPTH) {
|
|
204
|
+
out.push(...renderType(base, reg, new Set([...seen, base]), depth + 1));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The full RETURN TYPE definition — unwraps Promise<…> and GamesApiResponse<…> so
|
|
211
|
+
// the page describes the actual payload type (e.g. GamePickGameInfo), not just the
|
|
212
|
+
// wrapper, with nested types expanded.
|
|
213
|
+
function returnsSection(ret, reg) {
|
|
214
|
+
if (!ret || ret === 'void') return '_No return value._';
|
|
215
|
+
let inner = ret.replace(/^Promise\s*<([\s\S]*)>\s*$/, '$1').trim();
|
|
216
|
+
let prefix = '';
|
|
217
|
+
const gm = inner.match(/^GamesApiResponse\s*<([\s\S]*)>\s*$/);
|
|
218
|
+
if (gm) {
|
|
219
|
+
prefix = 'Wrapped in `GamesApiResponse`: `errCode` (number — `0` = success), `errMessage?` (string), `data?` — the payload:\n\n';
|
|
220
|
+
inner = gm[1].trim();
|
|
221
|
+
}
|
|
222
|
+
const isArr = /\[\]\s*$/.test(inner) || /^Array\s*</.test(inner);
|
|
223
|
+
const base = baseTypeName(inner);
|
|
224
|
+
if (!base || !reg[base]) return prefix + `Resolves to \`${inner}\`.`;
|
|
225
|
+
const lines = [prefix + (isArr ? `Array of \`${base}\`. Each item:` : `\`${base}\`:`)];
|
|
226
|
+
lines.push(...renderType(base, reg, new Set([base]), 0));
|
|
227
|
+
return lines.join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Renamed / shorthand / guessed names the IDE agent actually searched (from a
|
|
231
|
+
// 48h live-session analysis) → added to the page's Search-terms so the search
|
|
232
|
+
// lands first-shot. doSAWAcknowledge (8 hits) was the biggest single miss.
|
|
233
|
+
const ALIASES = {
|
|
234
|
+
miniGameWinAcknowledgeRequest: ['doSAWAcknowledge', 'SAWAcknowledge', 'acknowledge', 'SAWAcknowledgeType'],
|
|
235
|
+
getTournamentsList: ['getTournaments'],
|
|
236
|
+
getTournamentInstanceInfo: ['getTournament'],
|
|
237
|
+
getLeaderBoard: ['getLeaderboard', 'leaderboard'],
|
|
238
|
+
getLeaderBoards: ['getLeaderboards'],
|
|
239
|
+
getAvatarsList: ['getAvatars'],
|
|
240
|
+
getMiniGames: ['getSAW', 'spins', 'minigames', 'wheel', 'SAWPrizeType'],
|
|
241
|
+
playMiniGame: ['doSAWSpin', 'SAWSpin', 'spin'],
|
|
242
|
+
playMiniGameBatch: ['doSAWSpinBatch'],
|
|
243
|
+
getMiniGamesHistory: ['getSAWHistory'],
|
|
244
|
+
getCurrentLevel: ['getUserLevel'],
|
|
245
|
+
getLevels: ['getUserLevels'],
|
|
246
|
+
getMissions: ['getUserMissions', 'achievements'],
|
|
247
|
+
getBadges: ['getUserBadges'],
|
|
248
|
+
getUserProfile: ['getPublicProps', 'getProfile', 'balance', 'points'],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** Enum / sub-type names referenced by a return type's fields (1 level deep) —
|
|
252
|
+
* so a search by a cross-cutting enum name (SAWAcknowledgeType, PointChangeSourceType,
|
|
253
|
+
* GamePickResolutionType, …) lands on the page that owns it. */
|
|
254
|
+
function collectFieldTypeNames(typeName, reg, seen = new Set(), depth = 0) {
|
|
255
|
+
const out = new Set();
|
|
256
|
+
if (!typeName || depth > 1) return out;
|
|
257
|
+
for (const info of Object.values(resolveFields(typeName, reg))) {
|
|
258
|
+
for (const n of (info.typeText || '').match(/[A-Z][A-Za-z0-9_]+/g) || []) {
|
|
259
|
+
if (n.length < 4 || ['Array', 'Record', 'Date', 'Partial', 'Promise', 'This', 'The', 'URL', 'JSON'].includes(n)) continue;
|
|
260
|
+
out.add(n);
|
|
261
|
+
if (reg[n] && !seen.has(n)) { seen.add(n); for (const x of collectFieldTypeNames(n, reg, seen, depth + 1)) out.add(x); }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildPage(method, reg) {
|
|
268
|
+
const { name, domain, node, sf } = method;
|
|
269
|
+
const doc = readJSDoc(node, sf);
|
|
270
|
+
const params = node.parameters.map(p => p.getText(sf)).join(', ');
|
|
271
|
+
const ret = node.type ? node.type.getText(sf) : 'void';
|
|
272
|
+
const typeNames = [...new Set((ret.match(/[A-Z][A-Za-z0-9_]+/g) || [])
|
|
273
|
+
.filter(n => !['Promise', 'Partial', 'Array', 'Record', 'GamesApiResponse'].includes(n)))];
|
|
274
|
+
const typeName = typeNames[0] || '';
|
|
275
|
+
const summary = (doc.description.split(/(?<=[.!?])\s/)[0] || doc.description || name).replace(/\s+/g, ' ').trim();
|
|
276
|
+
|
|
277
|
+
const respPath = path.join(RESP_DIR, `${name}.json`);
|
|
278
|
+
const respJson = fs.existsSync(respPath) ? JSON.parse(fs.readFileSync(respPath, 'utf8')) : null;
|
|
279
|
+
const { contract, errors } = splitRemarks(doc.remarks);
|
|
280
|
+
|
|
281
|
+
const sampleKeys = respJson ? Object.keys((Array.isArray(respJson) ? respJson[0] : respJson) || {}).slice(0, 8) : [];
|
|
282
|
+
const onUpd = /\bonUpdate\b/.test(params) ? ['onUpdate', 'subscription'] : [];
|
|
283
|
+
const fieldTypes = [...collectFieldTypeNames(typeName, reg)].slice(0, 12);
|
|
284
|
+
const searchTerms = [name, domain, ...(ALIASES[name] || []), ...typeNames, ...fieldTypes, ...onUpd, ...sampleKeys]
|
|
285
|
+
.filter((v, i, a) => v && a.indexOf(v) === i).join(', ');
|
|
286
|
+
|
|
287
|
+
const o = [];
|
|
288
|
+
o.push(`# ${name} — API${typeName ? ` (${typeName})` : ''}`, '');
|
|
289
|
+
o.push(`> ${summary}`);
|
|
290
|
+
o.push(`> Import: \`import { ${typeNames.join(', ') || '/* types */'} } from '@smartico/public-api'\``);
|
|
291
|
+
o.push(`> Search terms: ${searchTerms}`, '');
|
|
292
|
+
o.push('## Signature', '```ts', `_smartico.api.${name}(${params}): ${ret}`, '```', '');
|
|
293
|
+
o.push('## Parameters', doc.params.length
|
|
294
|
+
? doc.params.map(p => `- ${p.name ? `\`${p.name}\` — ` : ''}${p.text}`.replace(/\s+/g, ' ').trim()).join('\n')
|
|
295
|
+
: '_None._', '');
|
|
296
|
+
o.push(`## Returns${typeName ? ` — \`${ret}\`` : ''}`, returnsSection(ret, reg), '');
|
|
297
|
+
o.push('## Behavioral contract', contract || `See the [UI guide](../ui/${domain}/UIGuide_${name}.md) if present.`, '');
|
|
298
|
+
if (doc.example) o.push('## Example', doc.example.trim(), '');
|
|
299
|
+
if (respJson) o.push('### Example response (REAL shape)',
|
|
300
|
+
'> Where this real payload differs from the typed Returns above (TS interface vs raw wire), the REAL shape is the runtime truth.',
|
|
301
|
+
'```json',
|
|
302
|
+
JSON.stringify(Array.isArray(respJson) ? [trimVal(respJson[0])] : trimVal(respJson), null, 2), '```', '');
|
|
303
|
+
o.push('## Errors', errors || "See this method's TSDoc / the mutation pages for `err_code` handling.", '');
|
|
304
|
+
if (doc.links.length) o.push('## Related', doc.links.filter(l => l !== name).map(l => `- \`${l}\``).join('\n'), '');
|
|
305
|
+
return o.join('\n');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── global `_smartico.*` surface + callbacks (non-.api.* — the live-session
|
|
309
|
+
// analysis showed these were unreachable in the `api` lane and live-probed) ──
|
|
310
|
+
|
|
311
|
+
function buildGlobalSurfacePage() {
|
|
312
|
+
const file = path.join(SRC, 'SmarticoGlobal.ts');
|
|
313
|
+
if (!fs.existsSync(file)) return null;
|
|
314
|
+
const sf = parse(file);
|
|
315
|
+
let iface = null;
|
|
316
|
+
const find = (n) => { if (ts.isInterfaceDeclaration(n) && n.name.text === 'SmarticoGlobal') iface = n; ts.forEachChild(n, find); };
|
|
317
|
+
find(sf);
|
|
318
|
+
if (!iface) return null;
|
|
319
|
+
const members = iface.members.filter(m => m.name).map(m => ({
|
|
320
|
+
name: m.name.getText(sf),
|
|
321
|
+
sig: m.getText(sf).replace(/;$/, '').replace(/\s+/g, ' ').trim(),
|
|
322
|
+
one: ts.getJSDocCommentsAndTags(m).map(d => tagText(d.comment)).filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(),
|
|
323
|
+
}));
|
|
324
|
+
const extra = ['_smartico', 'global', 'window._smartico', 'SmarticoGlobal', 'visitor mode', 'callbacks', 'events',
|
|
325
|
+
'onWin', 'onReady', 'deep link', 'getWidgetParams', 'IWidgetParams', 'widget params', 'readiness', 'lifecycle', 'push'];
|
|
326
|
+
const terms = [...new Set([...members.map(m => m.name), ...extra])].join(', ');
|
|
327
|
+
const o = [];
|
|
328
|
+
o.push('# _smartico — global SDK surface (non-`.api.*`)', '');
|
|
329
|
+
o.push('> The `window._smartico` control surface: init/identify, events, deep links, widgets, visitor-mode `vapi`, push, and utilities — everything that is NOT `_smartico.api.*` (those have their own capability pages).');
|
|
330
|
+
o.push("> Import: typed via `SmarticoGlobal` from '@smartico/public-api'; at runtime the object is on `window._smartico` (no import).");
|
|
331
|
+
o.push(`> Search terms: ${terms}`, '');
|
|
332
|
+
o.push('## Readiness gate', 'Call `window._smartico?.checkSuccessfullyIdentify()` (synchronous) before any `_smartico.api.*` — `true` once init + identify completed. Or subscribe `_smartico.on(\'identify\', cb)` (see the callbacks page).', '');
|
|
333
|
+
o.push('## Methods');
|
|
334
|
+
for (const m of members) o.push(`- \`_smartico.${m.sig}\`${m.one ? ` — ${m.one}` : ''}`);
|
|
335
|
+
o.push('');
|
|
336
|
+
o.push('## Visitor mode (`vapi`)', '`_smartico.vapi(language)` returns a `WSAPI` scoped to anonymous visitors — same method shapes as `_smartico.api.*`, for unidentified users (pre-login surfaces).', '');
|
|
337
|
+
o.push('## Reading widget params', 'A widget embedded via `showWidget` / `miniGame` receives `IWidgetParams` (theme, height, inline, …). Read them from the embedding snippet / `window.__SMARTICO_LP_PARAMS__`, not from an API call.', '');
|
|
338
|
+
o.push('## Related', '- `_smartico.on — events & callbacks` (the callbacks capability page)', '- Full prose reference: the SmarticoObject doc (search source `api-raw`).');
|
|
339
|
+
return o.join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildConceptPage(srcRel, title, terms) {
|
|
343
|
+
const p = path.join(ROOT, srcRel);
|
|
344
|
+
if (!fs.existsSync(p)) return null;
|
|
345
|
+
const body = fs.readFileSync(p, 'utf8').replace(/^#\s+.*\r?\n/, '').trim();
|
|
346
|
+
return [`# ${title}`, '', `> ${title}.`, `> Search terms: ${terms}`, '', body].join('\n');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function main() {
|
|
350
|
+
if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
351
|
+
const reg = buildTypeRegistry();
|
|
352
|
+
const methods = extractMethods();
|
|
353
|
+
let written = 0, withResp = 0;
|
|
354
|
+
for (const method of methods) {
|
|
355
|
+
fs.writeFileSync(path.join(OUT_DIR, `${method.name}.md`), buildPage(method, reg));
|
|
356
|
+
written++;
|
|
357
|
+
if (fs.existsSync(path.join(RESP_DIR, `${method.name}.json`))) withResp++;
|
|
358
|
+
}
|
|
359
|
+
let extra = 0;
|
|
360
|
+
const g = buildGlobalSurfacePage();
|
|
361
|
+
if (g) { fs.writeFileSync(path.join(OUT_DIR, 'smarticoGlobal.md'), g); extra++; }
|
|
362
|
+
const cb = buildConceptPage('docs/Callbacks.md', '_smartico.on — events & callbacks',
|
|
363
|
+
'on, off, callbacks, events, subscribe, subscribeUpdate, onWin, onReady, props_change, identify, EXTERNAL_CALLBACK, lifecycle, _smartico.on');
|
|
364
|
+
if (cb) { fs.writeFileSync(path.join(OUT_DIR, 'smarticoCallbacks.md'), cb); extra++; }
|
|
365
|
+
console.log(`[gen-capabilities] wrote ${written} method pages + ${extra} global/concept pages (${withResp} with captured response); type registry: ${Object.keys(reg).length} interfaces`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* run-public-api.mjs — run arbitrary public-API code as a REAL identified user, headless.
|
|
4
|
+
*
|
|
5
|
+
* A reusable test harness for verifying SDK behaviour against a live env (e.g.
|
|
6
|
+
* a new server field that's only deployed on one env). It loads the real
|
|
7
|
+
* smartico.js tracker bundle, init + identify as the given player, then evaluates
|
|
8
|
+
* SM_CODE inside the page against `window._smartico` and prints the JSON result.
|
|
9
|
+
*
|
|
10
|
+
* Unlike the server-side `run_public_api` MCP tool (which runs the PUBLISHED SDK
|
|
11
|
+
* in visitor mode), this runs the real browser SDK as an identified user, and can
|
|
12
|
+
* reach the raw protocol via `_smartico.api.api.*` — so it surfaces server fields
|
|
13
|
+
* the published transform/types don't yet map (e.g. SAW spin-expiration).
|
|
14
|
+
*
|
|
15
|
+
* Setup (one-time): npm i -D playwright && npx playwright install chromium
|
|
16
|
+
*
|
|
17
|
+
* Run the default test (SAW spin-expiration on env8 DEMO):
|
|
18
|
+
* node scripts/run-public-api.mjs
|
|
19
|
+
*
|
|
20
|
+
* Run your own snippet (async-function body; `_smartico` + `window._smartico_user_id` in scope):
|
|
21
|
+
* SM_CODE='return await _smartico.api.getMissions();' node scripts/run-public-api.mjs
|
|
22
|
+
* SM_CODE="$(cat my-test.js)" node scripts/run-public-api.mjs
|
|
23
|
+
*
|
|
24
|
+
* Point at another env/label/user:
|
|
25
|
+
* SM_LABEL=<label-key ending in env digit> SM_BRAND=<hex> SM_USER=<ext id> \
|
|
26
|
+
* SM_SALT=<label salt | 'null'> node scripts/run-public-api.mjs
|
|
27
|
+
*/
|
|
28
|
+
import { chromium } from 'playwright';
|
|
29
|
+
import crypto from 'crypto';
|
|
30
|
+
|
|
31
|
+
// Defaults: env8 DEMO label (SpaceX brand) with a test player that HAS expirable
|
|
32
|
+
// SAW spins — running bare validates SMR-48946 (mini-game spin expiration).
|
|
33
|
+
const LABEL = process.env.SM_LABEL || 'd78934f3-1d8b-461b-9e8f-287aae43c860-8';
|
|
34
|
+
const BRAND = process.env.SM_BRAND || 'c24a284e';
|
|
35
|
+
const USER = process.env.SM_USER || 'test98681514';
|
|
36
|
+
const SALT = process.env.SM_SALT || 'null';
|
|
37
|
+
const ORIGIN = process.env.SM_ORIGIN || 'https://app.smartico.ai/';
|
|
38
|
+
const SCRIPT_URL = process.env.SM_SCRIPT_URL || 'https://libs.smartico.ai/smartico.js';
|
|
39
|
+
|
|
40
|
+
// Default snippet: dump every SAW template that has spin-expiration set for this
|
|
41
|
+
// user, via the RAW command-700 response (the published transform drops the fields).
|
|
42
|
+
const DEFAULT_CODE = `
|
|
43
|
+
const ext = window._smartico_user_id;
|
|
44
|
+
const resp = await window._smartico.api.api.sawGetTemplates(ext);
|
|
45
|
+
const list = (resp && resp.templates) || [];
|
|
46
|
+
return {
|
|
47
|
+
user_ext_id: ext,
|
|
48
|
+
total_templates: list.length,
|
|
49
|
+
templates_with_spin_expiration: list
|
|
50
|
+
.filter(t => t.earliest_expiration_dt != null || t.latest_expiration_dt != null)
|
|
51
|
+
.map(t => ({ saw_template_id: t.saw_template_id, spin_count: t.spin_count,
|
|
52
|
+
earliest_expiration_dt: t.earliest_expiration_dt, latest_expiration_dt: t.latest_expiration_dt })),
|
|
53
|
+
};
|
|
54
|
+
`;
|
|
55
|
+
const CODE = process.env.SM_CODE || DEFAULT_CODE;
|
|
56
|
+
|
|
57
|
+
function userHash(user, salt) {
|
|
58
|
+
const ts = Math.floor(Date.now() / 1000) * 1000 + 24 * 3600 * 1000;
|
|
59
|
+
return crypto.createHash('md5').update(`${user.toLowerCase()}:${salt}:${ts}`.toLowerCase()).digest('hex') + ':' + ts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
(async () => {
|
|
63
|
+
const hash = userHash(USER, SALT);
|
|
64
|
+
const browser = await chromium.launch({ headless: true });
|
|
65
|
+
const ctx = await browser.newContext({
|
|
66
|
+
userAgent: process.env.SM_UA || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
|
67
|
+
viewport: { width: 1280, height: 800 }, locale: 'en-US',
|
|
68
|
+
});
|
|
69
|
+
const page = await ctx.newPage();
|
|
70
|
+
page.on('console', m => { if (m.type() === 'error') console.error(' [page error]', m.text().slice(0, 200)); });
|
|
71
|
+
|
|
72
|
+
// Self-served CSP-free origin (libs.smartico.ai 403s headless + CSP-blocks injection).
|
|
73
|
+
await page.route(ORIGIN + '**', r => r.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><html><head></head><body></body></html>' }));
|
|
74
|
+
await page.goto(ORIGIN, { waitUntil: 'domcontentloaded' }).catch(() => {});
|
|
75
|
+
|
|
76
|
+
await page.evaluate((u) => new Promise((res) => {
|
|
77
|
+
const s = document.createElement('script'); s.src = u; s.onload = () => res('loaded'); s.onerror = () => res('error'); document.head.appendChild(s);
|
|
78
|
+
}), SCRIPT_URL);
|
|
79
|
+
await page.waitForFunction(() => typeof window._smartico !== 'undefined', { timeout: 15000 });
|
|
80
|
+
|
|
81
|
+
await page.evaluate(({ LABEL, BRAND, USER, hash }) => {
|
|
82
|
+
window._smartico_user_id = USER;
|
|
83
|
+
window._smartico.init(LABEL, { brand_key: BRAND });
|
|
84
|
+
window._smartico.identify(USER, hash, {});
|
|
85
|
+
}, { LABEL, BRAND, USER, hash });
|
|
86
|
+
|
|
87
|
+
const ok = await page.waitForFunction(() => window._smartico?.checkSuccessfullyIdentify?.() === true, { timeout: 40000 }).then(() => true).catch(() => false);
|
|
88
|
+
if (!ok) { console.error(`[run] identify failed for ${USER} on ${LABEL} (check SM_SALT / allowed origin).`); await browser.close(); process.exit(3); }
|
|
89
|
+
console.error(`[run] identified ${USER} on ${LABEL} ✓`);
|
|
90
|
+
|
|
91
|
+
// Execute the operator-supplied snippet against window._smartico. SM_CODE is
|
|
92
|
+
// TRUSTED — the same trust model as `node -e`, `npx`, or the run_public_api MCP
|
|
93
|
+
// tool: it is the test author's own code, not external/untrusted input. There is
|
|
94
|
+
// no privilege boundary here (a "malicious SM_CODE" is the operator attacking
|
|
95
|
+
// their own dev machine). Do NOT feed untrusted/remote strings into SM_CODE.
|
|
96
|
+
const result = await page.evaluate(async (code) => {
|
|
97
|
+
const _smartico = window._smartico; // in scope for the snippet
|
|
98
|
+
try { return { ok: true, value: await (new Function('_smartico', `return (async () => { ${code} })()`))(_smartico) }; }
|
|
99
|
+
catch (e) { return { ok: false, error: String(e && e.message || e) }; }
|
|
100
|
+
}, CODE);
|
|
101
|
+
|
|
102
|
+
console.log(JSON.stringify(result.ok ? result.value : { ERROR: result.error }, null, 2));
|
|
103
|
+
await browser.close();
|
|
104
|
+
process.exit(result.ok ? 0 : 1);
|
|
105
|
+
})().catch(e => { console.error('[run] fatal:', e.message); process.exit(1); });
|
|
@@ -44,6 +44,8 @@ export const SAWTemplatesTransform = (items: SAWTemplate[]): TMiniGameTemplate[]
|
|
|
44
44
|
activeFromDate: r.activeFromDate,
|
|
45
45
|
activeTillDate: r.activeTillDate,
|
|
46
46
|
next_available_spin_ts: r.next_available_spin_ts,
|
|
47
|
+
earliest_expiration_dt: r.earliest_expiration_dt,
|
|
48
|
+
latest_expiration_dt: r.latest_expiration_dt,
|
|
47
49
|
steps_to_finish_game: r.saw_template_ui_definition.steps_to_finish_game,
|
|
48
50
|
custom_section_id: r.saw_template_ui_definition.custom_section_id,
|
|
49
51
|
saw_template_ui_definition: r.saw_template_ui_definition,
|
|
@@ -31,6 +31,11 @@ export interface SAWTemplate {
|
|
|
31
31
|
|
|
32
32
|
next_available_spin_ts?: number;
|
|
33
33
|
|
|
34
|
+
/** Soonest-expiring spin's expiration time for this user (epoch ms); `null`/absent when no expirable spins. */
|
|
35
|
+
earliest_expiration_dt?: number | null;
|
|
36
|
+
/** Latest-expiring spin's expiration time for this user (epoch ms); `null`/absent when no expirable spins. */
|
|
37
|
+
latest_expiration_dt?: number | null;
|
|
38
|
+
|
|
34
39
|
saw_skin_key: string;
|
|
35
40
|
saw_skin_ui_definition: {
|
|
36
41
|
skin_folder: string;
|
package/src/NodeCache.ts
CHANGED
|
@@ -39,6 +39,14 @@ class NodeCache {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
public removeByPrefix(prefix: string) {
|
|
43
|
+
for (const key in this.cache) {
|
|
44
|
+
if (this.cache.hasOwnProperty(key) && key.indexOf(prefix) === 0) {
|
|
45
|
+
delete this.cache[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
public flushAll() {
|
|
43
51
|
this.cache = {};
|
|
44
52
|
if (this.ttlChecker) {
|
package/src/OCache.ts
CHANGED
|
@@ -56,6 +56,23 @@ export class OCache {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Remove every entry whose string key starts with `oKeyBase` — the bare key
|
|
61
|
+
* AND all per-parameter composite variants (`${oKeyBase}:…` / `${oKeyBase}<id>`).
|
|
62
|
+
* Use when a cache is keyed per parameter (paginated, per-template, per-window):
|
|
63
|
+
* an exact-key `clear(oKeyBase)` would miss every variant. `oKeyBase` must be a string.
|
|
64
|
+
*/
|
|
65
|
+
public static async clearByPrefix(cacheContext: ECacheContext, oKeyBase: string) {
|
|
66
|
+
// `set`/`get` build keys as `<context>_<JSON.stringify(oKey)>`; JSON.stringify
|
|
67
|
+
// wraps a string in quotes, so dropping the trailing quote yields a prefix that
|
|
68
|
+
// matches both the bare `"base"` key and every `"base…"` composite key.
|
|
69
|
+
const prefix = cacheContext.toString() + '_' + JSON.stringify(oKeyBase).slice(0, -1);
|
|
70
|
+
|
|
71
|
+
if (this.cache[cacheContext]) {
|
|
72
|
+
this.cache[cacheContext].removeByPrefix(prefix);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
public static async clearContext(cacheContext: ECacheContext) {
|
|
60
77
|
if (this.cache[cacheContext]) {
|
|
61
78
|
this.cache[cacheContext].flushAll();
|
package/src/SmarticoGlobal.ts
CHANGED
|
@@ -192,13 +192,6 @@ export interface SmarticoGlobal {
|
|
|
192
192
|
|
|
193
193
|
declare global {
|
|
194
194
|
interface Window {
|
|
195
|
-
/**
|
|
196
|
-
* Params injected by the Smartico widget runtime (`widget.js`) when a widget
|
|
197
|
-
* is embedded via `_smartico.showWidget` / a widget script tag. Keys mirror
|
|
198
|
-
* the params passed in the embedding snippet (e.g. `id`).
|
|
199
|
-
*/
|
|
200
|
-
__SMARTICO_LP_PARAMS__?: Record<string, string | number>;
|
|
201
|
-
|
|
202
195
|
/**
|
|
203
196
|
* The global Smartico SDK object, present once the embed snippet has loaded.
|
|
204
197
|
* Optional because it is `undefined` until the script initializes — guard
|
package/src/WSAPI/WSAPI.ts
CHANGED
|
@@ -34,11 +34,11 @@ export class WSAPI extends WSAPIInbox {
|
|
|
34
34
|
on(ClassId.SAW_SHOW_SPIN_PUSH, () => this.reloadMiniGameTemplate());
|
|
35
35
|
on(ClassId.SAW_AKNOWLEDGE_RESPONSE, () => {
|
|
36
36
|
this.reloadMiniGameTemplate();
|
|
37
|
-
OCache.
|
|
37
|
+
OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
|
|
38
38
|
});
|
|
39
39
|
on(ClassId.SAW_DO_SPIN_RESPONSE, () => {
|
|
40
40
|
this.reloadMiniGameTemplate();
|
|
41
|
-
OCache.
|
|
41
|
+
OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
|
|
42
42
|
});
|
|
43
43
|
on(ClassId.MISSION_OPTIN_RESPONSE, () => this.updateMissions());
|
|
44
44
|
on(ClassId.ACHIEVEMENT_CLAIM_PRIZE_RESPONSE, () => this.updateMissions());
|
|
@@ -57,7 +57,7 @@ export class WSAPI extends WSAPIInbox {
|
|
|
57
57
|
on(ClassId.CLAIM_BONUS_RESPONSE, () => this.updateBonuses());
|
|
58
58
|
on(ClassId.SAW_DO_SPIN_BATCH_RESPONSE, () => {
|
|
59
59
|
this.reloadMiniGameTemplate();
|
|
60
|
-
OCache.
|
|
60
|
+
OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.SAWHistory);
|
|
61
61
|
});
|
|
62
62
|
on(ClassId.RAF_CLAIM_PRIZE_RESPONSE, () => this.updateRaffles());
|
|
63
63
|
on(ClassId.GET_INBOX_MESSAGES_RESPONSE, (res) => {
|
|
@@ -25,7 +25,10 @@ export class WSAPIJackpots extends WSAPIClans {
|
|
|
25
25
|
protected async jackpotClearCache() {
|
|
26
26
|
OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.Jackpots);
|
|
27
27
|
OCache.clear(ECacheContext.WSAPI, onUpdateContextKey.Pots);
|
|
28
|
-
|
|
28
|
+
// Winners + eligible-games are cached per jp_template_id (composite keys),
|
|
29
|
+
// so a bare clear misses every variant — clear by prefix.
|
|
30
|
+
OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.JackpotWinners);
|
|
31
|
+
OCache.clearByPrefix(ECacheContext.WSAPI, onUpdateContextKey.JackpotEligibleGames);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -198,7 +198,7 @@ export class WSAPIMiniGames extends WSAPIRaffles {
|
|
|
198
198
|
saw_template_id?: number;
|
|
199
199
|
}): Promise<TSawHistory[]> {
|
|
200
200
|
return OCache.use(
|
|
201
|
-
onUpdateContextKey.SAWHistory
|
|
201
|
+
`${onUpdateContextKey.SAWHistory}:${saw_template_id ?? ''}:${limit ?? ''}:${offset ?? ''}`,
|
|
202
202
|
ECacheContext.WSAPI,
|
|
203
203
|
() => this.api.getSawWinningHistoryT(this.userExtId, limit, offset, saw_template_id),
|
|
204
204
|
CACHE_DATA_SEC,
|
package/src/WSAPI/WSAPITypes.ts
CHANGED
|
@@ -171,6 +171,16 @@ export interface TMiniGameTemplate {
|
|
|
171
171
|
* */
|
|
172
172
|
next_available_spin_ts: number;
|
|
173
173
|
|
|
174
|
+
/** Soonest-expiring spin's expiration time for the current user, as an epoch-ms timestamp.
|
|
175
|
+
* `null` when the user has no expirable spins for this template — spins only expire when the
|
|
176
|
+
* template defines a spin-expiration rule (Wheel of Fortune, Loot Boxes, etc.). Pair with
|
|
177
|
+
* `latest_expiration_dt` to render a "spins expire between X and Y" window. */
|
|
178
|
+
earliest_expiration_dt?: number | null;
|
|
179
|
+
/** Latest-expiring spin's expiration time for the current user, as an epoch-ms timestamp.
|
|
180
|
+
* `null` when the user has no expirable spins; equals `earliest_expiration_dt` when a single
|
|
181
|
+
* expiration applies. */
|
|
182
|
+
latest_expiration_dt?: number | null;
|
|
183
|
+
|
|
174
184
|
/** The message that should be shown to the user when he cannot play the game, server rejected attempt with error code SAWSpinErrorCode.SAW_FAILED_MAX_SPINS_REACHED */
|
|
175
185
|
over_limit_message: string;
|
|
176
186
|
/** The message that should be shown to the user when he cannot play the game because he doesn't have spin attempts or points. */
|
|
@@ -261,6 +271,40 @@ export interface TUserProfile {
|
|
|
261
271
|
/** AI-recommended casino bet amount for this user. Undefined when no
|
|
262
272
|
* recommendation is currently available. */
|
|
263
273
|
core_recommended_casino_bet_amount?: number;
|
|
274
|
+
/** Display name of the user's current level (e.g. `"Silver"`); resolve the id via `getCurrentLevel()`. */
|
|
275
|
+
ach_level_current?: string;
|
|
276
|
+
/** `true` when the user is in the gamification A/B control group (gamification UI suppressed). */
|
|
277
|
+
ach_gamification_in_control_group?: boolean;
|
|
278
|
+
/** Smartico-internal numeric user id. */
|
|
279
|
+
user_id?: number;
|
|
280
|
+
/** ISO country code of the user (e.g. `"BG"`). */
|
|
281
|
+
user_country?: string;
|
|
282
|
+
/** Wallet currency code (e.g. `"EUR"`). */
|
|
283
|
+
core_wallet_currency?: string;
|
|
284
|
+
/** Registration timestamp (epoch ms); `0` when unknown. */
|
|
285
|
+
core_registration_date?: number;
|
|
286
|
+
/** Last-session browser push-permission state (e.g. `"BLOCKED"`, `"GRANTED"`). */
|
|
287
|
+
user_last_session_push_state?: string;
|
|
288
|
+
/** `true` when the account is flagged as a bonus abuser. */
|
|
289
|
+
acc_bonus_abuser?: boolean;
|
|
290
|
+
/** Selected avatar id (catalogue avatar or AI-variant base). */
|
|
291
|
+
avatar_id?: string;
|
|
292
|
+
/** `avatar_real_id` of the selected avatar; `0` when none. */
|
|
293
|
+
avatar_real_id?: number;
|
|
294
|
+
/** `avatar_real_id` of the user's core avatar; null when unset. */
|
|
295
|
+
core_avatar_real_id?: number | null;
|
|
296
|
+
/** Current clan id (string); empty/null when not in a clan. */
|
|
297
|
+
core_clan_id?: string;
|
|
298
|
+
/** `true` when the user was kicked from their clan; null when not applicable. */
|
|
299
|
+
core_clan_is_kicked?: boolean | null;
|
|
300
|
+
/** Id of the clan the user was kicked from; null when not applicable. */
|
|
301
|
+
core_clan_kicked_out_id?: number | null;
|
|
302
|
+
/** ext_user_id of the friend who referred this user; null when none. */
|
|
303
|
+
aff_referred_by_friend_ext_user_id?: string | null;
|
|
304
|
+
/** Refer-a-friend share URL; null when the feature is disabled. */
|
|
305
|
+
aff_refer_friend_url?: string | null;
|
|
306
|
+
/** Count of friends this user has referred. */
|
|
307
|
+
aff_refered_friends_count?: number;
|
|
264
308
|
}
|
|
265
309
|
|
|
266
310
|
/**
|