forked from gristlabs/grist-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
218 changed files
with
33,640 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}); | ||
} | ||
} |
Oops, something went wrong.