Skip to content

Commit

Permalink
Project identity-based cloud synchronization (#7743)
Browse files Browse the repository at this point in the history
* wip

* saving cloudUserId

* show icon for cloud projects

* can push and pull packages however conflict resolution doesnt work well due to spurious, aggressive local saving

* heuristically detect use changes

* improve detection of user changes

* fix new project creation; debugging editor not updating on sync

* two browser same-project editting & syncing works!!!

* track cloud sync time and improve messaging around synchronization

* fix missing field init

* cloud sync seems to work for must scenarios

* remove debug logging

* better error handling for empty cloud workspace; debug logging for tracking "project is open elsewhere"

* clarifying comment

* remove TODOs

* code card TODO

Co-authored-by: Eric Anderson <[email protected]>
  • Loading branch information
darzu and eanders-ms authored Jan 11, 2021
1 parent 72e5aa5 commit 9c3018c
Show file tree
Hide file tree
Showing 18 changed files with 637 additions and 402 deletions.
16 changes: 11 additions & 5 deletions localtypings/projectheader.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ declare namespace pxt.workspace {
tutorialCompleted?: pxt.tutorial.TutorialCompletionInfo;
// workspace guid of the extension under test
extensionUnderTest?: string;
cloudSync?: boolean; // Mark a header for syncing with a cloud provider
// id of cloud user who created this project
cloudUserId?: string;
}

export interface Header extends InstallHeader {
Expand All @@ -41,10 +42,15 @@ declare namespace pxt.workspace {
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

// 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
// DEPRECATED (formerly for cloud sync)
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 sync (local only metadata)
cloudVersion: string; // The cloud-assigned version number (e.g. etag)
cloudCurrent: boolean; // Has the current version of the project been pushed to cloud
cloudLastSyncTime: number; // seconds since epoch

// Used for Updating projects
backupRef?: string; // guid of backed-up project (present if an update was interrupted)
Expand Down
10 changes: 6 additions & 4 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" | "link";
type CodeCardEditorType = "blocks" | "js" | "py";
type CodeCardCloudState = "local" | "cloud";

interface Map<T> {
[index: string]: T;
Expand Down Expand Up @@ -93,19 +94,19 @@ declare namespace pxt {

interface PackageExtension {
// Namespace to add the button under, defaults to package name
namespace?: string;
namespace?: string;
// Group to place button in
group?: string;
// Label for the flyout button, defaults to `Editor`
label?: string;
label?: string;
// for new category, category color
color?: string;
color?: string;
// for new category, is category advanced
advanced?: boolean;
// trusted custom editor url, must be register in targetconfig.json under approvedEditorExtensionUrls
url?: string;
// local debugging URL used when served through pxt serve and debugExtensions=1 mode
localUrl?: string;
localUrl?: string;
}

interface PlatformIOConfig {
Expand Down Expand Up @@ -168,6 +169,7 @@ declare namespace pxt {
cardType?: CodeCardType;
editor?: CodeCardEditorType;
otherActions?: CodeCardAction[];
cloudState?: CodeCardCloudState;
directOpen?: boolean; // skip the details view, directly do the card action

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 @@ -216,12 +216,11 @@ namespace pxt.editor {
importExampleAsync(options: ExampleImportOptions): Promise<void>;
showScriptManager(): void;
importProjectDialog(): void;
cloudSync(): boolean;
cloudSignInDialog(): void;
cloudSignOut(): void;
removeProject(): void;
editText(): void;

hasCloudSync(): boolean;

getPreferredEditor(): string;
saveAndCompile(): void;
updateHeaderName(name: string): void;
Expand Down
10 changes: 7 additions & 3 deletions pxteditor/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ 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,
cloudLastSyncTime: 0,
isDeleted: false,
}
return header
Expand Down
95 changes: 31 additions & 64 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
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 @@ -1131,7 +1130,7 @@ export class ProjectView
return p.setContentAsync(name, content)
.then(() => {
if (open) this.setFile(p.lookupFile("this/" + name));
return p.savePkgAsync();
return p.cloudSavePkgAsync();
})
.then(() => this.reloadHeaderAsync())
}
Expand Down Expand Up @@ -1305,11 +1304,27 @@ export class ProjectView
if (checkAsync)
return checkAsync.then(() => this.openHome());

let doSync = false
// check our multi-tab session
if (workspace.isHeadersSessionOutdated()) {
// reload header before loading
pxt.log(`multi-tab sync before load`)
doSync = true;
}
// check our cloud sync
const RESYNC_TIME_SEC = 5 * 60 // 5 minutes
const headerCloudSyncOutdated = auth.loggedInSync()
&& Util.nowSeconds() - workspace.getHeaderLastCloudSync(h) > RESYNC_TIME_SEC
if (headerCloudSyncOutdated) {
pxt.log(`cloud sync before load`)
doSync = true;
}

let p = Promise.resolve();
if (workspace.isHeadersSessionOutdated()) { // reload header before loading
pxt.log(`sync before load`)
if (doSync) {
p = p.then(() => workspace.syncAsync().then(() => { }))
}

return p.then(() => {
workspace.acquireHeaderSession(h);
if (!h) return Promise.resolve();
Expand Down Expand Up @@ -1341,7 +1356,10 @@ 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()) {
h.cloudUserId = this.getUser()?.id;
}

return compiler.newProjectAsync()
.then(() => h.backupRef ? workspace.restoreFromBackupAsync(h) : Promise.resolve())
Expand Down Expand Up @@ -1772,7 +1790,7 @@ export class ProjectView
cfg.dependencies = {};
cfg.dependencies[n] = `pkg:${fn}`;
}))
.then(() => mpkg.savePkgAsync())
.then(() => mpkg.cloudSavePkgAsync())
.done(() => this.reloadHeaderAsync());
}
}
Expand Down Expand Up @@ -2042,62 +2060,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 @@ -2289,7 +2251,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 @@ -3128,6 +3090,10 @@ export class ProjectView
}
}

hasCloudSync() {
return this.isLoggedIn();
}

showScriptManager() {
this.scriptManagerDialog.show();
}
Expand Down Expand Up @@ -4617,6 +4583,7 @@ document.addEventListener("DOMContentLoaded", () => {
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");
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();
Loading

0 comments on commit 9c3018c

Please sign in to comment.