Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Workspace layer refactor #7657

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
46c524c
wip
eanders-ms Nov 23, 2020
cc33cf2
wip
darzu Dec 1, 2020
010da76
show icon for cloud projects
darzu Dec 1, 2020
5f8085b
remove debug statements
darzu Dec 1, 2020
e15f586
investigative changes
darzu Dec 3, 2020
195b4e1
trying out cloudworkspace again
darzu Dec 4, 2020
20328c6
add browserdbworkspace.ts
darzu Dec 4, 2020
6909468
refactor browser workspace to build off more pure table db worksapce
darzu Dec 4, 2020
1c81516
error TOOO
darzu Dec 7, 2020
b19911d
debug logging
darzu Dec 7, 2020
c975d5d
create workspaces/ folder
darzu Dec 7, 2020
3ff57dd
refactor setupWorkspace into pure chooseWorkspace
darzu Dec 8, 2020
a5063a7
export
darzu Dec 8, 2020
423fe0a
cloud debug logging
darzu Dec 8, 2020
7c73231
debugging and type cleanup
darzu Dec 8, 2020
142b6c6
investigative comments
darzu Dec 8, 2020
b64be99
starting JointWorkspace
darzu Dec 8, 2020
631f200
toying with sync workspace
darzu Dec 9, 2020
c0fd88f
trying out joint workspace
darzu Dec 9, 2020
6d05fa5
experimenting
darzu Dec 9, 2020
98d30cb
unreachable helper
darzu Dec 9, 2020
c7bed33
header comments
darzu Dec 9, 2020
ffdcbad
debug logging
darzu Dec 9, 2020
0300ad1
rename
darzu Dec 9, 2020
7da5f13
taking a stab at synchronization
darzu Dec 9, 2020
dfbd3db
wip on sync
darzu Dec 10, 2020
a7523ad
trying cloud sync workspace again
darzu Dec 11, 2020
bc8a1a3
lots of work on cloud sync
darzu Dec 12, 2020
b8e8393
much better joint workspace
darzu Dec 13, 2020
f8201ce
fix build issue
darzu Dec 14, 2020
6939562
fix lint issues
darzu Dec 14, 2020
e6c3f9b
lots of debugging
darzu Dec 16, 2020
94605ea
debugging
darzu Dec 16, 2020
592c1ec
horrible debugging
darzu Dec 17, 2020
7971ff3
fix project transfer & debugging
darzu Dec 17, 2020
88717cf
fixining sync
darzu Dec 18, 2020
d711126
fix debug crash
darzu Dec 19, 2020
71941fd
debugging
darzu Dec 21, 2020
ae41778
big progress on race conditions & sanity
darzu Dec 24, 2020
be015c5
deprecate allscripts
darzu Dec 24, 2020
e83fd6e
investigative comments and todos
darzu Dec 28, 2020
8700481
reworked headers hash (formerly last-mod-time)
darzu Dec 29, 2020
49fb645
todo list and progress on syncAsync
darzu Dec 29, 2020
6b388a8
return headers[] from sync; finer grain virtual api inval
darzu Jan 2, 2021
e970d53
better names
darzu Jan 2, 2021
bb676aa
unify and fix up maybeSyncHeadersAsync, refreshHeadersSession and syn…
darzu Jan 3, 2021
a62252d
undo typo
darzu Jan 3, 2021
1d52319
debugging spurious saves
darzu Jan 4, 2021
a3155bd
first-save TODOs
darzu Jan 4, 2021
d70f46a
debugging empty saves
darzu Jan 4, 2021
0ea5813
fix issues with getTextAsync and lookup(); investigating double save …
darzu Jan 4, 2021
1e93dd1
handling 404s from the cloud
darzu Jan 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions localtypings/projectheader.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,30 @@ declare namespace pxt.workspace {
tutorialCompleted?: pxt.tutorial.TutorialCompletionInfo;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is the difference between "Header" and "InstallHeader" ?

// workspace guid of the extension under test
extensionUnderTest?: string;
cloudSync?: boolean; // Mark a header for syncing with a cloud provider
Copy link
Contributor Author

@darzu darzu Nov 23, 2020

Choose a reason for hiding this comment

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

note:
was used to determine if a project was cloud synced (old Sam work?)
problem was it didn't distinguish account A or B

// id of cloud user who created this project
cloudUserId?: string;
}

