Updated Wrapper component, added queue for all ttp requests, updated some desktop app configuration,
This commit is contained in:
parent
e15ec4bb3f
commit
0903f96ba3
10 changed files with 207 additions and 106 deletions
|
|
@ -6,9 +6,11 @@ export default {
|
||||||
app: {
|
app: {
|
||||||
name: "tensamin",
|
name: "tensamin",
|
||||||
identifier: "client.tensamin.net",
|
identifier: "client.tensamin.net",
|
||||||
version: version,
|
version,
|
||||||
|
urlSchemes: ["tensamin"],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
useAsar: true,
|
||||||
views: {
|
views: {
|
||||||
mainview: {
|
mainview: {
|
||||||
entrypoint: "src/mainview/index.ts",
|
entrypoint: "src/mainview/index.ts",
|
||||||
|
|
@ -16,12 +18,17 @@ export default {
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
"../web/dist/": "views/mainview/",
|
"../web/dist/": "views/mainview/",
|
||||||
|
"../mobile/src-tauri/icons": "views/mainview/icons/",
|
||||||
},
|
},
|
||||||
mac: {
|
mac: {
|
||||||
|
icons: "",
|
||||||
|
bundleWGPU: false,
|
||||||
defaultRenderer: "cef",
|
defaultRenderer: "cef",
|
||||||
bundleCEF: true,
|
bundleCEF: true,
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
|
icon: "../mobile/src-tauri/icons/icon.png",
|
||||||
|
bundleWGPU: false,
|
||||||
defaultRenderer: "cef",
|
defaultRenderer: "cef",
|
||||||
bundleCEF: true,
|
bundleCEF: true,
|
||||||
chromiumFlags: {
|
chromiumFlags: {
|
||||||
|
|
@ -30,6 +37,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
win: {
|
win: {
|
||||||
|
icon: "../mobile/src-tauri/icons/icon.ico",
|
||||||
|
bundleWGPU: false,
|
||||||
defaultRenderer: "cef",
|
defaultRenderer: "cef",
|
||||||
bundleCEF: true,
|
bundleCEF: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "bunx prettier --write .",
|
"format": "bunx prettier --write .",
|
||||||
"lint": "echo desktop lint skipped",
|
"lint": "echo desktop lint skipped",
|
||||||
"build": "echo desktop build skipped",
|
"build": "electrobun build",
|
||||||
"start": "electrobun dev",
|
"start": "electrobun dev",
|
||||||
"dev": "electrobun dev --watch"
|
"dev": "electrobun dev --watch"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import { BrowserWindow } from "electrobun/bun";
|
import { BrowserWindow, Tray } from "electrobun/bun";
|
||||||
|
|
||||||
|
const tray = new Tray({
|
||||||
|
title: "Tensamin",
|
||||||
|
image: "views://mainview/icons/64x64.png",
|
||||||
|
height: 64,
|
||||||
|
width: 64,
|
||||||
|
});
|
||||||
|
|
||||||
// Create the main application window
|
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
title: "Hello Electrobun!",
|
title: "Tensamin",
|
||||||
url: "views://mainview/index.html",
|
url: "views://mainview/index.html",
|
||||||
renderer: "cef",
|
renderer: "cef",
|
||||||
frame: {
|
frame: {
|
||||||
|
|
@ -15,4 +21,8 @@ const mainWindow = new BrowserWindow({
|
||||||
|
|
||||||
mainWindow.url = "views://mainview/index.html";
|
mainWindow.url = "views://mainview/index.html";
|
||||||
|
|
||||||
console.log("Hello Electrobun app started!");
|
tray.on("tray-clicked", () => {
|
||||||
|
console.log("Tray clicked");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Started desktop app");
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
|
||||||
import Wrapper from "@tensamin/user/wrapper";
|
import Wrapper from "@tensamin/user/wrapper";
|
||||||
import * as React from "react";
|
|
||||||
import { Basic, Loading } from "./modals/basic";
|
import { Basic, Loading } from "./modals/basic";
|
||||||
import List from "@/features/conversation/list/body";
|
import List from "@/features/conversation/list/body";
|
||||||
|
|
||||||
|
|
@ -19,14 +17,7 @@ import { useLocation } from "@tanstack/react-router";
|
||||||
* @returns Sidebar JSX.
|
* @returns Sidebar JSX.
|
||||||
*/
|
*/
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const [userId, setUserId] = React.useState<undefined | number>(undefined);
|
|
||||||
const { load } = useStorage();
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
load("user_id").then(setUserId);
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -38,7 +29,7 @@ export default function Sidebar() {
|
||||||
<div>
|
<div>
|
||||||
<Wrapper
|
<Wrapper
|
||||||
loading={<Loading />}
|
loading={<Loading />}
|
||||||
userId={userId}
|
userId={"own"}
|
||||||
component={(user) => <Basic user={user} />}
|
component={(user) => <Basic user={user} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
3
bun.lock
3
bun.lock
|
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
"apps/mobile": {
|
"apps/mobile": {
|
||||||
"name": "@tensamin/mobile",
|
"name": "@tensamin/mobile",
|
||||||
"version": "0.1.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
|
@ -220,6 +220,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tensamin/shared": "workspace:*",
|
"@tensamin/shared": "workspace:*",
|
||||||
|
"@tensamin/storage": "workspace:*",
|
||||||
"@tensamin/ttp": "workspace:*",
|
"@tensamin/ttp": "workspace:*",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|
|
||||||
|
|
@ -189,14 +189,14 @@ export default function Provider(props: {
|
||||||
* Sends typed protocol messages through the active transport client.
|
* Sends typed protocol messages through the active transport client.
|
||||||
* @param type Protocol message type.
|
* @param type Protocol message type.
|
||||||
* @param data Optional request payload.
|
* @param data Optional request payload.
|
||||||
* @param options Optional request id and response mode.
|
* @param options Optional request id.
|
||||||
* @returns A promise for either void (no response) or typed message payload.
|
* @returns A promise for the typed message payload.
|
||||||
*/
|
*/
|
||||||
const send = useCallback<BoundSendFn<Schemas>>(
|
const send = useCallback<BoundSendFn<Schemas>>(
|
||||||
((
|
((
|
||||||
type: string,
|
type: string,
|
||||||
data?: Record<string, unknown>,
|
data?: Record<string, unknown>,
|
||||||
options?: { id?: number; noResponse?: boolean },
|
options?: { id?: number },
|
||||||
) => {
|
) => {
|
||||||
const client = clientRef.current;
|
const client = clientRef.current;
|
||||||
|
|
||||||
|
|
@ -204,16 +204,8 @@ export default function Provider(props: {
|
||||||
return Promise.reject(new Error("ttp is not connected"));
|
return Promise.reject(new Error("ttp is not connected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.noResponse) {
|
|
||||||
return client.send(type as keyof Schemas & string, data as never, {
|
|
||||||
...options,
|
|
||||||
noResponse: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.send(type as keyof Schemas & string, data as never, {
|
return client.send(type as keyof Schemas & string, data as never, {
|
||||||
...options,
|
...options,
|
||||||
noResponse: false,
|
|
||||||
});
|
});
|
||||||
}) as BoundSendFn<Schemas>,
|
}) as BoundSendFn<Schemas>,
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ type MockTransportInstance = {
|
||||||
ready: Promise<void>;
|
ready: Promise<void>;
|
||||||
closed: Promise<void>;
|
closed: Promise<void>;
|
||||||
createUnidirectionalStream: () => Promise<MockStream>;
|
createUnidirectionalStream: () => Promise<MockStream>;
|
||||||
incomingUnidirectionalStreams: {
|
incomingUnidirectionalStreams: ReadableStream<ReadableStream<Uint8Array>>;
|
||||||
getReader: () => MockReader;
|
|
||||||
};
|
|
||||||
close: () => void;
|
close: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,6 +67,10 @@ function createMockWebTransport() {
|
||||||
|
|
||||||
let readyResolve!: () => void;
|
let readyResolve!: () => void;
|
||||||
let closedResolve!: () => void;
|
let closedResolve!: () => void;
|
||||||
|
let incomingController:
|
||||||
|
| ReadableStreamDefaultController<ReadableStream<Uint8Array>>
|
||||||
|
| null = null;
|
||||||
|
const outgoingMessages: TypedMessage[] = [];
|
||||||
|
|
||||||
const ready = new Promise<void>((resolve) => {
|
const ready = new Promise<void>((resolve) => {
|
||||||
readyResolve = resolve;
|
readyResolve = resolve;
|
||||||
|
|
@ -87,17 +89,19 @@ function createMockWebTransport() {
|
||||||
getWriter: () => writer,
|
getWriter: () => writer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const incomingUnidirectionalStreams = new ReadableStream<
|
||||||
|
ReadableStream<Uint8Array>
|
||||||
|
>({
|
||||||
|
start(controller) {
|
||||||
|
incomingController = controller;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const transport: MockTransportInstance = {
|
const transport: MockTransportInstance = {
|
||||||
ready,
|
ready,
|
||||||
closed,
|
closed,
|
||||||
createUnidirectionalStream: async () => stream,
|
createUnidirectionalStream: async () => stream,
|
||||||
incomingUnidirectionalStreams: {
|
incomingUnidirectionalStreams,
|
||||||
getReader: () => ({
|
|
||||||
read: async () => ({ done: true }),
|
|
||||||
releaseLock: () => {},
|
|
||||||
cancel: async () => {},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
close: () => {},
|
close: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -109,10 +113,32 @@ function createMockWebTransport() {
|
||||||
close = transport.close;
|
close = transport.close;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pushIncomingMessage = (message: TypedMessage) => {
|
||||||
|
if (!incomingController) {
|
||||||
|
throw new Error("Incoming stream controller is not ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = wrapForDecode(encodeCommunicationMessage(message));
|
||||||
|
const incomingStream = new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(frame);
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
incomingController.enqueue(incomingStream);
|
||||||
|
};
|
||||||
|
|
||||||
|
writer.write = async (chunk: Uint8Array) => {
|
||||||
|
outgoingMessages.push(decodeCommunicationMessage(chunk));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readyResolve,
|
readyResolve,
|
||||||
closedResolve,
|
closedResolve,
|
||||||
MockWebTransport,
|
MockWebTransport,
|
||||||
|
outgoingMessages,
|
||||||
|
pushIncomingMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,6 +404,61 @@ describe("Core Protocol", () => {
|
||||||
|
|
||||||
await expectRejection(sendPromise);
|
await expectRejection(sendPromise);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("queues sends until the active request receives a response", async () => {
|
||||||
|
const {
|
||||||
|
readyResolve,
|
||||||
|
pushIncomingMessage,
|
||||||
|
outgoingMessages,
|
||||||
|
MockWebTransport,
|
||||||
|
} = createMockWebTransport();
|
||||||
|
const globalWithWebTransport = globalThis as unknown as {
|
||||||
|
WebTransport?: new (url: string) => MockTransportInstance;
|
||||||
|
};
|
||||||
|
globalWithWebTransport.WebTransport = MockWebTransport;
|
||||||
|
|
||||||
|
const schemas: SchemaMap = {
|
||||||
|
ping: {
|
||||||
|
request: z.object({}),
|
||||||
|
response: z.object({}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = createTransportClient(schemas);
|
||||||
|
const connectPromise = client.connect("http://localhost:8000");
|
||||||
|
readyResolve();
|
||||||
|
await connectPromise;
|
||||||
|
|
||||||
|
const firstSend = client.send("ping", {});
|
||||||
|
const secondSend = client.send("ping", {});
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 10 && outgoingMessages.length === 0; attempt += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(outgoingMessages.length).toBe(1);
|
||||||
|
|
||||||
|
pushIncomingMessage({
|
||||||
|
id: outgoingMessages[0].id,
|
||||||
|
type: "ping",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await firstSend;
|
||||||
|
for (let attempt = 0; attempt < 10 && outgoingMessages.length === 1; attempt += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(outgoingMessages.length).toBe(2);
|
||||||
|
|
||||||
|
pushIncomingMessage({
|
||||||
|
id: outgoingMessages[1].id,
|
||||||
|
type: "ping",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await secondSend;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Push subscriptions", () => {
|
describe("Push subscriptions", () => {
|
||||||
|
|
|
||||||
|
|
@ -69,20 +69,13 @@ export type SchemaMap = Record<
|
||||||
|
|
||||||
type SendOptions = {
|
type SendOptions = {
|
||||||
id?: number;
|
id?: number;
|
||||||
noResponse?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BoundSendFn<T extends SchemaMap> = {
|
export type BoundSendFn<T extends SchemaMap> = {
|
||||||
<K extends keyof T & string>(
|
<K extends keyof T & string>(
|
||||||
type: K,
|
type: K,
|
||||||
data: z.input<T[K]["request"]>,
|
data: z.input<T[K]["request"]>,
|
||||||
options: { id?: number; noResponse: true },
|
options?: { id?: number },
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
<K extends keyof T & string>(
|
|
||||||
type: K,
|
|
||||||
data: z.input<T[K]["request"]>,
|
|
||||||
options?: { id?: number; noResponse?: false },
|
|
||||||
): Promise<TypedMessage<z.output<T[K]["response"]>>>;
|
): Promise<TypedMessage<z.output<T[K]["response"]>>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,6 +118,7 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
let currentConnection: ActiveConnection | null = null;
|
let currentConnection: ActiveConnection | null = null;
|
||||||
let currentReadyState: number = READY_STATE.CLOSED;
|
let currentReadyState: number = READY_STATE.CLOSED;
|
||||||
let nextRequestId = 1;
|
let nextRequestId = 1;
|
||||||
|
let sendQueueTail: Promise<void> = Promise.resolve();
|
||||||
let configuredUrl = options.url;
|
let configuredUrl = options.url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -192,6 +186,20 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
notifyClosed(connection, error);
|
notifyClosed(connection, error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes outbound send work so only one request is active at a time.
|
||||||
|
* @param task Request task to run in queue order.
|
||||||
|
* @returns Promise for the task result.
|
||||||
|
*/
|
||||||
|
const enqueueSend = <T>(task: () => Promise<T>) => {
|
||||||
|
const queuedTask = sendQueueTail.then(task, task);
|
||||||
|
sendQueueTail = queuedTask.then(
|
||||||
|
() => undefined,
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
return queuedTask;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles decoded incoming messages and resolves request promises or push listeners.
|
* Handles decoded incoming messages and resolves request promises or push listeners.
|
||||||
* @param message Decoded incoming message.
|
* @param message Decoded incoming message.
|
||||||
|
|
@ -515,8 +523,8 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
* Sends a typed protocol request over the current connection.
|
* Sends a typed protocol request over the current connection.
|
||||||
* @param type Protocol message type.
|
* @param type Protocol message type.
|
||||||
* @param input Optional request payload.
|
* @param input Optional request payload.
|
||||||
* @param options Optional id and response behavior.
|
* @param options Optional request id.
|
||||||
* @returns Promise for response message or void when no response is expected.
|
* @returns Promise for the typed response message.
|
||||||
*/
|
*/
|
||||||
const send: BoundSendFn<T> = ((
|
const send: BoundSendFn<T> = ((
|
||||||
type: string,
|
type: string,
|
||||||
|
|
@ -527,8 +535,6 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
return Promise.reject(new Error("Transport is not connected"));
|
return Promise.reject(new Error("Transport is not connected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = currentConnection;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const schema = schemas[type];
|
const schema = schemas[type];
|
||||||
let payload: Record<string, unknown>;
|
let payload: Record<string, unknown>;
|
||||||
|
|
@ -555,69 +561,66 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
payload = coercePayload(input ?? {});
|
payload = coercePayload(input ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.id && !options?.noResponse) {
|
const requestOptions = options ? { ...options } : {};
|
||||||
const array = new Uint32Array(1);
|
const enqueuedConnection = currentConnection;
|
||||||
crypto.getRandomValues(array);
|
|
||||||
options = options ?? {};
|
|
||||||
options.id = array[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type !== "ping") {
|
return enqueueSend(() => {
|
||||||
log(2, "ttp", "gray", "Sent:", type, payload, { id: options.id });
|
if (
|
||||||
}
|
!enqueuedConnection ||
|
||||||
|
currentConnection !== enqueuedConnection ||
|
||||||
|
currentReadyState !== READY_STATE.OPEN ||
|
||||||
|
enqueuedConnection.closeNotified
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error("Transport is not connected"));
|
||||||
|
}
|
||||||
|
|
||||||
const expectsResponse = !options?.noResponse;
|
const requestId = resolveRequestId(
|
||||||
const requestId = resolveRequestId(
|
requestOptions.id,
|
||||||
options.id,
|
true,
|
||||||
expectsResponse,
|
pending,
|
||||||
pending,
|
() => {
|
||||||
() => {
|
const current = nextRequestId;
|
||||||
const current = nextRequestId;
|
nextRequestId = current >= MAX_REQUEST_ID ? 1 : current + 1;
|
||||||
nextRequestId = current >= MAX_REQUEST_ID ? 1 : current + 1;
|
return current;
|
||||||
return current;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageBytes = encodeCommunicationMessage({
|
|
||||||
id: requestId,
|
|
||||||
type,
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!expectsResponse) {
|
|
||||||
return writeMessageOnPersistentStream(connection, messageBytes).catch(
|
|
||||||
(error) => {
|
|
||||||
handleConnectionFailure(connection, error);
|
|
||||||
throw error;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<TypedMessage>((resolve, reject) => {
|
if (type !== "ping") {
|
||||||
const timeoutId = setTimeout(() => {
|
log(2, "ttp", "gray", "Sent:", type, payload, { id: requestId });
|
||||||
pending.delete(requestId);
|
}
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
`Request "${type}" timed out after ${RESPONSE_TIMEOUT}ms`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, RESPONSE_TIMEOUT);
|
|
||||||
|
|
||||||
pending.set(requestId, {
|
const messageBytes = encodeCommunicationMessage({
|
||||||
requestType: type,
|
id: requestId,
|
||||||
resolve,
|
type,
|
||||||
reject,
|
data: payload,
|
||||||
timeoutId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
void writeMessageOnPersistentStream(connection, messageBytes).catch(
|
return new Promise<TypedMessage>((resolve, reject) => {
|
||||||
(error) => {
|
const timeoutId = setTimeout(() => {
|
||||||
handleConnectionFailure(connection, error);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
pending.delete(requestId);
|
pending.delete(requestId);
|
||||||
reject(error);
|
reject(
|
||||||
},
|
new Error(
|
||||||
);
|
`Request "${type}" timed out after ${RESPONSE_TIMEOUT}ms`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, RESPONSE_TIMEOUT);
|
||||||
|
|
||||||
|
pending.set(requestId, {
|
||||||
|
requestType: type,
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
timeoutId,
|
||||||
|
});
|
||||||
|
|
||||||
|
void writeMessageOnPersistentStream(enqueuedConnection, messageBytes).catch(
|
||||||
|
(error) => {
|
||||||
|
handleConnectionFailure(enqueuedConnection, error);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
pending.delete(requestId);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tensamin/ttp": "workspace:*",
|
"@tensamin/ttp": "workspace:*",
|
||||||
"@tensamin/shared": "workspace:*",
|
"@tensamin/shared": "workspace:*",
|
||||||
|
"@tensamin/storage": "workspace:*",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,16 @@ import { useEffect, useState } from "react";
|
||||||
import { useUser, type User } from "./context";
|
import { useUser, type User } from "./context";
|
||||||
|
|
||||||
import { failedUser } from "@tensamin/shared/data";
|
import { failedUser } from "@tensamin/shared/data";
|
||||||
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
|
|
||||||
// Wrapper function to pass user data to some component
|
// Wrapper function to pass user data to some component
|
||||||
export default function Wrapper(props: {
|
export default function Wrapper(props: {
|
||||||
userId?: number;
|
userId?: number | "own";
|
||||||
loading: React.ReactNode;
|
loading: React.ReactNode;
|
||||||
component: (user: User) => React.ReactNode;
|
component: (user: User) => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { get } = useUser();
|
const { get } = useUser();
|
||||||
|
const { load } = useStorage();
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -20,6 +22,17 @@ export default function Wrapper(props: {
|
||||||
|
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
|
if (props.userId === "own") {
|
||||||
|
load("user_id")
|
||||||
|
.then((id) => {
|
||||||
|
get(id)
|
||||||
|
.then((user) => (active ? setUser(user) : null))
|
||||||
|
.catch(() => setUser(failedUser));
|
||||||
|
})
|
||||||
|
.catch(() => setUser(failedUser));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
get(props.userId)
|
get(props.userId)
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
|
|
@ -35,7 +48,7 @@ export default function Wrapper(props: {
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [get, props.userId]);
|
}, [load, get, props.userId]);
|
||||||
|
|
||||||
return <>{user ? props.component(user) : props.loading}</>;
|
return <>{user ? props.component(user) : props.loading}</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue