Skip to content

Commit

Permalink
Merge pull request #118 from cpinitiative/fix-copy
Browse files Browse the repository at this point in the history
Fix copying files
  • Loading branch information
thecodingwizard authored Aug 23, 2023
2 parents 37b5600 + 167f842 commit 67c2661
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
npm run dev &
- name: Setup background services
run: echo '{"rules":{".read":true,".write":true}}' > database.rules.json && yarn start &
run: echo '{"rules":{".read":true,".write":true}}' > database.rules.json && FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099" yarn start &

- name: Run Playwright tests
run: firebase emulators:exec "yarn playwright test"
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ This project uses the [Firebase Realtime Database](https://firebase.google.com/d

```
yarn install
yarn dev
firebase emulators:start # in a separate tab. make sure you are using emulators iff shouldUseEmulator is true!
FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099" yarn dev
# in a separate tab. make sure you are using emulators iff SHOULD_USE_FIREBASE_EMULATOR is true!
firebase emulators:start
```

Also, if you do not want to run the yjs server locally, change the yjs URLs in
`RealtimeEditor.tsx` and `copyFile.tsx` to point to the production one
(`yjs.usaco.guide`).
Note that FIREBASE_AUTH_EMULATOR_HOST must be set; otherwise you'll get a "Firebase ID token has no kid claim" error message when a nextjs function tries to decode a firebase auth token.

Also, if you do not want to run the yjs server locally, or if you don't want to use firebase emulators, edit `src/dev_constants.ts`.

Note: If you get a firebase emulators timeout error on Mac, see [firebase/firebase-tools#2379 (comment)](https://github.com/firebase/firebase-tools/issues/2379#issuecomment-951884721) and Issue #67 in this repo.

Expand All @@ -28,7 +29,7 @@ yarn playwright test

### Configuring Firebase

You can update the Firebase configuration (if you want to use a custom firebase project, for example) by modifying `pages/_app.tsx`. There, you can also set `shouldUseEmulator` to `false` if you don't want to use the firebase emulator.
You can update the Firebase configuration (if you want to use a custom firebase project, for example) by modifying `pages/_app.tsx`.

## Tech Stack

Expand Down
31 changes: 31 additions & 0 deletions e2e/copies_files.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { test, expect, Page } from '@playwright/test';
import { host } from './helpers';

test.describe('Basic Functionality', () => {
test('should copy files', async ({ page, context }) => {
await page.goto(`${host}/n`);
await page.waitForSelector('button:has-text("Run Code")');

// let monaco load
await page.waitForTimeout(500);

await page.click('[data-test-id="input-editor"]');
await page.keyboard.type('1 2 3');

await page.evaluate(
`this.monaco.editor.getModels().find(x => x.getLanguageId() === "cpp").setValue(\`code_value\`)`
);
await page.evaluate(
`this.monaco.editor.getModels().find(x => x.getLanguageId() === "plaintext").setValue(\`input_value\`)`
);

// sync with yjs server
await page.waitForTimeout(1500);

await page.goto(page.url() + '/copy');
await page.waitForSelector('button:has-text("Run Code")');

expect(await page.$('text="code_value"')).toBeTruthy();
expect(await page.$('text="input_value"')).toBeTruthy();
});
});
10 changes: 3 additions & 7 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ConnectionProvider } from '../src/context/ConnectionContext';
import { Toaster } from 'react-hot-toast';
import { Analytics } from '@vercel/analytics/react';
import { UserProvider } from '../src/context/UserContext';
import { SHOULD_USE_FIREBASE_EMULATOR } from '../src/dev_constants';

const firebaseConfig = {
apiKey: 'AIzaSyC2C7XWrCKcmM0RDAVZZHDQSxOlo6g3JTU',
Expand All @@ -21,17 +22,12 @@ const firebaseConfig = {
measurementId: 'G-9C903QL4KZ',
};

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const shouldUseEmulator =
typeof window !== 'undefined' && location.hostname === 'localhost' && true;

if (!firebase.apps?.length) {
if (shouldUseEmulator) {
if (SHOULD_USE_FIREBASE_EMULATOR) {
firebase.initializeApp({
...firebaseConfig,
authDomain: 'localhost:9099',
databaseURL: 'http://localhost:9000/?ns=cp-ide-default-rtdb',
databaseURL: 'http://localhost:9000/?ns=cp-ide-2-default-rtdb',
});
firebase.auth().useEmulator('http://localhost:9099');
firebase.database().useEmulator('localhost', 9000);
Expand Down
10 changes: 5 additions & 5 deletions pages/api/copyFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import colorFromUserId from '../../src/scripts/colorFromUserId';
import { getAuth } from 'firebase-admin/auth';
import firebaseApp from '../../src/firebaseAdmin';
import { getDatabase, ServerValue } from 'firebase-admin/database';
import { SHOULD_USE_DEV_YJS_SERVER } from '../../src/dev_constants';

type RequestData = {
idToken: string;
Expand Down Expand Up @@ -86,11 +87,10 @@ export default async (
});
const fileID: string = ref.key!;

const copyYjsPromies = ['code', 'input', 'scribble'].map(key => {
const HOST_URL =
process.env.NODE_ENV === 'production'
? 'https://yjs.usaco.guide'
: 'http://0.0.0.0:1234';
const copyYjsPromies = ['cpp', 'java', 'py', 'input', 'scribble'].map(key => {
const HOST_URL = SHOULD_USE_DEV_YJS_SERVER
? 'http://0.0.0.0:1234'
: 'https://yjs.usaco.guide';
return fetch(`${HOST_URL}/copyFile`, {
method: 'POST',
headers: {
Expand Down
6 changes: 3 additions & 3 deletions src/atoms/firebaseUserAtoms.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { atom } from 'jotai';
import firebase from 'firebase/app';
import { shouldUseEmulator } from '../../pages/_app';
import { ConnectionContextType } from '../context/ConnectionContext';
import { SHOULD_USE_FIREBASE_EMULATOR } from '../dev_constants';

/**
* This is set to a callback function when the modal asking the user
Expand All @@ -26,10 +26,10 @@ export const signInWithGoogleAtom = atom(
const prevConnectionRefs = connectionContext.getConnectionRefs();
connectionContext.clearConnectionRefs();

if (shouldUseEmulator) {
if (SHOULD_USE_FIREBASE_EMULATOR) {
// Note: for some reason firebase emulator does not work with `linkWithPopup`
// so we're just going to always sign up with popup instead.
// To test `linkWithPopup`, go to `src/components/WorkspaceInitializer.tsx`, find `shouldUseEmulator`,
// To test `linkWithPopup`, go to `src/dev_constants.ts`, find `shouldUseFirebaseEmulatorInDev`,
// and set that to false.
// a function returns a function because we want to set the atom to a callback function
// but if you pass a function into set() then jotai will use the function *return* value
Expand Down
40 changes: 27 additions & 13 deletions src/components/RealtimeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,27 @@ import EditorConnectionStatusIndicator from './editor/EditorConnectionStatusIndi
import colorFromUserId, { bgColorFromUserId } from '../scripts/colorFromUserId';
import { useUserContext } from '../context/UserContext';
import { useEditorContext } from '../context/EditorContext';
import { SHOULD_USE_DEV_YJS_SERVER } from '../dev_constants';

export interface RealtimeEditorProps extends EditorProps {
yjsDocumentId: string;
useEditorWithVim?: boolean;
dataTestId?: string;
}

const WEBSOCKET_SERVER =
process.env.NODE_ENV === 'development' || process.env.IS_TEST_ENV
? 'ws://localhost:1234'
: 'wss://yjs.usaco.guide:443';
const WEBSOCKET_SERVER = SHOULD_USE_DEV_YJS_SERVER
? 'ws://localhost:1234'
: 'wss://yjs.usaco.guide:443';

const RealtimeEditor = ({
onMount,
/**
* Warning: with the current implementation (EditorContext.doNotInitializeCodeRef),
* only one realtime editor can have defaultValue (the main code editor).
*/
defaultValue,
yjsDocumentId,
useEditorWithVim = false,
dataTestId = '',
...props
}: RealtimeEditorProps): JSX.Element => {
const { doNotInitializeCodeRef } = useEditorContext();
const { doNotInitializeTheseFileIdsRef } = useEditorContext();
const [editor, setEditor] =
useState<monaco.editor.IStandaloneCodeEditor | null>(null);
const { userData, firebaseUser } = useUserContext();
Expand Down Expand Up @@ -126,17 +122,35 @@ const RealtimeEditor = ({
);
provider.on('sync', (isSynced: boolean) => {
// Handle file initialization
// We need to check for doNotInitializeCodeRef.current here
// We need to check for doNotInitializeTheseFileIdsRef.current here
// to make sure we're the client that's supposed to initialize the document.
// This is to prevent multiple clients from initializing the document when the language changes.
// See EditorContext.tsx for more information
if (isSynced && defaultValue && !doNotInitializeCodeRef.current) {
if (isSynced && !doNotInitializeTheseFileIdsRef.current[yjsDocumentId]) {
const isInitializedMap = ydocument.getMap('isInitialized');
if (!isInitializedMap.get('isInitialized')) {
isInitializedMap.set('isInitialized', true);
if (monacoText.length === 0) monacoText.insert(0, defaultValue ?? '');
if (monacoText.length === 0 && defaultValue)
monacoText.insert(0, defaultValue ?? '');
}
doNotInitializeTheseFileIdsRef.current[yjsDocumentId] = true;

// special case: if yjsDocumentId ends in .cpp or .java or .py, don't initialize any
// of those file IDs to prevent the issue from multiple initializations when the language
// changes. (wow, this code is really messy and possibly overly complicated and should be refactored)
if (
yjsDocumentId.endsWith('cpp') ||
yjsDocumentId.endsWith('java') ||
yjsDocumentId.endsWith('py')
) {
let prefix = yjsDocumentId.substring(
0,
yjsDocumentId.lastIndexOf('.')
);
doNotInitializeTheseFileIdsRef.current[prefix + '.cpp'] = true;
doNotInitializeTheseFileIdsRef.current[prefix + '.java'] = true;
doNotInitializeTheseFileIdsRef.current[prefix + '.py'] = true;
}
doNotInitializeCodeRef.current = true;
}
setIsSynced(isSynced);
setLoading(false);
Expand Down
7 changes: 5 additions & 2 deletions src/components/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const SettingsModal = ({
const {
fileData,
updateFileData: updateRealFileData,
doNotInitializeCodeRef,
doNotInitializeTheseFileIdsRef,
} = useEditorContext();
const realFileSettings = fileData.settings;
const userPermission = useUserPermission();
Expand Down Expand Up @@ -154,7 +154,10 @@ export const SettingsModal = ({
// the code (we only want the client who initiated the language change
// to initialize the code)
// For more info, see EditorContex.tsx
doNotInitializeCodeRef.current = false;
doNotInitializeTheseFileIdsRef.current[
// the key is the yjs document ID
fileData.id + '.' + settingsToSet.language
] = false;
}
updateRealFileData({
settings: { ...realFileSettings, ...settingsToSet },
Expand Down
16 changes: 8 additions & 8 deletions src/context/EditorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ export type EditorContextType = {
fileData: FileData;
updateFileData: (firebaseUpdateData: Partial<FileData>) => Promise<any>;
/**
* Maps YJS File ID ==> true / false
* If file ID is not in the map, assume it's false
*
* If true, this client should NOT initialize the code if it's empty.
* This solves the bug that if multiple people are on the same document,
* and the document changes languages, every client will try to initialize
* the code (resuting in multiple templates being inserted).
*
* Instead, after the file is loaded & synced, doNotInitializeCode is set
* Instead, after the file is loaded & synced, doNotInitializeTheseFileIds is set
* to true. Then, if the language is changed, the client who triggered the
* language change (and only that client) will have this set to false.
*
* Note that with our current implementation, only one defaultValue will work
* (ie. you can only have one RealtimeEdtior component with a defaultValue)
*/
doNotInitializeCodeRef: MutableRefObject<boolean>;
doNotInitializeTheseFileIdsRef: MutableRefObject<Record<string, boolean>>;
};

const EditorContext = createContext<EditorContextType | null>(null);
Expand Down Expand Up @@ -92,7 +92,7 @@ export function EditorProvider({
const { userData } = useUserContext();
const [fileData, setFileData] = useState<FileData | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const doNotInitializeCodeRef = useRef<boolean>(false);
const doNotInitializeTheseFileIdsRef = useRef<Record<string, boolean>>({});

useEffect(() => {
setLoading(true);
Expand Down Expand Up @@ -132,8 +132,8 @@ export function EditorProvider({
);

const editorContextValue = useMemo(() => {
return { fileData, updateFileData, doNotInitializeCodeRef };
}, [fileData, updateFileData, doNotInitializeCodeRef]);
return { fileData, updateFileData, doNotInitializeTheseFileIdsRef };
}, [fileData, updateFileData, doNotInitializeTheseFileIdsRef]);

if (loading) {
return <>{loadingUI}</>;
Expand Down
10 changes: 10 additions & 0 deletions src/dev_constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const useFirebaseEmulatorInDev = true;
const useYjsDevSerInDev = false;

export const SHOULD_USE_FIREBASE_EMULATOR =
typeof window !== 'undefined' &&
location.hostname === 'localhost' &&
useFirebaseEmulatorInDev;
export const SHOULD_USE_DEV_YJS_SERVER =
(process.env.NODE_ENV !== 'production' && useYjsDevSerInDev) ||
process.env.IS_TEST_ENV;
4 changes: 2 additions & 2 deletions src/firebaseAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ if (getApps().length === 0) {
});
} else {
initializeApp({
projectId: 'cp-ide',
databaseURL: 'http://127.0.0.1:9000?ns=cp-ide-default-rtdb',
projectId: 'cp-ide-2',
databaseURL: 'http://127.0.0.1:9000?ns=cp-ide-2-default-rtdb',
});
}
}
Expand Down

1 comment on commit 67c2661

@vercel
Copy link

@vercel vercel bot commented on 67c2661 Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.