import {ProtoRecordRange, RecordRange} from './record_range'; import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang'; import {ListWrapper, MapWrapper} from 'facade/collection'; import {ClosureMap} from 'change_detection/parser/closure_map'; var _fresh = new Object(); export const PROTO_RECORD_CONST = 'const'; export const PROTO_RECORD_PURE_FUNCTION = 'func'; export const PROTO_RECORD_CLOSURE = 'closure'; export const PROTO_RECORD_FORMATTTER = 'formatter'; export const PROTO_RECORD_METHOD = 'method'; export const PROTO_RECORD_PROPERTY = 'property'; /** * For now we are dropping expression coalescence. We can always add it later, but * real world numbers show that it does not provide significant benefits. */ export class ProtoRecord { @FIELD('final recordRange:ProtoRecordRange') @FIELD('final context:Object') @FIELD('final funcOrValue:Object') @FIELD('final arity:int') @FIELD('final dest') @FIELD('next:ProtoRecord') @FIELD('prev:ProtoRecord') @FIELD('recordInConstruction:Record') constructor(recordRange:ProtoRecordRange, recordType:string, funcOrValue, arity:int, dest) { this.recordRange = recordRange; this.recordType = recordType; this.funcOrValue = funcOrValue; this.arity = arity; this.dest = dest; this.next = null; this.prev = null; this.recordInConstruction = null; } } /** * Represents a Record for keeping track of changes. A change is a difference between previous * and current value. * * By default changes are detected using dirty checking, but a notifier can be present which can * notify the records of changes by means other than dirty checking. For example Object.observe * or events on DOM elements. * * DESIGN NOTES: * - No inheritance allowed so that code is monomorphic for performance. * - Atomic watch operations * - Defaults to dirty checking * - Keep this object as lean as possible. (Lean in number of fields) */ export class Record { @FIELD('final recordRange:RecordRange') @FIELD('final protoRecord:ProtoRecord') @FIELD('next:Record') @FIELD('prev:Record') /// This reference can change. @FIELD('nextEnabled:Record') /// This reference can change. @FIELD('prevEnabled:Record') @FIELD('dest:Record') @FIELD('previousValue') @FIELD('currentValue') @FIELD('mode:int') @FIELD('context') @FIELD('funcOrValue') @FIELD('args:List') // Opaque data which will be the target of notification. // If the object is instance of Record, then it it is directly processed // Otherwise it is the context used by WatchGroupDispatcher. @FIELD('dest') constructor(recordRange:RecordRange, protoRecord:ProtoRecord, formatters:Map) { this.recordRange = recordRange; this.protoRecord = protoRecord; this.next = null; this.prev = null; this.nextEnabled = null; this.prevEnabled = null; this.disabled = false; this.dest = null; this.previousValue = null; this.currentValue = _fresh; this.mode = null; this.context = null; this.funcOrValue = null; this.args = null; if (isBlank(protoRecord)) { this.mode = MODE_STATE_MARKER; return; } var type = protoRecord.recordType; if (type === PROTO_RECORD_CONST) { this.mode = MODE_STATE_CONST; this.funcOrValue = protoRecord.funcOrValue; } else if (type === PROTO_RECORD_PURE_FUNCTION) { this.mode = MODE_STATE_INVOKE_PURE_FUNCTION; this.funcOrValue = protoRecord.funcOrValue; this.args = ListWrapper.createFixedSize(protoRecord.arity); } else if (type === PROTO_RECORD_FORMATTTER) { this.mode = MODE_STATE_INVOKE_PURE_FUNCTION; this.funcOrValue = MapWrapper.get(formatters, protoRecord.funcOrValue); this.args = ListWrapper.createFixedSize(protoRecord.arity); } else if (type === PROTO_RECORD_METHOD) { this.mode = MODE_STATE_INVOKE_METHOD; this.funcOrValue = protoRecord.funcOrValue; this.args = ListWrapper.createFixedSize(protoRecord.arity); } else if (type === PROTO_RECORD_CLOSURE) { this.mode = MODE_STATE_INVOKE_CLOSURE; this.args = ListWrapper.createFixedSize(protoRecord.arity); } else if (type === PROTO_RECORD_PROPERTY) { this.mode = MODE_STATE_PROPERTY; this.funcOrValue = protoRecord.funcOrValue; } } static createMarker(rr:RecordRange) { var r = new Record(rr, null, null); r.disabled = true; return r; } check():boolean { this.previousValue = this.currentValue; this.currentValue = this._calculateNewValue(); if (isSame(this.previousValue, this.currentValue)) return false; this._updateDestination(); return true; } _updateDestination() { // todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req) if (this.dest instanceof Record) { if (isPresent(this.protoRecord.dest.position)) { this.dest.updateArg(this.currentValue, this.protoRecord.dest.position); } else { this.dest.updateContext(this.currentValue); } } else { this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest); } } _calculateNewValue() { var state = this.mode; switch (state) { case MODE_STATE_PROPERTY: return this.funcOrValue(this.context); case MODE_STATE_INVOKE_METHOD: return this.funcOrValue(this.context, this.args); case MODE_STATE_INVOKE_CLOSURE: return FunctionWrapper.apply(this.context, this.args); case MODE_STATE_INVOKE_PURE_FUNCTION: this.recordRange.disableRecord(this); return FunctionWrapper.apply(this.funcOrValue, this.args); case MODE_STATE_CONST: this.recordRange.disableRecord(this); return this.funcOrValue; case MODE_STATE_MARKER: throw new BaseException('MODE_STATE_MARKER not implemented'); case MODE_STATE_MAP: throw new BaseException('MODE_STATE_MAP not implemented'); case MODE_STATE_LIST: throw new BaseException('MODE_STATE_LIST not implemented'); default: throw new BaseException('DEFAULT not implemented'); } } updateArg(value, position:int) { this.args[position] = value; this.recordRange.enableRecord(this); } updateContext(value) { this.context = value; if (!this.isMarkerRecord) { this.recordRange.enableRecord(this); } } get isMarkerRecord() { return this.mode == MODE_STATE_MARKER; } } // The mode is divided into two parts. Which notification mechanism // to use and which dereference mode to execute. // We use dirty checking aka no notification const MODE_MASK_NOTIFY = 0xFF00; // Encodes the state of dereference const MODE_MASK_STATE = 0x00FF; const MODE_PLUGIN_DIRTY_CHECK = 0x0000; const MODE_STATE_MARKER = 0x0000; /// _context[_protoRecord.propname] => _getter(_context) const MODE_STATE_PROPERTY = 0x0001; const MODE_STATE_INVOKE_PURE_FUNCTION = 0x0002; const MODE_STATE_INVOKE_METHOD = 0x0003; const MODE_STATE_INVOKE_CLOSURE = 0x0004; /// _context is Map => _previousValue is MapChangeRecord const MODE_STATE_MAP = 0x0005; /// _context is Array/List/Iterable => _previousValue = ListChangeRecord const MODE_STATE_LIST = 0x0006; /// _context is number/string const MODE_STATE_CONST = 0x0007; function isSame(a, b) { if (a === b) return true; if ((a !== a) && (b !== b)) return true; return false; }