Skip to content

Instantly share code, notes, and snippets.

@mhevery
Last active March 30, 2022 11:54
Show Gist options
  • Save mhevery/63fdcdf7c65886051d55 to your computer and use it in GitHub Desktop.
Save mhevery/63fdcdf7c65886051d55 to your computer and use it in GitHub Desktop.
TC39 Zone Proposal

Examples

Logging Zone

class LoggingZoneSpec {
  constructor() {
    this.name = 'logging';
    this.prefix = '';
  }

  onInvoke(parentZoneDelegate, currentZone, targetZone, callback, applyThis, applyArg) {
    console.log(this.prefix + 'Enter Zone:', targetZone.name);
    this.prefix += '  ';
    try {
      return parentZoneDelegate.invoke(targetZone, callback, applyThis, applyArg);
    } finally {
      this.prefix = this.prefix.substring(2);
      console.log(this.prefix + 'Leave Zone:', targetZone.name);
    }
  }
}

Zone.current.fork(new LoggingZoneSpec()).run(() => {
  Zone.current.fork({name: 'test'}).run(() => {
    setTimeout(() => console.log('  works'), 0);
  });
});

Expected Output:

Enter Zone: logging
  Enter Zone: test
  Leave Zone: test
Leave Zone: logging
Enter Zone: test
  works
Leave Zone: test

How Promise.prototype.then can be implemented or patched

Ideally we would not need to do this, since the Promise would do this for us. It could do this more efficiently by storing the Zone, and then invoking the callback using zone.run(...)

window.Promise.prototype.then = ((then) => {
  return function (onResolve, onError) {
     // Wrap callbacks without error handling.
     arguments[0] = Zone.current.wrap(onResolve, 'Promise.then.onResolve', false);
     arguments[1] = Zone.current.wrap(onError, 'Promise.then.onError', false);
     return then.apply(this, arguments);
  };
})(window.Promise.prototype.then);

Zone Motivation

Make writing asynchronous code easier by having a consistent way of propagating "context" across related asynchronous operations. Have the "context" be responsible for async-local-storage, allowing the execution before and after hooks, and "context"-local error handling. Finally make sure that the "context"s are composable.

This feature needs to be part of the platform so that library and framework authors can relay on a common well know API, otherwise adoption will be limited.

Zone Proposal

Zone Class Definition

NOTE: This is the only API which is needed as a user of Zone.

class Zone {
  constructor(parent = null, zoneSpec = null, properties = null) 
 
  static get current() 
  
  get name()
  get parent()
  
  fork(zoneSpec = null, properties = null)
  wrap(callback, source = null, catchErrors = true)
  run(callback, applyThis = null, applyArgs = null, catchErrors = false)
  handleError(error)
  
  get(key)
  
  toString()
}

Internal Slots

Internal Slot Description
[[parent]] A parent zone when describing composition of Zones.
[[name]] Name of the zone. (For debugging and toString().)
[[delegate]] A ZoneDelegate which handles the interception of the current zone methods.
[[properties]] Storage for the get method.

static get current() method

Returns the current zone. The only way to change the current zone is by invoking a run() method, which will update the current zone for the duration of the run method callback.

fork(zoneSpec = null, properties = null) method

Creates a new Zone which is a child of the current Zone. This method takes a ZoneSpec and an properties object literal which defines properties on the current zone. See: get().

wrap(callback, source, catchErrors = true) method

Wraps a callback function in a new function which will properly restore the current zone upon invocation. The source argument is a string useful for contextual debug information.

Simplified pseudo code:

wrap(callback, source, catchErrors = true) {
  callback = this.zoneDelegate.intercept(this, callback);
  let zone = this;
  return function(...args) {
     return zone.run(callback, this, args, catchErrors);
  };
}

run(callback, applyThis, applyArgs, catchErrors = false) method

Invokes callback function in the zone. Saves and restores previous zone.

Simplified pseudo code:

