1// Copyright 2012 the V8 project authors. All rights reserved. 2// Redistribution and use in source and binary forms, with or without 3// modification, are permitted provided that the following conditions are 4// met: 5// 6// * Redistributions of source code must retain the above copyright 7// notice, this list of conditions and the following disclaimer. 8// * Redistributions in binary form must reproduce the above 9// copyright notice, this list of conditions and the following 10// disclaimer in the documentation and/or other materials provided 11// with the distribution. 12// * Neither the name of Google Inc. nor the names of its 13// contributors may be used to endorse or promote products derived 14// from this software without specific prior written permission. 15// 16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 28"use strict"; 29 30// Overview: 31// 32// This file contains all of the routing and accounting for Object.observe. 33// User code will interact with these mechanisms via the Object.observe APIs 34// and, as a side effect of mutation objects which are observed. The V8 runtime 35// (both C++ and JS) will interact with these mechanisms primarily by enqueuing 36// proper change records for objects which were mutated. The Object.observe 37// routing and accounting consists primarily of three participants 38// 39// 1) ObjectInfo. This represents the observed state of a given object. It 40// records what callbacks are observing the object, with what options, and 41// what "change types" are in progress on the object (i.e. via 42// notifier.performChange). 43// 44// 2) CallbackInfo. This represents a callback used for observation. It holds 45// the records which must be delivered to the callback, as well as the global 46// priority of the callback (which determines delivery order between 47// callbacks). 48// 49// 3) observationState.pendingObservers. This is the set of observers which 50// have change records which must be delivered. During "normal" delivery 51// (i.e. not Object.deliverChangeRecords), this is the mechanism by which 52// callbacks are invoked in the proper order until there are no more 53// change records pending to a callback. 54// 55// Note that in order to reduce allocation and processing costs, the 56// implementation of (1) and (2) have "optimized" states which represent 57// common cases which can be handled more efficiently. 58 59var observationState = %GetObservationState(); 60if (IS_UNDEFINED(observationState.callbackInfoMap)) { 61 observationState.callbackInfoMap = %ObservationWeakMapCreate(); 62 observationState.objectInfoMap = %ObservationWeakMapCreate(); 63 observationState.notifierObjectInfoMap = %ObservationWeakMapCreate(); 64 observationState.pendingObservers = null; 65 observationState.nextCallbackPriority = 0; 66} 67 68function ObservationWeakMap(map) { 69 this.map_ = map; 70} 71 72ObservationWeakMap.prototype = { 73 get: function(key) { 74 key = %UnwrapGlobalProxy(key); 75 if (!IS_SPEC_OBJECT(key)) return UNDEFINED; 76 return %WeakCollectionGet(this.map_, key); 77 }, 78 set: function(key, value) { 79 key = %UnwrapGlobalProxy(key); 80 if (!IS_SPEC_OBJECT(key)) return UNDEFINED; 81 %WeakCollectionSet(this.map_, key, value); 82 }, 83 has: function(key) { 84 return !IS_UNDEFINED(this.get(key)); 85 } 86}; 87 88var callbackInfoMap = 89 new ObservationWeakMap(observationState.callbackInfoMap); 90var objectInfoMap = new ObservationWeakMap(observationState.objectInfoMap); 91var notifierObjectInfoMap = 92 new ObservationWeakMap(observationState.notifierObjectInfoMap); 93 94function TypeMapCreate() { 95 return { __proto__: null }; 96} 97 98function TypeMapAddType(typeMap, type, ignoreDuplicate) { 99 typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1; 100} 101 102function TypeMapRemoveType(typeMap, type) { 103 typeMap[type]--; 104} 105 106function TypeMapCreateFromList(typeList) { 107 var typeMap = TypeMapCreate(); 108 for (var i = 0; i < typeList.length; i++) { 109 TypeMapAddType(typeMap, typeList[i], true); 110 } 111 return typeMap; 112} 113 114function TypeMapHasType(typeMap, type) { 115 return !!typeMap[type]; 116} 117 118function TypeMapIsDisjointFrom(typeMap1, typeMap2) { 119 if (!typeMap1 || !typeMap2) 120 return true; 121 122 for (var type in typeMap1) { 123 if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type)) 124 return false; 125 } 126 127 return true; 128} 129 130var defaultAcceptTypes = TypeMapCreateFromList([ 131 'add', 132 'update', 133 'delete', 134 'setPrototype', 135 'reconfigure', 136 'preventExtensions' 137]); 138 139// An Observer is a registration to observe an object by a callback with 140// a given set of accept types. If the set of accept types is the default 141// set for Object.observe, the observer is represented as a direct reference 142// to the callback. An observer never changes its accept types and thus never 143// needs to "normalize". 144function ObserverCreate(callback, acceptList) { 145 return IS_UNDEFINED(acceptList) ? callback : { 146 __proto__: null, 147 callback: callback, 148 accept: TypeMapCreateFromList(acceptList) 149 }; 150} 151 152function ObserverGetCallback(observer) { 153 return IS_SPEC_FUNCTION(observer) ? observer : observer.callback; 154} 155 156function ObserverGetAcceptTypes(observer) { 157 return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept; 158} 159 160function ObserverIsActive(observer, objectInfo) { 161 return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo), 162 ObserverGetAcceptTypes(observer)); 163} 164 165function ObjectInfoGet(object) { 166 var objectInfo = objectInfoMap.get(object); 167 if (IS_UNDEFINED(objectInfo)) { 168 if (!%IsJSProxy(object)) 169 %SetIsObserved(object); 170 171 objectInfo = { 172 object: object, 173 changeObservers: null, 174 notifier: null, 175 performing: null, 176 performingCount: 0, 177 }; 178 objectInfoMap.set(object, objectInfo); 179 } 180 return objectInfo; 181} 182 183function ObjectInfoGetFromNotifier(notifier) { 184 return notifierObjectInfoMap.get(notifier); 185} 186 187function ObjectInfoGetNotifier(objectInfo) { 188 if (IS_NULL(objectInfo.notifier)) { 189 objectInfo.notifier = { __proto__: notifierPrototype }; 190 notifierObjectInfoMap.set(objectInfo.notifier, objectInfo); 191 } 192 193 return objectInfo.notifier; 194} 195 196function ObjectInfoGetObject(objectInfo) { 197 return objectInfo.object; 198} 199 200function ChangeObserversIsOptimized(changeObservers) { 201 return typeof changeObservers === 'function' || 202 typeof changeObservers.callback === 'function'; 203} 204 205// The set of observers on an object is called 'changeObservers'. The first 206// observer is referenced directly via objectInfo.changeObservers. When a second 207// is added, changeObservers "normalizes" to become a mapping of callback 208// priority -> observer and is then stored on objectInfo.changeObservers. 209function ObjectInfoNormalizeChangeObservers(objectInfo) { 210 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { 211 var observer = objectInfo.changeObservers; 212 var callback = ObserverGetCallback(observer); 213 var callbackInfo = CallbackInfoGet(callback); 214 var priority = CallbackInfoGetPriority(callbackInfo); 215 objectInfo.changeObservers = { __proto__: null }; 216 objectInfo.changeObservers[priority] = observer; 217 } 218} 219 220function ObjectInfoAddObserver(objectInfo, callback, acceptList) { 221 var callbackInfo = CallbackInfoGetOrCreate(callback); 222 var observer = ObserverCreate(callback, acceptList); 223 224 if (!objectInfo.changeObservers) { 225 objectInfo.changeObservers = observer; 226 return; 227 } 228 229 ObjectInfoNormalizeChangeObservers(objectInfo); 230 var priority = CallbackInfoGetPriority(callbackInfo); 231 objectInfo.changeObservers[priority] = observer; 232} 233 234function ObjectInfoRemoveObserver(objectInfo, callback) { 235 if (!objectInfo.changeObservers) 236 return; 237 238 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { 239 if (callback === ObserverGetCallback(objectInfo.changeObservers)) 240 objectInfo.changeObservers = null; 241 return; 242 } 243 244 var callbackInfo = CallbackInfoGet(callback); 245 var priority = CallbackInfoGetPriority(callbackInfo); 246 delete objectInfo.changeObservers[priority]; 247} 248 249function ObjectInfoHasActiveObservers(objectInfo) { 250 if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers) 251 return false; 252 253 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) 254 return ObserverIsActive(objectInfo.changeObservers, objectInfo); 255 256 for (var priority in objectInfo.changeObservers) { 257 if (ObserverIsActive(objectInfo.changeObservers[priority], objectInfo)) 258 return true; 259 } 260 261 return false; 262} 263 264function ObjectInfoAddPerformingType(objectInfo, type) { 265 objectInfo.performing = objectInfo.performing || TypeMapCreate(); 266 TypeMapAddType(objectInfo.performing, type); 267 objectInfo.performingCount++; 268} 269 270function ObjectInfoRemovePerformingType(objectInfo, type) { 271 objectInfo.performingCount--; 272 TypeMapRemoveType(objectInfo.performing, type); 273} 274 275function ObjectInfoGetPerformingTypes(objectInfo) { 276 return objectInfo.performingCount > 0 ? objectInfo.performing : null; 277} 278 279function AcceptArgIsValid(arg) { 280 if (IS_UNDEFINED(arg)) 281 return true; 282 283 if (!IS_SPEC_OBJECT(arg) || 284 !IS_NUMBER(arg.length) || 285 arg.length < 0) 286 return false; 287 288 return true; 289} 290 291// CallbackInfo's optimized state is just a number which represents its global 292// priority. When a change record must be enqueued for the callback, it 293// normalizes. When delivery clears any pending change records, it re-optimizes. 294function CallbackInfoGet(callback) { 295 return callbackInfoMap.get(callback); 296} 297 298function CallbackInfoGetOrCreate(callback) { 299 var callbackInfo = callbackInfoMap.get(callback); 300 if (!IS_UNDEFINED(callbackInfo)) 301 return callbackInfo; 302 303 var priority = observationState.nextCallbackPriority++ 304 callbackInfoMap.set(callback, priority); 305 return priority; 306} 307 308function CallbackInfoGetPriority(callbackInfo) { 309 if (IS_NUMBER(callbackInfo)) 310 return callbackInfo; 311 else 312 return callbackInfo.priority; 313} 314 315function CallbackInfoNormalize(callback) { 316 var callbackInfo = callbackInfoMap.get(callback); 317 if (IS_NUMBER(callbackInfo)) { 318 var priority = callbackInfo; 319 callbackInfo = new InternalArray; 320 callbackInfo.priority = priority; 321 callbackInfoMap.set(callback, callbackInfo); 322 } 323 return callbackInfo; 324} 325 326function ObjectObserve(object, callback, acceptList) { 327 if (!IS_SPEC_OBJECT(object)) 328 throw MakeTypeError("observe_non_object", ["observe"]); 329 if (!IS_SPEC_FUNCTION(callback)) 330 throw MakeTypeError("observe_non_function", ["observe"]); 331 if (ObjectIsFrozen(callback)) 332 throw MakeTypeError("observe_callback_frozen"); 333 if (!AcceptArgIsValid(acceptList)) 334 throw MakeTypeError("observe_accept_invalid"); 335 336 var objectInfo = ObjectInfoGet(object); 337 ObjectInfoAddObserver(objectInfo, callback, acceptList); 338 return object; 339} 340 341function ObjectUnobserve(object, callback) { 342 if (!IS_SPEC_OBJECT(object)) 343 throw MakeTypeError("observe_non_object", ["unobserve"]); 344 if (!IS_SPEC_FUNCTION(callback)) 345 throw MakeTypeError("observe_non_function", ["unobserve"]); 346 347 var objectInfo = objectInfoMap.get(object); 348 if (IS_UNDEFINED(objectInfo)) 349 return object; 350 351 ObjectInfoRemoveObserver(objectInfo, callback); 352 return object; 353} 354 355function ArrayObserve(object, callback) { 356 return ObjectObserve(object, callback, ['add', 357 'update', 358 'delete', 359 'splice']); 360} 361 362function ArrayUnobserve(object, callback) { 363 return ObjectUnobserve(object, callback); 364} 365 366function ObserverEnqueueIfActive(observer, objectInfo, changeRecord, 367 needsAccessCheck) { 368 if (!ObserverIsActive(observer, objectInfo) || 369 !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) { 370 return; 371 } 372 373 var callback = ObserverGetCallback(observer); 374 if (needsAccessCheck && 375 // Drop all splice records on the floor for access-checked objects 376 (changeRecord.type == 'splice' || 377 !%IsAccessAllowedForObserver( 378 callback, changeRecord.object, changeRecord.name))) { 379 return; 380 } 381 382 var callbackInfo = CallbackInfoNormalize(callback); 383 if (!observationState.pendingObservers) 384 observationState.pendingObservers = { __proto__: null }; 385 observationState.pendingObservers[callbackInfo.priority] = callback; 386 callbackInfo.push(changeRecord); 387 %SetMicrotaskPending(true); 388} 389 390function ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, type) { 391 if (!ObjectInfoHasActiveObservers(objectInfo)) 392 return; 393 394 var hasType = !IS_UNDEFINED(type); 395 var newRecord = hasType ? 396 { object: ObjectInfoGetObject(objectInfo), type: type } : 397 { object: ObjectInfoGetObject(objectInfo) }; 398 399 for (var prop in changeRecord) { 400 if (prop === 'object' || (hasType && prop === 'type')) continue; 401 %DefineOrRedefineDataProperty(newRecord, prop, changeRecord[prop], 402 READ_ONLY + DONT_DELETE); 403 } 404 ObjectFreeze(newRecord); 405 406 ObjectInfoEnqueueInternalChangeRecord(objectInfo, newRecord, 407 true /* skip access check */); 408} 409 410function ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord, 411 skipAccessCheck) { 412 // TODO(rossberg): adjust once there is a story for symbols vs proxies. 413 if (IS_SYMBOL(changeRecord.name)) return; 414 415 var needsAccessCheck = !skipAccessCheck && 416 %IsAccessCheckNeeded(changeRecord.object); 417 418 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { 419 var observer = objectInfo.changeObservers; 420 ObserverEnqueueIfActive(observer, objectInfo, changeRecord, 421 needsAccessCheck); 422 return; 423 } 424 425 for (var priority in objectInfo.changeObservers) { 426 var observer = objectInfo.changeObservers[priority]; 427 ObserverEnqueueIfActive(observer, objectInfo, changeRecord, 428 needsAccessCheck); 429 } 430} 431 432function BeginPerformSplice(array) { 433 var objectInfo = objectInfoMap.get(array); 434 if (!IS_UNDEFINED(objectInfo)) 435 ObjectInfoAddPerformingType(objectInfo, 'splice'); 436} 437 438function EndPerformSplice(array) { 439 var objectInfo = objectInfoMap.get(array); 440 if (!IS_UNDEFINED(objectInfo)) 441 ObjectInfoRemovePerformingType(objectInfo, 'splice'); 442} 443 444function EnqueueSpliceRecord(array, index, removed, addedCount) { 445 var objectInfo = objectInfoMap.get(array); 446 if (!ObjectInfoHasActiveObservers(objectInfo)) 447 return; 448 449 var changeRecord = { 450 type: 'splice', 451 object: array, 452 index: index, 453 removed: removed, 454 addedCount: addedCount 455 }; 456 457 ObjectFreeze(changeRecord); 458 ObjectFreeze(changeRecord.removed); 459 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord); 460} 461 462function NotifyChange(type, object, name, oldValue) { 463 var objectInfo = objectInfoMap.get(object); 464 if (!ObjectInfoHasActiveObservers(objectInfo)) 465 return; 466 467 var changeRecord; 468 if (arguments.length == 2) { 469 changeRecord = { type: type, object: object }; 470 } else if (arguments.length == 3) { 471 changeRecord = { type: type, object: object, name: name }; 472 } else { 473 changeRecord = { 474 type: type, 475 object: object, 476 name: name, 477 oldValue: oldValue 478 }; 479 } 480 481 ObjectFreeze(changeRecord); 482 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord); 483} 484 485var notifierPrototype = {}; 486 487function ObjectNotifierNotify(changeRecord) { 488 if (!IS_SPEC_OBJECT(this)) 489 throw MakeTypeError("called_on_non_object", ["notify"]); 490 491 var objectInfo = ObjectInfoGetFromNotifier(this); 492 if (IS_UNDEFINED(objectInfo)) 493 throw MakeTypeError("observe_notify_non_notifier"); 494 if (!IS_STRING(changeRecord.type)) 495 throw MakeTypeError("observe_type_non_string"); 496 497 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord); 498} 499 500function ObjectNotifierPerformChange(changeType, changeFn) { 501 if (!IS_SPEC_OBJECT(this)) 502 throw MakeTypeError("called_on_non_object", ["performChange"]); 503 504 var objectInfo = ObjectInfoGetFromNotifier(this); 505 506 if (IS_UNDEFINED(objectInfo)) 507 throw MakeTypeError("observe_notify_non_notifier"); 508 if (!IS_STRING(changeType)) 509 throw MakeTypeError("observe_perform_non_string"); 510 if (!IS_SPEC_FUNCTION(changeFn)) 511 throw MakeTypeError("observe_perform_non_function"); 512 513 ObjectInfoAddPerformingType(objectInfo, changeType); 514 515 var changeRecord; 516 try { 517 changeRecord = %_CallFunction(UNDEFINED, changeFn); 518 } finally { 519 ObjectInfoRemovePerformingType(objectInfo, changeType); 520 } 521 522 if (IS_SPEC_OBJECT(changeRecord)) 523 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, changeType); 524} 525 526function ObjectGetNotifier(object) { 527 if (!IS_SPEC_OBJECT(object)) 528 throw MakeTypeError("observe_non_object", ["getNotifier"]); 529 530 if (ObjectIsFrozen(object)) return null; 531 532 var objectInfo = ObjectInfoGet(object); 533 return ObjectInfoGetNotifier(objectInfo); 534} 535 536function CallbackDeliverPending(callback) { 537 var callbackInfo = callbackInfoMap.get(callback); 538 if (IS_UNDEFINED(callbackInfo) || IS_NUMBER(callbackInfo)) 539 return false; 540 541 // Clear the pending change records from callback and return it to its 542 // "optimized" state. 543 var priority = callbackInfo.priority; 544 callbackInfoMap.set(callback, priority); 545 546 if (observationState.pendingObservers) 547 delete observationState.pendingObservers[priority]; 548 549 var delivered = []; 550 %MoveArrayContents(callbackInfo, delivered); 551 552 try { 553 %_CallFunction(UNDEFINED, delivered, callback); 554 } catch (ex) {} // TODO(rossberg): perhaps log uncaught exceptions. 555 return true; 556} 557 558function ObjectDeliverChangeRecords(callback) { 559 if (!IS_SPEC_FUNCTION(callback)) 560 throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]); 561 562 while (CallbackDeliverPending(callback)) {} 563} 564 565function ObserveMicrotaskRunner() { 566 var pendingObservers = observationState.pendingObservers; 567 if (pendingObservers) { 568 observationState.pendingObservers = null; 569 for (var i in pendingObservers) { 570 CallbackDeliverPending(pendingObservers[i]); 571 } 572 } 573} 574RunMicrotasks.runners.push(ObserveMicrotaskRunner); 575 576function SetupObjectObserve() { 577 %CheckIsBootstrapping(); 578 InstallFunctions($Object, DONT_ENUM, $Array( 579 "deliverChangeRecords", ObjectDeliverChangeRecords, 580 "getNotifier", ObjectGetNotifier, 581 "observe", ObjectObserve, 582 "unobserve", ObjectUnobserve 583 )); 584 InstallFunctions($Array, DONT_ENUM, $Array( 585 "observe", ArrayObserve, 586 "unobserve", ArrayUnobserve 587 )); 588 InstallFunctions(notifierPrototype, DONT_ENUM, $Array( 589 "notify", ObjectNotifierNotify, 590 "performChange", ObjectNotifierPerformChange 591 )); 592} 593 594SetupObjectObserve(); 595