@hadoku/prefs-client 1.1.9 → 1.1.10
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/items.d.ts +37 -0
- package/dist/items.js +76 -0
- package/dist/service.d.ts +14 -0
- package/dist/service.js +66 -0
- package/package.json +5 -1
package/dist/items.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
interface Item<T = unknown> {
|
|
2
|
+
key: string;
|
|
3
|
+
value: T;
|
|
4
|
+
version: number;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
}
|
|
7
|
+
type ItemWriteResult<T = unknown> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
item: Item<T>;
|
|
10
|
+
}
|
|
11
|
+
/** Someone wrote first (or `expectedVersion: 0` and it exists): `current` is what is there. */
|
|
12
|
+
| {
|
|
13
|
+
ok: false;
|
|
14
|
+
conflict: Item<T> | null;
|
|
15
|
+
};
|
|
16
|
+
declare class ItemsError extends Error {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
constructor(status: number, message: string);
|
|
19
|
+
}
|
|
20
|
+
interface ItemsClientOptions {
|
|
21
|
+
appId: string;
|
|
22
|
+
/** The person's key, as the edge forwarded it. Omit in a browser (cookie). */
|
|
23
|
+
userKey?: string;
|
|
24
|
+
/** Default https://hadoku.me/prefs. */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
declare function createItemsClient(opts: ItemsClientOptions): {
|
|
29
|
+
get<T = unknown>(key: string): Promise<Item<T> | null>;
|
|
30
|
+
/** Every item under `prefix`, following pages. */
|
|
31
|
+
list<T = unknown>(prefix?: string): Promise<Item<T>[]>;
|
|
32
|
+
/** Replace one item. `expectedVersion` refuses a lost update; 0 means "only if absent". */
|
|
33
|
+
put<T = unknown>(key: string, value: T, expectedVersion?: number): Promise<ItemWriteResult<T>>;
|
|
34
|
+
delete(key: string): Promise<boolean>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { type Item, type ItemWriteResult, type ItemsClientOptions, ItemsError, createItemsClient };
|
package/dist/items.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var PREFS_API_BASE = "https://hadoku.me/prefs";
|
|
3
|
+
|
|
4
|
+
// src/items.ts
|
|
5
|
+
var ItemsError = class extends Error {
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "ItemsError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
};
|
|
13
|
+
var APP = /^[a-z0-9-]+$/;
|
|
14
|
+
function createItemsClient(opts) {
|
|
15
|
+
if (!APP.test(opts.appId)) throw new Error(`invalid appId: ${opts.appId}`);
|
|
16
|
+
const f = opts.fetch ?? fetch;
|
|
17
|
+
const base = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/${opts.appId}/items`;
|
|
18
|
+
const headers = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
...opts.userKey ? { "X-User-Key": opts.userKey } : {}
|
|
21
|
+
};
|
|
22
|
+
const url = (key) => `${base}/${encodeURIComponent(key)}`;
|
|
23
|
+
return {
|
|
24
|
+
async get(key) {
|
|
25
|
+
const res = await f(url(key), { headers, credentials: "same-origin" });
|
|
26
|
+
if (res.status === 404) return null;
|
|
27
|
+
if (!res.ok) throw await failure(res);
|
|
28
|
+
return await res.json();
|
|
29
|
+
},
|
|
30
|
+
/** Every item under `prefix`, following pages. */
|
|
31
|
+
async list(prefix = "") {
|
|
32
|
+
const out = [];
|
|
33
|
+
let after = null;
|
|
34
|
+
do {
|
|
35
|
+
const q = new URLSearchParams({ prefix, limit: "500", ...after ? { after } : {} });
|
|
36
|
+
const res = await f(`${base}?${q}`, { headers, credentials: "same-origin" });
|
|
37
|
+
if (!res.ok) throw await failure(res);
|
|
38
|
+
const page = await res.json();
|
|
39
|
+
out.push(...page.items);
|
|
40
|
+
after = page.next;
|
|
41
|
+
} while (after);
|
|
42
|
+
return out;
|
|
43
|
+
},
|
|
44
|
+
/** Replace one item. `expectedVersion` refuses a lost update; 0 means "only if absent". */
|
|
45
|
+
async put(key, value, expectedVersion) {
|
|
46
|
+
const res = await f(url(key), {
|
|
47
|
+
method: "PUT",
|
|
48
|
+
headers,
|
|
49
|
+
credentials: "same-origin",
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
value,
|
|
52
|
+
...expectedVersion !== void 0 ? { expectedVersion } : {}
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
if (res.status === 409) {
|
|
56
|
+
const body = await res.json().catch(() => null);
|
|
57
|
+
return { ok: false, conflict: body?.current ?? null };
|
|
58
|
+
}
|
|
59
|
+
if (!res.ok) throw await failure(res);
|
|
60
|
+
return { ok: true, item: await res.json() };
|
|
61
|
+
},
|
|
62
|
+
async delete(key) {
|
|
63
|
+
const res = await f(url(key), { method: "DELETE", headers, credentials: "same-origin" });
|
|
64
|
+
if (!res.ok) throw await failure(res);
|
|
65
|
+
return (await res.json()).deleted;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function failure(res) {
|
|
70
|
+
const body = await res.json().catch(() => null);
|
|
71
|
+
return new ItemsError(res.status, body?.message ?? body?.error ?? `HTTP ${res.status}`);
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
ItemsError,
|
|
75
|
+
createItemsClient
|
|
76
|
+
};
|
package/dist/service.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Item, ItemWriteResult } from './items.js';
|
|
2
|
+
|
|
1
3
|
interface ServicePrefsOptions {
|
|
2
4
|
/** The app's namespace: `cardtable` reaches `cardtable` and `cardtable-*`. */
|
|
3
5
|
app: string;
|
|
@@ -39,6 +41,18 @@ declare function createServicePrefsClient(opts: ServicePrefsOptions): {
|
|
|
39
41
|
read<T = Record<string, unknown>>(appId: string, userIds: readonly string[]): Promise<ServiceRow<T>[]>;
|
|
40
42
|
/** Every person's row in one app, page by page. */
|
|
41
43
|
all<T = Record<string, unknown>>(appId: string, pageSize?: number): AsyncGenerator<ServiceRow<T>>;
|
|
44
|
+
/** Items across people (these userIds, ≤90 a call, or everyone), under an optional prefix. */
|
|
45
|
+
readItems<T = unknown>(appId: string, opts?: {
|
|
46
|
+
userIds?: readonly string[];
|
|
47
|
+
prefix?: string;
|
|
48
|
+
}): Promise<(Item<T> & {
|
|
49
|
+
userId: string;
|
|
50
|
+
})[]>;
|
|
51
|
+
/** Write one of a person's items, the person named by their own live key. */
|
|
52
|
+
writeItemFor<T = unknown>(appId: string, userKey: string, key: string, value: T, expectedVersion?: number): Promise<ItemWriteResult<T>>;
|
|
53
|
+
deleteItemFor(appId: string, userKey: string, key: string): Promise<boolean>;
|
|
54
|
+
/** Delete EVERY person's items under a non-empty prefix (a deleted set, say). */
|
|
55
|
+
deleteItemsAcross(appId: string, prefix: string): Promise<number>;
|
|
42
56
|
/**
|
|
43
57
|
* Shallow-merge `patch` into the person's row, the person named by their
|
|
44
58
|
* own live key. Pass the `expectedVersion` you read to refuse a lost update.
|
package/dist/service.js
CHANGED
|
@@ -23,6 +23,11 @@ function createServicePrefsClient(opts) {
|
|
|
23
23
|
}
|
|
24
24
|
return `${base}/${appId}`;
|
|
25
25
|
};
|
|
26
|
+
const itemsBase = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_service/${opts.app}/items`;
|
|
27
|
+
const itemsUrl = (appId) => {
|
|
28
|
+
rowsUrl(appId);
|
|
29
|
+
return `${itemsBase}/${appId}`;
|
|
30
|
+
};
|
|
26
31
|
async function page(appId, body) {
|
|
27
32
|
const res = await f(`${rowsUrl(appId)}/read`, {
|
|
28
33
|
method: "POST",
|
|
@@ -55,6 +60,67 @@ function createServicePrefsClient(opts) {
|
|
|
55
60
|
after = p.next;
|
|
56
61
|
} while (after);
|
|
57
62
|
},
|
|
63
|
+
/** Items across people (these userIds, ≤90 a call, or everyone), under an optional prefix. */
|
|
64
|
+
async readItems(appId, opts2 = {}) {
|
|
65
|
+
const out = [];
|
|
66
|
+
const batches = opts2.userIds ? Array.from(
|
|
67
|
+
{ length: Math.ceil(opts2.userIds.length / SERVICE_READ_BATCH) },
|
|
68
|
+
(_, i) => opts2.userIds.slice(i * SERVICE_READ_BATCH, (i + 1) * SERVICE_READ_BATCH)
|
|
69
|
+
) : [void 0];
|
|
70
|
+
for (const userIds of batches) {
|
|
71
|
+
if (userIds && userIds.length === 0) continue;
|
|
72
|
+
let after = null;
|
|
73
|
+
do {
|
|
74
|
+
const res = await f(`${itemsUrl(appId)}/read`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers,
|
|
77
|
+
body: JSON.stringify({ userIds, prefix: opts2.prefix, after, limit: 500 })
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) throw await failure(res);
|
|
80
|
+
const page2 = await res.json();
|
|
81
|
+
out.push(...page2.items);
|
|
82
|
+
after = page2.next;
|
|
83
|
+
} while (after);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
},
|
|
87
|
+
/** Write one of a person's items, the person named by their own live key. */
|
|
88
|
+
async writeItemFor(appId, userKey, key, value, expectedVersion) {
|
|
89
|
+
const res = await f(`${itemsUrl(appId)}/${encodeURIComponent(key)}`, {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
headers,
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
userKey,
|
|
94
|
+
value,
|
|
95
|
+
...expectedVersion !== void 0 ? { expectedVersion } : {}
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
if (res.status === 409) {
|
|
99
|
+
const body = await res.json().catch(() => null);
|
|
100
|
+
return { ok: false, conflict: body?.current ?? null };
|
|
101
|
+
}
|
|
102
|
+
if (!res.ok) throw await failure(res);
|
|
103
|
+
return { ok: true, item: await res.json() };
|
|
104
|
+
},
|
|
105
|
+
async deleteItemFor(appId, userKey, key) {
|
|
106
|
+
const res = await f(`${itemsUrl(appId)}/${encodeURIComponent(key)}`, {
|
|
107
|
+
method: "DELETE",
|
|
108
|
+
headers,
|
|
109
|
+
body: JSON.stringify({ userKey })
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) throw await failure(res);
|
|
112
|
+
return (await res.json()).deleted;
|
|
113
|
+
},
|
|
114
|
+
/** Delete EVERY person's items under a non-empty prefix (a deleted set, say). */
|
|
115
|
+
async deleteItemsAcross(appId, prefix) {
|
|
116
|
+
const res = await f(`${itemsUrl(appId)}/delete-across`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers,
|
|
119
|
+
body: JSON.stringify({ prefix })
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) throw await failure(res);
|
|
122
|
+
return (await res.json()).deleted;
|
|
123
|
+
},
|
|
58
124
|
/**
|
|
59
125
|
* Shallow-merge `patch` into the person's row, the person named by their
|
|
60
126
|
* own live key. Pass the `expectedVersion` you read to refuse a lost update.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadoku/prefs-client",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
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",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"./service": {
|
|
30
30
|
"types": "./dist/service.d.ts",
|
|
31
31
|
"default": "./dist/service.js"
|
|
32
|
+
},
|
|
33
|
+
"./items": {
|
|
34
|
+
"types": "./dist/items.d.ts",
|
|
35
|
+
"default": "./dist/items.js"
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
38
|
"files": [
|