@axium/client 0.27.0 → 0.28.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/assets/theme.css CHANGED
@@ -50,6 +50,7 @@ html {
50
50
 
51
51
  --border-disabled: 1px solid hsl(0 5 calc(var(--bg-light) - var(--light-step)));
52
52
  --border-accent: 1px solid hsl(var(--hue) 10 calc(var(--bg-light) + (var(--light-step) * 3)));
53
+ --border-alt: 1px solid var(--bg-alt);
53
54
  --border-strong: 1px solid hsl(var(--hue) 20 calc(var(--bg-light) + (var(--light-step) * 3)));
54
55
 
55
56
  --border-error: 1px solid hsl(0 50 var(--fg-light));
package/dist/access.js CHANGED
@@ -1,20 +1,23 @@
1
- import * as cache from './cache.js';
1
+ import Cache from './cache.js';
2
2
  import { fetchAPI } from './requests.js';
3
+ const cache = new Cache((itemType, itemId) => fetchAPI('GET', 'acl/:itemType/:itemId', {}, itemType, itemId), {
4
+ ttl: 3600_000,
5
+ });
3
6
  export async function updateACL(itemType, itemId, target, permissions) {
4
7
  const result = await fetchAPI('PATCH', 'acl/:itemType/:itemId', { target, permissions }, itemType, itemId);
5
- cache.invalidate('acl', `${itemType}/${itemId}`);
8
+ cache.invalidate(itemType, itemId);
6
9
  return result;
7
10
  }
8
11
  export async function getACL(itemType, itemId) {
9
- return await cache.use('acl', `${itemType}/${itemId}`, () => fetchAPI('GET', 'acl/:itemType/:itemId', {}, itemType, itemId));
12
+ return await cache.get(itemType, itemId);
10
13
  }
11
14
  export async function addToACL(itemType, itemId, target) {
12
15
  const result = await fetchAPI('PUT', 'acl/:itemType/:itemId', target, itemType, itemId);
13
- cache.invalidate('acl', `${itemType}/${itemId}`);
16
+ cache.invalidate(itemType, itemId);
14
17
  return result;
15
18
  }
16
19
  export async function removeFromACL(itemType, itemId, target) {
17
20
  const result = await fetchAPI('DELETE', 'acl/:itemType/:itemId', target, itemType, itemId);
18
- cache.invalidate('acl', `${itemType}/${itemId}`);
21
+ cache.invalidate(itemType, itemId);
19
22
  return result;
20
23
  }
package/dist/cache.d.ts CHANGED
@@ -1,20 +1,53 @@
1
- export interface Options {
1
+ import * as z from 'zod';
2
+ export interface CacheOptions {
2
3
  /** Maximum number of items the cache can hold */
3
4
  itemLimit: number;
5
+ /** @todo replace with `Temporal.DurationLike` */
6
+ ttl: number;
4
7
  }
8
+ export declare const CacheData: z.ZodRecord<z.ZodString, z.ZodObject<{
9
+ $timestamp: z.ZodCoercedDate<unknown>;
10
+ }, z.core.$loose>>;
11
+ /** @todo replace `$timestamp` with `Temporal.InstantLike` */
12
+ export interface CacheData<V> extends Record<string, V & {
13
+ $timestamp: number;
14
+ }> {
15
+ }
16
+ declare const kTimestamp: unique symbol;
5
17
  /**
6
- * Create a cache manually. Only useful when you want to specify different options.
7
- */
8
- export declare function create(cacheName: string, options?: Partial<Options>): void;
9
- /**
10
- * Use a cache for some arbitrary operation.
18
+ * Cache some arbitrary operation.
11
19
  * This is primarily intended for de-duplicating API requests
12
- * @param cacheName The name of the cache to use, e.g. `'users'`
13
- * @param key The key for the item in the cache. This can be anything used for the key of a `Map`
14
- * @param miss The function to run on a cache miss
15
- * @remarks
16
- * Note that a cache will automatically be created if it doesn't already exist
17
20
  */
18
- export declare function use<Key, Result>(cacheName: string, key: Key, miss: () => Result): Result;
19
- export declare function invalidate(cacheName: string, key: any): void;
20
- export declare function update(cacheName: string, key: any, value: any): void;
21
+ export declare class Cache<Keys extends string[], V extends object> {
22
+ /**
23
+ * Function to run when there is a cache miss
24
+ * @
25
+ */
26
+ protected readonly miss: (...keys: Keys) => V | Promise<V>;
27
+ protected items: Map<string, V & {
28
+ [kTimestamp]: number;
29
+ }>;
30
+ protected pending: Map<string, Promise<V>>;
31
+ readonly options: CacheOptions;
32
+ private onWrite?;
33
+ constructor(
34
+ /**
35
+ * Function to run when there is a cache miss
36
+ * @
37
+ */
38
+ miss: (...keys: Keys) => V | Promise<V>, options?: Partial<CacheOptions>);
39
+ protected key(keys: Keys): string;
40
+ get size(): number;
41
+ protected valid(timestamp?: number): boolean;
42
+ protected write(key: string, value: V | Promise<V>): void;
43
+ get(...keys: Keys): V | Promise<V>;
44
+ set(...args: [...keys: Keys, value: V | Promise<V>]): void;
45
+ invalidate(...keys: Keys): void;
46
+ persist(onWrite: (data: CacheData<V>) => void,
47
+ /** @todo replace `$timestamp` with `Temporal.InstantLike` */
48
+ existingContent?: Record<string, V & {
49
+ $timestamp?: number;
50
+ }>): void;
51
+ toJSON(): CacheData<V>;
52
+ }
53
+ export default Cache;
package/dist/cache.js CHANGED
@@ -1,48 +1,99 @@
1
- const _caches = Object.create(null);
1
+ import * as z from 'zod';
2
2
  const defaultCacheOptions = {
3
3
  itemLimit: 10_000,
4
+ ttl: 3600_000,
4
5
  };
5
- /**
6
- * Create a cache manually. Only useful when you want to specify different options.
7
- */
8
- export function create(cacheName, options = {}) {
9
- if (_caches[cacheName])
10
- throw new Error('Multiple caches with the same name are not allowed');
11
- _caches[cacheName] = {
12
- items: new Map(),
13
- ...defaultCacheOptions,
14
- ...options,
15
- };
6
+ export const CacheData = z.record(z.string(), z.looseObject({
7
+ /** @todo replace with temporal instant */
8
+ $timestamp: z.coerce.date(),
9
+ }));
10
+ function isThenable(value) {
11
+ return !!(value && typeof value == 'object' && 'then' in value && typeof value.then == 'function');
16
12
  }
13
+ const kTimestamp = Symbol('kTimestamp');
17
14
  /**
18
- * Use a cache for some arbitrary operation.
15
+ * Cache some arbitrary operation.
19
16
  * This is primarily intended for de-duplicating API requests
20
- * @param cacheName The name of the cache to use, e.g. `'users'`
21
- * @param key The key for the item in the cache. This can be anything used for the key of a `Map`
22
- * @param miss The function to run on a cache miss
23
- * @remarks
24
- * Note that a cache will automatically be created if it doesn't already exist
25
17
  */
26
- export function use(cacheName, key, miss) {
27
- const cache = (_caches[cacheName] ||= { items: new Map(), ...defaultCacheOptions });
28
- const cached = cache.items.get(key);
29
- if (cached)
30
- return cached;
31
- const result = miss();
32
- cache.items.set(key, result);
33
- return result;
34
- }
35
- export function invalidate(cacheName, key) {
36
- const cache = _caches[cacheName];
37
- if (!cache)
38
- throw new ReferenceError("Can't invalidate key in non-existent cache: " + cacheName);
39
- if (cache)
40
- cache.items.delete(key);
41
- }
42
- export function update(cacheName, key, value) {
43
- const cache = _caches[cacheName];
44
- if (!cache)
45
- throw new ReferenceError("Can't update key in non-existent cache: " + cacheName);
46
- if (cache)
47
- cache.items.set(key, value);
18
+ export class Cache {
19
+ miss;
20
+ items = new Map();
21
+ pending = new Map();
22
+ options;
23
+ onWrite;
24
+ constructor(
25
+ /**
26
+ * Function to run when there is a cache miss
27
+ * @
28
+ */
29
+ miss, options = {}) {
30
+ this.miss = miss;
31
+ this.options = { ...defaultCacheOptions, ...options };
32
+ }
33
+ key(keys) {
34
+ return keys.join(':');
35
+ }
36
+ get size() {
37
+ return this.items.size;
38
+ }
39
+ valid(timestamp) {
40
+ return !this.options.ttl || !timestamp || timestamp.valueOf() + this.options.ttl < Date.now();
41
+ }
42
+ write(key, value) {
43
+ if (isThenable(value)) {
44
+ this.pending.set(key, value);
45
+ void value.then(resolved => {
46
+ this.write(key, resolved);
47
+ this.pending.delete(key);
48
+ });
49
+ return;
50
+ }
51
+ if (this.items.size >= this.options.itemLimit) {
52
+ const [key] = this.items.entries().next().value;
53
+ this.items.delete(key);
54
+ }
55
+ this.items.set(key, Object.assign(value, { [kTimestamp]: Date.now() }));
56
+ this.onWrite?.(this.toJSON());
57
+ }
58
+ get(...keys) {
59
+ const key = this.key(keys);
60
+ const pending = this.pending.get(key);
61
+ if (pending)
62
+ return pending;
63
+ const cached = this.items.get(key);
64
+ if (cached) {
65
+ if (this.valid(cached[kTimestamp]))
66
+ return cached;
67
+ this.items.delete(key);
68
+ }
69
+ const result = this.miss(...keys);
70
+ this.write(key, result);
71
+ return result;
72
+ }
73
+ set(...args) {
74
+ const value = args.pop();
75
+ const key = this.key(args);
76
+ this.write(key, value);
77
+ }
78
+ invalidate(...keys) {
79
+ this.items.delete(this.key(keys));
80
+ }
81
+ persist(onWrite,
82
+ /** @todo replace `$timestamp` with `Temporal.InstantLike` */
83
+ existingContent) {
84
+ this.onWrite = onWrite;
85
+ if (!existingContent)
86
+ return;
87
+ for (const [key, value] of Object.entries(existingContent)) {
88
+ const { $timestamp = Date.now() } = value;
89
+ if (!this.valid($timestamp))
90
+ continue;
91
+ delete value.$timestamp;
92
+ this.items.set(key, Object.assign(value, { [kTimestamp]: $timestamp }));
93
+ }
94
+ }
95
+ toJSON() {
96
+ return Object.fromEntries(this.items.entries().map(([k, v]) => [k, { ...v, $timestamp: v[kTimestamp] }]));
97
+ }
48
98
  }
99
+ export default Cache;
@@ -0,0 +1,2 @@
1
+ export declare const clientUA: string;
2
+ export declare function login(url?: string): Promise<void>;
@@ -0,0 +1,140 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { NewSessionResponse } from '@axium/core';
54
+ import * as io from 'ioium/node';
55
+ import { createServer } from 'node:http';
56
+ import { createInterface } from 'node:readline/promises';
57
+ import { styleText } from 'node:util';
58
+ import { config, resolveServerURL } from '../config.js';
59
+ import { prefix, setPrefix, setToken } from '../requests.js';
60
+ import { getCurrentSession } from '../user.js';
61
+ import * as cache from './cache.js';
62
+ import { saveConfig } from './config.js';
63
+ import * as os from 'node:os';
64
+ import $pkg from '../../package.json' with { type: 'json' };
65
+ export const clientUA = `Axium Client/${$pkg.version} (${os.type()}; ${process.arch})`;
66
+ export async function login(url) {
67
+ const env_1 = { stack: [], error: void 0, hasError: false };
68
+ try {
69
+ const rl = __addDisposableResource(env_1, createInterface({
70
+ input: process.stdin,
71
+ output: process.stdout,
72
+ }), false);
73
+ rl.on('SIGINT', () => io.exit('Aborted.', 7));
74
+ if (prefix[0] != '/')
75
+ url ||= prefix;
76
+ url ||= await rl.question('Axium server URL: ');
77
+ url = resolveServerURL(url);
78
+ setPrefix(url);
79
+ const sessionReady = Promise.withResolvers();
80
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
81
+ const server = createServer(async (req, res) => {
82
+ res.setHeader('access-control-allow-origin', '*');
83
+ res.setHeader('access-control-allow-methods', '*');
84
+ res.setHeader('access-control-allow-headers', '*');
85
+ if (req.method == 'HEAD' || req.method == 'OPTIONS') {
86
+ res.writeHead(200).end();
87
+ return;
88
+ }
89
+ if (!req.headers['content-type']?.endsWith('/json')) {
90
+ res.writeHead(415).end('Unexpected content type');
91
+ return;
92
+ }
93
+ if (req.method !== 'POST') {
94
+ res.writeHead(405).end('Unexpected request method');
95
+ return;
96
+ }
97
+ const { promise: bodyReady, resolve, reject } = Promise.withResolvers();
98
+ let body = '';
99
+ req.on('data', chunk => (body += chunk.toString()));
100
+ req.on('end', resolve);
101
+ req.on('error', reject);
102
+ try {
103
+ await bodyReady;
104
+ res.writeHead(200).end();
105
+ sessionReady.resolve(NewSessionResponse.parse(JSON.parse(body)));
106
+ }
107
+ catch (e) {
108
+ res.statusCode = 500;
109
+ res.end('Internal server error: ' + io.errorText(e));
110
+ sessionReady.reject(io.errorText(e));
111
+ }
112
+ });
113
+ const serverReady = Promise.withResolvers();
114
+ server.listen(() => {
115
+ const { port } = server.address();
116
+ serverReady.resolve(port);
117
+ });
118
+ server.on('error', e => io.exit('Failed to start local callback server: ' + io.errorText(e), 5));
119
+ const port = await serverReady.promise;
120
+ const authURL = new URL(`/login/client?port=${port}&client=${encodeURIComponent(clientUA)}`, url).href;
121
+ console.log('Authenticate by visiting this URL in your browser: ' + styleText('underline', authURL));
122
+ const { token } = await sessionReady.promise.catch(e => io.exit('Failed to obtain session: ' + e, 6));
123
+ setToken(token);
124
+ server.close();
125
+ const session = await io.track('Verifying session', getCurrentSession().catch(e => io.exit(e, 6)));
126
+ io.debug('Session UUID: ' + session.id);
127
+ console.log(`Welcome ${session.user.name}! Your session is valid until ${session.expires.toLocaleDateString()}.`);
128
+ config.token = token;
129
+ config.server = url;
130
+ saveConfig();
131
+ await cache.update(true);
132
+ }
133
+ catch (e_1) {
134
+ env_1.error = e_1;
135
+ env_1.hasError = true;
136
+ }
137
+ finally {
138
+ __disposeResources(env_1);
139
+ }
140
+ }
@@ -0,0 +1,80 @@
1
+ import * as z from 'zod';
2
+ export declare const dir: string;
3
+ declare const create: unique symbol;
4
+ export declare class Handle<S extends z.ZodObject> {
5
+ private init;
6
+ private _data?;
7
+ readonly path: string;
8
+ get schema(): S;
9
+ private constructor();
10
+ load(): void;
11
+ save(): void;
12
+ update(): Promise<void>;
13
+ isValid(): Promise<boolean>;
14
+ get data(): z.infer<S>;
15
+ static [create]<S extends z.ZodObject>(init: Init<S>): Handle<S>;
16
+ }
17
+ export interface Init<S extends z.ZodObject> {
18
+ path: string;
19
+ schema: S;
20
+ update(existing?: z.infer<S>): z.infer<S> | Promise<z.infer<S>>;
21
+ isValid(data: z.infer<S>, lastUpdatedTs: number): boolean | Promise<boolean>;
22
+ onLoad?(data: z.infer<S>): unknown;
23
+ }
24
+ export declare function useAt<S extends z.ZodObject>(init: Init<S>): Handle<S>;
25
+ export declare const meta: Handle<z.ZodObject<{
26
+ fetched: z.ZodInt;
27
+ session: z.ZodObject<{
28
+ id: z.ZodUUID;
29
+ userId: z.ZodUUID;
30
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ expires: z.ZodCoercedDate<unknown>;
32
+ created: z.ZodCoercedDate<unknown>;
33
+ elevated: z.ZodBoolean;
34
+ user: z.ZodObject<{
35
+ id: z.ZodUUID;
36
+ name: z.ZodString;
37
+ email: z.ZodEmail;
38
+ emailVerified: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
39
+ preferences: z.ZodLazy<z.ZodObject<{
40
+ debug: z.ZodDefault<z.ZodBoolean>;
41
+ }, z.core.$strip>>;
42
+ roles: z.ZodArray<z.ZodString>;
43
+ tags: z.ZodArray<z.ZodString>;
44
+ registeredAt: z.ZodCoercedDate<unknown>;
45
+ isAdmin: z.ZodBoolean;
46
+ isSuspended: z.ZodBoolean;
47
+ }, z.core.$strip>;
48
+ }, z.core.$strip>;
49
+ apps: z.ZodArray<z.ZodObject<{
50
+ id: z.ZodString;
51
+ name: z.ZodOptional<z.ZodString>;
52
+ image: z.ZodOptional<z.ZodString>;
53
+ icon: z.ZodOptional<z.ZodString>;
54
+ }, z.core.$strip>>;
55
+ }, z.core.$loose>>;
56
+ export declare const sync: Handle<z.ZodObject<{
57
+ objects: z.ZodArray<z.ZodObject<{
58
+ $type: z.ZodString;
59
+ id: z.ZodUUID;
60
+ }, z.core.$loose>>;
61
+ index: z.ZodCoercedBigInt<unknown>;
62
+ }, z.core.$strip>>;
63
+ export declare function load(): void;
64
+ export declare function update(force?: boolean): Promise<void>;
65
+ export declare function clear(): void;
66
+ export interface CacheInfo {
67
+ path: string;
68
+ size?: bigint;
69
+ exists: boolean;
70
+ }
71
+ export interface CacheInfoLocal extends CacheInfo {
72
+ valid: boolean;
73
+ fromAPI?: false;
74
+ }
75
+ export interface CacheInfoAPI extends CacheInfo {
76
+ entries: number;
77
+ fromAPI: true;
78
+ }
79
+ export declare function info(): AsyncGenerator<CacheInfoLocal | CacheInfoAPI>;
80
+ export {};
@@ -0,0 +1,155 @@
1
+ import { App, Session, SyncState, User } from '@axium/core';
2
+ import * as io from 'ioium/node';
3
+ import { existsSync, mkdirSync, statSync, unlinkSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path/posix';
6
+ import * as z from 'zod';
7
+ import { CacheData } from '../cache.js';
8
+ import { fetchAPI } from '../requests.js';
9
+ import { apiUserCache, getCurrentSession } from '../user.js';
10
+ export const dir = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'axium');
11
+ mkdirSync(dir, { recursive: true });
12
+ const create = Symbol('Handle::new');
13
+ export class Handle {
14
+ init;
15
+ _data;
16
+ path;
17
+ get schema() {
18
+ return this.init.schema;
19
+ }
20
+ constructor(init) {
21
+ this.init = init;
22
+ this.path = resolve(dir, init.path);
23
+ }
24
+ load() {
25
+ if (!existsSync(this.path)) {
26
+ io.debug('Ignoring missing cache file:', this.path);
27
+ return;
28
+ }
29
+ try {
30
+ this._data = io.readJSON(this.path, this.schema);
31
+ }
32
+ catch (e) {
33
+ io.warn(`Failed to load cache from '${this.path}':\n${e}`);
34
+ return;
35
+ }
36
+ this.init.onLoad?.(this._data);
37
+ }
38
+ save() {
39
+ if (!this._data)
40
+ throw new ReferenceError('Cache data is not loaded');
41
+ try {
42
+ io.writeJSON(this.path, this._data);
43
+ }
44
+ catch (e) {
45
+ io.error(e);
46
+ }
47
+ }
48
+ async update() {
49
+ this._data = await this.init.update(this._data);
50
+ this.init.onLoad?.(this._data);
51
+ this.save();
52
+ }
53
+ async isValid() {
54
+ if (!this._data)
55
+ return false;
56
+ return this.init.isValid(this._data, statSync(this.path).mtimeMs);
57
+ }
58
+ get data() {
59
+ return this._data;
60
+ }
61
+ static [create](init) {
62
+ return new Handle(init);
63
+ }
64
+ }
65
+ const handles = [];
66
+ export function useAt(init) {
67
+ const handle = Handle[create](init);
68
+ mkdirSync(dirname(handle.path), { recursive: true });
69
+ handles.push(handle);
70
+ return handle;
71
+ }
72
+ const _dayMs = 24 * 3600_000;
73
+ export const meta = useAt({
74
+ path: 'meta.json',
75
+ schema: z.looseObject({
76
+ fetched: z.int(),
77
+ session: Session.extend({ user: User }),
78
+ apps: App.array(),
79
+ }),
80
+ async update() {
81
+ const [session, apps] = await io.track('Fetching metadata', Promise.all([getCurrentSession(), fetchAPI('GET', 'apps')]));
82
+ return { fetched: Date.now(), session, apps };
83
+ },
84
+ isValid: meta => meta.fetched + _dayMs > Date.now(),
85
+ });
86
+ export const sync = useAt({
87
+ path: 'sync.json',
88
+ schema: SyncState,
89
+ async update(sync) {
90
+ if (!sync)
91
+ return await fetchAPI('GET', 'sync/init');
92
+ const diff = await fetchAPI('GET', 'sync', { since: sync.index });
93
+ const deleted = new Set(diff.deleted);
94
+ const objects = sync.objects.filter(o => !deleted.has(o.id));
95
+ const existing = Object.fromEntries(objects.map(o => [o.id, o]));
96
+ for (const obj of diff.created)
97
+ objects.push(obj);
98
+ for (const updated of diff.updated) {
99
+ const base = existing[updated.id];
100
+ if (!base)
101
+ throw new ReferenceError("Can not update object because it isn't cached");
102
+ if (base.$type !== updated.$type)
103
+ throw new ReferenceError(`Type mismatch whilst updating cache object: currently ${base.$type}, incoming ${updated.$type}`);
104
+ Object.assign(base, updated);
105
+ }
106
+ return { objects, index: diff.index };
107
+ },
108
+ async isValid({ index }) {
109
+ const md = await fetchAPI('GET', 'sync/metadata');
110
+ return md.index >= index;
111
+ },
112
+ });
113
+ const persistedAPICaches = [];
114
+ function persistAPI(cache, path) {
115
+ path = resolve(dir, path);
116
+ let data;
117
+ try {
118
+ data = io.readJSON(path, CacheData);
119
+ }
120
+ catch {
121
+ // missing
122
+ }
123
+ cache.persist(data => io.writeJSON(path, data), data);
124
+ persistedAPICaches.push({ path, cache });
125
+ }
126
+ export function load() {
127
+ for (const handle of handles)
128
+ handle.load();
129
+ persistAPI(apiUserCache, 'users.json');
130
+ }
131
+ export async function update(force = false) {
132
+ for (const handle of handles) {
133
+ if (force || !(await handle.isValid()))
134
+ await handle.update();
135
+ }
136
+ }
137
+ export function clear() {
138
+ for (const handle of handles)
139
+ unlinkSync(handle.path);
140
+ for (const { path } of persistedAPICaches)
141
+ unlinkSync(path);
142
+ }
143
+ export async function* info() {
144
+ for (const handle of handles) {
145
+ const { path } = handle;
146
+ const exists = existsSync(path);
147
+ const { size } = exists ? statSync(path, { bigint: true }) : {};
148
+ yield { path, exists, size, valid: await handle.isValid() };
149
+ }
150
+ for (const { path, cache } of persistedAPICaches) {
151
+ const exists = existsSync(path);
152
+ const { size } = exists ? statSync(path, { bigint: true }) : {};
153
+ yield { path, exists, size, entries: cache.size, fromAPI: true };
154
+ }
155
+ }
@@ -1,5 +1,4 @@
1
1
  export declare const configDir: string;
2
- export declare const cacheDir: string;
3
2
  export declare function session(): {
4
3
  id: string;
5
4
  userId: string;
@@ -24,5 +23,3 @@ export declare function session(): {
24
23
  };
25
24
  export declare function loadConfig(safe: boolean): Promise<void>;
26
25
  export declare function saveConfig(): void;
27
- export declare const _dayMs: number;
28
- export declare function updateCache(force: boolean): Promise<void>;
@@ -1,24 +1,22 @@
1
- import * as io from 'ioium/node';
2
1
  import { loadPlugin } from '@axium/core/node/plugins';
2
+ import * as io from 'ioium/node';
3
3
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path/posix';
6
6
  import { ClientConfig, config } from '../config.js';
7
- import { fetchAPI, setPrefix, setToken } from '../requests.js';
8
- import { getCurrentSession } from '../user.js';
7
+ import { setPrefix, setToken } from '../requests.js';
8
+ import * as cache from './cache.js';
9
9
  export const configDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'axium');
10
10
  mkdirSync(configDir, { recursive: true });
11
11
  const axcConfig = join(configDir, 'config.json');
12
12
  if (!existsSync(axcConfig))
13
13
  writeFileSync(axcConfig, '{}');
14
- export const cacheDir = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'axium');
15
- mkdirSync(cacheDir, { recursive: true });
16
14
  export function session() {
17
15
  if (!config.token)
18
16
  io.exit('Not logged in.', 4);
19
- if (!config.cache)
17
+ if (!cache.meta.data)
20
18
  io.exit('No session data available.', 3);
21
- return config.cache.session;
19
+ return cache.meta.data.session;
22
20
  }
23
21
  export async function loadConfig(safe) {
24
22
  try {
@@ -38,17 +36,3 @@ export function saveConfig() {
38
36
  io.writeJSON(axcConfig, config);
39
37
  io.debug('Saved config to ' + axcConfig);
40
38
  }
41
- export const _dayMs = 24 * 3600_000;
42
- export async function updateCache(force) {
43
- if (!force && config.cache && config.cache.fetched + _dayMs > Date.now())
44
- return;
45
- const [session, apps] = await io.track('Fetching metadata', Promise.all([getCurrentSession(), fetchAPI('GET', 'apps')]));
46
- config.cache = { fetched: Date.now(), session, apps };
47
- try {
48
- io.writeJSON(axcConfig, config);
49
- }
50
- catch (e) {
51
- io.error(e);
52
- return;
53
- }
54
- }
package/dist/cli/index.js CHANGED
@@ -1,77 +1,26 @@
1
1
  #! /usr/bin/env node
2
- var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
3
- if (value !== null && value !== void 0) {
4
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
5
- var dispose, inner;
6
- if (async) {
7
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
8
- dispose = value[Symbol.asyncDispose];
9
- }
10
- if (dispose === void 0) {
11
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
12
- dispose = value[Symbol.dispose];
13
- if (async) inner = dispose;
14
- }
15
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
16
- if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
17
- env.stack.push({ value: value, dispose: dispose, async: async });
18
- }
19
- else if (async) {
20
- env.stack.push({ async: true });
21
- }
22
- return value;
23
- };
24
- var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
25
- return function (env) {
26
- function fail(e) {
27
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
28
- env.hasError = true;
29
- }
30
- var r, s = 0;
31
- function next() {
32
- while (r = env.stack.pop()) {
33
- try {
34
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
35
- if (r.dispose) {
36
- var result = r.dispose.call(r.value);
37
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
38
- }
39
- else s |= 1;
40
- }
41
- catch (e) {
42
- fail(e);
43
- }
44
- }
45
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
46
- if (env.hasError) throw env.error;
47
- }
48
- return next();
49
- };
50
- })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
51
- var e = new Error(message);
52
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
- });
2
+ import { formatBytes } from '@axium/core';
54
3
  import { outputDaemonStatus, pluginText } from '@axium/core/node';
