@hadoku/prefs-client 1.1.7 → 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/dist/service.d.ts +49 -0
- package/dist/service.js +92 -0
- package/package.json +5 -1
|
@@ -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": [
|