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 36 var observationState; 37 38 function 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 54 function 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 75 var contextMaps; 76 77 function 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 91 function GetCallbackInfoMap() { 92 return GetContextMaps().callbackInfoMap; 93 } 94 95 function GetObjectInfoMap() { 96 return GetContextMaps().objectInfoMap; 97 } 98 99 function GetNotifierObjectInfoMap() { 100 return GetContextMaps().notifierObjectInfoMap; 101 } 102 103 function GetPendingObservers() { 104 return GetObservationStateJS().pendingObservers; 105 } 106 107 function SetPendingObservers(pendingObservers) { 108 GetObservationStateJS().pendingObservers = pendingObservers; 109 } 110 111 function GetNextCallbackPriority() { 112 return GetObservationStateJS().nextCallbackPriority++; 113 } 114 115 function nullProtoObject() { 116 return { __proto__: null }; 117 } 118 119 function TypeMapCreate() { 120 return nullProtoObject(); 121 } 122 123 function TypeMapAddType(typeMap, type, ignoreDuplicate) { 124 typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1; 125 } 126 127 function TypeMapRemoveType(typeMap, type) { 128 typeMap[type]--; 129 } 130 131 function 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 139 function TypeMapHasType(typeMap, type) { 140 return !!typeMap[type]; 141 } 142 143 function 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 155 var 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". 172 function 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 181 function ObserverGetCallback(observer) { 182 return IS_SPEC_FUNCTION(observer) ? observer : observer.callback; 183 } 184 185 function ObserverGetAcceptTypes(observer) { 186 return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept; 187 } 188 189 function ObserverIsActive(observer, objectInfo) { 190 return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo), 191 ObserverGetAcceptTypes(observer)); 192 } 193 194 function 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 212 function ObjectInfoGet(object) { 213 return GetObjectInfoMap().get(object); 214 } 215 216 function ObjectInfoGetFromNotifier(notifier) { 217 return GetNotifierObjectInfoMap().get(notifier); 218 } 219 220 function 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 229 function ObjectInfoGetObject(objectInfo) { 230 return objectInfo.object; 231 } 232 233 function 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. 242 function 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 253 function 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 267 function 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 282 function 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 298 function ObjectInfoAddPerformingType(objectInfo, type) { 299 objectInfo.performing = objectInfo.performing || TypeMapCreate(); 300 TypeMapAddType(objectInfo.performing, type); 301 objectInfo.performingCount++; 302 } 303 304 function ObjectInfoRemovePerformingType(objectInfo, type) { 305 objectInfo.performingCount--; 306 TypeMapRemoveType(objectInfo.performing, type); 307 } 308 309 function ObjectInfoGetPerformingTypes(objectInfo) { 310 return objectInfo.performingCount > 0 ? objectInfo.performing : null; 311 } 312 313 function 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. 330 function CallbackInfoGet(callback) { 331 return GetCallbackInfoMap().get(callback); 332 } 333 334 function 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 344 function CallbackInfoGetPriority(callbackInfo) { 345 if (IS_NUMBER(callbackInfo)) 346 return callbackInfo; 347 else 348 return callbackInfo.priority; 349 } 350 351 function 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 362 function 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 376 function NativeObjectObserve(object, callback, acceptList) { 377 var objectInfo = ObjectInfoGetOrCreate(object); 378 var typeList = ConvertAcceptListToTypeMap(acceptList); 379 ObjectInfoAddObserver(objectInfo, callback, typeList); 380 return object; 381 } 382 383 function 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 399 function ArrayObserve(object, callback) { 400 return ObjectObserve(object, callback, ['add', 401 'update', 402 'delete', 403 'splice']); 404 } 405 406 function ArrayUnobserve(object, callback) { 407 return ObjectUnobserve(object, callback); 408 } 409 410 function 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 442 function 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 461 function 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 479 function BeginPerformSplice(array) { 480 var objectInfo = ObjectInfoGet(array); 481 if (!IS_UNDEFINED(objectInfo)) 482 ObjectInfoAddPerformingType(objectInfo, 'splice'); 483 } 484 485 function EndPerformSplice(array) { 486 var objectInfo = ObjectInfoGet(array); 487 if (!IS_UNDEFINED(objectInfo)) 488 ObjectInfoRemovePerformingType(objectInfo, 'splice'); 489 } 490 491 function 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 509 function 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 532 var notifierPrototype = {}; 533 534 function 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 547 function 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 563 function 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 577 function 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 591 function NativeObjectGetNotifier(object) { 592 var objectInfo = ObjectInfoGetOrCreate(object); 593 return ObjectInfoGetNotifier(objectInfo); 594 } 595 596 function 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 618 function ObjectDeliverChangeRecords(callback) { 619 if (!IS_SPEC_FUNCTION(callback)) 620 throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]); 621 622 while (CallbackDeliverPending(callback)) {} 623 } 624 625 function 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 635 function 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 653 SetupObjectObserve(); 654