@hadoku/prefs-client 1.1.6 → 1.1.8
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/README.md +1 -4
- package/dist/service.d.ts +49 -0
- package/dist/service.js +92 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -158,10 +158,7 @@ Rules for an app that renders avatars:
|
|
|
158
158
|
- **`sprite`** is the character even when the picture is a Discord avatar: a game uses it as the
|
|
159
159
|
piece and its colour. The app that owns the art builds its URL (watchparty owns art revisions;
|
|
160
160
|
its public `GET /watchparty/api/characters` lists them). Without one, use your own default.
|
|
161
|
-
-
|
|
162
|
-
visible line such as "Steam avatars via the Steam Web API · not affiliated with Valve", linking
|
|
163
|
-
to `https://store.steampowered.com/`. Do not use Steam's or Discord's logos, and do not imply
|
|
164
|
-
either endorses the app.
|
|
161
|
+
- Do not use Steam's or Discord's logos, and do not imply either endorses the app.
|
|
165
162
|
- Resolve on join and on reconnect; avatars change rarely. `resolveAvatars` batches past 100 ids.
|
|
166
163
|
- Source choice, character choice, linking (`linkInPopup`: no page navigation), uploads and
|
|
167
164
|
removal all happen in the settings popout. Don't build your own.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface ServicePrefsOptions {
|
|
2
|
+
/** The app's namespace: `cardtable` reaches `cardtable` and `cardtable-*`. */
|
|
3
|
+
app: string;
|
|
4
|
+
/** The app's service key (on the edge's `prefs-<app>` allow list). */
|
|
5
|
+
serviceKey: string;
|
|
6
|
+
/** Default https://hadoku.me/prefs. */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
interface ServiceRow<T = Record<string, unknown>> {
|
|
11
|
+
userId: string;
|
|
12
|
+
blob: T;
|
|
13
|
+
version: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
}
|
|
16
|
+
type ServiceWriteResult<T = Record<string, unknown>> = {
|
|
17
|
+
ok: true;
|
|
18
|
+
blob: T;
|
|
19
|
+
version: number;
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
}
|
|
22
|
+
/** Someone wrote first; `current` is what is there now. Merge and retry. */
|
|
23
|
+
| {
|
|
24
|
+
ok: false;
|
|
25
|
+
conflict: {
|
|
26
|
+
blob: T;
|
|
27
|
+
version: number;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
} | null;
|
|
30
|
+
};
|
|
31
|
+
declare class ServicePrefsError extends Error {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
constructor(status: number, message: string);
|
|
34
|
+
}
|
|
35
|
+
/** prefs-api reads at most this many userIds a call (D1's bind limit). */
|
|
36
|
+
declare const SERVICE_READ_BATCH = 90;
|
|
37
|
+
declare function createServicePrefsClient(opts: ServicePrefsOptions): {
|
|
38
|
+
/** These people's rows (missing = never written), batched under the read cap. */
|
|
39
|
+
read<T = Record<string, unknown>>(appId: string, userIds: readonly string[]): Promise<ServiceRow<T>[]>;
|
|
40
|
+
/** Every person's row in one app, page by page. */
|
|
41
|
+
all<T = Record<string, unknown>>(appId: string, pageSize?: number): AsyncGenerator<ServiceRow<T>>;
|
|
42
|
+
/**
|
|
43
|
+
* Shallow-merge `patch` into the person's row, the person named by their
|
|
44
|
+
* own live key. Pass the `expectedVersion` you read to refuse a lost update.
|
|
45
|
+
*/
|
|
46
|
+
writeFor<T = Record<string, unknown>>(appId: string, userKey: string, patch: Partial<T>, expectedVersion?: number): Promise<ServiceWriteResult<T>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { SERVICE_READ_BATCH, ServicePrefsError, type ServicePrefsOptions, type ServiceRow, type ServiceWriteResult, createServicePrefsClient };
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var PREFS_API_BASE = "https://hadoku.me/prefs";
|
|
3
|
+
|
|
4
|
+
// src/service.ts
|
|
5
|
+
var ServicePrefsError = class extends Error {
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "ServicePrefsError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
};
|
|
13
|
+
var SERVICE_READ_BATCH = 90;
|
|
14
|
+
var APP = /^[a-z0-9-]+$/;
|
|
15
|
+
function createServicePrefsClient(opts) {
|
|
16
|
+
if (!APP.test(opts.app)) throw new Error(`invalid app: ${opts.app}`);
|
|
17
|
+
const f = opts.fetch ?? fetch;
|
|
18
|
+
const base = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_service/${opts.app}/rows`;
|
|
19
|
+
const headers = { "Content-Type": "application/json", "X-User-Key": opts.serviceKey };
|
|
20
|
+
const rowsUrl = (appId) => {
|
|
21
|
+
if (!APP.test(appId) || appId !== opts.app && !appId.startsWith(`${opts.app}-`)) {
|
|
22
|
+
throw new Error(`${appId} is not ${opts.app}'s app`);
|
|
23
|
+
}
|
|
24
|
+
return `${base}/${appId}`;
|
|
25
|
+
};
|
|
26
|
+
async function page(appId, body) {
|
|
27
|
+
const res = await f(`${rowsUrl(appId)}/read`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers,
|
|
30
|
+
body: JSON.stringify(body)
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) throw await failure(res);
|
|
33
|
+
return await res.json();
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
/** These people's rows (missing = never written), batched under the read cap. */
|
|
37
|
+
async read(appId, userIds) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (let i = 0; i < userIds.length; i += SERVICE_READ_BATCH) {
|
|
40
|
+
out.push(
|
|
41
|
+
...(await page(appId, { userIds: userIds.slice(i, i + SERVICE_READ_BATCH) })).rows
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
},
|
|
46
|
+
/** Every person's row in one app, page by page. */
|
|
47
|
+
async *all(appId, pageSize = 200) {
|
|
48
|
+
let after = null;
|
|
49
|
+
do {
|
|
50
|
+
const p = await page(appId, {
|
|
51
|
+
limit: pageSize,
|
|
52
|
+
...after ? { after } : {}
|
|
53
|
+
});
|
|
54
|
+
yield* p.rows;
|
|
55
|
+
after = p.next;
|
|
56
|
+
} while (after);
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* Shallow-merge `patch` into the person's row, the person named by their
|
|
60
|
+
* own live key. Pass the `expectedVersion` you read to refuse a lost update.
|
|
61
|
+
*/
|
|
62
|
+
async writeFor(appId, userKey, patch, expectedVersion) {
|
|
63
|
+
const res = await f(rowsUrl(appId), {
|
|
64
|
+
method: "PUT",
|
|
65
|
+
headers,
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
userKey,
|
|
68
|
+
patch,
|
|
69
|
+
...expectedVersion !== void 0 ? { expectedVersion } : {}
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
if (res.status === 409) {
|
|
73
|
+
const body = await res.json().catch(() => null);
|
|
74
|
+
return { ok: false, conflict: body?.current ?? null };
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) throw await failure(res);
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
...await res.json()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function failure(res) {
|
|
85
|
+
const body = await res.json().catch(() => null);
|
|
86
|
+
return new ServicePrefsError(res.status, body?.message ?? `HTTP ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
SERVICE_READ_BATCH,
|
|
90
|
+
ServicePrefsError,
|
|
91
|
+
createServicePrefsClient
|
|
92
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadoku/prefs-client",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Client SDK for the hadoku unified preferences store. Wraps /prefs/api/v1/* with localStorage caching, debounced writes, Zod-validated reads, and a usePrefs() React hook, plus match history, ratings and leaderboards for every game (./games), and the one resolved avatar per user (./avatar).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"./avatar": {
|
|
26
26
|
"types": "./dist/avatar.d.ts",
|
|
27
27
|
"default": "./dist/avatar.js"
|
|
28
|
+
},
|
|
29
|
+
"./service": {
|
|
30
|
+
"types": "./dist/service.d.ts",
|
|
31
|
+
"default": "./dist/service.js"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"files": [
|