@genex-ai/embed-sdk 0.3.2 → 0.4.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/dist/{chunk-A2RFRR7H.js → chunk-6E5DPEVG.js} +147 -0
- package/dist/index.d.ts +110 -1
- package/dist/index.js +13 -1
- package/dist/sentry.js +1 -1
- package/package.json +11 -10
|
@@ -22,6 +22,9 @@ var authWaiters = [];
|
|
|
22
22
|
var playerWaiters = [];
|
|
23
23
|
var overlayEl = null;
|
|
24
24
|
var popoverEl = null;
|
|
25
|
+
var pendingPlayerSave;
|
|
26
|
+
var hasPendingPlayerSave = false;
|
|
27
|
+
var pendingScores = /* @__PURE__ */ new Map();
|
|
25
28
|
function win() {
|
|
26
29
|
return globalThis.window;
|
|
27
30
|
}
|
|
@@ -129,6 +132,140 @@ function on(event, cb) {
|
|
|
129
132
|
set.delete(cb);
|
|
130
133
|
};
|
|
131
134
|
}
|
|
135
|
+
var KEEPALIVE_MAX_BYTES = 32 * 1024;
|
|
136
|
+
async function ensurePlayer() {
|
|
137
|
+
if (!config) throw new Error("genex embed: call initEmbed() first");
|
|
138
|
+
if (state === "pending") await waitForPlayer();
|
|
139
|
+
if (state === "blocked") throw new Error("genex embed auth: blocked");
|
|
140
|
+
}
|
|
141
|
+
function stateFetch(path, init = {}) {
|
|
142
|
+
const headers = { Authorization: `Bearer ${embedToken}` };
|
|
143
|
+
if (init.body !== void 0) headers["Content-Type"] = "application/json";
|
|
144
|
+
if (init.ifVersion !== void 0) headers["If-Match"] = String(init.ifVersion);
|
|
145
|
+
return doFetch(`${config.apiUrl}/api/projects/${encodeURIComponent(config.slug)}${path}`, {
|
|
146
|
+
method: init.method ?? "GET",
|
|
147
|
+
headers,
|
|
148
|
+
body: init.body,
|
|
149
|
+
keepalive: init.body !== void 0 && init.body.length <= KEEPALIVE_MAX_BYTES ? true : void 0
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function decodeSave(res) {
|
|
153
|
+
if (res.status === 409) {
|
|
154
|
+
const body2 = await res.json().catch(() => ({}));
|
|
155
|
+
return { saved: false, conflict: true, version: body2.version };
|
|
156
|
+
}
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
emit("error", { error: new Error(`genex state save failed (${res.status})`) });
|
|
159
|
+
return { saved: false };
|
|
160
|
+
}
|
|
161
|
+
const body = await res.json();
|
|
162
|
+
return { saved: true, version: body.version };
|
|
163
|
+
}
|
|
164
|
+
async function loadPlayerState() {
|
|
165
|
+
await ensurePlayer();
|
|
166
|
+
if (state === "guest") {
|
|
167
|
+
return { data: hasPendingPlayerSave ? pendingPlayerSave : null, version: 0, guest: true };
|
|
168
|
+
}
|
|
169
|
+
const res = await stateFetch("/state/me");
|
|
170
|
+
if (!res.ok) throw new Error(`genex load player state failed (${res.status})`);
|
|
171
|
+
const body = await res.json();
|
|
172
|
+
return { data: body.data, version: body.version };
|
|
173
|
+
}
|
|
174
|
+
async function savePlayerState(data, opts) {
|
|
175
|
+
await ensurePlayer();
|
|
176
|
+
if (state === "guest") {
|
|
177
|
+
pendingPlayerSave = data;
|
|
178
|
+
hasPendingPlayerSave = true;
|
|
179
|
+
return { saved: false, guest: true, queued: true };
|
|
180
|
+
}
|
|
181
|
+
return decodeSave(
|
|
182
|
+
await stateFetch("/state/me", {
|
|
183
|
+
method: "PUT",
|
|
184
|
+
body: JSON.stringify(data),
|
|
185
|
+
ifVersion: opts?.ifVersion
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
async function loadWorldState() {
|
|
190
|
+
await ensurePlayer();
|
|
191
|
+
if (state === "guest") return { data: null, version: 0, guest: true };
|
|
192
|
+
const res = await stateFetch("/state");
|
|
193
|
+
if (!res.ok) throw new Error(`genex load world state failed (${res.status})`);
|
|
194
|
+
const body = await res.json();
|
|
195
|
+
return { data: body.data, version: body.version };
|
|
196
|
+
}
|
|
197
|
+
async function saveWorldState(data, opts) {
|
|
198
|
+
await ensurePlayer();
|
|
199
|
+
if (state === "guest") return { saved: false, guest: true };
|
|
200
|
+
return decodeSave(
|
|
201
|
+
await stateFetch("/state", {
|
|
202
|
+
method: "PUT",
|
|
203
|
+
body: JSON.stringify(data),
|
|
204
|
+
ifVersion: opts?.ifVersion
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
async function submitScore(score, opts) {
|
|
209
|
+
const board = opts?.board ?? "main";
|
|
210
|
+
const mode = opts?.mode ?? "max";
|
|
211
|
+
await ensurePlayer();
|
|
212
|
+
if (state === "guest") {
|
|
213
|
+
const prev = pendingScores.get(board);
|
|
214
|
+
if (!prev || (mode === "min" ? score < prev.score : score > prev.score)) {
|
|
215
|
+
pendingScores.set(board, { score, mode });
|
|
216
|
+
}
|
|
217
|
+
return { submitted: false, guest: true, queued: true };
|
|
218
|
+
}
|
|
219
|
+
const res = await stateFetch("/scores", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
body: JSON.stringify({ score, board, mode })
|
|
222
|
+
});
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
emit("error", { error: new Error(`genex score submit failed (${res.status})`) });
|
|
225
|
+
return { submitted: false };
|
|
226
|
+
}
|
|
227
|
+
const body = await res.json();
|
|
228
|
+
return { submitted: true, best: body.best, improved: body.improved };
|
|
229
|
+
}
|
|
230
|
+
async function getLeaderboard(opts) {
|
|
231
|
+
if (!config) throw new Error("genex embed: call initEmbed() first");
|
|
232
|
+
if (state === "pending") {
|
|
233
|
+
try {
|
|
234
|
+
await waitForPlayer();
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const params = new URLSearchParams();
|
|
239
|
+
if (opts?.board !== void 0) params.set("board", opts.board);
|
|
240
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
241
|
+
if (opts?.order !== void 0) params.set("order", opts.order);
|
|
242
|
+
const qs = params.toString();
|
|
243
|
+
const headers = {};
|
|
244
|
+
if (embedToken) headers.Authorization = `Bearer ${embedToken}`;
|
|
245
|
+
const res = await doFetch(
|
|
246
|
+
`${config.apiUrl}/api/projects/${encodeURIComponent(config.slug)}/leaderboard${qs ? `?${qs}` : ""}`,
|
|
247
|
+
{ headers }
|
|
248
|
+
);
|
|
249
|
+
if (!res.ok) throw new Error(`genex leaderboard failed (${res.status})`);
|
|
250
|
+
return await res.json();
|
|
251
|
+
}
|
|
252
|
+
async function flushGuestQueue() {
|
|
253
|
+
try {
|
|
254
|
+
if (hasPendingPlayerSave) {
|
|
255
|
+
const save = pendingPlayerSave;
|
|
256
|
+
pendingPlayerSave = void 0;
|
|
257
|
+
hasPendingPlayerSave = false;
|
|
258
|
+
await savePlayerState(save);
|
|
259
|
+
}
|
|
260
|
+
const queued = [...pendingScores.entries()];
|
|
261
|
+
pendingScores.clear();
|
|
262
|
+
for (const [board, { score, mode }] of queued) {
|
|
263
|
+
await submitScore(score, { board, mode });
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
emit("error", { error });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
132
269
|
function emit(event, ctx) {
|
|
133
270
|
const set = listeners.get(event);
|
|
134
271
|
if (!set) return;
|
|
@@ -293,6 +430,7 @@ async function redeemTicket(ticket) {
|
|
|
293
430
|
if (isEmbedded() && parentOrigin) {
|
|
294
431
|
postToParent({ type: "genex:embed:authenticated", v: PROTOCOL_VERSION }, parentOrigin);
|
|
295
432
|
}
|
|
433
|
+
if (upgradingFromGuest) await flushGuestQueue();
|
|
296
434
|
const ctx = { user };
|
|
297
435
|
for (const waiter of authWaiters.splice(0)) waiter.resolve(ctx);
|
|
298
436
|
for (const waiter of playerWaiters.splice(0)) waiter.resolve({ user, guest: false });
|
|
@@ -550,6 +688,9 @@ function __resetForTests(overrides) {
|
|
|
550
688
|
authWaiters = [];
|
|
551
689
|
playerWaiters = [];
|
|
552
690
|
stashedTicketHash = null;
|
|
691
|
+
pendingPlayerSave = void 0;
|
|
692
|
+
hasPendingPlayerSave = false;
|
|
693
|
+
pendingScores.clear();
|
|
553
694
|
handshakeTimeoutMs = overrides?.handshakeTimeoutMs ?? 1e4;
|
|
554
695
|
refreshDelayMs = overrides?.refreshDelayMs ?? 10 * 6e4;
|
|
555
696
|
refreshRetryMs = overrides?.refreshRetryMs ?? 6e4;
|
|
@@ -565,6 +706,12 @@ export {
|
|
|
565
706
|
waitForAuth,
|
|
566
707
|
waitForPlayer,
|
|
567
708
|
on,
|
|
709
|
+
loadPlayerState,
|
|
710
|
+
savePlayerState,
|
|
711
|
+
loadWorldState,
|
|
712
|
+
saveWorldState,
|
|
713
|
+
submitScore,
|
|
714
|
+
getLeaderboard,
|
|
568
715
|
_stashTicketFromUrl,
|
|
569
716
|
__resetForTests
|
|
570
717
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,56 @@ interface EmbedUser {
|
|
|
17
17
|
}
|
|
18
18
|
type AuthState = 'pending' | 'authenticated' | 'guest' | 'blocked';
|
|
19
19
|
type EmbedEvent = 'authenticated' | 'guest' | 'blocked' | 'error';
|
|
20
|
+
interface PlayerStateResult {
|
|
21
|
+
/** The saved blob, or null when this player never saved. For guests: the queued pending save, if any. */
|
|
22
|
+
data: unknown;
|
|
23
|
+
/** Optimistic-concurrency version — pass to savePlayerState({ ifVersion }) to detect races. 0 = never saved. */
|
|
24
|
+
version: number;
|
|
25
|
+
/** True when the current identity is a guest (server storage untouched). */
|
|
26
|
+
guest?: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface WorldStateResult {
|
|
29
|
+
data: unknown;
|
|
30
|
+
version: number;
|
|
31
|
+
guest?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface SaveStateResult {
|
|
34
|
+
/** True when the write landed on the server. */
|
|
35
|
+
saved: boolean;
|
|
36
|
+
/** The row's version after a successful write — or the CURRENT server version on a conflict. */
|
|
37
|
+
version?: number;
|
|
38
|
+
/** True when If-Match lost the race (reload, merge, retry with the fresh version). */
|
|
39
|
+
conflict?: boolean;
|
|
40
|
+
/** True when the caller is a guest — accounts save, guests don't. */
|
|
41
|
+
guest?: boolean;
|
|
42
|
+
/** True when the value was queued to auto-flush if this guest signs in mid-game. */
|
|
43
|
+
queued?: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface SubmitScoreResult {
|
|
46
|
+
submitted: boolean;
|
|
47
|
+
/** The standing best after this submit (may be an older, better score). */
|
|
48
|
+
best?: number;
|
|
49
|
+
/** True when this submit improved the player's best. */
|
|
50
|
+
improved?: boolean;
|
|
51
|
+
guest?: boolean;
|
|
52
|
+
/** True when the score was queued to auto-submit if this guest signs in mid-game. */
|
|
53
|
+
queued?: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface LeaderboardEntry {
|
|
56
|
+
userId: string;
|
|
57
|
+
name: string;
|
|
58
|
+
image?: string | null;
|
|
59
|
+
score: number;
|
|
60
|
+
updatedAt: string;
|
|
61
|
+
}
|
|
62
|
+
interface Leaderboard {
|
|
63
|
+
items: LeaderboardEntry[];
|
|
64
|
+
/** This player's rank + best — null for guests, tokenless reads, and players with no score. */
|
|
65
|
+
me: {
|
|
66
|
+
rank: number;
|
|
67
|
+
score: number;
|
|
68
|
+
} | null;
|
|
69
|
+
}
|
|
20
70
|
interface EventContext {
|
|
21
71
|
user?: EmbedUser;
|
|
22
72
|
error?: unknown;
|
|
@@ -92,6 +142,65 @@ declare function waitForPlayer(): Promise<{
|
|
|
92
142
|
}>;
|
|
93
143
|
/** Subscribe to auth lifecycle events. Returns an unsubscribe function. */
|
|
94
144
|
declare function on(event: EmbedEvent, cb: (ctx?: EventContext) => void): () => void;
|
|
145
|
+
/**
|
|
146
|
+
* Load THIS player's own save slot (per-player, per-game — another player can
|
|
147
|
+
* never read or clobber it). Resolves { data: null, version: 0 } when they
|
|
148
|
+
* never saved. Guests get their queued pending save (if any) so a round-trip
|
|
149
|
+
* works mid-session; the server is untouched. Await-safe from boot: resolves
|
|
150
|
+
* after identity exists, rejects only when the session is blocked.
|
|
151
|
+
*/
|
|
152
|
+
declare function loadPlayerState(): Promise<PlayerStateResult>;
|
|
153
|
+
/**
|
|
154
|
+
* Save THIS player's own slot (any JSON ≤ 256KB; last-write-wins on their own
|
|
155
|
+
* row). Guests: the value is QUEUED in memory and auto-flushed if they sign in
|
|
156
|
+
* mid-game ({ saved: false, guest: true, queued: true }) — no extra game code.
|
|
157
|
+
* Pass { ifVersion } (from the last load/save) to detect same-account
|
|
158
|
+
* two-device races: a losing write resolves { conflict: true, version } —
|
|
159
|
+
* reload, merge, retry. Small saves survive tab close (fetch keepalive).
|
|
160
|
+
*/
|
|
161
|
+
declare function savePlayerState(data: unknown, opts?: {
|
|
162
|
+
ifVersion?: number;
|
|
163
|
+
}): Promise<SaveStateResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Load the game's SHARED world slot (one blob per game — every player reads
|
|
166
|
+
* the same world). Guests resolve { data: null, guest: true } — the server
|
|
167
|
+
* refuses guest reads; in multiplayer, guests receive the live world through
|
|
168
|
+
* the room instead.
|
|
169
|
+
*/
|
|
170
|
+
declare function loadWorldState(): Promise<WorldStateResult>;
|
|
171
|
+
/**
|
|
172
|
+
* Save the game's SHARED world slot (any JSON ≤ 1MB, any signed-in player may
|
|
173
|
+
* write — let ONE authority do it, e.g. the multiplayer host, and debounce).
|
|
174
|
+
* Strongly prefer { ifVersion } here: the slot is shared, so unconditional
|
|
175
|
+
* writes silently lose races. Guests: { saved: false, guest: true } — world
|
|
176
|
+
* saves are NEVER queued (flushing a stale world later would clobber the live
|
|
177
|
+
* one; the eventual account host keeps saving instead).
|
|
178
|
+
*/
|
|
179
|
+
declare function saveWorldState(data: unknown, opts?: {
|
|
180
|
+
ifVersion?: number;
|
|
181
|
+
}): Promise<SaveStateResult>;
|
|
182
|
+
/**
|
|
183
|
+
* Submit a score (soft-trust leaderboards: identity is verified server-side,
|
|
184
|
+
* one row per real player per board). Keep-best: only a strictly better score
|
|
185
|
+
* moves the row — `mode: "min"` for lap-time-style boards (send a consistent
|
|
186
|
+
* mode per board). Guests: the BEST value per board is queued and
|
|
187
|
+
* auto-submitted if they sign in mid-game.
|
|
188
|
+
*/
|
|
189
|
+
declare function submitScore(score: number, opts?: {
|
|
190
|
+
board?: string;
|
|
191
|
+
mode?: 'max' | 'min';
|
|
192
|
+
}): Promise<SubmitScoreResult>;
|
|
193
|
+
/**
|
|
194
|
+
* Read a leaderboard: top entries (verified display names) + this player's
|
|
195
|
+
* own rank when signed in. Works for EVERYONE on published games — guests and
|
|
196
|
+
* even tokenless contexts read the public board (me: null). limit ≤ 100;
|
|
197
|
+
* order "asc" for lower-is-better boards.
|
|
198
|
+
*/
|
|
199
|
+
declare function getLeaderboard(opts?: {
|
|
200
|
+
board?: string;
|
|
201
|
+
limit?: number;
|
|
202
|
+
order?: 'desc' | 'asc';
|
|
203
|
+
}): Promise<Leaderboard>;
|
|
95
204
|
/**
|
|
96
205
|
* @internal — for ./sentry.ts ONLY, not public API (games never call this).
|
|
97
206
|
* If the URL fragment carries any genex marker (`genex_ticket` — a secret —
|
|
@@ -105,4 +214,4 @@ declare function __resetForTests(overrides?: {
|
|
|
105
214
|
refreshRetryMs?: number;
|
|
106
215
|
}): void;
|
|
107
216
|
|
|
108
|
-
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getEmbedToken, getUser, initEmbed, isEmbedded, on, waitForAuth, waitForPlayer };
|
|
217
|
+
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, type Leaderboard, type LeaderboardEntry, type PlayerStateResult, type SaveStateResult, type SubmitScoreResult, type WorldStateResult, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getEmbedToken, getLeaderboard, getUser, initEmbed, isEmbedded, loadPlayerState, loadWorldState, on, savePlayerState, saveWorldState, submitScore, waitForAuth, waitForPlayer };
|
package/dist/index.js
CHANGED
|
@@ -4,23 +4,35 @@ import {
|
|
|
4
4
|
getAuthState,
|
|
5
5
|
getColyseusAuth,
|
|
6
6
|
getEmbedToken,
|
|
7
|
+
getLeaderboard,
|
|
7
8
|
getUser,
|
|
8
9
|
initEmbed,
|
|
9
10
|
isEmbedded,
|
|
11
|
+
loadPlayerState,
|
|
12
|
+
loadWorldState,
|
|
10
13
|
on,
|
|
14
|
+
savePlayerState,
|
|
15
|
+
saveWorldState,
|
|
16
|
+
submitScore,
|
|
11
17
|
waitForAuth,
|
|
12
18
|
waitForPlayer
|
|
13
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-6E5DPEVG.js";
|
|
14
20
|
export {
|
|
15
21
|
__resetForTests,
|
|
16
22
|
_stashTicketFromUrl,
|
|
17
23
|
getAuthState,
|
|
18
24
|
getColyseusAuth,
|
|
19
25
|
getEmbedToken,
|
|
26
|
+
getLeaderboard,
|
|
20
27
|
getUser,
|
|
21
28
|
initEmbed,
|
|
22
29
|
isEmbedded,
|
|
30
|
+
loadPlayerState,
|
|
31
|
+
loadWorldState,
|
|
23
32
|
on,
|
|
33
|
+
savePlayerState,
|
|
34
|
+
saveWorldState,
|
|
35
|
+
submitScore,
|
|
24
36
|
waitForAuth,
|
|
25
37
|
waitForPlayer
|
|
26
38
|
};
|
package/dist/sentry.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/embed-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Player identity for genex games — signed-in
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Player identity + durable game state for genex games — signed-in or guest play, per-player save slots, shared world state, and soft-trust leaderboards.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "node --test test/*.test.ts",
|
|
27
|
+
"check": "pnpm build && publint && attw --pack . --profile esm-only",
|
|
28
|
+
"prepack": "pnpm build && publint"
|
|
29
|
+
},
|
|
23
30
|
"publishConfig": {
|
|
24
31
|
"access": "public"
|
|
25
32
|
},
|
|
@@ -42,11 +49,5 @@
|
|
|
42
49
|
"url": "git+https://github.com/me-ai-org/genex-demo.git",
|
|
43
50
|
"directory": "packages/embed-sdk"
|
|
44
51
|
},
|
|
45
|
-
"license": "MIT"
|
|
46
|
-
|
|
47
|
-
"build": "tsup",
|
|
48
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
-
"test": "node --test test/*.test.ts",
|
|
50
|
-
"check": "pnpm build && publint && attw --pack . --profile esm-only"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|