55
4
  import { _findPlugin, plugins } from '@axium/core/plugins';
56
5
  import { CommanderError, program } from 'commander';
57
6
  import * as io from 'ioium/node';
58
- import { createServer } from 'node:http';
59
- import * as os from 'node:os';
60
- import { createInterface } from 'node:readline/promises';
7
+ import { basename } from 'node:path';
61
8
  import { styleText } from 'node:util';
62
9
  import * as z from 'zod';
63
10
  import $pkg from '../../package.json' with { type: 'json' };
64
- import { config, resolveServerURL } from '../config.js';
65
- import { prefix, setPrefix, setToken, useUserAgent } from '../requests.js';
66
- import { getCurrentSession, logout } from '../user.js';
67
- import { loadConfig, saveConfig, updateCache } from './config.js';
11
+ import { config } from '../config.js';
12
+ import { prefix, useUserAgent } from '../requests.js';
13
+ import { logout } from '../user.js';
14
+ import { clientUA, login } from './auth.js';
15
+ import * as cache from './cache.js';
16
+ import { loadConfig, saveConfig, session } from './config.js';
68
17
  const safe = z.stringbool().default(false).parse(process.env.SAFE?.toLowerCase()) || process.argv.includes('--safe');
69
18
  const debug = z.stringbool().default(false).parse(process.env.DEBUG?.toLowerCase()) || process.argv.includes('--debug');
