Skip to content

Commit

Permalink
(core) move home server into core
Browse files Browse the repository at this point in the history
Summary: This moves enough server material into core to run a home server.  The data engine is not yet incorporated (though in manual testing it works when ported).

Test Plan: existing tests pass

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2552
  • Loading branch information
paulfitz committed Jul 22, 2020
1 parent c756f66 commit 5ef889a
Show file tree
Hide file tree
Showing 218 changed files with 33,640 additions and 38 deletions.
74 changes: 74 additions & 0 deletions app/common/ActionBundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Basic definitions of types needed for ActionBundles.
* See also EncActionBundle for how these are packaged for encryption.
*/

import {DocAction, UserAction} from 'app/common/DocActions';

// Metadata about the action.
export interface ActionInfo {
time: number; // Milliseconds since epoch.
user: string;
inst: string;
desc?: string;
otherId: number;
linkId: number;
}

// Envelope contains information about recipients. In EncActionBundle, it's augmented with
// information about the symmetric key that encrypts this envelope's contents.
export interface Envelope {
recipients: string[]; // sorted array of recipient instanceIds
}

// EnvContent packages arbitrary content with the index of the envelope to which it belongs.
export type EnvContent<Content> = [number, Content];

// ActionBundle contains actions arranged into envelopes, i.e. split up by sets of recipients.
// Note that different Envelopes contain different sets of recipients (which may overlap however).
// ActionBundle is what gets encrypted/decrypted and then sent between hub and instance.
export interface ActionBundle {
actionNum: number;
actionHash: string|null; // a checksum of bundle, (not including actionHash and other parts).
parentActionHash: string|null; // a checksum of the parent action bundle, if there is one.
envelopes: Envelope[];
info: EnvContent<ActionInfo>; // Should be in the envelope addressed to all peers.
stored: Array<EnvContent<DocAction>>;
calc: Array<EnvContent<DocAction>>;
}

export function getEnvContent<Content>(items: Array<EnvContent<Content>>): Content[] {
return items.map((item) => item[1]);
}

// ======================================================================
// Types for ActionBundles used locally inside an instance.

// Local action received from the browser, that is not yet applied. It is usually one UserAction,
// but when multiple actions are sent by the browser in one call, they will form one bundle.
export interface UserActionBundle {
info: ActionInfo;
userActions: UserAction[];
}

// ActionBundle as received from the sandbox. It does not have some action metadata, but does have
// undo information and a retValue for each input UserAction. Note that it is satisfied by the
// ActionBundle structure defined in sandbox/grist/action_obj.py.
export interface SandboxActionBundle {
envelopes: Envelope[];
stored: Array<EnvContent<DocAction>>;
calc: Array<EnvContent<DocAction>>;
undo: Array<EnvContent<DocAction>>; // Inverse actions for all 'stored' actions.
retValues: any[]; // Contains retValue for each of userActions.
}

// Local action that's been applied. It now has an actionNum, and includes doc actions packaged
// into envelopes, as well as undo, and userActions, which allow rebasing.
export interface LocalActionBundle extends ActionBundle {
userActions: UserAction[];

// Inverse actions for all 'stored' actions. These aren't shared and not split by envelope.
// Applying 'undo' is governed by EDIT rather than READ permissions, so we always apply all undo
// actions. (It is the result of applying 'undo' that may be addressed to different recipients).
undo: DocAction[];
}
71 changes: 71 additions & 0 deletions app/common/ActionDispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import mapValues = require('lodash/mapValues');
import {BulkColValues, ColInfo, ColInfoWithId, ColValues, DocAction} from "./DocActions";

// TODO this replaces modelUtil's ActionDispatcher and bulkActionExpand. Those should be removed.

