@genex-ai/embed-sdk 0.3.2 → 0.4.1
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-LWEYUUBS.js} +152 -0
- package/dist/index.d.ts +113 -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,145 @@ 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
|
+
var lastWorldVersion;
|
|
142
|
+
function stateFetch(path, init = {}) {
|
|
143
|
+
const headers = { Authorization: `Bearer ${embedToken}` };
|
|
144
|
+
if (init.body !== void 0) headers["Content-Type"] = "application/json";
|
|
145
|
+
if (init.ifVersion !== void 0) headers["If-Match"] = String(init.ifVersion);
|
|
146
|
+
return doFetch(`${config.apiUrl}/api/projects/${encodeURIComponent(config.slug)}${path}`, {
|
|
147
|
+
method: init.method ?? "GET",
|
|
148
|
+
headers,
|
|
149
|
+
body: init.body,
|
|
150
|
+
keepalive: init.body !== void 0 && init.body.length <= KEEPALIVE_MAX_BYTES ? true : void 0
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function decodeSave(res) {
|
|
154
|
+
if (res.status === 409) {
|
|
155
|
+
const body2 = await res.json().catch(() => ({}));
|
|
156
|
+
return { saved: false, conflict: true, version: body2.version };
|
|
157
|
+
}
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
emit("error", { error: new Error(`genex state save failed (${res.status})`) });
|
|
160
|
+
return { saved: false };
|
|
161
|
+
}
|
|
162
|
+
const body = await res.json();
|
|
163
|
+
return { saved: true, version: body.version };
|
|
164
|
+
}
|
|
165
|
+
async function loadPlayerState() {
|
|
166
|
+
await ensurePlayer();
|
|
167
|
+
if (state === "guest") {
|
|
168
|
+
return { data: hasPendingPlayerSave ? pendingPlayerSave : null, version: 0, guest: true };
|
|
169
|
+
}
|
|
170
|
+
const res = await stateFetch("/state/me");
|
|
171
|
+
if (!res.ok) throw new Error(`genex load player state failed (${res.status})`);
|
|
172
|
+
const body = await res.json();
|
|
173
|
+
return { data: body.data, version: body.version };
|
|
174
|
+
}
|
|
175
|
+
async function savePlayerState(data, opts) {
|
|
176
|
+
await ensurePlayer();
|
|
177
|
+
if (state === "guest") {
|
|
178
|
+
pendingPlayerSave = data;
|
|
179
|
+
hasPendingPlayerSave = true;
|
|
180
|
+
return { saved: false, guest: true, queued: true };
|
|
181
|
+
}
|
|
182
|
+
return decodeSave(
|
|
183
|
+
await stateFetch("/state/me", {
|
|
184
|
+
method: "PUT",
|
|
185
|
+
body: JSON.stringify(data),
|
|
186
|
+
ifVersion: opts?.ifVersion
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
async function loadWorldState() {
|
|
191
|
+
await ensurePlayer();
|
|
192
|
+
if (state === "guest") return { data: null, version: 0, guest: true };
|
|
193
|
+
const res = await stateFetch("/state");
|
|
194
|
+
if (!res.ok) throw new Error(`genex load world state failed (${res.status})`);
|
|
195
|
+
const body = await res.json();
|
|
196
|
+
lastWorldVersion = body.version;
|
|
197
|
+
return { data: body.data, version: body.version };
|
|
198
|
+
}
|
|
199
|
+
async function saveWorldState(data, opts) {
|
|
200
|
+
await ensurePlayer();
|
|
201
|
+
if (state === "guest") return { saved: false, guest: true };
|
|
202
|
+
const guard = opts && "ifVersion" in opts ? opts.ifVersion : lastWorldVersion;
|
|
203
|
+
const result = await decodeSave(
|
|
204
|
+
await stateFetch("/state", {
|
|
205
|
+
method: "PUT",
|
|
206
|
+
body: JSON.stringify(data),
|
|
207
|
+
ifVersion: guard == null ? void 0 : guard
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
if (result.version !== void 0) lastWorldVersion = result.version;
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
async function submitScore(score, opts) {
|
|
214
|
+
const board = opts?.board ?? "main";
|
|
215
|
+
const mode = opts?.mode ?? "max";
|
|
216
|
+
await ensurePlayer();
|
|
217
|
+
if (state === "guest") {
|
|
218
|
+
const prev = pendingScores.get(board);
|
|
219
|
+
if (!prev || (mode === "min" ? score < prev.score : score > prev.score)) {
|
|
220
|
+
pendingScores.set(board, { score, mode });
|
|
221
|
+
}
|
|
222
|
+
return { submitted: false, guest: true, queued: true };
|
|
223
|
+
}
|
|
224
|
+
const res = await stateFetch("/scores", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify({ score, board, mode })
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
emit("error", { error: new Error(`genex score submit failed (${res.status})`) });
|
|
230
|
+
return { submitted: false };
|
|
231
|
+
}
|
|
232
|
+
const body = await res.json();
|
|
233
|
+
return { submitted: true, best: body.best, improved: body.improved };
|
|
234
|
+
}
|
|
235
|
+
async function getLeaderboard(opts) {
|
|
236
|
+
if (!config) throw new Error("genex embed: call initEmbed() first");
|
|
237
|
+
if (state === "pending") {
|
|
238
|
+
try {
|
|
239
|
+
await waitForPlayer();
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const params = new URLSearchParams();
|
|
244
|
+
if (opts?.board !== void 0) params.set("board", opts.board);
|
|
245
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
246
|
+
if (opts?.order !== void 0) params.set("order", opts.order);
|
|
247
|
+
const qs = params.toString();
|
|
248
|
+
const headers = {};
|
|
249
|
+
if (embedToken) headers.Authorization = `Bearer ${embedToken}`;
|
|
250
|
+
const res = await doFetch(
|
|
251
|
+
`${config.apiUrl}/api/projects/${encodeURIComponent(config.slug)}/leaderboard${qs ? `?${qs}` : ""}`,
|
|
252
|
+
{ headers }
|
|
253
|
+
);
|
|
254
|
+
if (!res.ok) throw new Error(`genex leaderboard failed (${res.status})`);
|
|
255
|
+
return await res.json();
|
|
256
|
+
}
|
|
257
|
+
async function flushGuestQueue() {
|
|
258
|
+
try {
|
|
259
|
+
if (hasPendingPlayerSave) {
|
|
260
|
+
const save = pendingPlayerSave;
|
|
261
|
+
pendingPlayerSave = void 0;
|
|
262
|
+
hasPendingPlayerSave = false;
|
|
263
|
+
await savePlayerState(save);
|
|
264
|
+
}
|
|
265
|
+
const queued = [...pendingScores.entries()];
|
|
266
|
+
pendingScores.clear();
|
|
267
|
+
for (const [board, { score, mode }] of queued) {
|
|
268
|
+
await submitScore(score, { board, mode });
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
emit("error", { error });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
132
274
|
function emit(event, ctx) {
|
|
133
275
|
const set = listeners.get(event);
|
|
134
276
|
if (!set) return;
|
|
@@ -293,6 +435,7 @@ async function redeemTicket(ticket) {
|
|
|
293
435
|
if (isEmbedded() && parentOrigin) {
|
|
294
436
|
postToParent({ type: "genex:embed:authenticated", v: PROTOCOL_VERSION }, parentOrigin);
|
|
295
437
|
}
|
|
438
|
+
if (upgradingFromGuest) await flushGuestQueue();
|
|
296
439
|
const ctx = { user };
|
|
297
440
|
for (const waiter of authWaiters.splice(0)) waiter.resolve(ctx);
|
|
298
441
|
for (const waiter of playerWaiters.splice(0)) waiter.resolve({ user, guest: false });
|
|
@@ -550,6 +693,9 @@ function __resetForTests(overrides) {
|
|
|
550
693
|
authWaiters = [];
|
|
551
694
|
playerWaiters = [];
|
|
552
695
|
stashedTicketHash = null;
|
|
696
|
+
pendingPlayerSave = void 0;
|
|
697
|
+
hasPendingPlayerSave = false;
|
|
698
|
+
pendingScores.clear();
|
|
553
699
|
handshakeTimeoutMs = overrides?.handshakeTimeoutMs ?? 1e4;
|
|
554
700
|
refreshDelayMs = overrides?.refreshDelayMs ?? 10 * 6e4;
|
|
555
701
|
refreshRetryMs = overrides?.refreshRetryMs ?? 6e4;
|
|
@@ -565,6 +711,12 @@ export {
|
|
|
565
711
|
waitForAuth,
|
|
566
712
|
waitForPlayer,
|
|
567
713
|
on,
|
|
714
|
+
loadPlayerState,
|
|
715
|
+
savePlayerState,
|
|
716
|
+
loadWorldState,
|
|
717
|
+
saveWorldState,
|
|
718
|
+
submitScore,
|
|
719
|
+
getLeaderboard,
|
|
568
720
|
_stashTicketFromUrl,
|
|
569
721
|
__resetForTests
|
|
570
722
|
};
|
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,68 @@ 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
|
+
* If-Match is sent BY DEFAULT (from the last version this SDK loaded/saved), so a
|
|
175
|
+
* shared write can't silently clobber a newer one — a lost race resolves
|
|
176
|
+
* { conflict: true, version }: reload, merge, retry. Pass { ifVersion: n } to
|
|
177
|
+
* guard against a specific version, or { ifVersion: null } to force an
|
|
178
|
+
* unconditional overwrite (the old last-write-wins behaviour). Guests:
|
|
179
|
+
* { saved: false, guest: true } — world saves are NEVER queued (flushing a stale
|
|
180
|
+
* world later would clobber the live one; the eventual account host keeps saving).
|
|
181
|
+
*/
|
|
182
|
+
declare function saveWorldState(data: unknown, opts?: {
|
|
183
|
+
ifVersion?: number | null;
|
|
184
|
+
}): Promise<SaveStateResult>;
|
|
185
|
+
/**
|
|
186
|
+
* Submit a score (soft-trust leaderboards: identity is verified server-side,
|
|
187
|
+
* one row per real player per board). Keep-best: only a strictly better score
|
|
188
|
+
* moves the row — `mode: "min"` for lap-time-style boards (send a consistent
|
|
189
|
+
* mode per board). Guests: the BEST value per board is queued and
|
|
190
|
+
* auto-submitted if they sign in mid-game.
|
|
191
|
+
*/
|
|
192
|
+
declare function submitScore(score: number, opts?: {
|
|
193
|
+
board?: string;
|
|
194
|
+
mode?: 'max' | 'min';
|
|
195
|
+
}): Promise<SubmitScoreResult>;
|
|
196
|
+
/**
|
|
197
|
+
* Read a leaderboard: top entries (verified display names) + this player's
|
|
198
|
+
* own rank when signed in. Works for EVERYONE on published games — guests and
|
|
199
|
+
* even tokenless contexts read the public board (me: null). limit ≤ 100;
|
|
200
|
+
* order "asc" for lower-is-better boards.
|
|
201
|
+
*/
|
|
202
|
+
declare function getLeaderboard(opts?: {
|
|
203
|
+
board?: string;
|
|
204
|
+
limit?: number;
|
|
205
|
+
order?: 'desc' | 'asc';
|
|
206
|
+
}): Promise<Leaderboard>;
|
|
95
207
|
/**
|
|
96
208
|
* @internal — for ./sentry.ts ONLY, not public API (games never call this).
|
|
97
209
|
* If the URL fragment carries any genex marker (`genex_ticket` — a secret —
|
|
@@ -105,4 +217,4 @@ declare function __resetForTests(overrides?: {
|
|
|
105
217
|
refreshRetryMs?: number;
|
|
106
218
|
}): void;
|
|
107
219
|
|
|
108
|
-
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getEmbedToken, getUser, initEmbed, isEmbedded, on, waitForAuth, waitForPlayer };
|
|
220
|
+
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-LWEYUUBS.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.1",
|
|
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
|
+
}
|