70
19
  if (debug)
71
20
  io._setDebugOutput(true);
72
- const clientUA = `Axium Client/${$pkg.version} (${os.type()}; ${process.arch})`;
73
21
  useUserAgent(clientUA);
74
22
  await loadConfig(safe);
23
+ cache.load();
75
24
  process.on('SIGHUP', () => {
76
25
  io.info('Reloading configuration due to SIGHUP.');
77
26
  void loadConfig(safe);
@@ -84,7 +33,7 @@ program
84
33
  .configureHelp({ showGlobalOptions: true })
85
34
  .option('--debug', 'override debug mode')
86
35
  .option('--no-debug', 'override debug mode')
87
- .option('--refresh-session', 'Force a refresh of session and user metadata from server', false)
36
+ .option('--refresh', 'Force an update of caches from server', false)
88
37
  .option('--cache-only', 'Run entirely from local cache, even if it is expired.', false)
89
38
  .option('--safe', 'do not execute code from plugins', false)
90
39
  .hook('preAction', async (axc, action) => {
@@ -92,101 +41,20 @@ program
92
41
  if (!config.token)
93
42
  return;
94
43
  if (!opt.cacheOnly && action.name() != 'login')
95
- await updateCache(opt.refreshSession);
44
+ await cache.update(opt.refresh);
96
45
  });
97
46
  program.on('option:debug', () => io._setDebugOutput(true));
98
- program
99
- .command('login')
100
- .description('Log in to your account on an Axium server')
101
- .argument('[server]', 'Axium server URL')
102
- .action(async (url) => {
103
- const env_1 = { stack: [], error: void 0, hasError: false };
104
- try {
105
- const rl = __addDisposableResource(env_1, createInterface({
106
- input: process.stdin,
107
- output: process.stdout,
108
- }), false);
109
- rl.on('SIGINT', () => io.exit('Aborted.', 7));
110
- if (prefix[0] != '/')
111
- url ||= prefix;
112
- url ||= await rl.question('Axium server URL: ');
113
- url = resolveServerURL(url);
114
- setPrefix(url);
115
- const sessionReady = Promise.withResolvers();
116
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
117
- const server = createServer(async (req, res) => {
118
- res.setHeader('access-control-allow-origin', '*');
119
- res.setHeader('access-control-allow-methods', '*');
120
- res.setHeader('access-control-allow-headers', '*');
121
- if (req.method == 'HEAD' || req.method == 'OPTIONS') {
122
- res.writeHead(200).end();
123
- return;
124
- }
125
- if (!req.headers['content-type']?.endsWith('/json')) {
126
- res.writeHead(415).end('Unexpected content type');
127
- return;
128
- }
129
- if (req.method !== 'POST') {
130
- res.writeHead(405).end('Unexpected request method');
131
- return;
132
- }
133
- const { promise: bodyReady, resolve, reject } = Promise.withResolvers();
134
- let body = '';
135
- req.on('data', chunk => (body += chunk.toString()));
136
- req.on('end', resolve);
137
- req.on('error', reject);
138
- try {
139
- await bodyReady;
140
- sessionReady.resolve(JSON.parse(body));
141
- res.writeHead(200).end();
142
- }
143
- catch (e) {
144
- res.statusCode = 500;
145
- res.end('Internal server error: ' + e.message);
146
- sessionReady.reject(e.message);
147
- }
148
- });
149
- const serverReady = Promise.withResolvers();
150
- server.listen(() => {
151
- const { port } = server.address();
152
- serverReady.resolve(port);
153
- });
154
- server.on('error', e => io.exit('Failed to start local callback server: ' + e.message, 5));
155
- const port = await serverReady.promise;
156
- const authURL = new URL(`/login/client?port=${port}&client=${encodeURIComponent(clientUA)}`, url).href;
157
- console.log('Authenticate by visiting this URL in your browser: ' + styleText('underline', authURL));
158
- const { token } = await sessionReady.promise.catch(e => io.exit('Failed to obtain session: ' + e, 6));
159
- setToken(token);
160
- server.close();
161
- const session = await io.track('Verifying session', getCurrentSession().catch(e => io.exit(e.message, 6)));
162
- io.debug('Session UUID: ' + session.id);
163
- console.log(`Welcome ${session.user.name}! Your session is valid until ${session.expires.toLocaleDateString()}.`);
164
- config.token = token;
165
- config.server = url;
166
- saveConfig();
167
- await updateCache(true);
168
- }
169
- catch (e_1) {
170
- env_1.error = e_1;
171
- env_1.hasError = true;
172
- }
173
- finally {
174
- __disposeResources(env_1);
175
- }
176
- });
47
+ program.command('login').description('Log in to your account on an Axium server').argument('[server]', 'Axium server URL').action(login);
177
48
  program.command('logout').action(async () => {
178
- if (!config.token)
179
- io.exit('Not logged in.', 4);
180
- if (!config.cache)
181
- io.exit('No session data available.', 3);
182
- await logout(config.cache.session.userId, config.cache.session.id);
49
+ const { id, userId } = session();
50
+ await logout(userId, id);
183
51
  });
184
52
  program.command('status').action(() => {
185
53
  if (!config.token)
186
54
  return console.log('Not logged in.');
187
55
  if (!config.cache)
188
56
  return console.log('No session data available.');
189
- const { session } = config.cache;
57
+ const { session } = cache.meta.data;
190
58
  console.log('Logged in to', new URL(prefix).host);
191
59
  console.log(styleText('whiteBright', 'Session:'), 'valid until', session.expires.toLocaleDateString(), styleText('dim', `(${session.id})`));
192
60
  const { user } = session;
@@ -205,8 +73,8 @@ program
205
73
  for (const plugin of plugins.values())
206
74
  await plugin._client?.run();
207
75
  });
208
- const axiumPlugin = program.command('plugin').alias('plugins').description('Manage plugins');
209
- axiumPlugin
76
+ const axcPlugin = program.command('plugin').alias('plugins').description('Manage plugins');
77
+ axcPlugin
210
78
  .command('list')
211
79
  .alias('ls')
212
80
  .description('List loaded plugins')
@@ -226,7 +94,7 @@ axiumPlugin
226
94
  console.log(plugin.name, opt.versions ? plugin.version : '');
227
95
  }
228
96
  });