export interface Header extends InstallHeader {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

header is meta info for a project

id: string; // guid (generated by us)
path?: string; // for workspaces that require it
recentUse: number; // seconds since epoch
modificationTime: number; // seconds since epoch
recentUse: number; // seconds since epoch UTC (cloud safe)
modificationTime: number; // seconds since epoch UTC (cloud safe)
icon?: string; // icon uri

isDeleted: boolean; // mark whether or not a header has been deleted
saveId?: any; // used to determine whether a project has been edited while we're saving to cloud

// TODO @darzu: remove all of these?

// For cloud providers
blobId: string; // id of the cloud blob holding this script
blobVersion: string; // version of the cloud blob
blobCurrent: boolean; // has the current version of the script been pushed to cloud
// For cloud providers -- DEPRECATED
blobId_: string; // id of the cloud blob holding this script
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note that "project id" is assigned by client and its user ID + project ID. Note "id" field

blobVersion_: string; // version of the cloud blob
blobCurrent_: boolean; // has the current version of the script been pushed to cloud
darzu marked this conversation as resolved.
Show resolved Hide resolved

cloudVersion: string; // The cloud-assigned version number (e.g. etag)
// TODO @darzu: "cloudCurrent" seems very bad. This is a stateful notation and it is hard to reason about whether or not this is true.
cloudCurrent: boolean; // Has the current version of the project been pushed to cloud
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: do we need / how do we use cloudCurrent exactly?


// Used for Updating projects
backupRef?: string; // guid of backed-up project (present if an update was interrupted)
Expand Down
2 changes: 2 additions & 0 deletions localtypings/pxtpackage.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ declare namespace pxt {

type CodeCardType = "file" | "example" | "codeExample" | "tutorial" | "side" | "template" | "package" | "hw" | "forumUrl" | "forumExample" | "sharedExample";
type CodeCardEditorType = "blocks" | "js" | "py";
type CodeCardCloudState = "local" | "cloud";

interface Map<T> {
[index: string]: T;
Expand Down Expand Up @@ -159,6 +160,7 @@ declare namespace pxt {
cardType?: CodeCardType;
editor?: CodeCardEditorType;
otherActions?: CodeCardAction[];
cloudState?: CodeCardCloudState;

header?: string;

Expand Down
5 changes: 2 additions & 3 deletions pxteditor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,11 @@ namespace pxt.editor {
importExampleAsync(options: ExampleImportOptions): Promise<void>;
showScriptManager(): void;
importProjectDialog(): void;
cloudSync(): boolean;
cloudSignInDialog(): void;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

cloudSignInDialog is unneeded because we have new dialog

cloudSignOut(): void;
removeProject(): void;
editText(): void;

hasCloudSync(): boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed from cloudSync to find usages

checks to see if we're signed in.


getPreferredEditor(): string;
saveAndCompile(): void;
updateHeaderName(name: string): void;
Expand Down
1 change: 1 addition & 0 deletions pxteditor/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace pxt.storage {
// TODO @darzu: why is this different from the WorkspaceProvider api?
interface IStorage {
removeItem(key: string): void;
getItem(key: string): string;
Expand Down
24 changes: 20 additions & 4 deletions pxteditor/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace pxt.workspace {
export type ScriptText = pxt.Map<string>;


// TODO @darzu: ugh. why is there a "Project" that is different from a "File". They are nearly identical...
export interface Project {
header?: Header;
text?: ScriptText;
Expand All @@ -14,17 +16,28 @@ namespace pxt.workspace {
url: string;
}

// TODO @darzu: why is version "any" ? that's really annoying to reason about. used as: string | ScriptText
// TODO @darzu: _rev is a string; modificationTime is an int
export type Version = any;

export interface File {
header: Header;
text: ScriptText;
// This version field is reserved for the storage mechanism. E.g. PouchDB requires a _rev field containing
// the currently stored version.
version: Version;
}

export interface WorkspaceProvider {
listAsync(): Promise<Header[]>; // called from workspace.syncAsync (including upon startup)
listAsync(): Promise<Header[]>;
/*
Tries to get the corrisponding File with the current version if it exists.
If it does not exist, returns undefined.
*/
getAsync(h: Header): Promise<File>;
/*
If text is empty, then only update the header.
*/
setAsync(h: Header, prevVersion: Version, text?: ScriptText): Promise<Version>;
deleteAsync?: (h: Header, prevVersion: Version) => Promise<void>;
resetAsync(): Promise<void>;
Expand Down Expand Up @@ -54,9 +67,12 @@ namespace pxt.workspace {
id: U.guidGen(),
recentUse: modTime,
modificationTime: modTime,
blobId: null,
blobVersion: null,
blobCurrent: false,
blobId_: null,
blobVersion_: null,
blobCurrent_: false,
cloudUserId: null,
cloudCurrent: false,
cloudVersion: null,
isDeleted: false,
}
return header
Expand Down
4 changes: 4 additions & 0 deletions pxtlib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,10 @@ namespace ts.pxtc.Util {
return res
})
}

export function unreachable(...ns: never[]): never {
throw new Error("Type error: this code should be unreachable");
}
}

namespace ts.pxtc.BrowserImpl {
Expand Down
97 changes: 32 additions & 65 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import * as React from "react";
import * as ReactDOM from "react-dom";
import * as workspace from "./workspace";
import * as cloudsync from "./cloudsync";
import * as workspace from "./workspaces/workspace";
import * as cloudsync from "./workspaces/cloudsync";
import * as data from "./data";
import * as pkg from "./package";
import * as core from "./core";
Expand Down Expand Up @@ -95,7 +95,7 @@ function setEditor(editor: ProjectView) {
}

export class ProjectView
extends data.Component<IAppProps, IAppState>
extends auth.Component<IAppProps, IAppState>
implements IProjectView {
editor: srceditor.Editor;
editorFile: pkg.File;
Expand Down Expand Up @@ -173,7 +173,6 @@ export class ProjectView
this.openDeviceSerial = this.openDeviceSerial.bind(this);
this.toggleGreenScreen = this.toggleGreenScreen.bind(this);
this.toggleSimulatorFullscreen = this.toggleSimulatorFullscreen.bind(this);
this.cloudSignInComplete = this.cloudSignInComplete.bind(this);
this.toggleSimulatorCollapse = this.toggleSimulatorCollapse.bind(this);
this.showKeymap = this.showKeymap.bind(this);
this.toggleKeymap = this.toggleKeymap.bind(this);
Expand Down Expand Up @@ -264,8 +263,13 @@ export class ProjectView
}
this.saveFileAsync().done();
} else if (active) {
// TODO @darzu: new code path maybe:
// workspace.syncAsync().then(changed => this.reloadAsync())
// reloadAsync: this.loadHeaderAsync()
// OR: subscribe to data api, on change, reload

data.invalidate("header:*")
if (workspace.isHeadersSessionOutdated()
if (workspace.isHeadersSessionOutdated() // TODO @darzu: sync check point
|| workspace.isHeaderSessionOutdated(this.state.header)) {
pxt.debug('workspace: changed, reloading...')
let id = this.state.header ? this.state.header.id : '';
Expand Down Expand Up @@ -1300,6 +1304,7 @@ export class ProjectView
return checkAsync.then(() => this.openHome());

let p = Promise.resolve();
// TODO @darzu: sync checkpoint
if (workspace.isHeadersSessionOutdated()) { // reload header before loading
pxt.log(`sync before load`)
p = p.then(() => workspace.syncAsync().then(() => { }))
Expand Down Expand Up @@ -1335,7 +1340,11 @@ export class ProjectView
if (editorState.searchBar === undefined) editorState.searchBar = oldEditorState.searchBar;
}

if (!h.cloudSync && this.cloudSync()) h.cloudSync = true;
// If user is signed in, sync this project to the cloud.
if (this.hasCloudSync()) {
// TODO @darzu: this might not be where we want to attach the user to the project
h.cloudUserId = this.getUser()?.id;
}

return compiler.newProjectAsync()
.then(() => h.backupRef ? workspace.restoreFromBackupAsync(h) : Promise.resolve())
Expand Down Expand Up @@ -1442,6 +1451,7 @@ export class ProjectView
}

// update recentUse on the header
// TODO @darzu: this is saving hte project without text...
return workspace.saveAsync(h)
}).then(() => this.loadTutorialFiltersAsync())
.finally(() => {
Expand Down Expand Up @@ -2006,62 +2016,6 @@ export class ProjectView
})
}

///////////////////////////////////////////////////////////
//////////// Cloud ////////////
///////////////////////////////////////////////////////////

cloudSync() {
return this.hasSync();
}

cloudSignInDialog() {
const providers = cloudsync.providers();
if (providers.length == 0)
return;
if (providers.length == 1)
providers[0].loginAsync().then(() => {
this.cloudSignInComplete();
})
else {
// TODO: Revisit in new cloud sync
//this.signInDialog.show();
}
}

cloudSignOut() {
core.confirmAsync({
header: lf("Sign out"),
body: lf("You are signing out. Make sure that you commited all your changes, local projects will be deleted."),
agreeClass: "red",
agreeIcon: "sign out",
agreeLbl: lf("Sign out"),
}).then(r => {
if (r) {
const inEditor = !!this.state.header;
// Reset the cloud workspace
return workspace.resetCloudAsync()
.then(() => {
if (inEditor) {
this.openHome();
}
if (this.home) {
this.home.forceUpdate();
}
})
}
return Promise.resolve();
});
}

cloudSignInComplete() {
pxt.log('cloud sign in complete');
initLogin();
cloudsync.syncAsync()
.then(() => {
this.forceUpdate();
}).done();
}

///////////////////////////////////////////////////////////
//////////// Home /////////////
///////////////////////////////////////////////////////////
Expand Down Expand Up @@ -2251,7 +2205,7 @@ export class ProjectView
pubCurrent: false,
target: pxt.appTarget.id,
targetVersion: pxt.appTarget.versions.target,
cloudSync: this.cloudSync(),
cloudUserId: this.getUser()?.id,
temporary: options.temporary,
tutorial: options.tutorial,
extensionUnderTest: options.extensionUnderTest
Expand Down Expand Up @@ -3090,6 +3044,10 @@ export class ProjectView
}
}

hasCloudSync() {
return this.isLoggedIn();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: maybe once we go to AAD we might not be able to store data ourselves and we might need OneDrive? Probably worth adding a TODO comment

}

showScriptManager() {
this.scriptManagerDialog.show();
}
Expand Down Expand Up @@ -3327,13 +3285,15 @@ export class ProjectView
}

showResetDialog() {
console.log("showResetDialog (1)")
dialogs.showResetDialogAsync().done(r => {
if (!r) return Promise.resolve();
return Promise.resolve()
.then(() => {
return pxt.winrt.releaseAllDevicesAsync();
})
.then(() => {
console.log("showResetDialog (2)")
return this.resetWorkspace();
});
});
Expand Down Expand Up @@ -4540,12 +4500,19 @@ document.addEventListener("DOMContentLoaded", () => {
const isSandbox = pxt.shell.isSandboxMode() || pxt.shell.isReadOnly();
const isController = pxt.shell.isControllerMode();
const theme = pxt.appTarget.appTheme;
if (query["ws"]) workspace.setupWorkspace(query["ws"]);
// TODO @darzu: this is bad. we shouldn't be choosing a workspace in two places (see "chooseWorkspace")
if (query["ws"]) {
workspace.setupWorkspace(query["ws"] as workspace.WorkspaceKind)
}
else if ((theme.allowParentController || isController) && pxt.BrowserUtils.isIFrame()) workspace.setupWorkspace("iframe");
else if (isSandbox) workspace.setupWorkspace("mem");
else if (pxt.winrt.isWinRT()) workspace.setupWorkspace("uwp");
else if (pxt.BrowserUtils.isIpcRenderer()) workspace.setupWorkspace("idb");
else if (pxt.BrowserUtils.isLocalHost() || pxt.BrowserUtils.isPxtElectron()) workspace.setupWorkspace("fs");
// TODO @darzu: uncomment. this disables filesystem workspace
//else if (pxt.BrowserUtils.isLocalHost() || pxt.BrowserUtils.isPxtElectron()) workspace.setupWorkspace("fs");
Copy link
Contributor Author

@darzu darzu Nov 23, 2020

Choose a reason for hiding this comment

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

TODO uncomment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this disabled the filesystem workspace

else {
workspace.setupWorkspace("browser");
}
Promise.resolve()
.then(async () => {
const href = window.location.href;
Expand Down
10 changes: 8 additions & 2 deletions webapp/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,12 +465,18 @@ export async function initialUserPreferences(): Promise<UserPreferences | undefi
return initialUserPreferences_;
}

function loggedInSync(): boolean {
export function loggedInSync(): boolean {
if (!hasIdentity()) { return false; }
const state = getState();
return !!state.profile?.id;
}

export function user(): UserProfile {
if (!hasIdentity()) { return null; }
const state = getState();
return { ...state.profile };
}

async function fetchUserAsync(): Promise<UserProfile | undefined> {
const state = getState();

Expand Down Expand Up @@ -637,5 +643,5 @@ data.mountVirtualApi(MODULE, { getSync: authApiHandler });


// ClouddWorkspace must be included after we mount our virtual APIs.
import * as cloudWorkspace from "./cloudworkspace";
import * as cloudWorkspace from "./cloud";
cloudWorkspace.init();
2 changes: 1 addition & 1 deletion webapp/src/blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as toolboxeditor from "./toolboxeditor"
import * as compiler from "./compiler"
import * as toolbox from "./toolbox";
import * as snippets from "./blocksSnippets";
import * as workspace from "./workspace";
import * as workspace from "./workspaces/workspace";
import * as simulator from "./simulator";
import * as dialogs from "./dialogs";
import * as blocklyFieldView from "./blocklyFieldView";
Expand Down
Loading