@platform/cell.typesystem
Version:
The 'strongly typed sheets' system of the CellOS.
193 lines (192 loc) • 6.93 kB
JavaScript
import { filter, map, share, takeUntil } from 'rxjs/operators';
import { TypeCache } from '../../TypeSystem.core';
import { deleteUndefined, R, Schema, t, Uri } from './common';
export class TypedSheetState {
constructor(args) {
this._changes = {};
this._dispose$ = new t.Subject();
this.dispose$ = this._dispose$.pipe(share());
this.clear = {
cache: () => {
const ns = this.uri.id;
const fetch = this.fetch;
const cache = fetch.cache;
const prefix = {
ns: fetch.cacheKey('getNs', ns),
cells: fetch.cacheKey('getCells', ns),
};
cache.keys
.filter(key => key.startsWith(prefix.cells) || key.startsWith(prefix.ns))
.forEach(key => cache.delete(key));
},
changes: (action) => {
const sheet = this._sheet;
const from = Object.assign({}, this._changes);
const to = {};
this._changes = {};
this.fire({
type: 'SHEET/changes/cleared',
payload: { sheet, from, to, action },
});
},
};
this.change = {
ns: (to) => {
const ns = this.uri.toString();
this.fire({
type: 'SHEET/change',
payload: { kind: 'NS', ns, to },
});
},
cell: (key, to) => {
const ns = this.uri.id;
this.fire({
type: 'SHEET/change',
payload: { kind: 'CELL', ns, key, to },
});
},
};
this._sheet = args.sheet;
this._event$ = args.event$;
const fetch = TypeCache.fetch(args.fetch, { cache: args.cache });
const getCells = async (args) => {
const res = await fetch.getCells(args);
const cellChanges = this._changes.cells || {};
const keys = Object.keys(cellChanges);
if (keys.length > 0) {
const cells = (res.cells = Object.assign({}, (res.cells || {})));
keys
.filter(key => Schema.coord.cell.isCell(key) && cells[key])
.forEach(key => (cells[key] = Object.assign({}, cellChanges[key].to)));
}
return res;
};
const getNs = async (args) => {
var _a;
const ns = (_a = this._changes.ns) === null || _a === void 0 ? void 0 : _a.to;
return ns ? { ns } : fetch.getNs(args);
};
this.fetch = Object.assign(Object.assign({}, fetch), { getCells, getNs });
this.event$ = this._event$.pipe(takeUntil(this._dispose$), share());
this.change$ = this.event$.pipe(takeUntil(this.dispose$), filter(e => e.type === 'SHEET/change'), map(e => e.payload), filter(e => this.isWithinNamespace(e.ns)), share());
this.changed$ = this.event$.pipe(takeUntil(this.dispose$), filter(e => e.type === 'SHEET/changed'), map(e => e.payload), filter(e => this.isWithinNamespace(e.sheet.uri.toString())), share());
this.change$
.pipe(filter(e => e.kind === 'CELL'), map(e => e))
.subscribe(({ key, to }) => this.fireCellChanged({ key, to }));
this.change$
.pipe(filter(e => e.kind === 'NS'))
.subscribe(({ to }) => this.fireNsChanged({ to }));
}
static create(args) {
return new TypedSheetState(args);
}
dispose() {
this._dispose$.next();
this._dispose$.complete();
}
get uri() {
return this._sheet.uri;
}
get isDisposed() {
return this._dispose$.isStopped;
}
get changes() {
const changes = this._changes;
return deleteUndefined({
ns: changes.ns ? Object.assign({}, changes.ns) : undefined,
cells: changes.cells ? Object.assign({}, changes.cells) : undefined,
});
}
get hasChanges() {
const changes = this._changes;
if (changes.ns) {
return true;
}
if (changes.cells && Object.keys(changes.cells).length > 0) {
return true;
}
return false;
}
async getNs() {
const ns = this.uri.id;
return (await this.fetch.getNs({ ns })).ns;
}
async getCell(key) {
if (!Schema.coord.cell.isCell(key)) {
throw new Error(`Expected a cell key (eg "A1").`);
}
const cellChanges = this._changes.cells || {};
if (cellChanges[key]) {
return cellChanges[key].to;
}
const ns = this.uri.id;
const query = `${key}:${key}`;
const res = await this.fetch.getCells({ ns, query });
return (res.cells || {})[key];
}
fire(e) {
this._event$.next(e);
}
async fireNsChanged(args) {
const { to } = args;
const existing = this._changes.ns;
if (existing && R.equals(existing.to, to)) {
return;
}
const from = (existing ? existing.from : await this.getNs()) || {};
const change = {
kind: 'NS',
ns: this.uri.id,
from,
to,
};
this._changes = Object.assign(Object.assign({}, this._changes), { ns: change });
this.fireChanged({ change });
}
async fireCellChanged(args) {
const { to, key } = args;
const existing = (this._changes.cells || {})[key];
if (existing && R.equals(existing.to, to)) {
return;
}
const ns = this.uri.id;
const from = (existing ? existing.from : await this.getCell(key)) || {};
delete from.hash;
delete to.hash;
const change = {
kind: 'CELL',
ns,
key,
from,
to,
};
const cells = Object.assign(Object.assign({}, (this._changes.cells || {})), { [key]: change });
this._changes = Object.assign(Object.assign({}, this._changes), { cells });
this.fireChanged({ change });
}
fireChanged(args) {
const { change } = args;
this.fire({
type: 'SHEET/changed',
payload: {
sheet: this._sheet,
change,
changes: this.changes,
},
});
}
isWithinNamespace(input) {
const text = (input || '').trim();
const ns = this.uri;
if (!text.includes(':')) {
return Uri.strip.ns(text) === ns.id;
}
if (text.startsWith('ns:') || !text.includes(':')) {
return text === ns.toString();
}
if (text.startsWith('cell:')) {
return text.startsWith(`cell:${ns.id}:`);
}
return false;
}
}