229
- axiumPlugin
97
+ axcPlugin
230
98
  .command('info')
231
99
  .description('Get information about a plugin')
232
100
  .argument('<plugin>', 'the plugin to get information about')
@@ -235,7 +103,7 @@ axiumPlugin
235
103
  for (const line of pluginText(plugin))
236
104
  console.log(line);
237
105
  });
238
- axiumPlugin
106
+ axcPlugin
239
107
  .command('remove')
240
108
  .alias('rm')
241
109
  .description('Remove a plugin')
@@ -246,6 +114,32 @@ axiumPlugin
246
114
  plugins.delete(plugin.name);
247
115
  saveConfig();
248
116
  });
117
+ const axcCache = program.command('cache').description('Manage the local cache');
118
+ axcCache
119
+ .command('info')
120
+ .description('Show information about what is being cached')
121
+ .action(async () => {
122
+ let size = 0n, files = 0;
123
+ for await (const info of cache.info()) {
124
+ process.stdout.write(basename(info.path) + ':');
125
+ if (!info.exists) {
126
+ console.log(styleText('dim', ' (missing)'));
127
+ continue;
128
+ }
129
+ files++;
130
+ size += info.size;
131
+ console.log('', styleText('blue', formatBytes(info.size)) + ',', info.fromAPI ? info.entries + ' entries' : info.valid ? styleText('green', 'valid') : styleText('yellow', 'invalid'));
132
+ }
133
+ console.log('Caching', styleText('blue', formatBytes(size)), 'across', styleText('blueBright', files.toString()), 'files');
134
+ });
135
+ axcCache.command('clear').description('Clear the local cache').action(cache.clear);
136
+ axcCache
137
+ .command('refresh')
138
+ .description('Update local caches')
139
+ .option('-f, --force', 'Force a refresh even if the cache is still valid')
140
+ .action(async (opt) => {
141
+ await cache.update(opt.force);
142
+ });
249
143
  try {
250
144
  await program.parseAsync();
251
145
  }