run(callback, applyThis, applyArgs, catchErrors = false) {
  let savedZone = Zone.current;
  Zone.current = this;
  try {
    return this.zoneDelegate.invoke(this, callback, applyThis, applyArgs);
  } catch (e) {
    // optionally do error handling if catchErrors
    if (this.handleError(e) throw e;
  } finally {
    Zone.current = savedZone;
  }
}

handleError(error) method

Asks zone to handle an error (report to console etc). If a method returns true then the error should be rethrown. This method is typically called from run() but it can also be called by the user code to report general errors.

get(key) method

Retrieves the value for a give key from the properties. If no value is found get delegates to the parent zone.

Note: We don't support set or delete because we want to make sure that running code in an extra zone should not break the behavior of existing code. Not delegating would mean that child zone code would not see the property. Allowing modification, would mean that child zone code could change the value, which would not be reflected in the parent zone, which would break code running in parent zone.

Root Zone

When the system initializes there is a special root zone created. The root zone's behavior is configured so that the root zone behaves in the same way as the current browser platform.

It is not possible to change the the behavior of an existing zone, so to do so the zone needs to be forked with a ZoneSpec definition. For this the following API is needed.

ZoneDelegate Class Definition

NOTE: This API is only relevant when defining child Zones.

A ZoneDelegate is needed because a child zone can't simply invoke a method on a parent zone. For example a child zone wrap can't just call parent zone wrap. Doing so would create a callback which is bound to the parent zone. What we are interested is intercepting the callback before it is bound to any zone. Furthermore, we also need to pass the targetZone (zone which received the original request) to the delegate.

The ZoneDelegate methods mirror those of Zone with an addition of extra targetZone argument in the method signature. (The original Zone which received the request.) Some methods are renamed to prevent confusion, because they have slightly different semantics and arguments.

  • wrap => intercept: The wrap method delegates to intercept. The wrap method returns a callback which will run in a given zone, where as intercept allows wrapping the callback so that additional code can be run before and after, but does not associated the callback with the zone.
  • run => invoke: The run method delegates to invoke to perform the actual execution of the callback. The run method switches to new zone; saves and restores the Zone.current; and optionally performs error handling. The invoke is not responsible for error handling, or zone management.

Not every method is usually overwritten in the child zone, for this reason the ZoneDelegate stores the closest zone which overwrites this behavior along with the closest ZoneSpec.

NOTE: We have tried to make this API analogous to Event bubbling with target and current properties.

class ZoneDelegate {
  constructor(zone, parentZoneDelegate, zoneSpec)
   
  fork(targetZone, zoneSpec, properties)
  intercept(targetZone, callback, source)
  invoke(targetZone, callback, applyThis, applyArgs)
  handleError(targetZone, error)
}

Note: The ZoneDelegate treats ZoneSpec as class. This allows the ZoneSpec to use its this to store internal state.

Internal Slots

Internal Slot Description
[[parent]] Parent ZoneDelegate.
[[zone]] Zone which is associated with this delegate
[[forkSpec]] Closest ZoneSpec for the fork function.
[[forkZone] Closest Zone for the fork function.
[[interceptSpec]] Closest ZoneSpec for the wrap function.
[[interceptZone]] Closest Zone for the wrap function.
[[invokeSpec]] Closest ZoneSpec for the run function.
[[invokeZone]] Closest Zone for the run function.
[[handleErrorSpec]] Closest ZoneSpec for the handleError function.
[[handleErrorZone]] Closest Zone for the handleError function.

ZoneSpec Interface Definition

NOTE: This API is only relevant when defining child Zones.

ZoneSpec provides a configuration when [Zone]s are forked. All properties of the ZoneSpec are optional when forking zone. There is no actual class, rather this is the shape of the class which the Zone.fork() expects.

interface ZoneSpec {
  get name()
  
  /* optional */ onFork(parentZoneDelegate, currentZone, targetZone, zoneSpec, properties)
  /* optional */ onIntercept(parentZoneDelegate, currentZone, targetZone, callback)
  /* optional */ onInvoke(parentZoneDelegate, currentZone, targetZone, callback, applyThis, applyArg)
  /* optional */ onHandleError(parentZoneDelegate, currentZone, targetZone, error)
}

The ZoneSpec is treated as class, and it can use its this to store internal state.

Arguments

  • parentZoneDelegate: The delegate where the requests should be delegated to once the current [ZoneSpec] performs its work.
  • currentZone: The current zone of the [ZoneSpec]. (The resulting zone of the fork operation for a given ZoneSpec)
  • targetZone: The zone which received the original request.
@aaaristo
Copy link

aaaristo commented Feb 9, 2016

What is the status of this proposal? Can't find it here:
https://github.com/tc39/ecma262/blob/master/stage0.md

@benjamingr
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment