eraspace 0.3.0 → 0.4.1
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 +2 -0
- package/bin.js +664 -177
- package/package.json +1 -1
- package/sdk/src/client/client.ts +10 -6
- package/sdk/src/client/files.ts +2 -0
- package/sdk/src/client/index.ts +1 -0
- package/sdk/src/client/sub-key.ts +3 -0
- package/templates/chat/client/index.tsx +11 -3
package/package.json
CHANGED
package/sdk/src/client/client.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { Pager } from "./pagination.ts";
|
|
|
13
13
|
|
|
14
14
|
export { readConfig, type EraspaceConfig, type ClientOptions } from "./config.ts";
|
|
15
15
|
export { subKey } from "./sub-key.ts";
|
|
16
|
-
export type { EraspaceClient, Room, PresenceEntry, MutationOptions, OptimisticStore, PaginatedQueryName, PaginatedQueryResult } from "./types.ts";
|
|
16
|
+
export type { EraspaceClient, Room, PresenceEntry, MutationOptions, OptimisticStore, PaginatedQueryName, PaginatedQueryResult, Status } from "./types.ts";
|
|
17
17
|
|
|
18
18
|
type Fn = (...args: any[]) => any;
|
|
19
19
|
|
|
@@ -114,11 +114,8 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
114
114
|
|
|
115
115
|
function useRoom(room: string, meta?: Record<string, unknown>): Room<any, any> {
|
|
116
116
|
const r = useRoomConn(room, meta);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self: r.self.value,
|
|
120
|
-
state: r.state.value,
|
|
121
|
-
error: r.error.value,
|
|
117
|
+
// Stable per room, so effects and memoized children that depend on them do not rerun every render.
|
|
118
|
+
const methods = useMemo(() => ({
|
|
122
119
|
broadcast: (event: string, payload?: unknown) => {
|
|
123
120
|
void r.channel.push("broadcast", { event, payload: payload ?? null }).catch(error => { r.error.value = EraspaceError.from(error); });
|
|
124
121
|
},
|
|
@@ -131,6 +128,13 @@ export function createClient<C>(options: ClientOptions = {}): EraspaceClient<C>
|
|
|
131
128
|
setState: (key: string, value: unknown) => {
|
|
132
129
|
r.setState(key, value);
|
|
133
130
|
},
|
|
131
|
+
}), [r]);
|
|
132
|
+
return {
|
|
133
|
+
presence: r.members.value,
|
|
134
|
+
self: r.self.value,
|
|
135
|
+
state: r.state.value,
|
|
136
|
+
error: r.error.value,
|
|
137
|
+
...methods,
|
|
134
138
|
};
|
|
135
139
|
}
|
|
136
140
|
|
package/sdk/src/client/files.ts
CHANGED
|
@@ -343,6 +343,8 @@ export function FilePreview(props: FilePreviewProps) {
|
|
|
343
343
|
if (meta === undefined || meta === null || meta === "failed" || broken === key) {
|
|
344
344
|
// A media error cannot tell a missing file from a failed request, so every failure offers a retry.
|
|
345
345
|
const retry = () => {
|
|
346
|
+
// Found metadata stays cached, so a file deleted since would show as found again and fail the same way.
|
|
347
|
+
infoCache.delete(client.storage.url(key));
|
|
346
348
|
setBroken(null);
|
|
347
349
|
setFetched(undefined);
|
|
348
350
|
setAttempt((n) => n + 1);
|
package/sdk/src/client/index.ts
CHANGED
|
@@ -18,9 +18,12 @@ function canonical(v: unknown): unknown {
|
|
|
18
18
|
// Own data properties, so a `__proto__` key stays a key rather than setting the prototype.
|
|
19
19
|
return Object.fromEntries(Object.keys(o).sort().map((k) => [k, canonical(o[k])]));
|
|
20
20
|
}
|
|
21
|
+
/** Sorted-key JSON. Like `JSON.stringify` and `canonicalArgs`, it serializes what `toJSON` returns, so two Dates differ. */
|
|
21
22
|
export function stableStringify(v: unknown): string {
|
|
22
23
|
if (v === undefined) return "null";
|
|
23
24
|
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
25
|
+
const json = (v as { toJSON?: unknown }).toJSON;
|
|
26
|
+
if (typeof json === "function") return stableStringify(json.call(v));
|
|
24
27
|
if (Array.isArray(v)) return "[" + v.map(stableStringify).join(",") + "]";
|
|
25
28
|
const o = v as Record<string, unknown>;
|
|
26
29
|
const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createClient, FilePreview, FileUpload, type FileRef } from "eraspace/client";
|
|
2
|
-
import { useState, useEffect } from "preact/hooks";
|
|
2
|
+
import { useState, useEffect, useRef } from "preact/hooks";
|
|
3
3
|
import type app from "../server/index";
|
|
4
4
|
const client = createClient<typeof app>();
|
|
5
5
|
export default function App() {
|
|
@@ -12,7 +12,12 @@ export default function App() {
|
|
|
12
12
|
const [file, setFile] = useState<FileRef | null>(null);
|
|
13
13
|
const messages = client.useQuery("messages", room);
|
|
14
14
|
const send = client.useMutation("send");
|
|
15
|
-
|
|
15
|
+
// Room messages share the app's request budget, so presence takes the name once typing pauses and the typing
|
|
16
|
+
// notice goes out at most once a second; the 1.5 s display below bridges the gap.
|
|
17
|
+
const [shownName, setShownName] = useState(author);
|
|
18
|
+
useEffect(() => { const timer = setTimeout(() => setShownName(author), 500); return () => clearTimeout(timer); }, [author]);
|
|
19
|
+
const lastTyping = useRef(0);
|
|
20
|
+
const channel = client.useRoom<{ name: string }> (room, { name: shownName });
|
|
16
21
|
useEffect(() => channel.on("typing", (payload: unknown) => {
|
|
17
22
|
if (typeof payload === "string") setTyping(payload);
|
|
18
23
|
}), [room]);
|
|
@@ -29,7 +34,10 @@ export default function App() {
|
|
|
29
34
|
try { await send(room, author, body, file); setBody(""); setFile(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSending(false); }
|
|
30
35
|
}}>
|
|
31
36
|
<input aria-label="Your name" value={author} maxLength={40} onInput={e => setAuthor(e.currentTarget.value)}/>
|
|
32
|
-
<input aria-label="Message" placeholder="Message" value={body} maxLength={2000} required={!file} onInput={e => {
|
|
37
|
+
<input aria-label="Message" placeholder="Message" value={body} maxLength={2000} required={!file} onInput={e => {
|
|
38
|
+
setBody(e.currentTarget.value);
|
|
39
|
+
if (Date.now() - lastTyping.current >= 1000) { lastTyping.current = Date.now(); channel.broadcast("typing", author); }
|
|
40
|
+
}}/>
|
|
33
41
|
<button disabled={sending || (!body.trim() && !file)}>Send</button>
|
|
34
42
|
{file
|
|
35
43
|
? <div class="pending"><FilePreview client={client} file={file} caption={false} class="attachment"/><span>{file.name}</span><button type="button" class="subtle" disabled={sending} onClick={() => { void client.storage.delete(file.key).catch(() => {}); setFile(null); }}>Remove</button></div>
|