@@ -0,0 +1,17 @@
1
+ import type { ZodObject, ZodUUID } from 'zod';
2
+ export interface $Objects {
3
+ }
4
+ export type ObjectType = keyof $Objects extends never ? string : keyof $Objects;
5
+ type ObjectValues = keyof $Objects extends never ? Record<string, {
6
+ id: string;
7
+ }[]> : {
8
+ [K in keyof $Objects]: ($Objects[K] & {
9
+ id: string;
10
+ })[];
11
+ };
12
+ export declare function useSchema<Type extends ObjectType, S extends ZodObject<{
13
+ id: ZodUUID;
14
+ }>>(type: Type, schema: S): void;
15
+ export declare function get<Type extends ObjectType>(type: Type): ObjectValues[Type];
16
+ export declare function save<Type extends ObjectType>(type: Type, objects: ObjectValues[Type]): void;
17
+ export {};
@@ -0,0 +1,24 @@
1
+ import { sync as syncCache } from './cache.js';
2
+ let _byType;
3
+ function byType() {
4
+ _byType ||= Object.groupBy(syncCache.data.objects, o => o.$type);
5
+ return _byType;
6
+ }
7
+ const schemas = new Map();
8
+ export function useSchema(type, schema) {
9
+ schemas.set(type, schema);
10
+ }
11
+ export function get(type) {
12
+ const value = byType()[type] || [];
13
+ const schema = schemas.get(type);
14
+ if (!schema)
15
+ return value;
16
+ return value.map(obj => schema.parse(obj));
17
+ }
18
+ export function save(type, objects) {
19
+ _byType ||= {};
20
+ _byType[type] = objects;
21
+ syncCache.data.objects = syncCache.data.objects.filter(o => o.$type !== type);
22
+ for (const object of objects)
23
+ syncCache.data.objects.push(Object.assign(object, { $type: type }));
24
+ }
package/dist/config.d.ts CHANGED
@@ -2,37 +2,6 @@ import * as z from 'zod';
2
2
  export declare const ClientConfig: z.ZodObject<{
3
3
  token: z.ZodOptional<z.ZodNullable<z.ZodBase64URL>>;
4
4
  server: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
5
- cache: z.ZodOptional<z.ZodNullable<z.ZodObject<{
6
- fetched: z.ZodInt;
7
- session: z.ZodObject<{
8
- id: z.ZodUUID;
9
- userId: z.ZodUUID;
10
- name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
11
- expires: z.ZodCoercedDate<unknown>;
12
- created: z.ZodCoercedDate<unknown>;
13
- elevated: z.ZodBoolean;
14
- user: z.ZodObject<{
15
- id: z.ZodUUID;
16
- name: z.ZodString;
17
- email: z.ZodEmail;
18
- emailVerified: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
19
- preferences: z.ZodLazy<z.ZodObject<{
20
- debug: z.ZodDefault<z.ZodBoolean>;
21
- }, z.core.$strip>>;
22
- roles: z.ZodArray<z.ZodString>;
23
- tags: z.ZodArray<z.ZodString>;
24
- registeredAt: z.ZodCoercedDate<unknown>;
25
- isAdmin: z.ZodBoolean;
26
- isSuspended: z.ZodBoolean;
27
- }, z.core.$strip>;
28
- }, z.core.$strip>;
29
- apps: z.ZodArray<z.ZodObject<{
30
- id: z.ZodString;
31
- name: z.ZodOptional<z.ZodString>;
32
- image: z.ZodOptional<z.ZodString>;
33
- icon: z.ZodOptional<z.ZodString>;
34
- }, z.core.$strip>>;
35
- }, z.core.$loose>>>;
36
5
  plugins: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
6
  }, z.core.$loose>;
