1// Copyright 2012 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5"use strict"; 6 7// Overview: 8// 9// This file contains all of the routing and accounting for Object.observe. 10// User code will interact with these mechanisms via the Object.observe APIs 11// and, as a side effect of mutation objects which are observed. The V8 runtime 12// (both C++ and JS) will interact with these mechanisms primarily by enqueuing 13// proper change records for objects which were mutated. The Object.observe 14// routing and accounting consists primarily of three participants 15// 16// 1) ObjectInfo. This represents the observed state of a given object. It 17// records what callbacks are observing the object, with what options, and 18// what "change types" are in progress on the object (i.e. via 19// notifier.performChange). 20// 21// 2) CallbackInfo. This represents a callback used for observation. It holds 22// the records which must be delivered to the callback, as well as the global 23// priority of the callback (which determines delivery order between 24// callbacks). 25// 26// 3) observationState.pendingObservers. This is the set of observers which 27// have change records which must be delivered. During "normal" delivery 28// (i.e. not Object.deliverChangeRecords), this is the mechanism by which 29// callbacks are invoked in the proper order until there are no more 30// change records pending to a callback. 31// 32// Note that in order to reduce allocation and processing costs, the 33// implementation of (1) and (2) have "optimized" states which represent 34// common cases which can be handled more efficiently. 35 36var observationState; 37 38function GetObservationStateJS() { 39 if (IS_UNDEFINED(observationState)) 40 observationState = %GetObservationState(); 41 42 if (IS_UNDEFINED(observationState.callbackInfoMap)) { 43 observationState.callbackInfoMap = %ObservationWeakMapCreate(); 44 observationState.objectInfoMap = %ObservationWeakMapCreate(); 45 observationState.notifierObjectInfoMap = %ObservationWeakMapCreate(); 46 observationState.pendingObservers = null; 47 observationState.nextCallbackPriority = 0; 48 observationState.lastMicrotaskId = 0; 49 } 50 51 return observationState; 52} 53 54function GetWeakMapWrapper() { 55 function MapWrapper(map) { 56 this.map_ = map; 57 }; 58 59 MapWrapper.prototype = { 60 __proto__: null, 61 get: function(key) { 62 return %WeakCollectionGet(this.map_, key); 63 }, 64 set: function(key, value) { 65 %WeakCollectionSet(this.map_, key, value); 66 }, 67 has: function(key) { 68 return !IS_UNDEFINED(this.get(key)); 69 } 70 }; 71 72 return MapWrapper; 73} 74 75var contextMaps; 76 77function GetContextMaps() { 78 if (IS_UNDEFINED(contextMaps)) { 79 var map = GetWeakMapWrapper(); 80 var observationState = GetObservationStateJS(); 81 contextMaps = { 82 callbackInfoMap: new map(observationState.callbackInfoMap), 83 objectInfoMap: new map(observationState.objectInfoMap), 84 notifierObjectInfoMap: new map(observationState.notifierObjectInfoMap) 85 }; 86 } 87 88 return contextMaps; 89} 90 91function GetCallbackInfoMap() { 92 return GetContextMaps().callbackInfoMap; 93} 94 95function GetObjectInfoMap() { 96 return GetContextMaps().objectInfoMap; 97} 98 99function GetNotifierObjectInfoMap() { 100 return GetContextMaps().notifierObjectInfoMap; 101} 102 103function GetPendingObservers() { 104 return GetObservationStateJS().pendingObservers; 105} 106 107function SetPendingObservers(pendingObservers) { 108 GetObservationStateJS().pendingObservers = pendingObservers; 109} 110 111function GetNextCallbackPriority() { 112 return GetObservationStateJS().nextCallbackPriority++; 113} 114 115function nullProtoObject() { 116 return { __proto__: null }; 117} 118 119function TypeMapCreate() { 120 return nullProtoObject(); 121} 122 123function TypeMapAddType(typeMap, type, ignoreDuplicate) { 124 typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1; 125} 126 127function TypeMapRemoveType(typeMap, type) { 128 typeMap[type]--; 129} 130 131function TypeMapCreateFromList(typeList, length) { 132 var typeMap = TypeMapCreate(); 133 for (var i = 0; i < length; i++) { 134 TypeMapAddType(typeMap, typeList[i], true); 135 } 136 return typeMap; 137} 138 139function TypeMapHasType(typeMap, type) { 140 return !!typeMap[type]; 141} 142 143function TypeMapIsDisjointFrom(typeMap1, typeMap2) { 144 if (!typeMap1 || !typeMap2) 145 return true; 146 147 for (var type in typeMap1) { 148 if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type)) 149 return false; 150 } 151 152 return true; 153} 154 155var defaultAcceptTypes = (function() { 156 var defaultTypes = [ 157 'add', 158 'update', 159 'delete', 160 'setPrototype', 161 'reconfigure', 162 'preventExtensions' 163 ]; 164 return TypeMapCreateFromList(defaultTypes, defaultTypes.length); 165})(); 166 167// An Observer is a registration to observe an object by a callback with 168// a given set of accept types. If the set of accept types is the default 169// set for Object.observe, the observer is represented as a direct reference 170// to the callback. An observer never changes its accept types and thus never 171// needs to "normalize". 172function ObserverCreate(callback, acceptList) { 173 if (IS_UNDEFINED(acceptList)) 174 return callback; 175 var observer = nullProtoObject(); 176 observer.callback = callback; 177 observer.accept = acceptList; 178 return observer; 179} 180 181function ObserverGetCallback(observer) { 182 return IS_SPEC_FUNCTION(observer) ? observer : observer.callback; 183} 184 185function ObserverGetAcceptTypes(observer) { 186 return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept; 187} 188 189function ObserverIsActive(observer, objectInfo) { 190 return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo), 191 ObserverGetAcceptTypes(observer)); 192} 193 194function ObjectInfoGetOrCreate(object) { 195 var objectInfo = ObjectInfoGet(object); 196 if (IS_UNDEFINED(objectInfo)) { 197 if (!%IsJSProxy(object)) 198 %SetIsObserved(object); 199 200 objectInfo = { 201 object: object, 202 changeObservers: null, 203 notifier: null, 204 performing: null, 205 performingCount: 0, 206 }; 207 GetObjectInfoMap().set(object, objectInfo); 208 } 209 return objectInfo; 210} 211 212function ObjectInfoGet(object) { 213 return GetObjectInfoMap().get(object); 214} 215 216function ObjectInfoGetFromNotifier(notifier) { 217 return GetNotifierObjectInfoMap().get(notifier); 218} 219 220function ObjectInfoGetNotifier(objectInfo) { 221 if (IS_NULL(objectInfo.notifier)) { 222 objectInfo.notifier = { __proto__: notifierPrototype }; 223 GetNotifierObjectInfoMap().set(objectInfo.notifier, objectInfo); 224 } 225 226 return objectInfo.notifier; 227} 228 229function ObjectInfoGetObject(objectInfo) { 230 return objectInfo.object; 231} 232 233function ChangeObserversIsOptimized(changeObservers) { 234 return typeof changeObservers === 'function' || 235 typeof changeObservers.callback === 'function'; 236} 237 238// The set of observers on an object is called 'changeObservers'. The first 239// observer is referenced directly via objectInfo.changeObservers. When a second 240// is added, changeObservers "normalizes" to become a mapping of callback 241// priority -> observer and is then stored on objectInfo.changeObservers. 242function ObjectInfoNormalizeChangeObservers(objectInfo) { 243 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { 244 var observer = objectInfo.changeObservers; 245 var callback = ObserverGetCallback(observer); 246 var callbackInfo = CallbackInfoGet(callback); 247 var priority = CallbackInfoGetPriority(callbackInfo); 248 objectInfo.changeObservers = nullProtoObject(); 249 objectInfo.changeObservers[priority] = observer; 250 } 251} 252 253function ObjectInfoAddObserver(objectInfo, callback, acceptList) { 254 var callbackInfo = CallbackInfoGetOrCreate(callback); 255 var observer = ObserverCreate(callback, acceptList); 256 257 if (!objectInfo.changeObservers) { 258 objectInfo.changeObservers = observer; 259 return; 260 } 261 262 ObjectInfoNormalizeChangeObservers(objectInfo); 263 var priority = CallbackInfoGetPriority(callbackInfo); 264 objectInfo.changeObservers[priority] = observer; 265} 266 267function ObjectInfoRemoveObserver(objectInfo, callback) { 268 if (!objectInfo.changeObservers) 269 return; 270 271 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { 272 if (callback === ObserverGetCallback(objectInfo.changeObservers)) 273 objectInfo.changeObservers = null; 274 return; 275 } 276 277 var callbackInfo = CallbackInfoGet(callback); 278 var priority = CallbackInfoGetPriority(callbackInfo); 279 objectInfo.changeObservers[priority] = null; 280} 281 282function ObjectInfoHasActiveObservers(objectInfo) { 283 if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers) 284 return false; 285 286 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) 287 return ObserverIsActive(objectInfo.changeObservers, objectInfo); 288 289 for (var priority in objectInfo.changeObservers) { 290 var observer = objectInfo.changeObservers[priority]; 291 if (!IS_NULL(observer) && ObserverIsActive(observer, objectInfo)) 292 return true; 293 } 294 295 return false; 296} 297 298function ObjectInfoAddPerformingType(objectInfo, type) { 299 objectInfo.performing = objectInfo.performing || TypeMapCreate(); 300 TypeMapAddType(objectInfo.performing, type); 301 objectInfo.performingCount++; 302} 303 304function ObjectInfoRemovePerformingType(objectInfo, type) { 305 objectInfo.performingCount--; 306 TypeMapRemoveType(objectInfo.performing, type); 307} 308 309function ObjectInfoGetPerformingTypes(objectInfo) { 310 return objectInfo.performingCount > 0 ? objectInfo.performing : null; 311} 312 313function ConvertAcceptListToTypeMap(arg) { 314 // We use undefined as a sentinel for the default accept list. 315 if (IS_UNDEFINED(arg)) 316 return arg; 317 318 if (!IS_SPEC_OBJECT(arg)) 319 throw MakeTypeError("observe_accept_invalid"); 320 321 var len = ToInteger(arg.length); 322 if (len < 0) len = 0; 323 324 return TypeMapCreateFromList(arg, len); 325} 326 327// CallbackInfo's optimized state is just a number which represents its global 328// priority. When a change record must be enqueued for the callback, it 329// normalizes. When delivery clears any pending change records, it re-optimizes. 330function CallbackInfoGet(callback) { 331 return GetCallbackInfoMap().get(callback); 332} 333 334function CallbackInfoGetOrCreate(callback) { 335 var callbackInfo = GetCallbackInfoMap().get(callback); 336 if (!IS_UNDEFINED(callbackInfo)) 337 return callbackInfo; 338 339 var priority = GetNextCallbackPriority(); 340 GetCallbackInfoMap().set(callback, priority); 341 return priority; 342} 343 344function CallbackInfoGetPriority(callbackInfo) { 345 if (IS_NUMBER(callbackInfo)) 346 return callbackInfo; 347 else 348 return callbackInfo.priority; 349} 350 351function CallbackInfoNormalize(callback) { 352 var callbackInfo = GetCallbackInfoMap().get(callback); 353 if (IS_NUMBER(callbackInfo)) { 354 var priority = callbackInfo; 355 callbackInfo = new InternalArray; 356 callbackInfo.priority = priority; 357 GetCallbackInfoMap().set(callback, callbackInfo); 358 } 359 return callbackInfo; 360} 361 362function ObjectObserve(object, callback, acceptList) { 363 if (!IS_SPEC_OBJECT(object)) 364 throw MakeTypeError("observe_non_object", ["observe"]); 365 if (%IsJSGlobalProxy(object)) 366 throw MakeTypeError("observe_global_proxy", ["observe"]); 367 if (!IS_SPEC_FUNCTION(callback)) 368 throw MakeTypeError("observe_non_function", ["observe"]); 369 if (ObjectIsFrozen(callback)) 370 throw MakeTypeError("observe_callback_frozen"); 371 372 var objectObserveFn = %GetObjectContextObjectObserve(object); 373 return objectObserveFn(object, callback, acceptList); 374} 375 376function NativeObjectObserve(object, callback, acceptList) { 377 var objectInfo = ObjectInfoGetOrCreate(object); 378 var typeList = ConvertAcceptListToTypeMap(acceptList); 379 ObjectInfoAddObserver(objectInfo, callback, typeList); 380 return object; 381} 382 383function ObjectUnobserve(object, callback) { 384 if (!IS_SPEC_OBJECT(object)) 385 throw MakeTypeError("observe_non_object", ["unobserve"]); 386 if (%IsJSGlobalProxy(object)) 387 throw MakeTypeError("observe_global_proxy", ["unobserve"]); 388 if (!IS_SPEC_FUNCTION(callback)) 389 throw MakeTypeError("observe_non_function", ["unobserve"]); 390 391 var objectInfo = ObjectInfoGet(object); 392 if (IS_UNDEFINED(objectInfo)) 393 return object; 394 395 ObjectInfoRemoveObserver(objectInfo, callback); 396 return object; 397} 398 399function ArrayObserve(object, callback) { 400 return ObjectObserve(object, callback, ['add', 401 'update', 402 'delete', 403 'splice']); 404} 405 406function ArrayUnobserve(object, callback) { 407 return ObjectUnobserve(object, callback); 408} 409 410function ObserverEnqueueIfActive(observer, objectInfo, changeRecord) { 411 if (!ObserverIsActive(observer, objectInfo) || 412 !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) { 413 return; 414 } 415 416 var callback = ObserverGetCallback(observer); 417 if (!%ObserverObjectAndRecordHaveSameOrigin(callback, changeRecord.object, 418 changeRecord)) { 419 return; 420 } 421 422 var callbackInfo = CallbackInfoNormalize(callback); 423 if (IS_NULL(GetPendingObservers())) { 424 SetPendingObservers(nullProtoObject()); 425 if (DEBUG_IS_ACTIVE) { 426 var id = ++GetObservationStateJS().lastMicrotaskId; 427 var name = "Object.observe"; 428 %EnqueueMicrotask(function() { 429 %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name }); 430 ObserveMicrotaskRunner(); 431 %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name }); 432 }); 433 %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name }); 434 } else { 435 %EnqueueMicrotask(ObserveMicrotaskRunner); 436 } 437 } 438 GetPendingObservers()[callbackInfo.priority] = callback; 439 callbackInfo.push(changeRecord); 440} 441 442function ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, type) { 443 if (!ObjectInfoHasActiveObservers(objectInfo)) 444 return; 445 446 var hasType = !IS_UNDEFINED(type); 447 var newRecord = hasType ? 448 { object: ObjectInfoGetObject(objectInfo), type: type } : 449 { object: ObjectInfoGetObject(objectInfo) }; 450 451 for (var prop in changeRecord) { 452 if (prop === 'object' || (hasType && prop === 'type')) continue; 453 %DefineDataPropertyUnchecked( 454 newRecord, prop, changeRecord[prop], READ_ONLY + DONT_DELETE); 455 } 456 ObjectFreezeJS(newRecord); 457 458 ObjectInfoEnqueueInternalChangeRecord(objectInfo, newRecord); 459} 460 461function ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord) { 462 // TODO(rossberg): adjust once there is a story for symbols vs proxies. 463 if (IS_SYMBOL(changeRecord.name)) return; 464 465 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { 466 var observer = objectInfo.changeObservers; 467 ObserverEnqueueIfActive(observer, objectInfo, changeRecord); 468 return; 469 } 470 471 for (var priority in objectInfo.changeObservers) { 472 var observer = objectInfo.changeObservers[priority]; 473 if (IS_NULL(observer)) 474 continue; 475 ObserverEnqueueIfActive(observer, objectInfo, changeRecord); 476 } 477} 478 479function BeginPerformSplice(array) { 480 var objectInfo = ObjectInfoGet(array); 481 if (!IS_UNDEFINED(objectInfo)) 482 ObjectInfoAddPerformingType(objectInfo, 'splice'); 483} 484 485function EndPerformSplice(array) { 486 var objectInfo = ObjectInfoGet(array); 487 if (!IS_UNDEFINED(objectInfo)) 488 ObjectInfoRemovePerformingType(objectInfo, 'splice'); 489} 490 491function EnqueueSpliceRecord(array, index, removed, addedCount) { 492 var objectInfo = ObjectInfoGet(array); 493 if (!ObjectInfoHasActiveObservers(objectInfo)) 494 return; 495 496 var changeRecord = { 497 type: 'splice', 498 object: array, 499 index: index, 500 removed: removed, 501 addedCount: addedCount 502 }; 503 504 ObjectFreezeJS(changeRecord); 505 ObjectFreezeJS(changeRecord.removed); 506 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord); 507} 508 509function NotifyChange(type, object, name, oldValue) { 510 var objectInfo = ObjectInfoGet(object); 511 if (!ObjectInfoHasActiveObservers(objectInfo)) 512 return; 513 514 var changeRecord; 515 if (arguments.length == 2) { 516 changeRecord = { type: type, object: object }; 517 } else if (arguments.length == 3) { 518 changeRecord = { type: type, object: object, name: name }; 519 } else { 520 changeRecord = { 521 type: type, 522 object: object, 523 name: name, 524 oldValue: oldValue 525 }; 526 } 527 528 ObjectFreezeJS(changeRecord); 529 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord); 530} 531 532var notifierPrototype = {}; 533 534function ObjectNotifierNotify(changeRecord) { 535 if (!IS_SPEC_OBJECT(this)) 536 throw MakeTypeError("called_on_non_object", ["notify"]); 537 538 var objectInfo = ObjectInfoGetFromNotifier(this); 539 if (IS_UNDEFINED(objectInfo)) 540 throw MakeTypeError("observe_notify_non_notifier"); 541 if (!IS_STRING(changeRecord.type)) 542 throw MakeTypeError("observe_type_non_string"); 543 544 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord); 545} 546 547function ObjectNotifierPerformChange(changeType, changeFn) { 548 if (!IS_SPEC_OBJECT(this)) 549 throw MakeTypeError("called_on_non_object", ["performChange"]); 550 551 var objectInfo = ObjectInfoGetFromNotifier(this); 552 if (IS_UNDEFINED(objectInfo)) 553 throw MakeTypeError("observe_notify_non_notifier"); 554 if (!IS_STRING(changeType)) 555 throw MakeTypeError("observe_perform_non_string"); 556 if (!IS_SPEC_FUNCTION(changeFn)) 557 throw MakeTypeError("observe_perform_non_function"); 558 559 var performChangeFn = %GetObjectContextNotifierPerformChange(objectInfo); 560 performChangeFn(objectInfo, changeType, changeFn); 561} 562 563function NativeObjectNotifierPerformChange(objectInfo, changeType, changeFn) { 564 ObjectInfoAddPerformingType(objectInfo, changeType); 565 566 var changeRecord; 567 try { 568 changeRecord = %_CallFunction(UNDEFINED, changeFn); 569 } finally { 570 ObjectInfoRemovePerformingType(objectInfo, changeType); 571 } 572 573 if (IS_SPEC_OBJECT(changeRecord)) 574 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, changeType); 575} 576 577function ObjectGetNotifier(object) { 578 if (!IS_SPEC_OBJECT(object)) 579 throw MakeTypeError("observe_non_object", ["getNotifier"]); 580 if (%IsJSGlobalProxy(object)) 581 throw MakeTypeError("observe_global_proxy", ["getNotifier"]); 582 583 if (ObjectIsFrozen(object)) return null; 584 585 if (!%ObjectWasCreatedInCurrentOrigin(object)) return null; 586 587 var getNotifierFn = %GetObjectContextObjectGetNotifier(object); 588 return getNotifierFn(object); 589} 590 591function NativeObjectGetNotifier(object) { 592 var objectInfo = ObjectInfoGetOrCreate(object); 593 return ObjectInfoGetNotifier(objectInfo); 594} 595 596function CallbackDeliverPending(callback) { 597 var callbackInfo = GetCallbackInfoMap().get(callback); 598 if (IS_UNDEFINED(callbackInfo) || IS_NUMBER(callbackInfo)) 599 return false; 600 601 // Clear the pending change records from callback and return it to its 602 // "optimized" state. 603 var priority = callbackInfo.priority; 604 GetCallbackInfoMap().set(callback, priority); 605 606 if (GetPendingObservers()) 607 delete GetPendingObservers()[priority]; 608 609 var delivered = []; 610 %MoveArrayContents(callbackInfo, delivered); 611 612 try { 613 %_CallFunction(UNDEFINED, delivered, callback); 614 } catch (ex) {} // TODO(rossberg): perhaps log uncaught exceptions. 615 return true; 616} 617 618function ObjectDeliverChangeRecords(callback) { 619 if (!IS_SPEC_FUNCTION(callback)) 620 throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]); 621 622 while (CallbackDeliverPending(callback)) {} 623} 624 625function ObserveMicrotaskRunner() { 626 var pendingObservers = GetPendingObservers(); 627 if (pendingObservers) { 628 SetPendingObservers(null); 629 for (var i in pendingObservers) { 630 CallbackDeliverPending(pendingObservers[i]); 631 } 632 } 633} 634 635function SetupObjectObserve() { 636 %CheckIsBootstrapping(); 637 InstallFunctions($Object, DONT_ENUM, $Array( 638 "deliverChangeRecords", ObjectDeliverChangeRecords, 639 "getNotifier", ObjectGetNotifier, 640 "observe", ObjectObserve, 641 "unobserve", ObjectUnobserve 642 )); 643 InstallFunctions($Array, DONT_ENUM, $Array( 644 "observe", ArrayObserve, 645 "unobserve", ArrayUnobserve 646 )); 647 InstallFunctions(notifierPrototype, DONT_ENUM, $Array( 648 "notify", ObjectNotifierNotify, 649 "performChange", ObjectNotifierPerformChange 650 )); 651} 652 653SetupObjectObserve(); 654