Skip to content

Commit

Permalink
fix for read-only mode
Browse files Browse the repository at this point in the history
  • Loading branch information
hxhxhx88 committed Mar 26, 2024
1 parent bfc1e89 commit 5c8dda3
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 24 deletions.
12 changes: 9 additions & 3 deletions app/action/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,18 @@ func mustStartYJSServer() int {
// prepare arguments
internalPort := mustFindFreePort()

// execute the binary in a new process
cmd := exec.Command(bin.Name())
cmd.Env = []string{
// set envs
envs := []string{
fmt.Sprintf("PORT=%d", internalPort),
fmt.Sprintf("DATABASE_PATH=%s", databasePath()),
}
if StartOption.Readonly {
envs = append(envs, "READ_ONLY=true")
}

// execute the binary in a new process
cmd := exec.Command(bin.Name())
cmd.Env = envs
zap.L().Info("start yjs-server server", zap.Int("port", internalPort))

cmd.Stderr = os.Stderr
Expand Down
21 changes: 14 additions & 7 deletions app/frontend/src/page/annotate/Panel/Load.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,26 @@ import {NutshClientContext} from 'common/context';
import PageLayout from 'page/Layout';
import type {Video} from 'openapi/nutsh';
import {PanelLoadProject} from './LoadProject';
import {useJoinYjs} from '@@frontend/state/server/annotation';
import {useAnnotationSync} from '@@frontend/state/server/annotation';

export const PanelLoad: FC<{id: Video['id']}> = ({id}) => {
const client = useContext(NutshClientContext);

// client state
const isLoaded = useRenderStore(s => s.sliceUrls.length > 0);
const startAnnotation = useRenderStore(s => s.startAnnotation);
const setAnnotation = useAnnoStore(s => s.setAnnotation);

// server state
const {isFetching: isFetchingVideo, data: videoData} = useGetVideo(client, id);

// yjs
useJoinYjs(id);
// sync
const {initial} = useAnnotationSync(id);
const setAnnotation = useAnnoStore(s => s.setAnnotation);
useEffect(() => {
if (initial) {
setAnnotation(initial);
}
}, [initial, setAnnotation]);

// local state
const [errorCode, setErrorCode] = useState<string | undefined>(undefined);
Expand All @@ -38,15 +43,17 @@ export const PanelLoad: FC<{id: Video['id']}> = ({id}) => {
}

startAnnotation(frameUrls, '');
}, [videoData, setAnnotation, startAnnotation]);
}, [videoData, startAnnotation]);

if (!isLoaded || !videoData) {
if (!isLoaded || !videoData || initial === undefined || errorCode) {
return (
<PageLayout loading={isFetchingVideo}>
<PageLayout loading={isFetchingVideo || initial === undefined}>
{errorCode && <Alert showIcon={true} type="error" message={intl.get(errorCode)} />}
</PageLayout>
);
}

// Only AFTER the annotation is initialized should we render the panel, otherwise its yjs update listener will respond
// to the initialization, causing the page to re-render frequently and impossible to load heavy annotations.
return <PanelLoadProject video={videoData.video} />;
};
1 change: 1 addition & 0 deletions app/frontend/src/page/project/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const DetailReady: FC<{project: Project; spec: ProjectSpec}> = ({project, spec})
/>,
config.readonly ? (
<Button
key="delete"
icon={<DeleteOutlined />}
type="text"
loading={isDeletingProject}
Expand Down
44 changes: 41 additions & 3 deletions app/frontend/src/state/server/annotation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {ConfigContext, NutshClientContext} from '@@frontend/common/context';
import {useQuery, useMutation} from '@tanstack/react-query';
import {useYjsContext} from 'common/yjs/context';
import type {NutshClient, DefaultService, Video} from 'openapi/nutsh';
import {useEffect} from 'react';
import {useContext, useEffect, useState} from 'react';
import {WebsocketProvider} from 'y-websocket';
import {Annotation, mustDecodeJsonStr as mustDecodeAnnotationJsonStr} from 'type/annotation';
import {writeAnnotationToYjs} from '@@frontend/common/yjs/convert';

/**
* @deprecated Use `useGetVideoAnnotationYjs`.
Expand All @@ -23,12 +26,47 @@ export const usePatchVideoAnnotation = (client: NutshClient) => {
});
};

export const useJoinYjs = (id: Video['id']) => {
// Start synchronization for the annotation of the given video.
// If running in the read-only mode, the synchronization will be a fake one limited to the local.
// The initial annotation used to initialize the local state is returned:
// - `undefined` means the fetching is not ready;
// - for non-ready-only case, `null` will be returned, meaning no local annotation need to be initialzied;
// - for read-only case, a value will be returned, if any.
export const useAnnotationSync = (id: Video['id']): {initial: Annotation | null | undefined} => {
const {doc} = useYjsContext();

// We need to proceed differently for readonly mode.
const {readonly} = useContext(ConfigContext);

// The value used to initialize the local state.
const [initial, setInitial] = useState<Annotation | null | undefined>(undefined);

// for not read-only
useEffect(() => {
if (readonly) {
return;
}

const origin = wsOrigin();
new WebsocketProvider(origin, `ws/video/${id}`, doc);
}, [doc, id]);
setInitial(null);
}, [doc, id, readonly]);

// for read-only
const client = useContext(NutshClientContext).default;
useEffect(() => {
client.getVideoAnnotation({videoId: id}).then(({annotation_json: annoStr}) => {
if (annoStr) {
const anno = mustDecodeAnnotationJsonStr(annoStr);
writeAnnotationToYjs(anno, doc);
setInitial(anno);
} else {
setInitial(null);
}
});
}, [client, doc, id]);

return {initial};
};

function wsOrigin(): string {
Expand Down
28 changes: 17 additions & 11 deletions app/yjs-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ if (!databasePath) {
throw new Error("missing DATABASE_PATH");
}

const readOnly = process.env.READ_ONLY;

verbose();
const db = new Database(databasePath);

Expand Down Expand Up @@ -88,18 +90,22 @@ setPersistence({

// It is IMPORTANT to start listenning the `update` event AFTER the conversion,
// since the conversion itself will trigger the update event.
doc.on("update", () => {
console.log(`doc updated for video ${videoId}`);

const a = readAnnotationFromYjs(doc);
db.run("UPDATE videos SET annotation_json = ? WHERE id = ?", [JSON.stringify(a), videoId], (e) => {
if (e) {
console.error(`failed to save annotation for video ${videoId}`, e.message);
} else {
console.log(`persisted annotation for video ${videoId}`);
}
if (readOnly) {
console.log("will NOT persist data in read-only mode");
} else {
doc.on("update", () => {
console.log(`doc updated for video ${videoId}`);

const a = readAnnotationFromYjs(doc);
db.run("UPDATE videos SET annotation_json = ? WHERE id = ?", [JSON.stringify(a), videoId], (e) => {
if (e) {
console.error(`failed to save annotation for video ${videoId}`, e.message);
} else {
console.log(`persisted annotation for video ${videoId}`);
}
});
});
});
}

resolve();
}
Expand Down

0 comments on commit 5c8dda3

Please sign in to comment.