/**
* Helper class which provides a `dispatchAction` method that dispatches DocActions received from
* the server to methods `this.on{ActionType}`, e.g. `this.onUpdateRecord`.
*
* Implementation methods `on*` are called with the action as the first argument, and with
* the action arguments as additional method arguments, for convenience.
*
* Methods for bulk actions may be implemented directly, or will iterate through each record in
* the action, and call the single-record methods for each one.
*/
export abstract class ActionDispatcher {
public dispatchAction(action: DocAction): void {
// In node 6 testing, this switch is 5+ times faster than looking up "on"+action[0].
const a: any[] = action;
switch (action[0]) {
case "AddRecord": return this.onAddRecord (action, a[1], a[2], a[3]);
case "UpdateRecord": return this.onUpdateRecord (action, a[1], a[2], a[3]);
case "RemoveRecord": return this.onRemoveRecord (action, a[1], a[2]);
case "BulkAddRecord": return this.onBulkAddRecord (action, a[1], a[2], a[3]);
case "BulkUpdateRecord": return this.onBulkUpdateRecord(action, a[1], a[2], a[3]);
case "BulkRemoveRecord": return this.onBulkRemoveRecord(action, a[1], a[2]);
case "ReplaceTableData": return this.onReplaceTableData(action, a[1], a[2], a[3]);
case "AddColumn": return this.onAddColumn (action, a[1], a[2], a[3]);
case "RemoveColumn": return this.onRemoveColumn (action, a[1], a[2]);
case "RenameColumn": return this.onRenameColumn (action, a[1], a[2], a[3]);
case "ModifyColumn": return this.onModifyColumn (action, a[1], a[2], a[3]);
case "AddTable": return this.onAddTable (action, a[1], a[2]);
case "RemoveTable": return this.onRemoveTable (action, a[1]);
case "RenameTable": return this.onRenameTable (action, a[1], a[2]);
default: throw new Error(`Received unknown action ${action[0]}`);
}
}

protected abstract onAddRecord(action: DocAction, tableId: string, rowId: number, colValues: ColValues): void;
protected abstract onUpdateRecord(action: DocAction, tableId: string, rowId: number, colValues: ColValues): void;
protected abstract onRemoveRecord(action: DocAction, tableId: string, rowId: number): void;

// If not overridden, these will make multiple calls to single-record action methods.
protected onBulkAddRecord(action: DocAction, tableId: string, rowIds: number[], colValues: BulkColValues): void {
for (let i = 0; i < rowIds.length; i++) {
this.onAddRecord(action, tableId, rowIds[i], mapValues(colValues, (values) => values[i]));
}
}
protected onBulkUpdateRecord(action: DocAction, tableId: string, rowIds: number[], colValues: BulkColValues): void {
for (let i = 0; i < rowIds.length; i++) {
this.onUpdateRecord(action, tableId, rowIds[i], mapValues(colValues, (values) => values[i]));
}
}
protected onBulkRemoveRecord(action: DocAction, tableId: string, rowIds: number[]) {
for (const r of rowIds) {
this.onRemoveRecord(action, tableId, r);
}
}

protected abstract onReplaceTableData(
action: DocAction, tableId: string, rowIds: number[], colValues: BulkColValues): void;

protected abstract onAddColumn(action: DocAction, tableId: string, colId: string, colInfo: ColInfo): void;
protected abstract onRemoveColumn(action: DocAction, tableId: string, colId: string): void;
protected abstract onRenameColumn(action: DocAction, tableId: string, oldColId: string, newColId: string): void;
protected abstract onModifyColumn(action: DocAction, tableId: string, colId: string, colInfo: ColInfo): void;

protected abstract onAddTable(action: DocAction, tableId: string, columns: ColInfoWithId[]): void;
protected abstract onRemoveTable(action: DocAction, tableId: string): void;
protected abstract onRenameTable(action: DocAction, oldTableId: string, newTableId: string): void;
}
19 changes: 19 additions & 0 deletions app/common/ActionGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {ActionSummary} from 'app/common/ActionSummary';

/** This is the action representation the client works with. */
export interface ActionGroup {
actionNum: number;
actionHash: string;
desc?: string;
actionSummary: ActionSummary;
fromSelf: boolean;
linkId: number;
otherId: number;
time: number;
user: string;
rowIdHint: number; // If non-zero, this is a rowId that would be a good place to put
// the cursor after an undo.
primaryAction: string; // The name of the first user action in the ActionGroup.
isUndo: boolean; // True if the first user action is ApplyUndoActions.
internal: boolean; // True if it is inappropriate to log/undo the action.
}
49 changes: 49 additions & 0 deletions app/common/ActionRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Rpc } from "grain-rpc";

/**
* ActionRouter allows to choose what actions to send over rpc. Action are posted as message `{type:
* "docAction", action }` over rpc.
*/
export class ActionRouter {

private _subscribedTables: Set<string> = new Set();

constructor(private _rpc: Rpc) {}

/**
* Subscribe to send all actions related to a table. Keeps sending actions if table is renamed.
*/
public subscribeTable(tableId: string): Promise<void> {
this._subscribedTables.add(tableId);
return Promise.resolve();
}

/**
* Stop sending all message related to a table.
*/
public unsubscribeTable(tableId: string): Promise<void> {
this._subscribedTables.delete(tableId);
return Promise.resolve();
}

/**
* Process a action updates subscription set in case of table rename and table remove, and post
* action if it matches a subscriptions.
*/
public process(action: any[]): Promise<void> {
const tableId = action[1];
if (!this._subscribedTables.has(tableId)) {
return Promise.resolve();
}
switch (action[0]) {
case "RemoveTable":
this._subscribedTables.delete(tableId);
break;
case "RenameTable":
this._subscribedTables.delete(tableId);
this._subscribedTables.add(action[2]);
break;
}
return this._rpc.postMessage({type: "docAction", action});
}
}
Loading

0 comments on commit 5ef889a

Please sign in to comment.