38
7
  export interface ClientConfig extends z.infer<typeof ClientConfig> {
package/dist/config.js CHANGED
@@ -1,17 +1,8 @@
1
1
  import { debug, warn } from 'ioium';
2
- import { App, Session, User } from '@axium/core';
3
2
  import * as z from 'zod';
4
3
  export const ClientConfig = z.looseObject({
5
4
  token: z.base64url().nullish(),
6
5
  server: z.url().nullish(),
7
- // Cache to reduce server load:
8
- cache: z
9
- .looseObject({
10
- fetched: z.int(),
11
- session: Session.extend({ user: User }),
12
- apps: App.array(),
13
- })
14
- .nullish(),
15
6
  plugins: z.string().array().default([]),
16
7
  });
17
8
  export const config = {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from './access.js';
2
- export * from './apps.js';
3
- export * as cache from './cache.js';
2
+ export * as preferences from './preferences.js';
3
+ export * from './cache.js';
4
4
  export * from './config.js';
5
5
  export * from './locales.js';
6
6
  export * from './requests.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from './access.js';
2
- export * from './apps.js';
3
- export * as cache from './cache.js';
2
+ export * as preferences from './preferences.js';
3
+ export * from './cache.js';
4
4
  export * from './config.js';
5
5
  export * from './locales.js';
6
6
  export * from './requests.js';
@@ -0,0 +1,5 @@
1
+ import type { AppPreferences } from '@axium/core';
2
+ export declare function get<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
3
+ export declare function set<A extends string>(userId: string, appId: A, preferences: AppPreferences<A>): Promise<AppPreferences<A>>;
4
+ export declare function clear<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
5
+ export declare function appPref<A extends string, K extends keyof AppPreferences<A>>(userId: string, appId: A, key: K): Promise<AppPreferences<A>[K]>;
@@ -1,33 +1,33 @@
1
1
  import { appPreferences } from '@axium/core';
2
- import * as cache from './cache.js';
2
+ import Cache from './cache.js';
3
3
  import { fetchAPI } from './requests.js';
4
- export async function getAppPreferences(userId, appId) {
4
+ const cache = new Cache(async (userId, appId) => {
5
5
  const schema = appPreferences.get(appId);
6
6
  if (!schema)
7
7
  throw new Error(`Missing schema for "${appId}"`);
8
- const pref = await cache.use('app_preferences', userId + ':' + appId, async () => {
9
- const result = await fetchAPI('GET', 'users/:id/preferences/:appId', {}, userId, appId);
10
- return schema.parse(result);
11
- });
12
- return pref;
8
+ const result = await fetchAPI('GET', 'users/:id/preferences/:appId', {}, userId, appId);
9
+ return schema.parse(result);
10
+ });
11
+ export async function get(userId, appId) {
12
+ return (await cache.get(userId, appId));
13
13
  }
14
- export async function setAppPreferences(userId, appId, preferences) {
14
+ export async function set(userId, appId, preferences) {
15
15
  const schema = appPreferences.get(appId);
16
16
  if (!schema)
17
17
  throw new Error(`Missing schema for "${appId}"`);
18
18
  const result = await fetchAPI('POST', 'users/:id/preferences/:appId', preferences, userId, appId);
19
- cache.update('app_preferences', userId + ':' + appId, result);
19
+ cache.set(userId, appId, result);
20
20
  return schema.parse(result);
21
21
  }
22
- export async function clearAppPreferences(userId, appId) {
22
+ export async function clear(userId, appId) {
23
23
  const schema = appPreferences.get(appId);
24
24
  if (!schema)
25
25
  throw new Error(`Missing schema for "${appId}"`);
26
26
  const result = await fetchAPI('DELETE', 'users/:id/preferences/:appId', {}, userId, appId);
27
- cache.invalidate('app_preferences', userId + ':' + appId);
27
+ cache.invalidate(userId, appId);
28
28
  return schema.parse(result);
29
29
  }
30
30
  export async function appPref(userId, appId, key) {
31
- const pref = await getAppPreferences(userId, appId);
31
+ const pref = await get(userId, appId);
32
32
  return pref[key];
33
33
  }
@@ -1,4 +1,4 @@
1
- import type { APIParameters, APIValues, Endpoint, RequestBody } from '@axium/core/api';
1
+ import type { APIParameters, Endpoint, RequestBody, Result } from '@axium/core/api';
2
2
  import { $API } from '@axium/core/api';
3
3
  import type { RequestMethod } from '@axium/core/requests';
4
4
  export declare let token: string | null;
@@ -9,4 +9,4 @@ export declare function setPrefix(value: string): void;
9
9
  * Only for use on non-browser clients, e.g. Node.js
10
10
  */
11
11
  export declare function useUserAgent(ua: string | null): void;
12
- export declare function fetchAPI<const E extends Endpoint, const M extends keyof $API[E] & RequestMethod>(method: M, endpoint: E, data?: RequestBody<M, E>, ...params: APIParameters<E>): Promise<M extends keyof APIValues[E] ? (APIValues[E][M] extends readonly [unknown, infer R] ? R : APIValues[E][M]) : unknown>;
12
+ export declare function fetchAPI<const E extends Endpoint, const M extends keyof $API[E] & RequestMethod>(method: M, endpoint: E, data?: RequestBody<M, E>, ...params: APIParameters<E>): Promise<Result<M, E>>;
package/dist/user.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { NewSessionResponse, Passkey, PasskeyChangeable, Session, User, UserPublic, Verification } from '@axium/core';
2
2
  import * as z from 'zod';
3
+ import Cache from './cache.js';
3
4
  export declare function login(userId: string): Promise<NewSessionResponse>;
4
5
  /**
5
6
  * Create an elevated session for the user to perform sensitive actions.
@@ -14,7 +15,9 @@ export declare function logout(userId: string, ...sessionId: string[]): Promise<
14
15
  export declare function logoutAll(userId: string): Promise<Session[]>;
15
16
  export declare function logoutCurrentSession(): Promise<Session>;
16
17
  export declare function register(_data: Record<string, unknown>): Promise<void>;
17
- export declare function userInfo(userId: string): Promise<UserPublic & Partial<User>>;
18
+ declare const userCache: Cache<[string], UserPublic>;
19
+ export { userCache as apiUserCache };
20
+ export declare function userInfo(userId: string): Promise<UserPublic>;
18
21
  export declare function updateUser(userId: string, data: Record<string, FormDataEntryValue>): Promise<User>;
19
22
  export declare function fullUserInfo(userId: string): Promise<User & {
20
23
  sessions: Session[];
package/dist/user.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
2
2
  import * as z from 'zod';
3
3
  import { fetchAPI } from './requests.js';
4
- import * as cache from './cache.js';
4
+ import Cache from './cache.js';
5
5
  export async function login(userId) {
6
6
  const options = await fetchAPI('PUT', 'users/:id/auth', { type: 'login' }, userId);
7
7
  const response = await startAuthentication({ optionsJSON: options });
@@ -62,14 +62,16 @@ function _checkId(userId) {
62
62
  throw e instanceof z.core.$ZodError ? z.prettifyError(e) : e;
63
63
  }
64
64
  }
65
+ const userCache = new Cache((userId) => fetchAPI('GET', 'users/:id', {}, userId), { ttl: 86400_000 });
66
+ export { userCache as apiUserCache };
65
67
  export async function userInfo(userId) {
66
68
  _checkId(userId);
67
- return await cache.use('user', userId, () => fetchAPI('GET', 'users/:id', {}, userId));
69
+ return await userCache.get(userId);
68
70
  }
69
71
  export async function updateUser(userId, data) {
70
72
  _checkId(userId);
71
73
  const result = await fetchAPI('PATCH', 'users/:id', data, userId);
72
- cache.update('user', userId, result);
74
+ userCache.set(userId, result);
73
75
  return result;
74
76
  }
75
77
  export async function fullUserInfo(userId) {
@@ -86,7 +88,7 @@ export async function deleteUser(userId, deletingId = userId) {
86
88
  const response = await startAuthentication({ optionsJSON: options });
87
89
  await fetchAPI('POST', 'users/:id/auth', response, deletingId);
88
90
  const result = await fetchAPI('DELETE', 'users/:id', response, userId);
89
- cache.invalidate('user', userId);
91
+ userCache.invalidate(userId);
90
92
  return result;
91
93
  }
92
94
  export async function emailVerificationEnabled(userId) {
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { getAppPreferences, setAppPreferences, text } from '@axium/client';
2
+ import { text } from '@axium/client';
3
+ import { preferences as uap } from '@axium/client';
3
4
  import { structurallyEqual } from 'utilium';
4
5
  import type { ZodObject } from 'zod';
5
6
  import ZodInput from './ZodInput.svelte';
@@ -12,7 +13,7 @@
12
13
  _parentDialog,
13
14
  }: { userId: string; appId: string; schema: ZodObject; _parentDialog?: HTMLDialogElement } = $props();
14
15
 
15
- let initialValue = $state(await getAppPreferences(userId, appId));
16
+ let initialValue = $state(await uap.get(userId, appId));
16
17
  let currentValue = $state({ ...initialValue });
17
18
 
18
19
  function cancel() {
@@ -21,7 +22,7 @@
21
22
  }
22
23
 
23
24
  async function save() {
24
- initialValue = await setAppPreferences(userId, appId, currentValue);
25
+ initialValue = await uap.set(userId, appId, currentValue);
25
26
  if (!_parentDialog) return;
26
27
  _parentDialog.close();
27
28
  toast('success', text('AppPreferences.toast_saved'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/client",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "author": "James Prevett <jp@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -45,7 +45,7 @@
45
45
  "build": "tsc"
46
46
  },
47
47
  "peerDependencies": {
48
- "@axium/core": ">=0.30.0",
48
+ "@axium/core": ">=0.32.0",
49
49
  "ioium": "^1.0.2",
50
50
  "semver": "^7.7.4",
51
51
  "svelte": "^5.36.0",
package/dist/apps.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import type { AppPreferences } from '@axium/core';
2
- export declare function getAppPreferences<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
3
- export declare function setAppPreferences<A extends string>(userId: string, appId: A, preferences: AppPreferences<A>): Promise<AppPreferences<A>>;
4
- export declare function clearAppPreferences<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
5
- export declare function appPref<A extends string, K extends keyof AppPreferences<A>>(userId: string, appId: A, key: K): Promise<AppPreferences<A>[K]>;