@pixelume/sdk 0.2.0 → 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/CHANGELOG.md +62 -1
- package/README.md +113 -22
- package/dist/contracts/index.d.ts +42 -2
- package/dist/contracts/index.js +107 -8
- package/dist/contracts/index.js.map +1 -1
- package/dist/core/PixelumeClient.d.ts +12 -2
- package/dist/core/PixelumeClient.js +16 -2
- package/dist/core/PixelumeClient.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.js +8 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +44 -2
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/core/types.js.map +1 -1
- package/dist/http/HttpClient.js +1 -0
- package/dist/http/HttpClient.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/auth/AuthModule.d.ts +28 -3
- package/dist/modules/auth/AuthModule.js +82 -40
- package/dist/modules/auth/AuthModule.js.map +1 -1
- package/dist/modules/auth/SessionManager.d.ts +91 -0
- package/dist/modules/auth/SessionManager.js +475 -0
- package/dist/modules/auth/SessionManager.js.map +1 -0
- package/dist/modules/saves/SavesModule.d.ts +100 -0
- package/dist/modules/saves/SavesModule.js +238 -0
- package/dist/modules/saves/SavesModule.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/styles.d.ts +1 -0
- package/dist/ui/styles.js +402 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/ui/widget.d.ts +29 -0
- package/dist/ui/widget.js +435 -0
- package/dist/ui/widget.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { parseCloudSaveEnvelope, parseEmptyResponse, parseSaveList, parseSaveMetadata, parseSaveMetadataEnvelope, } from '../../contracts/index.js';
|
|
2
|
+
import { requireGameId } from '../../core/config.js';
|
|
3
|
+
import { PixelumeConfigError, PixelumeHttpError, PixelumeSaveConflictError, isPixelumeError, } from '../../core/errors.js';
|
|
4
|
+
/** Same rules as the API (`pixelume-api/src/domain/save/cloud-save.ts`). */
|
|
5
|
+
const SLOT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
|
|
6
|
+
const MAX_SCHEMA_VERSION = 1_000_000;
|
|
7
|
+
const MAX_JSON_DEPTH = 32;
|
|
8
|
+
const IDLE = Object.freeze({ pending: 0, pendingWrites: 0, last: null });
|
|
9
|
+
export class SavesModule {
|
|
10
|
+
#session;
|
|
11
|
+
#config;
|
|
12
|
+
#listeners = new Set();
|
|
13
|
+
#activity = IDLE;
|
|
14
|
+
constructor(session, config) {
|
|
15
|
+
this.#session = session;
|
|
16
|
+
this.#config = config;
|
|
17
|
+
}
|
|
18
|
+
get activity() {
|
|
19
|
+
return this.#activity;
|
|
20
|
+
}
|
|
21
|
+
subscribe(listener) {
|
|
22
|
+
this.#listeners.add(listener);
|
|
23
|
+
return () => this.#listeners.delete(listener);
|
|
24
|
+
}
|
|
25
|
+
list(options = {}) {
|
|
26
|
+
return this.#track('list', null, async () => {
|
|
27
|
+
const gameId = this.#gameId();
|
|
28
|
+
const response = await this.#session.authorizedRequest({
|
|
29
|
+
method: 'GET',
|
|
30
|
+
path: `/v1/games/${gameId}/saves`,
|
|
31
|
+
signal: options.signal,
|
|
32
|
+
timeoutMs: options.timeoutMs,
|
|
33
|
+
parse: parseSaveList,
|
|
34
|
+
});
|
|
35
|
+
const { saves, limits, usage } = response.data;
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
gameId: response.data.gameId,
|
|
38
|
+
saves: Object.freeze(saves.map(freezeMetadata)),
|
|
39
|
+
limits: Object.freeze({ ...limits }),
|
|
40
|
+
usage: Object.freeze({ ...usage }),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
load(slotId, options = {}) {
|
|
45
|
+
return this.#track('load', slotId, async () => {
|
|
46
|
+
const gameId = this.#gameId();
|
|
47
|
+
validateSlotId(slotId);
|
|
48
|
+
try {
|
|
49
|
+
const response = await this.#session.authorizedRequest({
|
|
50
|
+
method: 'GET',
|
|
51
|
+
path: `/v1/games/${gameId}/saves/${slotId}`,
|
|
52
|
+
signal: options.signal,
|
|
53
|
+
timeoutMs: options.timeoutMs,
|
|
54
|
+
parse: parseCloudSaveEnvelope,
|
|
55
|
+
});
|
|
56
|
+
const { data, ...metadata } = response.data;
|
|
57
|
+
return Object.freeze({ ...metadata, data: data });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isSaveNotFound(error))
|
|
61
|
+
return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
save(slotId, write, options = {}) {
|
|
67
|
+
return this.#track('save', slotId, async () => {
|
|
68
|
+
const gameId = this.#gameId();
|
|
69
|
+
validateSlotId(slotId);
|
|
70
|
+
const untrusted = write; // JavaScript callers may pass anything
|
|
71
|
+
const input = (typeof untrusted === 'object' && untrusted !== null ? untrusted : {});
|
|
72
|
+
const schemaVersion = validateSchemaVersion(input.schemaVersion);
|
|
73
|
+
const expectedRevision = validateRevision(input.expectedRevision, 0);
|
|
74
|
+
validateSaveData(input.data);
|
|
75
|
+
try {
|
|
76
|
+
const response = await this.#session.authorizedRequest({
|
|
77
|
+
method: 'PUT',
|
|
78
|
+
path: `/v1/games/${gameId}/saves/${slotId}`,
|
|
79
|
+
json: { schemaVersion, expectedRevision, data: input.data },
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
timeoutMs: options.timeoutMs,
|
|
82
|
+
parse: parseSaveMetadataEnvelope,
|
|
83
|
+
});
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
created: response.status === 201,
|
|
86
|
+
save: freezeMetadata(response.data),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw toConflict(error, expectedRevision);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
delete(slotId, options) {
|
|
95
|
+
return this.#track('delete', slotId, async () => {
|
|
96
|
+
const gameId = this.#gameId();
|
|
97
|
+
validateSlotId(slotId);
|
|
98
|
+
const untrusted = options;
|
|
99
|
+
const input = (typeof untrusted === 'object' && untrusted !== null ? untrusted : {});
|
|
100
|
+
const expectedRevision = validateRevision(input.expectedRevision, 1);
|
|
101
|
+
try {
|
|
102
|
+
await this.#session.authorizedRequest({
|
|
103
|
+
method: 'DELETE',
|
|
104
|
+
path: `/v1/games/${gameId}/saves/${slotId}`,
|
|
105
|
+
query: { expectedRevision },
|
|
106
|
+
signal: input.signal,
|
|
107
|
+
timeoutMs: input.timeoutMs,
|
|
108
|
+
parse: parseEmptyResponse,
|
|
109
|
+
});
|
|
110
|
+
return 'deleted';
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (isSaveNotFound(error))
|
|
114
|
+
return 'not_found';
|
|
115
|
+
throw toConflict(error, expectedRevision);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
#gameId() {
|
|
120
|
+
const gameId = requireGameId(this.#config, 'Cloud Saves');
|
|
121
|
+
if (this.#config.clientId !== gameId) {
|
|
122
|
+
// The API only accepts tokens issued to the game in the URL (403 FORBIDDEN_GAME).
|
|
123
|
+
throw new PixelumeConfigError('Cloud Saves requires clientId to be equal to gameId');
|
|
124
|
+
}
|
|
125
|
+
return gameId;
|
|
126
|
+
}
|
|
127
|
+
async #track(operation, slotId, run) {
|
|
128
|
+
const write = operation === 'save' || operation === 'delete';
|
|
129
|
+
this.#update({
|
|
130
|
+
pending: this.#activity.pending + 1,
|
|
131
|
+
pendingWrites: this.#activity.pendingWrites + (write ? 1 : 0),
|
|
132
|
+
last: this.#activity.last,
|
|
133
|
+
});
|
|
134
|
+
let outcome = 'success';
|
|
135
|
+
let failure = null;
|
|
136
|
+
try {
|
|
137
|
+
return await run();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
outcome = error instanceof PixelumeSaveConflictError ? 'conflict' : 'error';
|
|
141
|
+
failure = isPixelumeError(error) ? error : null;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
this.#update({
|
|
146
|
+
pending: this.#activity.pending - 1,
|
|
147
|
+
pendingWrites: this.#activity.pendingWrites - (write ? 1 : 0),
|
|
148
|
+
last: Object.freeze({
|
|
149
|
+
operation,
|
|
150
|
+
slotId: typeof slotId === 'string' ? slotId : null,
|
|
151
|
+
outcome,
|
|
152
|
+
at: Date.now(),
|
|
153
|
+
error: failure,
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
#update(next) {
|
|
159
|
+
const previous = this.#activity;
|
|
160
|
+
this.#activity = Object.freeze(next);
|
|
161
|
+
for (const listener of [...this.#listeners])
|
|
162
|
+
listener(this.#activity, previous);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function freezeMetadata(metadata) {
|
|
166
|
+
return Object.freeze({ ...metadata });
|
|
167
|
+
}
|
|
168
|
+
function isSaveNotFound(error) {
|
|
169
|
+
return (error instanceof PixelumeHttpError && error.status === 404 && error.code === 'SAVE_NOT_FOUND');
|
|
170
|
+
}
|
|
171
|
+
function toConflict(error, expectedRevision) {
|
|
172
|
+
if (!(error instanceof PixelumeHttpError) || error.status !== 409)
|
|
173
|
+
return error;
|
|
174
|
+
const current = parseSaveMetadata(error.details?.current);
|
|
175
|
+
return new PixelumeSaveConflictError(error, current && freezeMetadata(current), expectedRevision);
|
|
176
|
+
}
|
|
177
|
+
function validateSlotId(slotId) {
|
|
178
|
+
if (typeof slotId !== 'string' || !SLOT_ID_PATTERN.test(slotId)) {
|
|
179
|
+
throw new PixelumeConfigError('slotId must match ^[a-z0-9][a-z0-9_-]{0,63}$ (e.g. "main", "slot-2", "autosave_1")');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function validateSchemaVersion(value) {
|
|
183
|
+
if (typeof value !== 'number' ||
|
|
184
|
+
!Number.isInteger(value) ||
|
|
185
|
+
value < 1 ||
|
|
186
|
+
value > MAX_SCHEMA_VERSION) {
|
|
187
|
+
throw new PixelumeConfigError(`schemaVersion must be an integer between 1 and ${MAX_SCHEMA_VERSION}`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
function validateRevision(value, minimum) {
|
|
192
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum) {
|
|
193
|
+
throw new PixelumeConfigError(minimum === 0
|
|
194
|
+
? 'expectedRevision must be a non-negative integer (0 creates a new save)'
|
|
195
|
+
: 'expectedRevision must be a positive integer (the current revision of the save)');
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Rejects values `JSON.stringify` would silently drop or change (undefined, functions, NaN,
|
|
201
|
+
* Dates, class instances…), so what is stored is exactly what the game passed. Also mirrors the
|
|
202
|
+
* API depth limit (root object = level 1, at most 32 levels) and stops on cycles.
|
|
203
|
+
*/
|
|
204
|
+
function validateSaveData(value) {
|
|
205
|
+
if (!isPlainObject(value))
|
|
206
|
+
throw new PixelumeConfigError('Save data must be a plain JSON object');
|
|
207
|
+
const visit = (item, level) => {
|
|
208
|
+
if (level > MAX_JSON_DEPTH) {
|
|
209
|
+
throw new PixelumeConfigError(`Save data cannot be nested deeper than ${MAX_JSON_DEPTH} levels (or contains a cycle)`);
|
|
210
|
+
}
|
|
211
|
+
if (item === null || typeof item === 'string' || typeof item === 'boolean')
|
|
212
|
+
return;
|
|
213
|
+
if (typeof item === 'number') {
|
|
214
|
+
if (Number.isFinite(item))
|
|
215
|
+
return;
|
|
216
|
+
throw new PixelumeConfigError('Save data cannot contain NaN or Infinity');
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(item)) {
|
|
219
|
+
for (const entry of item)
|
|
220
|
+
visit(entry, level + 1);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (isPlainObject(item)) {
|
|
224
|
+
for (const entry of Object.values(item))
|
|
225
|
+
visit(entry, level + 1);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new PixelumeConfigError('Save data can contain only plain objects, arrays, strings, finite numbers, booleans and null');
|
|
229
|
+
};
|
|
230
|
+
visit(value, 1);
|
|
231
|
+
}
|
|
232
|
+
function isPlainObject(value) {
|
|
233
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
234
|
+
return false;
|
|
235
|
+
const prototype = Object.getPrototypeOf(value);
|
|
236
|
+
return prototype === Object.prototype || prototype === null;
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=SavesModule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SavesModule.js","sourceRoot":"","sources":["../../../src/modules/saves/SavesModule.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n parseCloudSaveEnvelope,\n parseEmptyResponse,\n parseSaveList,\n parseSaveMetadata,\n parseSaveMetadataEnvelope,\n type SaveMetadataWire,\n} from '../../contracts/index.js';\nimport { requireGameId, type ResolvedConfig } from '../../core/config.js';\nimport {\n PixelumeConfigError,\n PixelumeHttpError,\n PixelumeSaveConflictError,\n isPixelumeError,\n type PixelumeError,\n type PixelumeSaveMetadata,\n} from '../../core/errors.js';\nimport type { PixelumeRequestOptions } from '../../core/types.js';\nimport type { SessionManager } from '../auth/SessionManager.js';\n\n/** Same rules as the API (`pixelume-api/src/domain/save/cloud-save.ts`). */\nconst SLOT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/u;\nconst MAX_SCHEMA_VERSION = 1_000_000;\nconst MAX_JSON_DEPTH = 32;\n\n/** A cloud save with its data. `data` is whatever the game stored: validate it before use. */\nexport interface PixelumeSave<\n T extends object = Record<string, unknown>,\n> extends PixelumeSaveMetadata {\n readonly data: T;\n}\n\nexport interface PixelumeSaveList {\n readonly gameId: string;\n /** Metadata of every live slot of the current player in this game (no data). */\n readonly saves: readonly PixelumeSaveMetadata[];\n /** Limits currently enforced by the API. */\n readonly limits: {\n readonly maxSlotsPerGame: number;\n readonly maxDataBytes: number;\n /** Sum of all games of the player. */\n readonly maxBytesPerPlayer: number;\n };\n readonly usage: { readonly slots: number; readonly playerBytes: number };\n}\n\nexport interface PixelumeSaveWrite<T extends object> {\n /** Version of the game's save format (integer 1–1 000 000), used by the game to migrate. */\n readonly schemaVersion: number;\n /**\n * Revision the game last saw for this slot: `0` creates a new save (fails if one exists);\n * `n` updates only if the slot is still at revision `n`. There is no blind overwrite.\n */\n readonly expectedRevision: number;\n /** A JSON object (plain objects, arrays, strings, finite numbers, booleans, null). */\n readonly data: T;\n}\n\nexport interface PixelumeSaveDeleteOptions extends PixelumeRequestOptions {\n /** Current revision of the save to delete (≥ 1). */\n readonly expectedRevision: number;\n}\n\nexport interface PixelumeSaveResult {\n /** `true` when the slot was created (201), `false` when an existing save was updated (200). */\n readonly created: boolean;\n readonly save: PixelumeSaveMetadata;\n}\n\n/**\n * `deleted`: the save at `expectedRevision` is gone (repeating the same delete also returns\n * `deleted`). `not_found`: there was no save in this slot.\n */\nexport type PixelumeSaveDeleteResult = 'deleted' | 'not_found';\n\nexport type PixelumeSavesOperation = 'list' | 'load' | 'save' | 'delete';\n\n/** Outcome of the last explicit Saves call, for status UIs. Never contains save data. */\nexport interface PixelumeSavesActivity {\n /** Saves calls currently in flight. */\n readonly pending: number;\n /** `save`/`delete` calls currently in flight. */\n readonly pendingWrites: number;\n readonly last: {\n readonly operation: PixelumeSavesOperation;\n readonly slotId: string | null;\n readonly outcome: 'success' | 'conflict' | 'error';\n /** Local time (ms since epoch) when the call settled. */\n readonly at: number;\n readonly error: PixelumeError | null;\n } | null;\n}\n\nexport type PixelumeSavesListener = (\n activity: PixelumeSavesActivity,\n previous: PixelumeSavesActivity,\n) => void;\n\n/**\n * Cloud Saves of the signed-in player for the configured `gameId`. Every call needs a session\n * with the `saves` scope; the API derives the player from the access token, never from input.\n *\n * Saves are unverified, client-supplied personal data: never use them as proof of achievements,\n * rankings or rewards, and never store secrets or personal data in them.\n */\nexport interface PixelumeSavesApi {\n /** Lists slot metadata, limits and usage. */\n list(options?: PixelumeRequestOptions): Promise<PixelumeSaveList>;\n /** Loads one slot, or `null` when it has no save. */\n load<T extends object = Record<string, unknown>>(\n slotId: string,\n options?: PixelumeRequestOptions,\n ): Promise<PixelumeSave<T> | null>;\n /**\n * Creates (`expectedRevision: 0`) or updates a slot. A revision mismatch rejects with\n * `PixelumeSaveConflictError`; the SDK never retries it or overwrites the remote save.\n */\n save<T extends object>(\n slotId: string,\n write: PixelumeSaveWrite<T>,\n options?: PixelumeRequestOptions,\n ): Promise<PixelumeSaveResult>;\n /** Deletes a slot at `expectedRevision`. Conflicts reject with `PixelumeSaveConflictError`. */\n delete(slotId: string, options: PixelumeSaveDeleteOptions): Promise<PixelumeSaveDeleteResult>;\n /** Current activity of explicit Saves calls. */\n readonly activity: PixelumeSavesActivity;\n /** Subscribes to `activity` changes. The returned function removes the listener. */\n subscribe(listener: PixelumeSavesListener): () => void;\n}\n\nconst IDLE: PixelumeSavesActivity = Object.freeze({ pending: 0, pendingWrites: 0, last: null });\n\nexport class SavesModule implements PixelumeSavesApi {\n readonly #session: SessionManager;\n readonly #config: ResolvedConfig;\n readonly #listeners = new Set<PixelumeSavesListener>();\n #activity: PixelumeSavesActivity = IDLE;\n\n constructor(session: SessionManager, config: ResolvedConfig) {\n this.#session = session;\n this.#config = config;\n }\n\n get activity(): PixelumeSavesActivity {\n return this.#activity;\n }\n\n subscribe(listener: PixelumeSavesListener): () => void {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n }\n\n list(options: PixelumeRequestOptions = {}): Promise<PixelumeSaveList> {\n return this.#track('list', null, async () => {\n const gameId = this.#gameId();\n const response = await this.#session.authorizedRequest({\n method: 'GET',\n path: `/v1/games/${gameId}/saves`,\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parseSaveList,\n });\n const { saves, limits, usage } = response.data;\n return Object.freeze({\n gameId: response.data.gameId,\n saves: Object.freeze(saves.map(freezeMetadata)),\n limits: Object.freeze({ ...limits }),\n usage: Object.freeze({ ...usage }),\n });\n });\n }\n\n load<T extends object = Record<string, unknown>>(\n slotId: string,\n options: PixelumeRequestOptions = {},\n ): Promise<PixelumeSave<T> | null> {\n return this.#track('load', slotId, async () => {\n const gameId = this.#gameId();\n validateSlotId(slotId);\n try {\n const response = await this.#session.authorizedRequest({\n method: 'GET',\n path: `/v1/games/${gameId}/saves/${slotId}`,\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parseCloudSaveEnvelope,\n });\n const { data, ...metadata } = response.data;\n return Object.freeze({ ...metadata, data: data as T });\n } catch (error) {\n if (isSaveNotFound(error)) return null;\n throw error;\n }\n });\n }\n\n save<T extends object>(\n slotId: string,\n write: PixelumeSaveWrite<T>,\n options: PixelumeRequestOptions = {},\n ): Promise<PixelumeSaveResult> {\n return this.#track('save', slotId, async () => {\n const gameId = this.#gameId();\n validateSlotId(slotId);\n const untrusted: unknown = write; // JavaScript callers may pass anything\n const input = (\n typeof untrusted === 'object' && untrusted !== null ? untrusted : {}\n ) as Partial<PixelumeSaveWrite<T>>;\n const schemaVersion = validateSchemaVersion(input.schemaVersion);\n const expectedRevision = validateRevision(input.expectedRevision, 0);\n validateSaveData(input.data);\n try {\n const response = await this.#session.authorizedRequest({\n method: 'PUT',\n path: `/v1/games/${gameId}/saves/${slotId}`,\n json: { schemaVersion, expectedRevision, data: input.data },\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n parse: parseSaveMetadataEnvelope,\n });\n return Object.freeze({\n created: response.status === 201,\n save: freezeMetadata(response.data),\n });\n } catch (error) {\n throw toConflict(error, expectedRevision);\n }\n });\n }\n\n delete(slotId: string, options: PixelumeSaveDeleteOptions): Promise<PixelumeSaveDeleteResult> {\n return this.#track('delete', slotId, async () => {\n const gameId = this.#gameId();\n validateSlotId(slotId);\n const untrusted: unknown = options;\n const input = (\n typeof untrusted === 'object' && untrusted !== null ? untrusted : {}\n ) as Partial<PixelumeSaveDeleteOptions>;\n const expectedRevision = validateRevision(input.expectedRevision, 1);\n try {\n await this.#session.authorizedRequest({\n method: 'DELETE',\n path: `/v1/games/${gameId}/saves/${slotId}`,\n query: { expectedRevision },\n signal: input.signal,\n timeoutMs: input.timeoutMs,\n parse: parseEmptyResponse,\n });\n return 'deleted';\n } catch (error) {\n if (isSaveNotFound(error)) return 'not_found';\n throw toConflict(error, expectedRevision);\n }\n });\n }\n\n #gameId(): string {\n const gameId = requireGameId(this.#config, 'Cloud Saves');\n if (this.#config.clientId !== gameId) {\n // The API only accepts tokens issued to the game in the URL (403 FORBIDDEN_GAME).\n throw new PixelumeConfigError('Cloud Saves requires clientId to be equal to gameId');\n }\n return gameId;\n }\n\n async #track<T>(\n operation: PixelumeSavesOperation,\n slotId: string | null,\n run: () => Promise<T>,\n ): Promise<T> {\n const write = operation === 'save' || operation === 'delete';\n this.#update({\n pending: this.#activity.pending + 1,\n pendingWrites: this.#activity.pendingWrites + (write ? 1 : 0),\n last: this.#activity.last,\n });\n let outcome: 'success' | 'conflict' | 'error' = 'success';\n let failure: PixelumeError | null = null;\n try {\n return await run();\n } catch (error) {\n outcome = error instanceof PixelumeSaveConflictError ? 'conflict' : 'error';\n failure = isPixelumeError(error) ? error : null;\n throw error;\n } finally {\n this.#update({\n pending: this.#activity.pending - 1,\n pendingWrites: this.#activity.pendingWrites - (write ? 1 : 0),\n last: Object.freeze({\n operation,\n slotId: typeof slotId === 'string' ? slotId : null,\n outcome,\n at: Date.now(),\n error: failure,\n }),\n });\n }\n }\n\n #update(next: PixelumeSavesActivity): void {\n const previous = this.#activity;\n this.#activity = Object.freeze(next);\n for (const listener of [...this.#listeners]) listener(this.#activity, previous);\n }\n}\n\nfunction freezeMetadata(metadata: SaveMetadataWire): PixelumeSaveMetadata {\n return Object.freeze({ ...metadata });\n}\n\nfunction isSaveNotFound(error: unknown): boolean {\n return (\n error instanceof PixelumeHttpError && error.status === 404 && error.code === 'SAVE_NOT_FOUND'\n );\n}\n\nfunction toConflict(error: unknown, expectedRevision: number): unknown {\n if (!(error instanceof PixelumeHttpError) || error.status !== 409) return error;\n const current = parseSaveMetadata(error.details?.current);\n return new PixelumeSaveConflictError(error, current && freezeMetadata(current), expectedRevision);\n}\n\nfunction validateSlotId(slotId: unknown): asserts slotId is string {\n if (typeof slotId !== 'string' || !SLOT_ID_PATTERN.test(slotId)) {\n throw new PixelumeConfigError(\n 'slotId must match ^[a-z0-9][a-z0-9_-]{0,63}$ (e.g. \"main\", \"slot-2\", \"autosave_1\")',\n );\n }\n}\n\nfunction validateSchemaVersion(value: unknown): number {\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < 1 ||\n value > MAX_SCHEMA_VERSION\n ) {\n throw new PixelumeConfigError(\n `schemaVersion must be an integer between 1 and ${MAX_SCHEMA_VERSION}`,\n );\n }\n return value;\n}\n\nfunction validateRevision(value: unknown, minimum: 0 | 1): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum) {\n throw new PixelumeConfigError(\n minimum === 0\n ? 'expectedRevision must be a non-negative integer (0 creates a new save)'\n : 'expectedRevision must be a positive integer (the current revision of the save)',\n );\n }\n return value;\n}\n\n/**\n * Rejects values `JSON.stringify` would silently drop or change (undefined, functions, NaN,\n * Dates, class instances…), so what is stored is exactly what the game passed. Also mirrors the\n * API depth limit (root object = level 1, at most 32 levels) and stops on cycles.\n */\nfunction validateSaveData(value: unknown): void {\n if (!isPlainObject(value)) throw new PixelumeConfigError('Save data must be a plain JSON object');\n const visit = (item: unknown, level: number): void => {\n if (level > MAX_JSON_DEPTH) {\n throw new PixelumeConfigError(\n `Save data cannot be nested deeper than ${MAX_JSON_DEPTH} levels (or contains a cycle)`,\n );\n }\n if (item === null || typeof item === 'string' || typeof item === 'boolean') return;\n if (typeof item === 'number') {\n if (Number.isFinite(item)) return;\n throw new PixelumeConfigError('Save data cannot contain NaN or Infinity');\n }\n if (Array.isArray(item)) {\n for (const entry of item as unknown[]) visit(entry, level + 1);\n return;\n }\n if (isPlainObject(item)) {\n for (const entry of Object.values(item)) visit(entry, level + 1);\n return;\n }\n throw new PixelumeConfigError(\n 'Save data can contain only plain objects, arrays, strings, finite numbers, booleans and null',\n );\n };\n visit(value, 1);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const prototype: unknown = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n"]}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export { mountPixelumeWidget } from './widget.js';\nexport type {\n PixelumeWidgetController,\n PixelumeWidgetOptions,\n PixelumeWidgetPosition,\n} from './widget.js';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WIDGET_STYLES = "\n:host {\n all: initial;\n position: fixed;\n z-index: var(--pixelume-z-index, 2147483000);\n display: block;\n color-scheme: dark;\n contain: layout style;\n pointer-events: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([data-contained]) {\n position: absolute;\n}\n\n:host([data-position=\"top-left\"]) {\n inset-block-start: calc(var(--pixelume-offset) + env(safe-area-inset-top, 0px));\n inset-inline-start: calc(var(--pixelume-offset) + env(safe-area-inset-left, 0px));\n}\n\n:host([data-position=\"top-right\"]) {\n inset-block-start: calc(var(--pixelume-offset) + env(safe-area-inset-top, 0px));\n inset-inline-end: calc(var(--pixelume-offset) + env(safe-area-inset-right, 0px));\n}\n\n:host([data-position=\"bottom-left\"]) {\n inset-block-end: calc(var(--pixelume-offset) + env(safe-area-inset-bottom, 0px));\n inset-inline-start: calc(var(--pixelume-offset) + env(safe-area-inset-left, 0px));\n}\n\n:host([data-position=\"bottom-right\"]) {\n inset-block-end: calc(var(--pixelume-offset) + env(safe-area-inset-bottom, 0px));\n inset-inline-end: calc(var(--pixelume-offset) + env(safe-area-inset-right, 0px));\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbutton {\n font: inherit;\n}\n\n.root {\n --night: oklch(25% 0.035 263);\n --night-deep: oklch(18% 0.03 263);\n --surface: oklch(30% 0.04 263);\n --surface-raised: oklch(35% 0.045 263);\n --lume: oklch(79% 0.13 67);\n --lume-bright: oklch(88% 0.1 76);\n --lume-deep: oklch(63% 0.13 48);\n --mint: oklch(81% 0.08 171);\n --text: oklch(94% 0.02 78);\n --muted: oklch(80% 0.02 78);\n --danger: oklch(78% 0.12 24);\n --line: oklch(92% 0.02 78 / 0.24);\n --pixel: 3px;\n display: flex;\n gap: 0.75rem;\n align-items: flex-end;\n inline-size: max-content;\n max-inline-size: calc(100vw - 2rem);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n font-size: 1rem;\n line-height: 1.45;\n color: var(--text);\n pointer-events: auto;\n -webkit-font-smoothing: antialiased;\n}\n\n:host([data-position^=\"bottom\"]) .root {\n flex-direction: column-reverse;\n}\n\n:host([data-position^=\"top\"]) .root {\n flex-direction: column;\n}\n\n:host([data-position$=\"left\"]) .root {\n align-items: flex-start;\n}\n\n.trigger,\n.panel,\n.action,\n.close {\n border: 0;\n border-radius: 0;\n}\n\n.trigger,\n.action {\n box-shadow:\n 0 calc(-1 * var(--pixel)) 0 var(--edge),\n 0 var(--pixel) 0 var(--edge-bottom),\n calc(-1 * var(--pixel)) 0 var(--edge),\n var(--pixel) 0 var(--edge);\n}\n\n.trigger {\n --edge: var(--lume);\n --edge-bottom: var(--lume-deep);\n display: inline-flex;\n align-items: center;\n gap: 0.625rem;\n min-block-size: 2.75rem;\n margin: var(--pixel);\n padding: 0.625rem 0.875rem;\n color: var(--night-deep);\n background: var(--lume);\n font-weight: 750;\n cursor: pointer;\n touch-action: manipulation;\n transition:\n transform 160ms cubic-bezier(0.16, 1, 0.3, 1),\n background-color 160ms linear;\n}\n\n.trigger:hover {\n background: var(--lume-bright);\n}\n\n.trigger:active {\n transform: translateY(2px);\n}\n\n.trigger:focus-visible,\n.action:focus-visible,\n.close:focus-visible {\n outline: 3px solid var(--mint);\n outline-offset: 4px;\n}\n\n.mark {\n position: relative;\n flex: none;\n inline-size: 1rem;\n block-size: 1rem;\n color: currentColor;\n}\n\n.mark::before,\n.mark::after {\n content: \"\";\n position: absolute;\n background: currentColor;\n}\n\n.mark::before {\n inset: 0.25rem;\n}\n\n.mark::after {\n inset: 0;\n clip-path: polygon(37.5% 0, 62.5% 0, 62.5% 37.5%, 100% 37.5%, 100% 62.5%, 62.5% 62.5%, 62.5% 100%, 37.5% 100%, 37.5% 62.5%, 0 62.5%, 0 37.5%, 37.5% 37.5%);\n opacity: 0.72;\n}\n\n.presence {\n inline-size: 0.5rem;\n block-size: 0.5rem;\n background: var(--night-deep);\n opacity: 0.5;\n}\n\n.trigger[data-authenticated] .presence {\n background: oklch(48% 0.12 168);\n opacity: 1;\n}\n\n.panel {\n inline-size: min(21.5rem, calc(100vw - 2rem));\n margin: var(--pixel);\n color: var(--text);\n background: var(--night);\n box-shadow:\n 0 calc(-1 * var(--pixel)) 0 var(--line),\n 0 var(--pixel) 0 var(--surface-raised),\n calc(-1 * var(--pixel)) 0 var(--line),\n var(--pixel) 0 var(--line),\n 0 1rem 2.5rem oklch(8% 0.03 263 / 0.42);\n overflow: hidden;\n}\n\n.panel[hidden] {\n display: none;\n}\n\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n min-block-size: 3rem;\n padding: 0.625rem 0.75rem 0.625rem 1rem;\n border-block-end: 1px solid var(--line);\n}\n\n.brand {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n color: var(--lume-bright);\n font-family: ui-monospace, \"SFMono-Regular\", Consolas, monospace;\n font-size: 0.8125rem;\n font-weight: 750;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n.brand .mark {\n inline-size: 0.75rem;\n block-size: 0.75rem;\n}\n\n.close {\n display: grid;\n place-items: center;\n inline-size: 2.5rem;\n block-size: 2.5rem;\n padding: 0;\n color: var(--muted);\n background: transparent;\n font-size: 1.5rem;\n line-height: 1;\n cursor: pointer;\n touch-action: manipulation;\n}\n\n.close:hover {\n color: var(--text);\n background: var(--surface);\n}\n\n.content {\n display: grid;\n gap: 1rem;\n padding: 1.25rem;\n}\n\n.identity {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n align-items: center;\n gap: 0.875rem;\n}\n\n.avatar {\n display: grid;\n place-items: center;\n inline-size: 2.75rem;\n block-size: 2.75rem;\n color: var(--night-deep);\n background: var(--mint);\n box-shadow:\n 0 -3px 0 var(--mint),\n 0 3px 0 oklch(62% 0.08 171),\n -3px 0 0 var(--mint),\n 3px 0 0 var(--mint);\n font-family: ui-monospace, \"SFMono-Regular\", Consolas, monospace;\n font-weight: 800;\n}\n\n.title,\n.message {\n margin: 0;\n}\n\n.title {\n color: var(--text);\n font-size: 1.125rem;\n font-weight: 760;\n line-height: 1.25;\n overflow-wrap: anywhere;\n}\n\n.message {\n max-inline-size: 34ch;\n color: var(--muted);\n font-size: 0.875rem;\n}\n\n.state-copy {\n display: grid;\n gap: 0.375rem;\n}\n\n.notice {\n margin: 0;\n padding: 0.75rem;\n color: var(--danger);\n background: oklch(25% 0.055 24);\n border: 1px solid oklch(78% 0.12 24 / 0.35);\n font-size: 0.8125rem;\n}\n\n.action {\n --edge: var(--lume);\n --edge-bottom: var(--lume-deep);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-block-size: 2.75rem;\n inline-size: calc(100% - 2 * var(--pixel));\n margin: var(--pixel);\n padding: 0.6875rem 1rem;\n color: var(--night-deep);\n background: var(--lume);\n font-weight: 750;\n cursor: pointer;\n touch-action: manipulation;\n transition:\n transform 160ms cubic-bezier(0.16, 1, 0.3, 1),\n background-color 160ms linear;\n}\n\n.action:hover:not(:disabled) {\n background: var(--lume-bright);\n}\n\n.action:active:not(:disabled) {\n transform: translateY(2px);\n}\n\n.action.secondary {\n --edge: var(--line);\n --edge-bottom: var(--surface-raised);\n color: var(--text);\n background: var(--surface);\n}\n\n.action.secondary:hover:not(:disabled) {\n --edge: var(--mint);\n color: var(--mint);\n background: var(--surface-raised);\n}\n\n.action:disabled {\n cursor: wait;\n opacity: 0.68;\n}\n\n.loading {\n display: grid;\n gap: 0.625rem;\n padding-block: 0.25rem;\n}\n\n.loading-line {\n block-size: 0.75rem;\n background: var(--surface-raised);\n animation: pixel-pulse 1.2s steps(4, end) infinite;\n}\n\n.loading-line:first-child {\n inline-size: 62%;\n}\n\n.loading-line:last-child {\n inline-size: 88%;\n}\n\n.sr-only {\n position: absolute;\n inline-size: 1px;\n block-size: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n@keyframes pixel-pulse {\n 0%, 100% { opacity: 0.45; }\n 50% { opacity: 1; }\n}\n\n@media (max-width: 30rem) {\n .trigger {\n padding-inline: 0.75rem;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n";
|