mudb
Version:
Real-time database for multiplayer games
976 lines (915 loc) • 38.4 kB
text/typescript
import { MuSchema } from '../schema/schema';
import { MuDictionary } from '../schema/dictionary';
import { MuStruct } from '../schema/struct';
import { MuUnion } from '../schema/union';
import { MuSortedArray } from '../schema/sorted-array';
import { MuVoid } from '../schema/void';
import { MuVarint } from '../schema/varint';
import { MuBoolean } from '../schema/boolean';
import { MuRDA, MuRDAActionMeta, MuRDABindableActionMeta, MuRDAStore, MuRDATypes } from './rda';
import { MuArray } from '../schema';
function compareKey (a:string, b:string) {
return a < b ? -1 : (b < a ? 1 : 0);
}
function compareStoreElements<T extends MuRDAMapStoreElement<any>> (a:T, b:T) {
return (
a.sequence - b.sequence ||
a.id - b.id
);
}
function compareNum (a:number, b:number) {
return a - b;
}
type StripBindMeta<Meta extends MuRDABindableActionMeta> =
Meta extends { type:'store'; action:MuRDAActionMeta; }
? Meta['action']
: Meta extends MuRDAActionMeta
? Meta
: never;
export interface MuRDAMapTypes<
KeySchema extends MuSchema<string>,
ValueRDA extends MuRDA<any, any, any, any>> {
value:ValueRDA['stateSchema']['identity'];
stateSchema:MuDictionary<ValueRDA['stateSchema']>;
state:MuRDAMapTypes<KeySchema, ValueRDA>['stateSchema']['identity'];
storeElementSchema:MuStruct<{
id:MuVarint;
sequence:MuVarint;
deleted:MuBoolean;
key:KeySchema;
value:ValueRDA['storeSchema'];
}>;
storeSchema:MuSortedArray<MuRDAMapTypes<KeySchema, ValueRDA>['storeElementSchema']>;
store:MuRDAMapTypes<KeySchema, ValueRDA>['storeSchema']['identity'];
upsertActionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['storeElementSchema'];
resetActionSchema:MuStruct<{
upserts:MuRDAMapTypes<KeySchema, ValueRDA>['storeSchema'];
deletes:MuSortedArray<MuVarint>;
undeletes:MuSortedArray<MuVarint>;
sequenceIds:MuArray<MuVarint>;
sequenceVals:MuArray<MuVarint>;
}>;
updateActionSchema:MuStruct<{
id:MuVarint;
action:ValueRDA['actionSchema'];
}>;
moveActionSchema:MuStruct<{
id:MuVarint;
sequence:MuVarint;
key:KeySchema;
}>;
setDeletedActionSchema:MuStruct<{
id:MuVarint;
sequence:MuVarint;
deleted:MuBoolean;
}>;
actionSchema:MuUnion<{
reset:MuRDAMapTypes<KeySchema, ValueRDA>['resetActionSchema'];
upsert:MuRDAMapTypes<KeySchema, ValueRDA>['upsertActionSchema'];
update:MuRDAMapTypes<KeySchema, ValueRDA>['updateActionSchema'];
move:MuRDAMapTypes<KeySchema, ValueRDA>['moveActionSchema'];
setDeleted:MuRDAMapTypes<KeySchema, ValueRDA>['setDeletedActionSchema'];
noop:MuVoid;
}>;
action:MuRDAMapTypes<KeySchema, ValueRDA>['actionSchema']['identity'];
resetAction:{
type:'reset';
data:MuRDAMapTypes<KeySchema, ValueRDA>['resetActionSchema']['identity'];
};
upsertAction:{
type:'upsert';
data:MuRDAMapTypes<KeySchema, ValueRDA>['upsertActionSchema']['identity'];
};
updateAction:{
type:'update';
data:MuRDAMapTypes<KeySchema, ValueRDA>['updateActionSchema']['identity'];
};
moveAction:{
type:'move';
data:MuRDAMapTypes<KeySchema, ValueRDA>['moveActionSchema']['identity'];
};
setDeletedAction:{
type:'setDeleted';
data:MuRDAMapTypes<KeySchema, ValueRDA>['setDeletedActionSchema']['identity'];
};
noopAction:{
type:'noop';
data:undefined;
};
actionMeta:{
type:'store';
action:{
type:'table';
table:{
set:{
type:'unit';
};
remove:{
type:'unit';
};
clear:{
type:'unit';
};
reset:{
type:'unit';
};
move:{
type:'unit';
};
update:{
type:'partial';
action:StripBindMeta<ValueRDA['actionMeta']>;
};
};
};
};
}
export class MuRDAMapStoreElement<MapRDA extends MuRDAMap<any, any>> {
constructor (
public id:number,
public sequence:number,
public deleted:boolean,
public key:MapRDA['keySchema']['identity'],
public value:MuRDATypes<MapRDA['valueRDA']>['store'],
) {}
// currently a linked list, though this should probably be replaced by a pairing heap to prevent adversarial attacks
public next:this|null = null;
public prev:this|null = null;
}
export class MuRDAMapStore<MapRDA extends MuRDAMap<any, any>> implements MuRDAStore<MapRDA> {
public keyIndex:{ [key:string]:MuRDAMapStoreElement<MapRDA>; } = {};
public idIndex:{ [id:string]:MuRDAMapStoreElement<MapRDA>; } = {};
constructor (elements:MuRDAMapStoreElement<MapRDA>[]) {
const { idIndex } = this;
for (let i = 0; i < elements.length; ++i) {
const element = elements[i];
const id = element.id;
idIndex[id] = element;
this._insertElement(element);
}
}
public state(rda:MapRDA, out:MuRDATypes<MapRDA>['state']) : MuRDATypes<MapRDA>['state'] {
const keyIndex = this.keyIndex;
const outKeys = Object.keys(out);
for (let i = 0; i < outKeys.length; ++i) {
const key = outKeys[i];
const cur = keyIndex[key];
if (!cur || cur.deleted) {
rda.valueRDA.stateSchema.free(out[key]);
delete out[key];
}
}
const keys = Object.keys(keyIndex);
const valueRDA = rda.valueRDA;
const valueSchema = valueRDA.stateSchema;
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const final = keyIndex[key];
if (!final.deleted) {
(<any>out)[key] = final.value.state(valueRDA, out[key] || valueSchema.alloc());
}
}
return out;
}
private _moveElement (element:MuRDAMapStoreElement<MapRDA>, key:string) {
if (key === element.key) {
return;
}
// remove from list
const { next, prev, sequence } = element;
if (this.keyIndex[element.key] === element) {
if (next) {
this.keyIndex[element.key] = next;
if (next.sequence >= sequence) {
next.deleted = false;
}
} else {
delete this.keyIndex[element.key];
}
}
if (next) {
next.prev = prev;
}
if (prev) {
prev.next = next;
}
element.next = element.prev = null;
// insert into new bucket
element.key = key;
this._insertElement(element);
}
private _insertElement (element:MuRDAMapStoreElement<MapRDA>) {
const key = element.key;
const head = this.keyIndex[key];
if (head) {
// insert into list
if (compareStoreElements(head, element) < 0) {
element.next = head;
head.prev = element;
head.deleted = true;
this.keyIndex[key] = element;
} else {
let prev = head;
while (prev) {
const next = prev.next;
if (!next) {
prev.next = element;
element.prev = prev;
break;
} else if (compareStoreElements(next, element) < 0) {
prev.next = element;
next.prev = element;
element.next = next;
element.prev = prev;
break;
}
prev = next;
}
}
} else {
this.keyIndex[key] = element;
}
}
private _applyUpsert (valueRDA:MapRDA['valueRDA'], upsertAction:MapRDA['upsertActionSchema']['identity']) {
const prev = this.idIndex[upsertAction.id];
if (prev) {
prev.id = upsertAction.id;
prev.deleted = upsertAction.deleted;
prev.sequence = upsertAction.sequence;
prev.value.free(valueRDA);
prev.value = valueRDA.parse(upsertAction.value);
this._moveElement(prev, upsertAction.key);
} else {
const element = new MuRDAMapStoreElement<MapRDA>(
upsertAction.id,
upsertAction.sequence,
upsertAction.deleted,
upsertAction.key,
valueRDA.parse(upsertAction.value));
this.idIndex[element.id] = element;
this._insertElement(element);
}
}
public apply(rda:MapRDA, action:MuRDATypes<MapRDA>['action']) : boolean {
const { type, data } = action;
if (type === 'reset') {
const { upserts, deletes, undeletes, sequenceIds, sequenceVals } = <MapRDA['resetActionSchema']['identity']>data;
for (let i = 0; i < upserts.length; ++i) {
this._applyUpsert(rda.valueRDA, upserts[i]);
}
for (let i = 0; i < deletes.length; ++i) {
const element = this.idIndex[deletes[i]];
if (element) {
element.deleted = true;
}
}
for (let i = 0; i < undeletes.length; ++i) {
const element = this.idIndex[undeletes[i]];
if (element) {
element.deleted = false;
}
}
if (sequenceVals.length === sequenceIds.length) {
for (let i = 0; i < sequenceIds.length; ++i) {
const element = this.idIndex[sequenceIds[i]];
if (element) {
element.sequence = sequenceVals[i];
}
}
}
return true;
} else if (type === 'upsert') {
const upsertAction = <MapRDA['upsertActionSchema']['identity']>data;
this._applyUpsert(rda.valueRDA, upsertAction);
return true;
} else if (type === 'update') {
const updateAction = <MapRDA['updateActionSchema']['identity']>data;
const element = this.idIndex[updateAction.id];
if (!element) {
return false;
}
return element.value.apply(rda.valueRDA, updateAction.action);
} else if (type === 'move') {
const moveAction = <MapRDA['moveActionSchema']['identity']>data;
const element = this.idIndex[moveAction.id];
if (!element) {
return false;
}
element.sequence = moveAction.sequence;
this._moveElement(element, moveAction.key);
return true;
} else if (type === 'setDeleted') {
const setDeletedAction = <MapRDA['setDeletedActionSchema']['identity']>data;
const element = this.idIndex[setDeletedAction.id];
if (!element) {
return false;
}
element.deleted = setDeletedAction.deleted;
element.sequence = setDeletedAction.sequence;
return true;
} else if (type === 'noop') {
return true;
}
return false;
}
public inverse(rda:MapRDA, action:MuRDATypes<MapRDA>['action']) {
const { type, data } = action;
const idIndex = this.idIndex;
const result = rda.actionSchema.alloc();
result.type = 'noop';
result.data = undefined;
if (type === 'reset') {
result.type = 'reset';
const { upserts, deletes, undeletes, sequenceIds, sequenceVals } = <MapRDA['resetActionSchema']['identity']>data;
const inverseReset = result.data = rda.resetActionSchema.clone(rda.resetActionSchema.identity);
// first compute inverse userts
const inverseUndelete = inverseReset.undeletes;
const inverseDelete = inverseReset.deletes;
const inverseUpsert = inverseReset.upserts;
const invserseSequenceIds = inverseReset.sequenceIds;
const invserseSequenceVals = inverseReset.sequenceVals;
for (let i = 0; i < upserts.length; ++i) {
const upsertAction = upserts[i];
const id = upsertAction.id;
const prev = this.idIndex[id];
if (prev) {
const inverseUpsertAction = rda.upsertActionSchema.alloc();
inverseUpsertAction.id = id;
inverseUpsertAction.key = prev.key;
inverseUpsertAction.deleted = prev.deleted;
inverseUpsertAction.sequence = prev.sequence;
inverseUpsertAction.value = prev.value.serialize(rda.valueRDA, inverseUpsertAction.value);
inverseUpsert.push(inverseUpsertAction);
} else {
inverseDelete.push(id);
invserseSequenceIds.push(id);
invserseSequenceVals.push(0);
}
}
inverseUpsert.sort(rda.storeSchema.compare);
// compute inverse deletes
for (let i = 0; i < deletes.length; ++i) {
const id = deletes[i];
const element = this.idIndex[id];
if (element && !element.deleted) {
inverseUndelete.push(id);
}
}
inverseUndelete.sort(compareNum);
// compute inverse undeletes
for (let i = 0; i < undeletes.length; ++i) {
const id = undeletes[i];
const element = this.idIndex[id];
if (element && element.deleted) {
inverseDelete.push(id);
}
}
inverseDelete.sort(compareNum);
// compute inverse sequence swaps
for (let i = 0; i < sequenceIds.length; ++i) {
const id = sequenceIds[i];
const element = this.idIndex[id];
if (element && element.sequence !== sequenceVals[i]) {
invserseSequenceIds.push(id);
invserseSequenceVals.push(element.sequence);
}
}
} else if (type === 'upsert') {
const upsertAction = <MapRDA['upsertActionSchema']['identity']>data;
const id = upsertAction.id;
const prev = this.idIndex[id];
if (prev) {
result.type = 'upsert';
const inverseUpsertAction = result.data = rda.upsertActionSchema.alloc();
inverseUpsertAction.id = id;
inverseUpsertAction.key = prev.key;
inverseUpsertAction.deleted = prev.deleted;
inverseUpsertAction.sequence = prev.sequence;
inverseUpsertAction.value = prev.value.serialize(rda.valueRDA, inverseUpsertAction.value);
} else {
result.type = 'setDeleted';
result.data = rda.setDeletedActionSchema.alloc();
result.data.id = id;
result.data.deleted = true;
result.data.sequence = 0;
}
} else if (type === 'update') {
const updateAction = <MapRDA['updateActionSchema']['identity']>data;
const id = updateAction.id;
const prev = idIndex[id];
if (prev) {
result.type = 'update';
const inverseUpdate = result.data = rda.updateActionSchema.alloc();
inverseUpdate.id = id;
inverseUpdate.action = prev.value.inverse(rda.valueRDA, updateAction.action);
}
} else if (type === 'move') {
const moveAction = <MapRDA['moveActionSchema']['identity']>data;
const id = moveAction.id;
const prev = idIndex[id];
if (prev) {
result.type = 'move';
const inverseMove = result.data = rda.moveActionSchema.alloc();
inverseMove.id = id;
inverseMove.key = prev.key;
inverseMove.sequence = prev.sequence;
}
} else if (type === 'setDeleted') {
result.type = 'setDeleted';
const inverseDelete = result.data = rda.setDeletedActionSchema.alloc();
const setDeletedAction = <MapRDA['setDeletedActionSchema']['identity']>data;
const id = setDeletedAction.id;
const prev = idIndex[id];
inverseDelete.id = id;
if (prev) {
inverseDelete.deleted = prev.deleted;
inverseDelete.sequence = prev.sequence;
} else {
inverseDelete.deleted = true;
inverseDelete.sequence = 0;
}
}
return <any>result;
}
public serialize (rda:MapRDA, result:MuRDATypes<MapRDA>['serializedStore']) : MuRDATypes<MapRDA>['serializedStore'] {
const idIndex = this.idIndex;
const ids = Object.keys(idIndex);
while (result.length > ids.length) {
const element = result.pop();
if (element) {
rda.storeElementSchema.free(element);
}
}
while (result.length < ids.length) {
result.push(rda.storeElementSchema.alloc());
}
for (let i = 0; i < ids.length; ++i) {
const element = idIndex[ids[i]];
const store = result[i];
store.id = element.id;
store.key = element.key;
store.sequence = element.sequence;
store.deleted = element.deleted;
store.value = element.value.serialize(rda.valueRDA, store.value);
}
return result;
}
public free (rda:MapRDA) {
const idIndex = this.idIndex;
const ids = Object.keys(idIndex);
const valueRDA = rda.valueRDA;
for (let i = 0; i < ids.length; ++i) {
const element = idIndex[ids[i]];
element.value.free(valueRDA);
}
this.idIndex = {};
this.keyIndex = {};
}
public genId () {
let shift = 0;
while (true) {
shift = Math.max(30, shift + 7);
const id = (Math.random() * (1 << shift)) >>> 0;
if (!this.idIndex[id]) {
return id;
}
}
}
}
type WrapAction<Key, Meta, Dispatch> =
Meta extends { type:'unit' }
? Dispatch extends (...args:infer ArgType) => infer RetType
? (...args:ArgType) => {
type:'noop';
data:MuVoid['identity'];
} | {
type:'update';
data:{
id:number;
action:RetType;
};
}
: never
: Meta extends { action:MuRDAActionMeta }
? Dispatch extends (...args:infer ArgType) => infer RetType
? (...args:ArgType) => WrapAction<Key, Meta['action'], RetType>
: never
: Meta extends { table:{ [id in keyof Dispatch]:MuRDAActionMeta } }
? Dispatch extends { [id in keyof Meta['table']]:any }
? { [id in keyof Meta['table']]:WrapAction<Key, Meta['table'][id], Dispatch[id]>; }
: never
: never;
type StripBindAndWrap<Key, ValueRDA extends MuRDA<any, any, any, any>> =
ValueRDA['actionMeta'] extends { type:'store'; action:MuRDAActionMeta; }
? ValueRDA['action'] extends (store) => infer RetAction
? WrapAction<Key, ValueRDA['actionMeta']['action'], RetAction>
: never
: WrapAction<Key, ValueRDA['actionMeta'], ValueRDA['action']>;
export class MuRDAMap<
KeySchema extends MuSchema<any>,
ValueRDA extends MuRDA<any, any, any, any>>
implements MuRDA<
MuRDAMapTypes<KeySchema, ValueRDA>['stateSchema'],
MuRDAMapTypes<KeySchema, ValueRDA>['actionSchema'],
MuRDAMapTypes<KeySchema, ValueRDA>['storeSchema'],
MuRDAMapTypes<KeySchema, ValueRDA>['actionMeta']> {
public readonly keySchema:KeySchema;
public readonly valueRDA:ValueRDA;
public readonly resetActionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['resetActionSchema'];
public readonly upsertActionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['upsertActionSchema'];
public readonly updateActionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['updateActionSchema'];
public readonly moveActionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['moveActionSchema'];
public readonly setDeletedActionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['setDeletedActionSchema'];
public readonly stateSchema:MuRDAMapTypes<KeySchema, ValueRDA>['stateSchema'];
public readonly actionSchema:MuRDAMapTypes<KeySchema, ValueRDA>['actionSchema'];
public readonly storeElementSchema:MuRDAMapTypes<KeySchema, ValueRDA>['storeElementSchema'];
public readonly storeSchema:MuRDAMapTypes<KeySchema, ValueRDA>['storeSchema'];
public readonly actionMeta:MuRDAMapTypes<KeySchema, ValueRDA>['actionMeta'];
public readonly emptyStore:MuRDAMapStore<this>;
private _savedStore:MuRDAMapStore<this> = <any>null;
private _savedElement:MuRDATypes<ValueRDA>['store'] = <any>null;
private _savedAction:MuRDAMapTypes<KeySchema, ValueRDA>['action'] = <any>null;
private _savedUpdate:MuRDAMapTypes<KeySchema, ValueRDA>['updateActionSchema']['identity'] = <any>null;
private _updateDispatcher:any = null;
private _noopDispatcher:any = null;
private _actionDispatchers:any = {
set: (key:KeySchema['identity'], state:ValueRDA['stateSchema']['identity']) => {
const result = this.actionSchema.alloc();
result.type = 'upsert';
const upsertAction = result.data = this.storeElementSchema.alloc();
const prev = this._savedStore.keyIndex[key];
upsertAction.key = key;
upsertAction.deleted = false;
const tmp = this.valueRDA.createStore(state);
upsertAction.value = tmp.serialize(this.valueRDA, upsertAction.value);
tmp.free(this.valueRDA);
if (prev) {
upsertAction.id = prev.id;
upsertAction.sequence = prev.sequence;
} else {
upsertAction.id = this._savedStore.genId();
upsertAction.sequence = 1;
}
return result;
},
remove: (key:KeySchema['identity']) => {
const result = this.actionSchema.alloc();
const prev = this._savedStore.keyIndex[key];
if (prev && !prev.deleted) {
result.type = 'setDeleted';
const action = result.data = this.setDeletedActionSchema.alloc();
action.id = prev.id;
action.deleted = true;
action.sequence = prev.sequence;
} else {
result.type = 'noop';
result.data = undefined;
}
return result;
},
move: (from:KeySchema['identity'], to:KeySchema['identity']) => {
const result = this.actionSchema.alloc();
const prev = this._savedStore.keyIndex[from];
if (prev && !prev.deleted) {
result.type = 'move';
const moveAction = result.data = this.moveActionSchema.alloc();
moveAction.id = prev.id;
moveAction.key = to;
const target = this._savedStore.keyIndex[to];
if (target) {
moveAction.sequence = target.sequence + 1;
} else if (prev.next) {
moveAction.sequence = prev.next.sequence + 1;
} else {
moveAction.sequence = 1;
}
} else {
result.type = 'noop';
result.data = undefined;
}
return result;
},
clear: () => {
const result = this.actionSchema.alloc();
result.type = 'reset';
const resetAction = result.data = this.resetActionSchema.alloc();
const keys = Object.keys(this._savedStore.keyIndex);
for (let i = 0; i < keys.length; ++i) {
const prev = this._savedStore.keyIndex[keys[i]];
if (!prev || prev.deleted) {
continue;
}
resetAction.deletes.push(prev.id);
}
resetAction.deletes.sort(compareNum);
return result;
},
reset: (state:MuRDAMapTypes<KeySchema, ValueRDA>['state']) => {
const result = this.actionSchema.alloc();
result.type = 'reset';
const resetAction = result.data = this.resetActionSchema.alloc();
const keys = Object.keys(state);
const upsertAction = resetAction.upserts;
const ids = Object.keys(this._savedStore.idIndex);
const idConstraint:{ [id:string]:boolean} = {};
for (let i = 0; i < ids.length; ++i) {
idConstraint[ids[i]] = true;
}
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const upsert = this.storeElementSchema.alloc();
upsert.key = key;
const tmp = this.valueRDA.createStore(state[key]);
upsert.value = tmp.serialize(this.valueRDA, upsert.value);
tmp.free(this.valueRDA);
upsert.deleted = false;
const prev = this._savedStore.keyIndex[key];
if (prev) {
upsert.id = prev.id;
upsert.sequence = prev.sequence;
} else {
let id = 0;
let shift = 0;
while (true) {
shift = Math.max(30, shift + 7);
id = (Math.random() * (1 << shift)) >>> 0;
if (!idConstraint[id]) {
idConstraint[id] = true;
break;
}
}
upsert.id = id;
upsert.sequence = 1;
}
upsertAction.push(upsert);
}
upsertAction.sort(this.storeSchema.compare);
const prevKeys = Object.keys(this._savedStore.keyIndex);
const deleteActions = resetAction.deletes;
for (let i = 0; i < prevKeys.length; ++i) {
const key = prevKeys[i];
if (key in state) {
continue;
}
const prev = this._savedStore.keyIndex[key];
if (!prev || prev.deleted) {
continue;
}
deleteActions.push(prev.id);
}
deleteActions.sort(compareNum);
return result;
},
update: (key:KeySchema['identity']) => {
const prev = this._savedStore.keyIndex[key];
this._savedAction = this.actionSchema.alloc();
if (prev) {
this._savedElement = prev.value;
this._savedAction.type = 'update';
this._savedUpdate = this._savedAction.data = this.updateActionSchema.alloc();
this._savedUpdate.id = prev.id;
this.valueRDA.actionSchema.free(this._savedUpdate.action);
return this._updateDispatcher;
} else {
this._savedAction.type = 'noop';
this._savedAction.data = undefined;
return this._noopDispatcher;
}
},
};
public readonly action:(store:MuRDAMapStore<this>) => {
set:(key:KeySchema['identity'], value:ValueRDA['stateSchema']['identity']) => MuRDAMapTypes<KeySchema, ValueRDA>['upsertAction'];
remove:(key:KeySchema['identity']) => MuRDAMapTypes<KeySchema, ValueRDA>['setDeletedAction'];
update:(key:KeySchema['identity']) => StripBindAndWrap<KeySchema['identity'], ValueRDA>;
move:(from:KeySchema['identity'], to:KeySchema['identity']) => MuRDAMapTypes<KeySchema, ValueRDA>['moveAction']
clear:() => MuRDAMapTypes<KeySchema, ValueRDA>['resetAction'];
reset:(state:MuRDAMapTypes<KeySchema, ValueRDA>['state']) => MuRDAMapTypes<KeySchema, ValueRDA>['resetAction'];
} = (store) => {
this._savedStore = store;
return this._actionDispatchers;
}
private _constructUpdateDispatcher () {
const self = this;
function wrapPartial (root, dispatcher) {
const savedPartial = { data:<any>null };
function wrapPartialRec (meta, index:string) {
if (meta.type === 'unit') {
return (new Function(
'rda',
'saved',
`return function () { rda._savedUpdate.action = saved.data${index}.apply(null, arguments); return rda._savedAction; }`,
))(self, savedPartial);
} else if (meta.type === 'table') {
const result = {};
const keys = Object.keys(meta.table);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
result[key] = wrapPartialRec(meta.table[key], `${index}["${key}"]`);
}
return result;
} else if (meta.type === 'partial') {
return wrapPartial(
meta.action,
(new Function(
'saved',
`return function () { return saved.data${index}.apply(null, arguments); }`,
))(savedPartial));
}
return {};
}
return (new Function(
'savedPartial',
'dispatch',
'wrapped',
`return function () { savedPartial.data = dispatch.apply(null, arguments); return wrapped; }`,
))(savedPartial, dispatcher, wrapPartialRec(root, ''));
}
function wrapStore (meta, dispatcher, index) {
if (meta.type === 'unit') {
return (new Function(
'rda',
'dispatch',
`return function () { rda._savedUpdate.action = dispatch(rda._savedElement)${index}.apply(null, arguments); return rda._savedAction; }`,
))(self, dispatcher);
} else if (meta.type === 'table') {
const result:any = {};
const keys = Object.keys(meta.table);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
result[key] = wrapStore(meta.table[key], dispatcher, `${index}["${key}"]`);
}
return result;
} else if (meta.type === 'partial') {
return wrapPartial(
meta.action,
(new Function(
'rda',
'dispatch',
`return function () { return dispatch(rda._savedElement)${index}.apply(null, arguments); }`,
))(self, dispatcher));
}
return {};
}
function wrapAction (meta, dispatcher) {
if (meta.type === 'unit') {
return function (...args) {
self._savedUpdate.action = dispatcher.apply(null, args);
return self._savedAction;
};
} else if (meta.type === 'table') {
const result:any = {};
const keys = Object.keys(meta.table);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
result[key] = wrapAction(meta.table[key], dispatcher[key]);
}
return result;
} else if (meta.type === 'partial') {
return wrapPartial(meta.action, dispatcher);
}
return {};
}
if (this.valueRDA.actionMeta.type === 'store') {
return wrapStore(this.valueRDA.actionMeta.action, this.valueRDA.action, '');
} else {
return wrapAction(this.valueRDA.actionMeta, this.valueRDA.action);
}
}
private _constructNoopDispatcher () {
const self = this;
function mockDispatcher (meta) {
if (meta.type === 'unit') {
return function () { return self._savedAction; };
} else if (meta.type === 'partial') {
const child = mockDispatcher(meta.action);
return function () { return child; };
} else if (meta.type === 'table') {
const result:any = {};
const keys = Object.keys(meta.table);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
result[key] = mockDispatcher(meta.table[key]);
}
return result;
}
return {};
}
if (this.valueRDA.actionMeta.type === 'store') {
return mockDispatcher(this.valueRDA.actionMeta.action);
} else {
return mockDispatcher(this.valueRDA.actionMeta);
}
}
constructor(keySchema:KeySchema, valueRDA:ValueRDA) {
this.keySchema = keySchema;
this.valueRDA = valueRDA;
this.stateSchema = new MuDictionary(valueRDA.stateSchema, Infinity);
this.storeElementSchema = new MuStruct({
id: new MuVarint(0),
sequence: new MuVarint(0),
deleted: new MuBoolean(true),
key: keySchema,
value: valueRDA.storeSchema,
});
this.storeSchema = new MuSortedArray(this.storeElementSchema, Infinity,
function (a, b) {
return (
compareKey(a.key, b.key) ||
a.sequence - b.sequence ||
a.id - b.id ||
(+a.deleted) - (+b.deleted));
});
this.resetActionSchema = new MuStruct({
upserts: this.storeSchema,
deletes: new MuSortedArray(new MuVarint(), Infinity, compareNum),
undeletes: new MuSortedArray(new MuVarint(), Infinity, compareNum),
sequenceIds: new MuArray(new MuVarint(), Infinity),
sequenceVals: new MuArray(new MuVarint(), Infinity),
});
this.upsertActionSchema = this.storeElementSchema;
this.updateActionSchema = new MuStruct({
id: new MuVarint(),
action: valueRDA.actionSchema,
});
this.moveActionSchema = new MuStruct({
id: new MuVarint(),
sequence: new MuVarint(),
key: keySchema,
});
this.setDeletedActionSchema = new MuStruct({
id: new MuVarint(),
sequence: new MuVarint(),
deleted: new MuBoolean(false),
});
this.actionSchema = new MuUnion({
reset: this.resetActionSchema,
upsert: this.upsertActionSchema,
update: this.updateActionSchema,
move: this.moveActionSchema,
setDeleted: this.setDeletedActionSchema,
noop: new MuVoid(),
});
this.actionMeta = {
type: 'store',
action: {
type: 'table',
table: {
set: { type: 'unit' },
remove: { type: 'unit' },
clear: { type: 'unit' },
reset: { type: 'unit' },
move: { type: 'unit' },
update: {
type: 'partial',
action:
valueRDA.actionMeta.type === 'store'
? valueRDA.actionMeta.action
: valueRDA.actionMeta,
},
},
},
};
this._updateDispatcher = this._constructUpdateDispatcher();
this._noopDispatcher = this._constructNoopDispatcher();
this.emptyStore = new MuRDAMapStore<this>([]);
}
public createStore (initialState:MuRDAMapTypes<KeySchema, ValueRDA>['state']) {
const keys:string[]|number[] = Object.keys(initialState);
if (typeof this.keySchema.identity === 'number') {
for (let i = 0; i < keys.length; ++i) {
const k:any = +keys[i];
if (k !== k) {
throw new Error(`invalid key ${keys[i]}`);
}
keys[i] = k;
}
}
const elements:MuRDAMapStoreElement<this>[] = new Array(keys.length);
let idCounter = 1;
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
const value = initialState[key];
elements[i] = new MuRDAMapStoreElement<this>(
idCounter++,
1,
false,
key,
this.valueRDA.createStore(value));
}
return new MuRDAMapStore<this>(elements);
}
public parse (store:MuRDAMapTypes<KeySchema, ValueRDA>['store']) {
const elements:MuRDAMapStoreElement<this>[] = new Array(store.length);
const valueRDA = this.valueRDA;
for (let i = 0; i < elements.length; ++i) {
const e = store[i];
elements[i] = new MuRDAMapStoreElement<this>(
e.id,
e.sequence,
e.deleted,
e.key,
valueRDA.parse(e.value));
}
return new MuRDAMapStore<this>(elements);
}
}