Workerific

This commit is contained in:
Justin Maier
2023-02-09 02:58:57 -07:00
parent 593becaa19
commit bc5df84ed1
9 changed files with 290 additions and 262 deletions
+27
View File
@@ -62,6 +62,7 @@
"embla-carousel-react": "^7.0.3",
"exifr": "^7.1.3",
"gray-matter": "^4.0.3",
"idb-keyval": "^6.2.0",
"immer": "^9.0.15",
"lodash": "^4.17.21",
"masonic": "^3.7.0",
@@ -6791,6 +6792,14 @@
"entities": "^3.0.1"
}
},
"node_modules/idb-keyval": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz",
"integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==",
"dependencies": {
"safari-14-idb-fix": "^3.0.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -9286,6 +9295,11 @@
"node": ">=6"
}
},
"node_modules/safari-14-idb-fix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz",
"integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog=="
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -15589,6 +15603,14 @@
"entities": "^3.0.1"
}
},
"idb-keyval": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz",
"integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==",
"requires": {
"safari-14-idb-fix": "^3.0.0"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -17265,6 +17287,11 @@
"mri": "^1.1.0"
}
},
"safari-14-idb-fix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz",
"integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+1
View File
@@ -79,6 +79,7 @@
"embla-carousel-react": "^7.0.3",
"exifr": "^7.1.3",
"gray-matter": "^4.0.3",
"idb-keyval": "^6.2.0",
"immer": "^9.0.15",
"lodash": "^4.17.21",
"masonic": "^3.7.0",
@@ -44,7 +44,7 @@ export function CivitaiLinkPopover() {
*/
function LinkDropdown() {
const { selectedInstance, connected, socketConnected, instances } = useCivitaiLink();
const { instance: selectedInstance, connected, socketConnected, instances } = useCivitaiLink();
return (
<Paper style={{ overflow: 'hidden' }}>
<Stack spacing={0}>
@@ -101,7 +101,7 @@ function GetStarted() {
}
function ActivityList() {
const { selectedInstance } = useCivitaiLink();
const { instance: selectedInstance } = useCivitaiLink();
const ids = useCivitaiLinkStore((state) => state.ids);
const { classes } = useActivityListStyles();
return selectedInstance?.connected ? (
@@ -1,9 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import {
CivitaiLinkInstance,
useGetLinkInstances,
} from '~/components/CivitaiLink/civitai-link-api';
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import { CivitaiLinkInstance } from '~/components/CivitaiLink/civitai-link-api';
import {
Command,
ResponseResourcesList,
@@ -17,55 +14,40 @@ import { showNotification } from '@mantine/notifications';
import { v4 as uuid } from 'uuid';
import { immer } from 'zustand/middleware/immer';
import create from 'zustand';
import { useLocalStorage } from '@mantine/hooks';
import { useCurrentUser } from '~/hooks/useCurrentUser';
import isEqual from 'lodash/isEqual';
// #region types
type Instance = {
key: string | null;
connected: boolean; // general connection status - aggregate of `clientsConnected` and `sdConnected`
clientsConnected: number; // number of people in room, even though it's probably just you
sdConnected: boolean; // if the sd instance is available to connect to
};
type SelectedInstance = CivitaiLinkInstance & Omit<Instance, 'key'>;
type IncomingMessage =
| { type: 'ready' }
| { type: 'socketConnection'; payload: boolean }
| { type: 'error'; msg: string }
| { type: 'message'; msg: string }
| { type: 'activitiesUpdate'; payload: ActivitiesResponse[] }
| { type: 'resourcesUpdate'; payload: ResponseResourcesList['resources'] }
| { type: 'commandComplete'; payload: Response }
| { type: 'instance'; payload: Instance }
| { type: 'socketConnection'; payload: boolean };
// #endregion
import {
WorkerOutgoingMessage,
WorkerIncomingMessage,
Instance,
} from '~/workers/civitai-link-worker-types';
// #region context
type CivitaiLinkState = {
instances: CivitaiLinkInstance[];
selectedInstance?: SelectedInstance;
instance?: Instance;
socketConnected: boolean;
connected: boolean;
resources: ResponseResourcesList['resources'];
fetchInstances: () => Promise<void>;
selectInstance: (instance: { key: string }) => Promise<void>;
runCommand: (command: CommandRequest) => Promise<unknown>;
createInstance: (id?: number) => Promise<void>;
deleteInstance: (id: number) => Promise<void>;
renameInstance: (id: number, name: string) => Promise<void>;
selectInstance: (id: number) => Promise<void>;
deselectInstance: () => Promise<void>;
runCommand: (command: CommandRequest) => Promise<unknown>;
};
const CivitaiLinkCtx = createContext<CivitaiLinkState>({
instances: [],
selectedInstance: undefined,
instance: undefined,
connected: false,
socketConnected: false,
resources: [],
fetchInstances: async () => {},
createInstance: async () => {},
deleteInstance: async () => {},
renameInstance: async () => {},
selectInstance: async () => {},
runCommand: async () => {},
deselectInstance: async () => {},
runCommand: async () => {},
} as CivitaiLinkState);
// #endregion
@@ -110,38 +92,16 @@ const commandPromises: Record<
export const useCivitaiLink = () => useContext(CivitaiLinkCtx);
export const CivitaiLinkProvider = ({ children }: { children: React.ReactNode }) => {
const user = useCurrentUser();
const canUseLink = user != null; // TODO: Briant - Check for subscription...
const workerRef = useRef<SharedWorker>();
const workerPromise = useRef<Promise<SharedWorker>>();
const [socketConnected, setSocketConnected] = useState(false);
const [selectedInstanceId, setSelectedInstanceId] = useLocalStorage<number | undefined>({
key: 'civitai-link-instance-id',
});
const { data: instances = [], refetch } = useGetLinkInstances();
// const [instances, setInstances] = useState<CivitaiLinkInstance[]>([]);
const [selectedInstance, setSelectedInstance] = useState<SelectedInstance>();
const [instances, setInstances] = useState<CivitaiLinkInstance[]>([]);
const [instance, setInstance] = useState<Instance>();
const [resources, setResources] = useState<ResponseResourcesList['resources']>([]);
const [connected, setConnected] = useState(false);
const setActivities = useCivitaiLinkStore((state) => state.setActivities);
console.log({ selectedInstance });
const updateSelectedInstance = useCallback(
(instance: Instance) => {
const detectedInstance = instances.find((x) => x.key === instance.key);
console.log('FIRFIREA', { instance, detectedInstance, instances });
if (detectedInstance) {
setSelectedInstanceId(detectedInstance?.id);
setSelectedInstance({ ...instance, ...detectedInstance });
}
setConnected(instance.connected);
},
[instances, setSelectedInstanceId]
);
const getWorker = () => {
if (!canUseLink) throw Error('User is not logged in');
if (workerPromise.current) return workerPromise.current;
if (workerRef.current) return Promise.resolve(workerRef.current);
const worker = new SharedWorker(
@@ -157,15 +117,9 @@ export const CivitaiLinkProvider = ({ children }: { children: React.ReactNode })
showNotification({ message: msg });
};
const handleInstance = (instance: Instance) => {
// // const detectedInstance = instances.find((x) => x.key === instance.key);
// // console.log('FIRFIREA', { instance, detectedInstance, instances });
// // if (detectedInstance) {
// setSelectedInstanceId(instance.id);
// setSelectedInstance({ ...instance });
// // }
// setConnected(instance.connected);
updateSelectedInstance(instance);
const handleInstance = (payload: Instance) => {
setInstance(payload);
setConnected(payload?.connected ?? false);
};
const handleActivities = (activities: ActivitiesResponse[]) => {
@@ -175,15 +129,6 @@ export const CivitaiLinkProvider = ({ children }: { children: React.ReactNode })
return bDate.getTime() - aDate.getTime();
});
// const removed = sorted.filter((x) => x.type === 'resources:remove');
// const added = sorted.filter((x) => x.type === 'resources:add');
// // TODO - determine how to show that an item has been removed while still being able to show the correct status if removing an item fails
// const filtered = added.map((activity) => {
// const index = removed.findIndex((x) => x.resource.hash === activity.resource.hash);
// return index > -1 ? removed[index] : activity;
// });
setActivities(sorted);
};
@@ -200,40 +145,39 @@ export const CivitaiLinkProvider = ({ children }: { children: React.ReactNode })
resolve(worker);
};
worker.port.onmessage = async function ({ data }: { data: IncomingMessage }) {
worker.port.onmessage = async function ({ data }: { data: WorkerOutgoingMessage }) {
if (data.type === 'ready') handleReady();
else if (data.type === 'error') handleError(data.msg);
else if (data.type === 'message') handleMessage(data.msg);
else if (data.type === 'instance') handleInstance(data.payload);
else if (data.type === 'instancesUpdate') setInstances(data.payload);
else if (data.type === 'resourcesUpdate') setResources(data.payload);
else if (data.type === 'activitiesUpdate') handleActivities(data.payload);
else if (data.type === 'commandComplete') handleCommandComplete(data.payload);
else if (data.type === 'socketConnection') setSocketConnected(data.payload);
//TODO.Justin
// else if (data.type === 'instances.list') setSocketConnected(data.payload);
};
});
return workerPromise.current;
};
const fetchInstances = useCallback(async () => {
await refetch();
}, [refetch]);
const selectInstance = async (instance: { key: string }) => {
const boot = async () => {
const worker = await getWorker();
worker.port.postMessage({ type: 'join', key: instance.key });
return worker;
};
// TODO.Justin
// const createInstance = async (instance: { key: string }) => {
// const worker = await getWorker();
// worker.port.postMessage({ type: 'join', key: instance.key });
// };
const workerReq = async (req: WorkerIncomingMessage) => {
const worker = await getWorker();
worker.port.postMessage(req);
};
const selectInstance = (id: number) => workerReq({ type: 'join', id });
const deselectInstance = () => workerReq({ type: 'leave' });
const createInstance = (id?: number) => workerReq({ type: 'create', id });
const deleteInstance = (id: number) => workerReq({ type: 'delete', id });
const renameInstance = (id: number, name: string) => workerReq({ type: 'rename', id, name });
const runCommand = async (command: CommandRequest, timeout = 0) => {
const worker = await getWorker();
const payload = command as Command;
payload.id = uuid();
@@ -248,37 +192,26 @@ export const CivitaiLinkProvider = ({ children }: { children: React.ReactNode })
}, timeout);
});
worker.port.postMessage({ type: 'command', payload });
await workerReq({ type: 'command', payload });
return promise;
};
const deselectInstance = async () => {
if (!selectedInstance) return;
const worker = await getWorker();
worker.port.postMessage({ type: 'leave' });
};
useEffect(() => {
if (!canUseLink) return;
fetchInstances();
}, [fetchInstances, canUseLink]);
useEffect(() => {
if (!canUseLink || !selectedInstanceId || !instances.length || selectedInstance) return;
const storedInstance = instances.find((x) => x.id === selectedInstanceId);
if (storedInstance) selectInstance(storedInstance);
}, [instances, canUseLink]);
boot();
}, [boot]);
return (
<CivitaiLinkCtx.Provider
value={{
instances,
selectedInstance,
instance,
connected,
socketConnected,
resources,
fetchInstances,
createInstance,
deleteInstance,
renameInstance,
selectInstance,
deselectInstance,
runCommand,
@@ -288,12 +221,3 @@ export const CivitaiLinkProvider = ({ children }: { children: React.ReactNode })
</CivitaiLinkCtx.Provider>
);
};
// export function ActualProvider({ children }) {
// const { civitaiLink } = useFeatureFlags();
// return civitaiLink ? <CivitaiLinkProvider>{children}</CivitaiLinkProvider> : children;
// }
// export function ConditionalProvider({ children, provider, condition }) {
// return condition ? provider({ children }) : children;
// }
@@ -18,10 +18,6 @@ import {
import { IconCheck, IconChevronRight, IconCopy } from '@tabler/icons';
import { useState } from 'react';
import { z } from 'zod';
import {
useCreateLinkInstance,
useUpdateLinkInstance,
} from '~/components/CivitaiLink/civitai-link-api';
import { useCivitaiLink } from '~/components/CivitaiLink/CivitaiLinkProvider';
import { createContextModal } from '~/components/Modals/utils/createContextModal';
import { Form, InputText, useForm } from '~/libs/form';
@@ -37,7 +33,6 @@ const { openModal, Modal } = createContextModal({
withCloseButton: false,
closeOnClickOutside: false,
Element: ({ context, props }) => {
const [key, setKey] = useState<string>();
const [active, setActive] = useState(0);
const nextStep = () => setActive((current) => (current < 2 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
@@ -46,35 +41,22 @@ const { openModal, Modal } = createContextModal({
schema,
});
const { connected, selectInstance, selectedInstance } = useCivitaiLink();
const { mutate: createLinkInstance, isLoading: isCreatingLinkInstance } =
useCreateLinkInstance();
const { mutate: updateLinkInstance, isLoading: isUpdatingLinkInstance } =
useUpdateLinkInstance();
const {
connected,
instance: selectedInstance,
createInstance,
renameInstance,
} = useCivitaiLink();
const handleCreateInstance = () => {
nextStep();
if (!key && !isCreatingLinkInstance) {
createLinkInstance(undefined, {
onSuccess: (result) => {
selectInstance({ key: result.key });
setKey(result.key);
nextStep();
},
});
}
createInstance();
};
const handleSubmit = (data: z.infer<typeof schema>) => {
if (selectedInstance)
updateLinkInstance(
{ ...data, id: selectedInstance.id },
{
onSuccess: () => {
context.close();
},
}
);
if (!selectedInstance?.id) return;
renameInstance(selectedInstance.id, data.name);
context.close();
};
return (
@@ -123,7 +105,6 @@ const { openModal, Modal } = createContextModal({
<Button
onClick={handleCreateInstance}
leftIcon={<IconChevronRight />}
loading={isCreatingLinkInstance}
>{`Ok, it's installed`}</Button>
</Group>
</Stack>
@@ -136,8 +117,8 @@ const { openModal, Modal } = createContextModal({
</Text>
<Text> Paste this code into the Civitai Link settings and save.</Text>
<Center>
{key ? (
<CopyButton value={key}>
{selectedInstance?.key ? (
<CopyButton value={selectedInstance.key}>
{({ copied, copy }) => (
<Tooltip label="copy">
<Button
@@ -145,7 +126,7 @@ const { openModal, Modal } = createContextModal({
onClick={copy}
rightIcon={copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
>
{!copied ? key : 'Copied'}
{!copied ? selectedInstance.key : 'Copied'}
</Button>
</Tooltip>
)}
@@ -184,9 +165,7 @@ const { openModal, Modal } = createContextModal({
label="Name your stable diffusion instance"
placeholder="name"
/>
<Button type="submit" loading={isUpdatingLinkInstance}>
Save
</Button>
<Button type="submit">Save</Button>
</Stack>
</Form>
</Stack>
+2 -54
View File
@@ -1,6 +1,4 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { env } from '~/env/client.mjs';
import { queryClient } from '~/utils/trpc';
export type CivitaiLinkInstance = {
id: number;
@@ -11,25 +9,6 @@ export type CivitaiLinkInstance = {
createdAt: Date;
};
const links = [
{
id: 2,
activated: true,
key: '327f5d153ddd160e95e97465ae0aaa4893f74e1477fa4a1215ab9e77aac2d51276385398b4e1c94bcaf9e4ab705e0b245f2d1ff05dda9fc6aed8b729072c1d52',
origin: null,
name: null,
createdAt: new Date('2023-02-01T18:50:51.760Z'),
},
{
id: 3,
activated: true,
key: '920ceef9e51f59ecaa5b790c32dea22a162188eab624929a2cccabd18d40c75c2987b9f92ffaef104b3d9425fd9b9b3794b0975a1ac433f9d7cf55310e7d3d9a',
origin: null,
name: null,
createdAt: new Date('2023-02-02T21:39:08.522Z'),
},
] as CivitaiLinkInstance[];
const clFetch = async (url: string, options: RequestInit = {}) => {
if (!url.startsWith('/')) url = '/' + url;
const response = await fetch(env.NEXT_PUBLIC_CIVITAI_LINK + url, {
@@ -44,36 +23,19 @@ const clFetch = async (url: string, options: RequestInit = {}) => {
};
export const getLinkInstances = async () => {
// return links; // TODO.civitai-link - comment this out
return (await clFetch('/api/link')) as CivitaiLinkInstance[];
};
const queryKey = ['civitai-link-instances'];
export const useGetLinkInstances = () => {
return useQuery({
queryKey,
queryFn: getLinkInstances,
});
};
export const createLinkInstance = async (data?: { id?: number }) => {
export const createLinkInstance = async (id?: number) => {
return (await clFetch(`/api/link`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
body: id ? JSON.stringify({ id }) : undefined,
})) as { id: number; key: string; instanceCount: number; instanceLimit: number; name: string };
};
export const useCreateLinkInstance = () => {
return useMutation(createLinkInstance, {
onSuccess: async (result) => {
await queryClient.invalidateQueries({ queryKey });
},
});
};
export const updateLinkInstance = async (data: { id: number; name: string }) => {
if (!data.id) throw new Error('Missing id');
@@ -86,22 +48,8 @@ export const updateLinkInstance = async (data: { id: number; name: string }) =>
})) as { id: number; name: string };
};
export const useUpdateLinkInstance = () =>
useMutation(updateLinkInstance, {
onSuccess: async (result) => {
await queryClient.invalidateQueries({ queryKey });
},
});
export const deleteLinkInstance = async (id: number) => {
return (await clFetch(`/api/link?id=${id}`, {
method: 'DELETE',
})) as { success: boolean };
};
export const useDeleteLinkInstance = () =>
useMutation(deleteLinkInstance, {
onSuccess: async (result) => {
await queryClient.invalidateQueries({ queryKey });
},
});
@@ -3,6 +3,7 @@ import { Socket } from 'socket.io-client';
export type ClientType = 'client' | 'sd';
export interface ServerToClientEvents {
kicked: () => void;
roomPresence: (msg: { client: number; sd: number }) => void;
upgradeKey: (msg: { key: string }) => void;
error: (msg: { msg: string }) => void;
+35
View File
@@ -0,0 +1,35 @@
import { CivitaiLinkInstance } from '~/components/CivitaiLink/civitai-link-api';
import {
ActivitiesResponse,
Command,
ResponseResourcesList,
Response,
} from '~/components/CivitaiLink/shared-types';
export type Instance = {
id: number | null;
name: string | null;
key: string | null;
connected: boolean; // general connection status - aggregate of `clientsConnected` and `sdConnected`
clientsConnected: number; // number of people in room, even though it's probably just you
sdConnected: boolean; // if the sd instance is available to connect to
};
export type WorkerOutgoingMessage =
| { type: 'ready' }
| { type: 'socketConnection'; payload: boolean }
| { type: 'error'; msg: string }
| { type: 'message'; msg: string }
| { type: 'activitiesUpdate'; payload: ActivitiesResponse[] }
| { type: 'instancesUpdate'; payload: CivitaiLinkInstance[] }
| { type: 'resourcesUpdate'; payload: ResponseResourcesList['resources'] }
| { type: 'commandComplete'; payload: Response }
| { type: 'instance'; payload: Instance };
export type WorkerIncomingMessage =
| { type: 'create'; id?: number }
| { type: 'delete'; id: number }
| { type: 'rename'; id: number; name: string }
| { type: 'join'; id: number }
| { type: 'leave' }
| { type: 'command'; payload: Command };
+164 -51
View File
@@ -6,9 +6,23 @@ import {
Response,
ResponseResourcesList,
ResponseStatus,
ActivitiesResponse,
} from '~/components/CivitaiLink/shared-types';
import { env } from '~/env/client.mjs';
import { v4 as uuid } from 'uuid';
import {
CivitaiLinkInstance,
createLinkInstance,
deleteLinkInstance,
getLinkInstances,
updateLinkInstance,
} from '~/components/CivitaiLink/civitai-link-api';
import {
WorkerIncomingMessage,
Instance,
WorkerOutgoingMessage,
} from '~/workers/civitai-link-worker-types';
import { get, set, del } from 'idb-keyval';
// --------------------------------
// Types
@@ -18,18 +32,6 @@ interface SharedWorkerGlobalScope {
}
const _self: SharedWorkerGlobalScope = self as any;
type IncomingMessage =
| { type: 'join'; key: string }
| { type: 'leave' }
| { type: 'command'; payload: Command };
type Instance = {
key: string | null;
connected: boolean;
sdConnected: boolean;
clientsConnected: number;
};
// --------------------------------
// Setup Socket
// --------------------------------
@@ -50,32 +52,38 @@ const sendCommand = (payload: Omit<Command, 'id' | 'createdAt'>) => {
// --------------------------------
// Setup shared state
// --------------------------------
let initialized = false;
const instance: Instance = {
const defaultInstance: Instance = {
id: null,
key: null,
name: null,
connected: false,
sdConnected: false,
clientsConnected: 0,
};
let initialized: number | null = null;
let instance: Instance = { ...defaultInstance };
let instances: CivitaiLinkInstance[] = [];
let resources: ResponseResourcesList['resources'] = [];
let activities: Response[] = [];
let activities: ActivitiesResponse[] = [];
// Shared value events
const sharedCallbacks = {
resources: [] as (() => void)[],
activities: [] as (() => void)[],
instance: [] as (() => void)[],
instances: [] as (() => void)[],
error: [] as ((msg: string) => void)[],
message: [] as ((msg: string) => void)[],
completion: [] as ((response: Response) => void)[],
socketConnection: [] as ((connected: boolean) => void)[],
};
const onUpdate = (type: 'resources' | 'activities' | 'instance', cb: () => void) => {
const onUpdate = (type: UpdateSharedValueProps['type'], cb: () => void) => {
sharedCallbacks[type].push(cb);
};
type UpdateSharedValueProps =
| { type: 'resources'; value: ResponseResourcesList['resources'] }
| { type: 'activities'; value: Response[] }
| { type: 'activities'; value: ActivitiesResponse[] }
| { type: 'instances'; value: CivitaiLinkInstance[] }
| { type: 'instance'; value: Partial<Instance> };
const updateSharedValue = ({ type, value }: UpdateSharedValueProps) => {
console.log('updateSharedValue', { type, value });
@@ -86,9 +94,11 @@ const updateSharedValue = ({ type, value }: UpdateSharedValueProps) => {
activities = value;
sharedCallbacks.activities.forEach((cb) => cb());
} else if (type === 'instance') {
if (value.key) instance.key = value.key;
if (value.connected) instance.connected = value.connected;
instance = { ...instance, ...value };
sharedCallbacks.instance.forEach((cb) => cb());
} else if (type === 'instances') {
instances = value;
sharedCallbacks.instances.forEach((cb) => cb());
}
};
@@ -128,15 +138,39 @@ const emitMessage = (msg: string) => {
sharedCallbacks.message.forEach((cb) => cb(msg));
};
// Storage
const storageKey = 'cl-id';
const storeInstanceId = async () => {
if (!instance.id) {
console.log(`${storageKey}: clear`);
await del(storageKey);
} else {
console.log(`${storageKey}: ${instance.id}`);
await set(storageKey, instance.id.toString());
}
};
const getStoredInstanceId = async () => {
const id = await get(storageKey);
console.log(`${storageKey}: ${id}`);
if (!id) return null;
return Number(id);
};
// --------------------------------
// Handle Instance Events
// --------------------------------
// --------------------------------
// Handle Socket Events
// --------------------------------
socket.on('connect', () => {
socket.emit('iam', { type: 'client' });
emitSocketConnection(true);
if (instance.key) {
// rejoin if key is set
handleJoin(instance.key);
handleInitialization();
if (instance.id) {
// rejoin if id is set
initialized = null;
handleJoin(instance.id);
}
});
socket.on('disconnect', () => {
@@ -152,16 +186,16 @@ socket.on('commandStatus', (payload: Response) => {
return;
}
let value: Response[] = [];
let value: ActivitiesResponse[] = [];
if (payload.type === 'activities:list' || payload.type === 'activities:clear') {
value = payload.activities;
value = payload.activities as ActivitiesResponse[];
} else {
let found = false;
for (const activity of activities) {
if (activity.id !== payload.id) value.push(activity);
else {
found = true;
value.push(payload);
value.push(payload as ActivitiesResponse);
// emit completion if status changed to a completed status
const activityCompleted =
@@ -169,50 +203,74 @@ socket.on('commandStatus', (payload: Response) => {
if (activityCompleted) emitCompletion(payload);
}
}
if (!found) value.push(payload);
if (!found) value.push(payload as ActivitiesResponse);
}
updateSharedValue({ type: 'activities', value });
});
socket.on('upgradeKey', ({ key }) => {
const match = instances.find((x) => x.id === instance.id);
if (match) match.key = key;
updateSharedValue({ type: 'instance', value: { key } });
});
socket.on('kicked', () => {
updateSharedValue({ type: 'instance', value: defaultInstance });
storeInstanceId();
});
socket.on('error', ({ msg }) => {
emitError(msg);
});
socket.on('roomPresence', ({ client, sd }) => {
console.log('roomPresence', { client, sd });
if (!instance.sdConnected && sd > 0) emitMessage('Stable Diffusion service connected');
else if (instance.sdConnected && sd === 0) emitMessage('Stable Diffusion service disconnected');
const connected = sd > 0 && client > 0;
if (connected && !instance.connected) handleInitialization();
else if (!connected && instance.connected) initialized = null;
updateSharedValue({
type: 'instance',
value: { sdConnected: sd > 0, clientsConnected: client, connected: sd > 0 && client > 0 },
value: { sdConnected: sd > 0, clientsConnected: client, connected },
});
});
// --------------------------------
// Handle Incoming Messages
// --------------------------------
const handleJoin = (key: string) => {
if (instance.key === key && instance.connected) return;
const handleJoin = (id: number) => {
if (instance.id === id && instance.connected) return;
const targetInstance = instances.find((i) => i.id === id);
if (!targetInstance) {
emitError('Could not find instance');
return;
}
const { key, name } = targetInstance;
if (!socket.connected) {
socket.connect();
socket.emit('iam', { type: 'client' });
}
socket.emit('join', key, ({ success, msg }) => {
updateSharedValue({ type: 'instance', value: { key } });
socket.emit('join', targetInstance.key, ({ success, msg }) => {
if (!success && msg) emitError(msg);
else updateSharedValue({ type: 'instance', value: { id, key, name } });
});
};
const handleLeave = () => {
if (!instance.key) return;
if (!instance.id) return;
socket.emit('leave');
updateSharedValue({
type: 'instance',
value: { key: null, sdConnected: false, clientsConnected: 0, connected: false },
value: defaultInstance,
});
storeInstanceId();
};
const handleCommand = (payload: Command) => {
@@ -223,46 +281,101 @@ const handleCommand = (payload: Command) => {
socket.emit('command', { ...payload, createdAt: new Date() });
};
const handleLoadInstances = async () => {
try {
const result = await getLinkInstances();
updateSharedValue({ type: 'instances', value: result });
} catch (err: any) {
emitError(`Error loading instances: ${err.message}`);
}
};
const handleRename = async (id: number, name: string) => {
try {
await updateLinkInstance({ id, name });
if (instance.id === id) updateSharedValue({ type: 'instance', value: { name } });
await handleLoadInstances();
} catch (err: any) {
emitError(`Error renaming instance: ${err.message}`);
}
};
const handleDelete = async (id: number) => {
try {
if (instance.id === id) handleLeave();
await deleteLinkInstance(id);
await handleLoadInstances();
} catch (err: any) {
emitError(`Error deleting instance: ${err.message}`);
}
};
const handleCreate = async (id?: number) => {
try {
const result = await createLinkInstance(id);
await handleLoadInstances();
handleJoin(result.id);
} catch (err: any) {
emitError(`Error creating instance: ${err.message}`);
}
};
const handleInitialization = () => {
if (!instance.id || initialized === instance.id) return;
sendCommand({ type: 'activities:list' });
sendCommand({ type: 'resources:list' });
initialized = true;
initialized = instance.id;
console.log(`Initialized instance: ${instance.id}`);
storeInstanceId();
};
// --------------------------------
// Bootstrap Worker
// --------------------------------
const start = (port: MessagePort) => {
const start = async (port: MessagePort) => {
if (!port.postMessage) return;
onError((msg) => port.postMessage({ type: 'error', msg }));
onMessage((msg) => port.postMessage({ type: 'message', msg }));
onCompletion((payload) => port.postMessage({ type: 'commandComplete', payload }));
port.postMessage({ type: 'instance', payload: instance });
const portReq = (req: WorkerOutgoingMessage) => port.postMessage(req);
onError((msg) => portReq({ type: 'error', msg }));
onMessage((msg) => portReq({ type: 'message', msg }));
onCompletion((payload) => portReq({ type: 'commandComplete', payload }));
portReq({ type: 'instance', payload: instance });
onUpdate('instance', () => {
if (instance.connected && !initialized) handleInitialization();
port.postMessage({ type: 'instance', payload: instance });
portReq({ type: 'instance', payload: instance });
});
port.postMessage({ type: 'resourcesUpdate', payload: resources });
portReq({ type: 'resourcesUpdate', payload: resources });
onUpdate('resources', () => {
port.postMessage({ type: 'resourcesUpdate', payload: resources });
portReq({ type: 'resourcesUpdate', payload: resources });
});
port.postMessage({ type: 'activitiesUpdate', payload: activities });
portReq({ type: 'activitiesUpdate', payload: activities });
onUpdate('activities', () => {
port.postMessage({ type: 'activitiesUpdate', payload: activities });
portReq({ type: 'activitiesUpdate', payload: activities });
});
port.postMessage({ type: 'socketConnection', payload: socket.connected });
portReq({ type: 'instancesUpdate', payload: instances });
onUpdate('instances', () => {
portReq({ type: 'instancesUpdate', payload: instances });
});
portReq({ type: 'socketConnection', payload: socket.connected });
onSocketConnection((connected) => {
port.postMessage({ type: 'socketConnection', payload: connected });
portReq({ type: 'socketConnection', payload: connected });
});
port.onmessage = ({ data }: { data: IncomingMessage }) => {
if (data.type === 'join') handleJoin(data.key);
port.onmessage = ({ data }: { data: WorkerIncomingMessage }) => {
if (data.type === 'join') handleJoin(data.id);
else if (data.type === 'create') handleCreate(data.id);
else if (data.type === 'delete') handleDelete(data.id);
else if (data.type === 'rename') handleRename(data.id, data.name);
else if (data.type === 'leave') handleLeave();
else if (data.type === 'command') handleCommand(data.payload);
};
port.postMessage({ type: 'ready' });
handleLoadInstances().finally(async () => {
portReq({ type: 'ready' });
const storedInstanceId = await getStoredInstanceId();
if (storedInstanceId) handleJoin(Number(storedInstanceId));
});
};
_self.onconnect = (e) => {