mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
worker: add web locks api
PR-URL: https://github.com/nodejs/node/pull/58666 Fixes: https://github.com/nodejs/node/pull/36502 Refs: https://w3c.github.io/web-locks Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
parent
0629a175c0
commit
062e8b5a74
70 changed files with 5030 additions and 0 deletions
|
@ -768,6 +768,55 @@ consisting of the runtime name and major version number.
|
|||
console.log(`The user-agent is ${navigator.userAgent}`); // Prints "Node.js/21"
|
||||
```
|
||||
|
||||
### `navigator.locks`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
The `navigator.locks` read-only property returns a [`LockManager`][] instance that
|
||||
can be used to coordinate access to resources that may be shared across multiple
|
||||
threads within the same process. This global implementation matches the semantics
|
||||
of the [browser `LockManager`][] API.
|
||||
|
||||
```mjs
|
||||
// Request an exclusive lock
|
||||
await navigator.locks.request('my_resource', async (lock) => {
|
||||
// The lock has been acquired.
|
||||
console.log(`Lock acquired: ${lock.name}`);
|
||||
// Lock is automatically released when the function returns
|
||||
});
|
||||
|
||||
// Request a shared lock
|
||||
await navigator.locks.request('shared_resource', { mode: 'shared' }, async (lock) => {
|
||||
// Multiple shared locks can be held simultaneously
|
||||
console.log(`Shared lock acquired: ${lock.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
```cjs
|
||||
// Request an exclusive lock
|
||||
navigator.locks.request('my_resource', async (lock) => {
|
||||
// The lock has been acquired.
|
||||
console.log(`Lock acquired: ${lock.name}`);
|
||||
// Lock is automatically released when the function returns
|
||||
}).then(() => {
|
||||
console.log('Lock released');
|
||||
});
|
||||
|
||||
// Request a shared lock
|
||||
navigator.locks.request('shared_resource', { mode: 'shared' }, async (lock) => {
|
||||
// Multiple shared locks can be held simultaneously
|
||||
console.log(`Shared lock acquired: ${lock.name}`);
|
||||
}).then(() => {
|
||||
console.log('Shared lock released');
|
||||
});
|
||||
```
|
||||
|
||||
See [`worker.locks`][] for detailed API documentation.
|
||||
|
||||
## Class: `PerformanceEntry`
|
||||
|
||||
<!-- YAML
|
||||
|
@ -1263,6 +1312,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
|
|||
[`CountQueuingStrategy`]: webstreams.md#class-countqueuingstrategy
|
||||
[`DecompressionStream`]: webstreams.md#class-decompressionstream
|
||||
[`EventTarget` and `Event` API]: events.md#eventtarget-and-event-api
|
||||
[`LockManager`]: worker_threads.md#class-lockmanager
|
||||
[`MessageChannel`]: worker_threads.md#class-messagechannel
|
||||
[`MessagePort`]: worker_threads.md#class-messageport
|
||||
[`PerformanceEntry`]: perf_hooks.md#class-performanceentry
|
||||
|
@ -1313,6 +1363,8 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
|
|||
[`setTimeout`]: timers.md#settimeoutcallback-delay-args
|
||||
[`structuredClone`]: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
|
||||
[`window.navigator`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator
|
||||
[`worker.locks`]: worker_threads.md#workerlocks
|
||||
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
|
||||
[buffer section]: buffer.md
|
||||
[built-in objects]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
|
||||
[timers]: timers.md
|
||||
|
|
|
@ -755,6 +755,153 @@ if (isMainThread) {
|
|||
}
|
||||
```
|
||||
|
||||
## `worker.locks`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* {LockManager}
|
||||
|
||||
An instance of a [`LockManager`][LockManager] that can be used to coordinate
|
||||
access to resources that may be shared across multiple threads within the same
|
||||
process. The API mirrors the semantics of the
|
||||
[browser `LockManager`][]
|
||||
|
||||
### Class: `Lock`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
The `Lock` interface provides information about a lock that has been granted via
|
||||
[`locks.request()`][locks.request()]
|
||||
|
||||
#### `lock.name`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* {string}
|
||||
|
||||
The name of the lock.
|
||||
|
||||
#### `lock.mode`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* {string}
|
||||
|
||||
The mode of the lock. Either `shared` or `exclusive`.
|
||||
|
||||
### Class: `LockManager`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
The `LockManager` interface provides methods for requesting and introspecting
|
||||
locks. To obtain a `LockManager` instance use
|
||||
|
||||
```mjs
|
||||
import { locks } from 'node:worker_threads';
|
||||
```
|
||||
|
||||
```cjs
|
||||
'use strict';
|
||||
|
||||
const { locks } = require('node:worker_threads');
|
||||
```
|
||||
|
||||
This implementation matches the [browser `LockManager`][] API.
|
||||
|
||||
#### `locks.request(name[, options], callback)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `name` {string}
|
||||
* `options` {Object}
|
||||
* `mode` {string} Either `'exclusive'` or `'shared'`. **Default:** `'exclusive'`.
|
||||
* `ifAvailable` {boolean} If `true`, the request will only be granted if the
|
||||
lock is not already held. If it cannot be granted, `callback` will be
|
||||
invoked with `null` instead of a `Lock` instance. **Default:** `false`.
|
||||
* `steal` {boolean} If `true`, any existing locks with the same name are
|
||||
released and the request is granted immediately, pre-empting any queued
|
||||
requests. **Default:** `false`.
|
||||
* `signal` {AbortSignal} that can be used to abort a
|
||||
pending (but not yet granted) lock request.
|
||||
* `callback` {Function} Invoked once the lock is granted (or immediately with
|
||||
`null` if `ifAvailable` is `true` and the lock is unavailable). The lock is
|
||||
released automatically when the function returns, or—if the function returns
|
||||
a promise—when that promise settles.
|
||||
* Returns: {Promise} Resolves once the lock has been released.
|
||||
|
||||
```mjs
|
||||
import { locks } from 'node:worker_threads';
|
||||
|
||||
await locks.request('my_resource', async (lock) => {
|
||||
// The lock has been acquired.
|
||||
});
|
||||
// The lock has been released here.
|
||||
```
|
||||
|
||||
```cjs
|
||||
'use strict';
|
||||
|
||||
const { locks } = require('node:worker_threads');
|
||||
|
||||
locks.request('my_resource', async (lock) => {
|
||||
// The lock has been acquired.
|
||||
}).then(() => {
|
||||
// The lock has been released here.
|
||||
});
|
||||
```
|
||||
|
||||
#### `locks.query()`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* Returns: {Promise}
|
||||
|
||||
Resolves with a `LockManagerSnapshot` describing the currently held and pending
|
||||
locks for the current process.
|
||||
|
||||
```mjs
|
||||
import { locks } from 'node:worker_threads';
|
||||
|
||||
const snapshot = await locks.query();
|
||||
for (const lock of snapshot.held) {
|
||||
console.log(`held lock: name ${lock.name}, mode ${lock.mode}`);
|
||||
}
|
||||
for (const pending of snapshot.pending) {
|
||||
console.log(`pending lock: name ${pending.name}, mode ${pending.mode}`);
|
||||
}
|
||||
```
|
||||
|
||||
```cjs
|
||||
'use strict';
|
||||
|
||||
const { locks } = require('node:worker_threads');
|
||||
|
||||
locks.query().then((snapshot) => {
|
||||
for (const lock of snapshot.held) {
|
||||
console.log(`held lock: name ${lock.name}, mode ${lock.mode}`);
|
||||
}
|
||||
for (const pending of snapshot.pending) {
|
||||
console.log(`pending lock: name ${pending.name}, mode ${pending.mode}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Class: `BroadcastChannel extends EventTarget`
|
||||
|
||||
<!-- YAML
|
||||
|
@ -1937,6 +2084,7 @@ thread spawned will spawn another until the application crashes.
|
|||
[Addons worker support]: addons.md#worker-support
|
||||
[ECMAScript module loader]: esm.md#data-imports
|
||||
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
[LockManager]: #class-lockmanager
|
||||
[Signals events]: process.md#signal-events
|
||||
[Web Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
||||
[`'close'` event]: #event-close
|
||||
|
@ -1992,7 +2140,9 @@ thread spawned will spawn another until the application crashes.
|
|||
[`worker.terminate()`]: #workerterminate
|
||||
[`worker.threadId`]: #workerthreadid_1
|
||||
[async-resource-worker-pool]: async_context.md#using-asyncresource-for-a-worker-thread-pool
|
||||
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
|
||||
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
|
||||
[child processes]: child_process.md
|
||||
[contextified]: vm.md#what-does-it-mean-to-contextify-an-object
|
||||
[locks.request()]: #locksrequestname-options-callback
|
||||
[v8.serdes]: v8.md#serialization-api
|
||||
|
|
293
lib/internal/locks.js
Normal file
293
lib/internal/locks.js
Normal file
|
@ -0,0 +1,293 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ObjectDefineProperties,
|
||||
Promise,
|
||||
PromiseResolve,
|
||||
Symbol,
|
||||
SymbolToStringTag,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
ERR_ILLEGAL_CONSTRUCTOR,
|
||||
ERR_INVALID_THIS,
|
||||
} = require('internal/errors');
|
||||
const {
|
||||
kEmptyObject,
|
||||
lazyDOMException,
|
||||
} = require('internal/util');
|
||||
const {
|
||||
validateAbortSignal,
|
||||
validateFunction,
|
||||
} = require('internal/validators');
|
||||
const { threadId } = require('internal/worker');
|
||||
const {
|
||||
converters,
|
||||
createEnumConverter,
|
||||
createDictionaryConverter,
|
||||
} = require('internal/webidl');
|
||||
|
||||
const locks = internalBinding('locks');
|
||||
|
||||
const kName = Symbol('kName');
|
||||
const kMode = Symbol('kMode');
|
||||
const kConstructLock = Symbol('kConstructLock');
|
||||
const kConstructLockManager = Symbol('kConstructLockManager');
|
||||
|
||||
// WebIDL dictionary LockOptions
|
||||
const convertLockOptions = createDictionaryConverter([
|
||||
{
|
||||
key: 'mode',
|
||||
converter: createEnumConverter('LockMode', [
|
||||
'shared',
|
||||
'exclusive',
|
||||
]),
|
||||
defaultValue: () => 'exclusive',
|
||||
},
|
||||
{
|
||||
key: 'ifAvailable',
|
||||
converter: (value) => !!value,
|
||||
defaultValue: () => false,
|
||||
},
|
||||
{
|
||||
key: 'steal',
|
||||
converter: (value) => !!value,
|
||||
defaultValue: () => false,
|
||||
},
|
||||
{
|
||||
key: 'signal',
|
||||
converter: converters.object,
|
||||
},
|
||||
]);
|
||||
|
||||
// https://w3c.github.io/web-locks/#api-lock
|
||||
class Lock {
|
||||
constructor(symbol = undefined, name, mode) {
|
||||
if (symbol !== kConstructLock) {
|
||||
throw new ERR_ILLEGAL_CONSTRUCTOR();
|
||||
}
|
||||
this[kName] = name;
|
||||
this[kMode] = mode;
|
||||
}
|
||||
|
||||
get name() {
|
||||
if (this instanceof Lock) {
|
||||
return this[kName];
|
||||
}
|
||||
throw new ERR_INVALID_THIS('Lock');
|
||||
}
|
||||
|
||||
get mode() {
|
||||
if (this instanceof Lock) {
|
||||
return this[kMode];
|
||||
}
|
||||
throw new ERR_INVALID_THIS('Lock');
|
||||
}
|
||||
}
|
||||
|
||||
ObjectDefineProperties(Lock.prototype, {
|
||||
name: { __proto__: null, enumerable: true },
|
||||
mode: { __proto__: null, enumerable: true },
|
||||
[SymbolToStringTag]: {
|
||||
__proto__: null,
|
||||
value: 'Lock',
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Helper to create Lock objects from internal C++ lock data
|
||||
function createLock(internalLock) {
|
||||
return internalLock === null ? null : new Lock(kConstructLock, internalLock.name, internalLock.mode);
|
||||
}
|
||||
|
||||
// Convert LOCK_STOLEN_ERROR to AbortError DOMException
|
||||
function convertLockError(error) {
|
||||
if (error?.message === locks.LOCK_STOLEN_ERROR) {
|
||||
return lazyDOMException('The operation was aborted', 'AbortError');
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/web-locks/#api-lock-manager
|
||||
class LockManager {
|
||||
constructor(symbol = undefined) {
|
||||
if (symbol !== kConstructLockManager) {
|
||||
throw new ERR_ILLEGAL_CONSTRUCTOR();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a Web Lock for a named resource.
|
||||
* @param {string} name - The name of the lock resource
|
||||
* @param {object} [options] - Lock options (optional)
|
||||
* @param {string} [options.mode] - Lock mode: 'exclusive' or 'shared' default is exclusive
|
||||
* @param {boolean} [options.ifAvailable] - Only grant if immediately available
|
||||
* @param {boolean} [options.steal] - Steal existing locks with same name
|
||||
* @param {AbortSignal} [options.signal] - Signal to abort pending lock request
|
||||
* @param {Function} callback - Function called when lock is granted
|
||||
* @returns {Promise} Promise that resolves when the lock is released
|
||||
* @throws {TypeError} When name is not a string or callback is not a function
|
||||
* @throws {DOMException} When validation fails or operation is not supported
|
||||
*/
|
||||
// https://w3c.github.io/web-locks/#api-lock-manager-request
|
||||
async request(name, options, callback) {
|
||||
if (callback === undefined) {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
name = converters.DOMString(name);
|
||||
validateFunction(callback, 'callback');
|
||||
|
||||
if (options === undefined || typeof options === 'function') {
|
||||
options = kEmptyObject;
|
||||
}
|
||||
|
||||
// Convert LockOptions dictionary
|
||||
options = convertLockOptions(options);
|
||||
|
||||
const { mode, ifAvailable, steal, signal } = options;
|
||||
|
||||
validateAbortSignal(signal, 'options.signal');
|
||||
|
||||
if (signal) {
|
||||
signal.throwIfAborted();
|
||||
}
|
||||
|
||||
if (name.startsWith('-')) {
|
||||
// If name starts with U+002D HYPHEN-MINUS (-), then reject promise with a
|
||||
// "NotSupportedError" DOMException.
|
||||
throw lazyDOMException('Lock name may not start with hyphen',
|
||||
'NotSupportedError');
|
||||
}
|
||||
|
||||
if (ifAvailable === true && steal === true) {
|
||||
// If both options' steal dictionary member and option's
|
||||
// ifAvailable dictionary member are true, then reject promise with a
|
||||
// "NotSupportedError" DOMException.
|
||||
throw lazyDOMException('ifAvailable and steal are mutually exclusive',
|
||||
'NotSupportedError');
|
||||
}
|
||||
|
||||
if (mode !== locks.LOCK_MODE_EXCLUSIVE && steal === true) {
|
||||
// If options' steal dictionary member is true and options' mode
|
||||
// dictionary member is not "exclusive", then return a promise rejected
|
||||
// with a "NotSupportedError" DOMException.
|
||||
throw lazyDOMException(`mode: "${locks.LOCK_MODE_SHARED}" and steal are mutually exclusive`,
|
||||
'NotSupportedError');
|
||||
}
|
||||
|
||||
if (signal && (steal === true || ifAvailable === true)) {
|
||||
// If options' signal dictionary member is present, and either of
|
||||
// options' steal dictionary member or options' ifAvailable dictionary
|
||||
// member is true, then return a promise rejected with a
|
||||
// "NotSupportedError" DOMException.
|
||||
throw lazyDOMException('signal cannot be used with steal or ifAvailable',
|
||||
'NotSupportedError');
|
||||
}
|
||||
|
||||
const clientId = `node-${process.pid}-${threadId}`;
|
||||
|
||||
// Handle requests with AbortSignal
|
||||
if (signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lockGranted = false;
|
||||
|
||||
const abortListener = () => {
|
||||
if (!lockGranted) {
|
||||
reject(signal.reason || lazyDOMException('The operation was aborted', 'AbortError'));
|
||||
}
|
||||
};
|
||||
|
||||
signal.addEventListener('abort', abortListener, { once: true });
|
||||
|
||||
const wrappedCallback = (lock) => {
|
||||
return PromiseResolve().then(() => {
|
||||
if (signal.aborted) {
|
||||
return undefined;
|
||||
}
|
||||
lockGranted = true;
|
||||
return callback(createLock(lock));
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const released = locks.request(
|
||||
name,
|
||||
clientId,
|
||||
mode,
|
||||
steal,
|
||||
ifAvailable,
|
||||
wrappedCallback,
|
||||
);
|
||||
|
||||
// When released promise settles, clean up listener and resolve main promise
|
||||
released
|
||||
.then(resolve, (error) => reject(convertLockError(error)))
|
||||
.finally(() => {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
});
|
||||
} catch (error) {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
reject(convertLockError(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When ifAvailable: true and lock is not available, C++ passes null to indicate no lock granted
|
||||
const wrapCallback = (internalLock) => {
|
||||
const lock = createLock(internalLock);
|
||||
return callback(lock);
|
||||
};
|
||||
|
||||
// Standard request without signal
|
||||
try {
|
||||
return await locks.request(name, clientId, mode, steal, ifAvailable, wrapCallback);
|
||||
} catch (error) {
|
||||
const convertedError = convertLockError(error);
|
||||
throw convertedError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the current state of locks for this environment.
|
||||
* @returns {Promise<{held: Array<object>, pending: Array<object>}>} Promise resolving to lock manager snapshot
|
||||
*/
|
||||
// https://w3c.github.io/web-locks/#api-lock-manager-query
|
||||
async query() {
|
||||
if (this instanceof LockManager) {
|
||||
return locks.query();
|
||||
}
|
||||
throw new ERR_INVALID_THIS('LockManager');
|
||||
}
|
||||
}
|
||||
|
||||
ObjectDefineProperties(LockManager.prototype, {
|
||||
request: { __proto__: null, enumerable: true },
|
||||
query: { __proto__: null, enumerable: true },
|
||||
[SymbolToStringTag]: {
|
||||
__proto__: null,
|
||||
value: 'LockManager',
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
ObjectDefineProperties(LockManager.prototype.request, {
|
||||
length: {
|
||||
__proto__: null,
|
||||
value: 2,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Lock,
|
||||
LockManager,
|
||||
locks: new LockManager(kConstructLockManager),
|
||||
};
|
|
@ -81,6 +81,7 @@ function getNavigatorPlatform(arch, platform) {
|
|||
class Navigator {
|
||||
// Private properties are used to avoid brand validations.
|
||||
#availableParallelism;
|
||||
#locks;
|
||||
#userAgent;
|
||||
#platform;
|
||||
#languages;
|
||||
|
@ -100,6 +101,14 @@ class Navigator {
|
|||
return this.#availableParallelism;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {LockManager}
|
||||
*/
|
||||
get locks() {
|
||||
this.#locks ??= require('internal/locks').locks;
|
||||
return this.#locks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
|
@ -140,6 +149,7 @@ ObjectDefineProperties(Navigator.prototype, {
|
|||
languages: kEnumerableProperty,
|
||||
userAgent: kEnumerableProperty,
|
||||
platform: kEnumerableProperty,
|
||||
locks: kEnumerableProperty,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -29,6 +29,8 @@ const {
|
|||
isMarkedAsUntransferable,
|
||||
} = require('internal/buffer');
|
||||
|
||||
const { locks } = require('internal/locks');
|
||||
|
||||
module.exports = {
|
||||
isInternalThread,
|
||||
isMainThread,
|
||||
|
@ -49,4 +51,5 @@ module.exports = {
|
|||
BroadcastChannel,
|
||||
setEnvironmentData,
|
||||
getEnvironmentData,
|
||||
locks,
|
||||
};
|
||||
|
|
2
node.gyp
2
node.gyp
|
@ -120,6 +120,7 @@
|
|||
'src/node_http_parser.cc',
|
||||
'src/node_http2.cc',
|
||||
'src/node_i18n.cc',
|
||||
'src/node_locks.cc',
|
||||
'src/node_main_instance.cc',
|
||||
'src/node_messaging.cc',
|
||||
'src/node_metadata.cc',
|
||||
|
@ -251,6 +252,7 @@
|
|||
'src/node_http2_state.h',
|
||||
'src/node_i18n.h',
|
||||
'src/node_internals.h',
|
||||
'src/node_locks.h',
|
||||
'src/node_main_instance.h',
|
||||
'src/node_mem.h',
|
||||
'src/node_mem-inl.h',
|
||||
|
|
|
@ -51,6 +51,7 @@ namespace node {
|
|||
V(HTTP2SETTINGS) \
|
||||
V(HTTPINCOMINGMESSAGE) \
|
||||
V(HTTPCLIENTREQUEST) \
|
||||
V(LOCKS) \
|
||||
V(JSSTREAM) \
|
||||
V(JSUDPWRAP) \
|
||||
V(MESSAGEPORT) \
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
V(changes_string, "changes") \
|
||||
V(channel_string, "channel") \
|
||||
V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \
|
||||
V(client_id_string, "clientId") \
|
||||
V(clone_unsupported_type_str, "Cannot clone object of unsupported type.") \
|
||||
V(clone_transfer_needed_str, \
|
||||
"Object that needs transfer was found in message but not listed in " \
|
||||
|
@ -193,6 +194,7 @@
|
|||
V(h2_string, "h2") \
|
||||
V(handle_string, "handle") \
|
||||
V(hash_algorithm_string, "hashAlgorithm") \
|
||||
V(held_string, "held") \
|
||||
V(help_text_string, "helpText") \
|
||||
V(homedir_string, "homedir") \
|
||||
V(host_string, "host") \
|
||||
|
@ -254,6 +256,7 @@
|
|||
V(messageerror_string, "messageerror") \
|
||||
V(mgf1_hash_algorithm_string, "mgf1HashAlgorithm") \
|
||||
V(minttl_string, "minttl") \
|
||||
V(mode_string, "mode") \
|
||||
V(module_string, "module") \
|
||||
V(modulus_string, "modulus") \
|
||||
V(modulus_length_string, "modulusLength") \
|
||||
|
@ -300,6 +303,7 @@
|
|||
V(path_string, "path") \
|
||||
V(pathname_string, "pathname") \
|
||||
V(pending_handle_string, "pendingHandle") \
|
||||
V(pending_string, "pending") \
|
||||
V(permission_string, "permission") \
|
||||
V(phase_string, "phase") \
|
||||
V(pid_string, "pid") \
|
||||
|
@ -449,6 +453,7 @@
|
|||
V(intervalhistogram_constructor_template, v8::FunctionTemplate) \
|
||||
V(js_transferable_constructor_template, v8::FunctionTemplate) \
|
||||
V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \
|
||||
V(lock_holder_constructor_template, v8::FunctionTemplate) \
|
||||
V(message_port_constructor_template, v8::FunctionTemplate) \
|
||||
V(module_wrap_constructor_template, v8::FunctionTemplate) \
|
||||
V(microtask_queue_ctor_template, v8::FunctionTemplate) \
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
V(internal_only_v8) \
|
||||
V(js_stream) \
|
||||
V(js_udp_wrap) \
|
||||
V(locks) \
|
||||
V(messaging) \
|
||||
V(modules) \
|
||||
V(module_wrap) \
|
||||
|
|
|
@ -53,6 +53,7 @@ static_assert(static_cast<int>(NM_F_LINKED) ==
|
|||
V(fs) \
|
||||
V(fs_dir) \
|
||||
V(http_parser) \
|
||||
V(locks) \
|
||||
V(messaging) \
|
||||
V(mksnapshot) \
|
||||
V(modules) \
|
||||
|
|
|
@ -79,6 +79,7 @@ class ExternalReferenceRegistry {
|
|||
V(heap_utils) \
|
||||
V(http_parser) \
|
||||
V(internal_only_v8) \
|
||||
V(locks) \
|
||||
V(messaging) \
|
||||
V(mksnapshot) \
|
||||
V(module_wrap) \
|
||||
|
|
931
src/node_locks.cc
Normal file
931
src/node_locks.cc
Normal file
|
@ -0,0 +1,931 @@
|
|||
#include "node_locks.h"
|
||||
|
||||
#include "base_object-inl.h"
|
||||
#include "env-inl.h"
|
||||
#include "node_errors.h"
|
||||
#include "node_external_reference.h"
|
||||
#include "node_internals.h"
|
||||
#include "util-inl.h"
|
||||
#include "v8.h"
|
||||
|
||||
namespace node::worker::locks {
|
||||
|
||||
using node::errors::TryCatchScope;
|
||||
using v8::Array;
|
||||
using v8::Context;
|
||||
using v8::Exception;
|
||||
using v8::Function;
|
||||
using v8::FunctionCallbackInfo;
|
||||
using v8::FunctionTemplate;
|
||||
using v8::HandleScope;
|
||||
using v8::Isolate;
|
||||
using v8::Local;
|
||||
using v8::MaybeLocal;
|
||||
using v8::NewStringType;
|
||||
using v8::Object;
|
||||
using v8::ObjectTemplate;
|
||||
using v8::Promise;
|
||||
using v8::String;
|
||||
using v8::Value;
|
||||
|
||||
static constexpr const char* kSharedMode = "shared";
|
||||
static constexpr const char* kExclusiveMode = "exclusive";
|
||||
static constexpr const char* kLockStolenError = "LOCK_STOLEN";
|
||||
|
||||
// Reject two promises and return `false` on failure.
|
||||
static bool RejectBoth(Local<Context> ctx,
|
||||
Local<Promise::Resolver> first,
|
||||
Local<Promise::Resolver> second,
|
||||
Local<Value> reason) {
|
||||
if (first->Reject(ctx, reason).IsNothing()) return false;
|
||||
if (second->Reject(ctx, reason).IsNothing()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static MaybeLocal<Object> CreateLockInfoObject(Isolate* isolate,
|
||||
Local<Context> context,
|
||||
const std::u16string& name,
|
||||
Lock::Mode mode,
|
||||
const std::string& client_id);
|
||||
|
||||
Lock::Lock(Environment* env,
|
||||
const std::u16string& name,
|
||||
Mode mode,
|
||||
const std::string& client_id,
|
||||
Local<Promise::Resolver> waiting,
|
||||
Local<Promise::Resolver> released)
|
||||
: env_(env), name_(name), mode_(mode), client_id_(client_id) {
|
||||
waiting_promise_.Reset(env_->isolate(), waiting);
|
||||
released_promise_.Reset(env_->isolate(), released);
|
||||
}
|
||||
|
||||
LockRequest::LockRequest(Environment* env,
|
||||
Local<Promise::Resolver> waiting,
|
||||
Local<Promise::Resolver> released,
|
||||
Local<Function> callback,
|
||||
const std::u16string& name,
|
||||
Lock::Mode mode,
|
||||
const std::string& client_id,
|
||||
bool steal,
|
||||
bool if_available)
|
||||
: env_(env),
|
||||
name_(name),
|
||||
mode_(mode),
|
||||
client_id_(client_id),
|
||||
steal_(steal),
|
||||
if_available_(if_available) {
|
||||
waiting_promise_.Reset(env_->isolate(), waiting);
|
||||
released_promise_.Reset(env_->isolate(), released);
|
||||
callback_.Reset(env_->isolate(), callback);
|
||||
}
|
||||
|
||||
bool LockManager::IsGrantable(const LockRequest* request) const {
|
||||
// Steal requests bypass all normal granting rules
|
||||
if (request->steal()) return true;
|
||||
|
||||
auto held_locks_iter = held_locks_.find(request->name());
|
||||
// No existing locks for this resource name
|
||||
if (held_locks_iter == held_locks_.end()) return true;
|
||||
|
||||
// Exclusive requests cannot coexist with any existing locks
|
||||
if (request->mode() == Lock::Mode::Exclusive) return false;
|
||||
|
||||
// For shared requests, check if any existing lock is exclusive
|
||||
for (const auto& existing_lock : held_locks_iter->second) {
|
||||
if (existing_lock->mode() == Lock::Mode::Exclusive) return false;
|
||||
}
|
||||
// All existing locks are shared, so this shared request can be granted
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called when the user callback promise fulfills
|
||||
static void OnLockCallbackFulfilled(const FunctionCallbackInfo<Value>& info) {
|
||||
HandleScope handle_scope(info.GetIsolate());
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
|
||||
BaseObjectPtr<LockHolder> lock_holder{
|
||||
BaseObject::FromJSObject<LockHolder>(info.Data())};
|
||||
std::shared_ptr<Lock> lock = lock_holder->lock();
|
||||
|
||||
// Release the lock and continue processing the queue.
|
||||
LockManager::GetCurrent()->ReleaseLockAndProcessQueue(
|
||||
env, lock, info[0], false);
|
||||
}
|
||||
|
||||
// Called when the user callback promise rejects
|
||||
static void OnLockCallbackRejected(const FunctionCallbackInfo<Value>& info) {
|
||||
HandleScope handle_scope(info.GetIsolate());
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
|
||||
BaseObjectPtr<LockHolder> lock_holder{
|
||||
BaseObject::FromJSObject<LockHolder>(info.Data())};
|
||||
std::shared_ptr<Lock> lock = lock_holder->lock();
|
||||
|
||||
LockManager::GetCurrent()->ReleaseLockAndProcessQueue(
|
||||
env, lock, info[0], true);
|
||||
}
|
||||
|
||||
// Called when the promise returned from the user's callback resolves
|
||||
static void OnIfAvailableFulfill(const FunctionCallbackInfo<Value>& info) {
|
||||
HandleScope handle_scope(info.GetIsolate());
|
||||
USE(info.Data().As<Promise::Resolver>()->Resolve(
|
||||
info.GetIsolate()->GetCurrentContext(), info[0]));
|
||||
}
|
||||
|
||||
// Called when the promise returned from the user's callback rejects
|
||||
static void OnIfAvailableReject(const FunctionCallbackInfo<Value>& info) {
|
||||
USE(info.Data().As<Promise::Resolver>()->Reject(
|
||||
info.GetIsolate()->GetCurrentContext(), info[0]));
|
||||
}
|
||||
|
||||
void LockManager::CleanupStolenLocks(Environment* env) {
|
||||
std::vector<std::u16string> resources_to_clean;
|
||||
|
||||
// Iterate held locks and remove entries that were stolen from other envs.
|
||||
{
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
|
||||
for (auto resource_iter = held_locks_.begin();
|
||||
resource_iter != held_locks_.end();
|
||||
++resource_iter) {
|
||||
auto& resource_locks = resource_iter->second;
|
||||
bool has_stolen_from_other_env = false;
|
||||
|
||||
// Check if this resource has stolen locks from other environments
|
||||
for (const auto& lock_ptr : resource_locks) {
|
||||
if (lock_ptr->is_stolen() && lock_ptr->env() != env) {
|
||||
has_stolen_from_other_env = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_stolen_from_other_env) {
|
||||
resources_to_clean.push_back(resource_iter->first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
for (const auto& resource_name : resources_to_clean) {
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
|
||||
auto resource_iter = held_locks_.find(resource_name);
|
||||
if (resource_iter != held_locks_.end()) {
|
||||
auto& resource_locks = resource_iter->second;
|
||||
|
||||
// Remove stolen locks from other environments
|
||||
for (auto lock_iter = resource_locks.begin();
|
||||
lock_iter != resource_locks.end();) {
|
||||
if ((*lock_iter)->is_stolen() && (*lock_iter)->env() != env) {
|
||||
lock_iter = resource_locks.erase(lock_iter);
|
||||
} else {
|
||||
++lock_iter;
|
||||
}
|
||||
}
|
||||
|
||||
if (resource_locks.empty()) {
|
||||
held_locks_.erase(resource_iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Locks algorithm implementation
|
||||
* https://w3c.github.io/web-locks/#algorithms
|
||||
*/
|
||||
void LockManager::ProcessQueue(Environment* env) {
|
||||
Isolate* isolate = env->isolate();
|
||||
HandleScope handle_scope(isolate);
|
||||
Local<Context> context = env->context();
|
||||
|
||||
// Remove locks that were stolen from this Environment first
|
||||
CleanupStolenLocks(env);
|
||||
|
||||
while (true) {
|
||||
std::unique_ptr<LockRequest> grantable_request;
|
||||
std::unique_ptr<LockRequest> if_available_request;
|
||||
std::unordered_set<Environment*> other_envs_to_wake;
|
||||
|
||||
/**
|
||||
* First pass over pending_queue_
|
||||
* 1- Build first_seen_for_resource: the oldest pending request
|
||||
* for every resource name we encounter
|
||||
* 2- Decide what to do with each entry:
|
||||
* – If it belongs to another Environment, remember that env so we
|
||||
* can wake it later
|
||||
* – For our Environment, pick one of:
|
||||
* * grantable_request – can be granted now
|
||||
* * if_available_request – user asked for ifAvailable and the
|
||||
* resource is currently busy
|
||||
* * otherwise we skip and keep scanning
|
||||
*/
|
||||
|
||||
{
|
||||
std::unordered_map<std::u16string, LockRequest*> first_seen_for_resource;
|
||||
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
for (auto queue_iter = pending_queue_.begin();
|
||||
queue_iter != pending_queue_.end();
|
||||
++queue_iter) {
|
||||
LockRequest* request = queue_iter->get();
|
||||
|
||||
// Collect unique environments to wake up later
|
||||
if (request->env() != env) {
|
||||
other_envs_to_wake.insert(request->env());
|
||||
}
|
||||
|
||||
// During a single pass, the first time we see a resource name is the
|
||||
// earliest pending request
|
||||
auto& first_for_resource = first_seen_for_resource[request->name()];
|
||||
if (first_for_resource == nullptr) {
|
||||
first_for_resource = request; // Mark as first seen for this resource
|
||||
}
|
||||
|
||||
bool has_earlier_request_for_same_resource =
|
||||
(first_for_resource != request);
|
||||
|
||||
bool should_wait_for_earlier_requests = false;
|
||||
|
||||
if (has_earlier_request_for_same_resource) {
|
||||
// Check if this request is compatible with the earliest pending
|
||||
// request first_for_resource
|
||||
if (request->mode() == Lock::Mode::Exclusive ||
|
||||
first_for_resource->mode() == Lock::Mode::Exclusive) {
|
||||
// Exclusive locks are incompatible with everything
|
||||
should_wait_for_earlier_requests = true;
|
||||
}
|
||||
// If both are shared, they're compatible and can proceed
|
||||
}
|
||||
|
||||
// Only process requests from the current environment
|
||||
if (request->env() != env) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (should_wait_for_earlier_requests || !IsGrantable(request)) {
|
||||
if (request->if_available()) {
|
||||
// ifAvailable request when resource not available: grant with null
|
||||
if_available_request = std::move(*queue_iter);
|
||||
pending_queue_.erase(queue_iter);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a request that can be granted normally
|
||||
grantable_request = std::move(*queue_iter);
|
||||
pending_queue_.erase(queue_iter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Wake each environment only once
|
||||
for (Environment* target_env : other_envs_to_wake) {
|
||||
WakeEnvironment(target_env);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1- We call the user callback immediately with `null` to signal
|
||||
* that the lock was not granted - Check wrapCallback function in
|
||||
* locks.js 2- Depending on what the callback returns we settle the two
|
||||
* internal promises
|
||||
* 3- No lock is added to held_locks_ in this path, so nothing to
|
||||
* remove later
|
||||
*/
|
||||
if (if_available_request) {
|
||||
Local<Value> null_arg = Null(isolate);
|
||||
Local<Value> callback_result;
|
||||
{
|
||||
TryCatchScope try_catch_scope(env);
|
||||
if (!if_available_request->callback()
|
||||
->Call(context, Undefined(isolate), 1, &null_arg)
|
||||
.ToLocal(&callback_result)) {
|
||||
if (!RejectBoth(context,
|
||||
if_available_request->waiting_promise(),
|
||||
if_available_request->released_promise(),
|
||||
try_catch_scope.Exception()))
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (callback_result->IsPromise()) {
|
||||
Local<Promise> p = callback_result.As<Promise>();
|
||||
|
||||
Local<Function> on_fulfilled;
|
||||
Local<Function> on_rejected;
|
||||
CHECK(Function::New(context,
|
||||
OnIfAvailableFulfill,
|
||||
if_available_request->released_promise())
|
||||
.ToLocal(&on_fulfilled));
|
||||
CHECK(Function::New(context,
|
||||
OnIfAvailableReject,
|
||||
if_available_request->released_promise())
|
||||
.ToLocal(&on_rejected));
|
||||
|
||||
{
|
||||
TryCatchScope try_catch_scope(env);
|
||||
if (p->Then(context, on_fulfilled, on_rejected).IsEmpty()) {
|
||||
if (!try_catch_scope.CanContinue()) return;
|
||||
|
||||
Local<Value> err_val;
|
||||
if (try_catch_scope.HasCaught() &&
|
||||
!try_catch_scope.Exception().IsEmpty()) {
|
||||
err_val = try_catch_scope.Exception();
|
||||
} else {
|
||||
err_val = Exception::Error(FIXED_ONE_BYTE_STRING(
|
||||
isolate, "Failed to attach promise handlers"));
|
||||
}
|
||||
|
||||
RejectBoth(context,
|
||||
if_available_request->waiting_promise(),
|
||||
if_available_request->released_promise(),
|
||||
err_val);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// After handlers are attached, resolve waiting_promise with the
|
||||
// promise.
|
||||
if (if_available_request->waiting_promise()
|
||||
->Resolve(context, p)
|
||||
.IsNothing())
|
||||
return;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-promise callback result: settle both promises right away.
|
||||
if (if_available_request->waiting_promise()
|
||||
->Resolve(context, callback_result)
|
||||
.IsNothing())
|
||||
return;
|
||||
if (if_available_request->released_promise()
|
||||
->Resolve(context, callback_result)
|
||||
.IsNothing())
|
||||
return;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!grantable_request) return;
|
||||
|
||||
/**
|
||||
* 1- We grant the lock immediately even if other envs hold it
|
||||
* 2- All existing locks with the same name are marked stolen, their
|
||||
* released_promise is rejected, and their owners are woken so they
|
||||
* can observe the rejection
|
||||
* 3- We remove stolen locks that belong to this env right away; other
|
||||
* envs will clean up in their next queue pass
|
||||
*/
|
||||
if (grantable_request->steal()) {
|
||||
std::unordered_set<Environment*> envs_to_notify;
|
||||
|
||||
{
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
auto held_locks_iter = held_locks_.find(grantable_request->name());
|
||||
if (held_locks_iter != held_locks_.end()) {
|
||||
// Mark existing locks as stolen and collect environments to notify
|
||||
for (auto& existing_lock : held_locks_iter->second) {
|
||||
existing_lock->mark_stolen();
|
||||
envs_to_notify.insert(existing_lock->env());
|
||||
|
||||
// Immediately reject the stolen lock's released_promise
|
||||
Local<String> error_string;
|
||||
if (!String::NewFromUtf8(isolate, kLockStolenError)
|
||||
.ToLocal(&error_string)) {
|
||||
return;
|
||||
}
|
||||
Local<Value> error = Exception::Error(error_string);
|
||||
|
||||
if (existing_lock->released_promise()
|
||||
->Reject(context, error)
|
||||
.IsNothing())
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove stolen locks from current environment immediately
|
||||
for (auto lock_iter = held_locks_iter->second.begin();
|
||||
lock_iter != held_locks_iter->second.end();) {
|
||||
if ((*lock_iter)->env() == env) {
|
||||
lock_iter = held_locks_iter->second.erase(lock_iter);
|
||||
} else {
|
||||
++lock_iter;
|
||||
}
|
||||
}
|
||||
|
||||
if (held_locks_iter->second.empty()) {
|
||||
held_locks_.erase(held_locks_iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wake other environments
|
||||
for (Environment* target_env : envs_to_notify) {
|
||||
if (target_env != env) {
|
||||
WakeEnvironment(target_env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and store the new granted lock
|
||||
auto granted_lock =
|
||||
std::make_shared<Lock>(env,
|
||||
grantable_request->name(),
|
||||
grantable_request->mode(),
|
||||
grantable_request->client_id(),
|
||||
grantable_request->waiting_promise(),
|
||||
grantable_request->released_promise());
|
||||
{
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
held_locks_[grantable_request->name()].push_back(granted_lock);
|
||||
}
|
||||
|
||||
// Create and store the new granted lock
|
||||
Local<Object> lock_info;
|
||||
if (!CreateLockInfoObject(isolate,
|
||||
context,
|
||||
grantable_request->name(),
|
||||
grantable_request->mode(),
|
||||
grantable_request->client_id())
|
||||
.ToLocal(&lock_info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call user callback
|
||||
Local<Value> callback_arg = lock_info;
|
||||
Local<Value> callback_result;
|
||||
{
|
||||
TryCatchScope try_catch_scope(env);
|
||||
if (!grantable_request->callback()
|
||||
->Call(context, Undefined(isolate), 1, &callback_arg)
|
||||
.ToLocal(&callback_result)) {
|
||||
if (!RejectBoth(context,
|
||||
grantable_request->waiting_promise(),
|
||||
grantable_request->released_promise(),
|
||||
try_catch_scope.Exception()))
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create LockHolder BaseObjects to safely manage the lock's lifetime
|
||||
// until the user's callback promise settles.
|
||||
auto lock_resolve_holder = LockHolder::Create(env, granted_lock);
|
||||
auto lock_reject_holder = LockHolder::Create(env, granted_lock);
|
||||
Local<Function> on_fulfilled_callback;
|
||||
Local<Function> on_rejected_callback;
|
||||
|
||||
// Create fulfilled callback first
|
||||
if (!Function::New(
|
||||
context, OnLockCallbackFulfilled, lock_resolve_holder->object())
|
||||
.ToLocal(&on_fulfilled_callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create rejected callback second
|
||||
if (!Function::New(
|
||||
context, OnLockCallbackRejected, lock_reject_holder->object())
|
||||
.ToLocal(&on_rejected_callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle promise chain
|
||||
if (callback_result->IsPromise()) {
|
||||
Local<Promise> promise = callback_result.As<Promise>();
|
||||
{
|
||||
TryCatchScope try_catch_scope(env);
|
||||
if (promise->Then(context, on_fulfilled_callback, on_rejected_callback)
|
||||
.IsEmpty()) {
|
||||
if (!try_catch_scope.CanContinue()) return;
|
||||
|
||||
Local<Value> err_val;
|
||||
if (try_catch_scope.HasCaught() &&
|
||||
!try_catch_scope.Exception().IsEmpty()) {
|
||||
err_val = try_catch_scope.Exception();
|
||||
} else {
|
||||
err_val = Exception::Error(FIXED_ONE_BYTE_STRING(
|
||||
isolate, "Failed to attach promise handlers"));
|
||||
}
|
||||
|
||||
RejectBoth(context,
|
||||
grantable_request->waiting_promise(),
|
||||
grantable_request->released_promise(),
|
||||
err_val);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Lock granted: waiting_promise resolves now with the promise returned
|
||||
// by the callback; on_fulfilled/on_rejected will release the lock when
|
||||
// that promise settles.
|
||||
if (grantable_request->waiting_promise()
|
||||
->Resolve(context, callback_result)
|
||||
.IsNothing()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (grantable_request->waiting_promise()
|
||||
->Resolve(context, callback_result)
|
||||
.IsNothing()) {
|
||||
return;
|
||||
}
|
||||
Local<Value> promise_args[] = {callback_result};
|
||||
if (on_fulfilled_callback
|
||||
->Call(context, Undefined(isolate), 1, promise_args)
|
||||
.IsEmpty()) {
|
||||
// Callback threw an error, handle it like a rejected promise
|
||||
// The error is already propagated through the TryCatch in the
|
||||
// callback
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* name : string – resource identifier
|
||||
* clientId : string – client identifier
|
||||
* mode : string – lock mode
|
||||
* steal : boolean – whether to steal existing locks
|
||||
* ifAvailable : boolean – only grant if immediately available
|
||||
* callback : Function - JS callback
|
||||
*/
|
||||
void LockManager::Request(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Isolate* isolate = env->isolate();
|
||||
HandleScope scope(isolate);
|
||||
Local<Context> context = env->context();
|
||||
|
||||
CHECK_EQ(args.Length(), 6);
|
||||
CHECK(args[0]->IsString()); // name
|
||||
CHECK(args[1]->IsString()); // clientId
|
||||
CHECK(args[2]->IsString()); // mode
|
||||
CHECK(args[3]->IsBoolean()); // steal
|
||||
CHECK(args[4]->IsBoolean()); // ifAvailable
|
||||
CHECK(args[5]->IsFunction()); // callback
|
||||
|
||||
Local<String> resource_name_str = args[0].As<String>();
|
||||
TwoByteValue resource_name_utf16(isolate, resource_name_str);
|
||||
std::u16string resource_name(
|
||||
reinterpret_cast<const char16_t*>(*resource_name_utf16),
|
||||
resource_name_utf16.length());
|
||||
String::Utf8Value client_id_utf8(isolate, args[1]);
|
||||
std::string client_id(*client_id_utf8);
|
||||
String::Utf8Value mode_utf8(isolate, args[2]);
|
||||
std::string mode_str(*mode_utf8);
|
||||
bool steal = args[3]->BooleanValue(isolate);
|
||||
bool if_available = args[4]->BooleanValue(isolate);
|
||||
Local<Function> callback = args[5].As<Function>();
|
||||
|
||||
Lock::Mode lock_mode =
|
||||
mode_str == kSharedMode ? Lock::Mode::Shared : Lock::Mode::Exclusive;
|
||||
|
||||
Local<Promise::Resolver> waiting_promise;
|
||||
Local<Promise::Resolver> released_promise;
|
||||
|
||||
if (!Promise::Resolver::New(context).ToLocal(&waiting_promise) ||
|
||||
!Promise::Resolver::New(context).ToLocal(&released_promise)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark both internal promises as handled to prevent unhandled rejection
|
||||
// warnings
|
||||
waiting_promise->GetPromise()->MarkAsHandled();
|
||||
released_promise->GetPromise()->MarkAsHandled();
|
||||
|
||||
LockManager* manager = GetCurrent();
|
||||
{
|
||||
Mutex::ScopedLock scoped_lock(manager->mutex_);
|
||||
|
||||
// Register cleanup hook for the environment only once
|
||||
if (manager->registered_envs_.insert(env).second) {
|
||||
env->AddCleanupHook(LockManager::OnEnvironmentCleanup, env);
|
||||
}
|
||||
|
||||
auto lock_request = std::make_unique<LockRequest>(env,
|
||||
waiting_promise,
|
||||
released_promise,
|
||||
callback,
|
||||
resource_name,
|
||||
lock_mode,
|
||||
client_id,
|
||||
steal,
|
||||
if_available);
|
||||
// Steal requests get priority by going to front of queue
|
||||
if (steal) {
|
||||
manager->pending_queue_.emplace_front(std::move(lock_request));
|
||||
} else {
|
||||
manager->pending_queue_.push_back(std::move(lock_request));
|
||||
}
|
||||
}
|
||||
|
||||
manager->ProcessQueue(env);
|
||||
|
||||
args.GetReturnValue().Set(released_promise->GetPromise());
|
||||
}
|
||||
|
||||
void LockManager::Query(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Isolate* isolate = env->isolate();
|
||||
HandleScope scope(isolate);
|
||||
Local<Context> context = env->context();
|
||||
|
||||
Local<Promise::Resolver> resolver;
|
||||
if (!Promise::Resolver::New(context).ToLocal(&resolver)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always set the return value first so Javascript gets a promise
|
||||
args.GetReturnValue().Set(resolver->GetPromise());
|
||||
|
||||
Local<Object> result = Object::New(isolate);
|
||||
Local<Array> held_list = Array::New(isolate);
|
||||
Local<Array> pending_list = Array::New(isolate);
|
||||
LockManager* manager = GetCurrent();
|
||||
|
||||
{
|
||||
Mutex::ScopedLock scoped_lock(manager->mutex_);
|
||||
|
||||
uint32_t index = 0;
|
||||
Local<Object> lock_info;
|
||||
for (const auto& resource_entry : manager->held_locks_) {
|
||||
for (const auto& held_lock : resource_entry.second) {
|
||||
if (held_lock->env() == env) {
|
||||
if (!CreateLockInfoObject(isolate,
|
||||
context,
|
||||
held_lock->name(),
|
||||
held_lock->mode(),
|
||||
held_lock->client_id())
|
||||
.ToLocal(&lock_info)) {
|
||||
THROW_ERR_OPERATION_FAILED(env,
|
||||
"Failed to create lock info object");
|
||||
return;
|
||||
}
|
||||
if (held_list->Set(context, index++, lock_info).IsNothing()) {
|
||||
THROW_ERR_OPERATION_FAILED(env, "Failed to build held locks array");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index = 0;
|
||||
for (const auto& pending_request : manager->pending_queue_) {
|
||||
if (pending_request->env() == env) {
|
||||
if (!CreateLockInfoObject(isolate,
|
||||
context,
|
||||
pending_request->name(),
|
||||
pending_request->mode(),
|
||||
pending_request->client_id())
|
||||
.ToLocal(&lock_info)) {
|
||||
THROW_ERR_OPERATION_FAILED(env, "Failed to create lock info object");
|
||||
return;
|
||||
}
|
||||
if (pending_list->Set(context, index++, lock_info).IsNothing()) {
|
||||
THROW_ERR_OPERATION_FAILED(env,
|
||||
"Failed to build pending locks array");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result->Set(context, env->held_string(), held_list).IsNothing() ||
|
||||
result->Set(context, env->pending_string(), pending_list).IsNothing()) {
|
||||
THROW_ERR_OPERATION_FAILED(env, "Failed to build query result object");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolver->Resolve(context, result).IsNothing()) return;
|
||||
}
|
||||
|
||||
// Runs after the user callback (or its returned promise) settles.
|
||||
void LockManager::ReleaseLockAndProcessQueue(Environment* env,
|
||||
std::shared_ptr<Lock> lock,
|
||||
Local<Value> callback_result,
|
||||
bool was_rejected) {
|
||||
{
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
ReleaseLock(lock.get());
|
||||
}
|
||||
|
||||
Local<Context> context = env->context();
|
||||
|
||||
// For stolen locks, the released_promise was already rejected when marked as
|
||||
// stolen.
|
||||
if (!lock->is_stolen()) {
|
||||
if (was_rejected) {
|
||||
// Propagate rejection from the user callback
|
||||
if (lock->released_promise()
|
||||
->Reject(context, callback_result)
|
||||
.IsNothing())
|
||||
return;
|
||||
} else {
|
||||
// Propagate fulfilment
|
||||
if (lock->released_promise()
|
||||
->Resolve(context, callback_result)
|
||||
.IsNothing())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ProcessQueue(env);
|
||||
}
|
||||
|
||||
// Remove a lock from held_locks_ when it's no longer needed
|
||||
void LockManager::ReleaseLock(Lock* lock) {
|
||||
const std::u16string& resource_name = lock->name();
|
||||
auto resource_iter = held_locks_.find(resource_name);
|
||||
if (resource_iter == held_locks_.end()) return;
|
||||
|
||||
auto& resource_locks = resource_iter->second;
|
||||
for (auto lock_iter = resource_locks.begin();
|
||||
lock_iter != resource_locks.end();
|
||||
++lock_iter) {
|
||||
if (lock_iter->get() == lock) {
|
||||
resource_locks.erase(lock_iter);
|
||||
if (resource_locks.empty()) held_locks_.erase(resource_iter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wakeup of target Environment's event loop
|
||||
void LockManager::WakeEnvironment(Environment* target_env) {
|
||||
if (target_env == nullptr || target_env->is_stopping()) return;
|
||||
|
||||
// Schedule ProcessQueue in the target Environment on its event loop.
|
||||
target_env->SetImmediateThreadsafe([](Environment* env_to_wake) {
|
||||
if (env_to_wake != nullptr && !env_to_wake->is_stopping()) {
|
||||
LockManager::GetCurrent()->ProcessQueue(env_to_wake);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove all held locks and pending requests that belong to an Environment
|
||||
// that is being destroyed
|
||||
void LockManager::CleanupEnvironment(Environment* env_to_cleanup) {
|
||||
Mutex::ScopedLock scoped_lock(mutex_);
|
||||
|
||||
// Remove every held lock that belongs to this Environment.
|
||||
for (auto resource_iter = held_locks_.begin();
|
||||
resource_iter != held_locks_.end();) {
|
||||
auto& resource_locks = resource_iter->second;
|
||||
for (auto lock_iter = resource_locks.begin();
|
||||
lock_iter != resource_locks.end();) {
|
||||
if ((*lock_iter)->env() == env_to_cleanup) {
|
||||
lock_iter = resource_locks.erase(lock_iter);
|
||||
} else {
|
||||
++lock_iter;
|
||||
}
|
||||
}
|
||||
if (resource_locks.empty()) {
|
||||
resource_iter = held_locks_.erase(resource_iter);
|
||||
} else {
|
||||
++resource_iter;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove every pending request submitted by this Environment.
|
||||
for (auto request_iter = pending_queue_.begin();
|
||||
request_iter != pending_queue_.end();) {
|
||||
if ((*request_iter)->env() == env_to_cleanup) {
|
||||
request_iter = pending_queue_.erase(request_iter);
|
||||
} else {
|
||||
++request_iter;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, remove it from registered_envs_
|
||||
registered_envs_.erase(env_to_cleanup);
|
||||
}
|
||||
|
||||
// Cleanup hook wrapper
|
||||
void LockManager::OnEnvironmentCleanup(void* arg) {
|
||||
Environment* env = static_cast<Environment*>(arg);
|
||||
LockManager::GetCurrent()->CleanupEnvironment(env);
|
||||
}
|
||||
|
||||
static MaybeLocal<Object> CreateLockInfoObject(Isolate* isolate,
|
||||
Local<Context> context,
|
||||
const std::u16string& name,
|
||||
Lock::Mode mode,
|
||||
const std::string& client_id) {
|
||||
Local<Object> obj = Object::New(isolate);
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
|
||||
// TODO(ilyasshabi): Add ToV8Value that directly accepts std::u16string
|
||||
// so we can avoid the manual String::NewFromTwoByte()
|
||||
Local<String> name_string;
|
||||
if (!String::NewFromTwoByte(isolate,
|
||||
reinterpret_cast<const uint16_t*>(name.data()),
|
||||
NewStringType::kNormal,
|
||||
static_cast<int>(name.length()))
|
||||
.ToLocal(&name_string)) {
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
if (obj->Set(context, env->name_string(), name_string).IsNothing()) {
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
|
||||
Local<String> mode_string;
|
||||
if (!String::NewFromUtf8(
|
||||
isolate,
|
||||
mode == Lock::Mode::Exclusive ? kExclusiveMode : kSharedMode)
|
||||
.ToLocal(&mode_string)) {
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
if (obj->Set(context, env->mode_string(), mode_string).IsNothing()) {
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
|
||||
Local<String> client_id_string;
|
||||
if (!String::NewFromUtf8(isolate, client_id.c_str())
|
||||
.ToLocal(&client_id_string)) {
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
if (obj->Set(context, env->client_id_string(), client_id_string)
|
||||
.IsNothing()) {
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
LockManager LockManager::current_;
|
||||
|
||||
void CreatePerIsolateProperties(IsolateData* isolate_data,
|
||||
Local<ObjectTemplate> target) {
|
||||
Isolate* isolate = isolate_data->isolate();
|
||||
SetMethod(isolate, target, "request", LockManager::Request);
|
||||
SetMethod(isolate, target, "query", LockManager::Query);
|
||||
|
||||
Local<String> shared_mode;
|
||||
if (String::NewFromUtf8(isolate, kSharedMode).ToLocal(&shared_mode)) {
|
||||
target->Set(FIXED_ONE_BYTE_STRING(isolate, "LOCK_MODE_SHARED"),
|
||||
shared_mode);
|
||||
}
|
||||
Local<String> exclusive_mode;
|
||||
if (String::NewFromUtf8(isolate, kExclusiveMode).ToLocal(&exclusive_mode)) {
|
||||
target->Set(FIXED_ONE_BYTE_STRING(isolate, "LOCK_MODE_EXCLUSIVE"),
|
||||
exclusive_mode);
|
||||
}
|
||||
Local<String> stolen_error;
|
||||
if (String::NewFromUtf8(isolate, kLockStolenError).ToLocal(&stolen_error)) {
|
||||
target->Set(FIXED_ONE_BYTE_STRING(isolate, "LOCK_STOLEN_ERROR"),
|
||||
stolen_error);
|
||||
}
|
||||
}
|
||||
|
||||
void CreatePerContextProperties(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
void* priv) {}
|
||||
|
||||
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(LockManager::Request);
|
||||
registry->Register(LockManager::Query);
|
||||
registry->Register(OnLockCallbackFulfilled);
|
||||
registry->Register(OnLockCallbackRejected);
|
||||
registry->Register(OnIfAvailableFulfill);
|
||||
registry->Register(OnIfAvailableReject);
|
||||
}
|
||||
|
||||
BaseObjectPtr<LockHolder> LockHolder::Create(Environment* env,
|
||||
std::shared_ptr<Lock> lock) {
|
||||
Local<Object> obj;
|
||||
if (!GetConstructorTemplate(env)
|
||||
->InstanceTemplate()
|
||||
->NewInstance(env->context())
|
||||
.ToLocal(&obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return MakeBaseObject<LockHolder>(env, obj, std::move(lock));
|
||||
}
|
||||
|
||||
Local<FunctionTemplate> LockHolder::GetConstructorTemplate(Environment* env) {
|
||||
IsolateData* isolate_data = env->isolate_data();
|
||||
Local<FunctionTemplate> tmpl =
|
||||
isolate_data->lock_holder_constructor_template();
|
||||
if (tmpl.IsEmpty()) {
|
||||
Isolate* isolate = isolate_data->isolate();
|
||||
tmpl = NewFunctionTemplate(isolate, nullptr);
|
||||
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "LockHolder"));
|
||||
tmpl->InstanceTemplate()->SetInternalFieldCount(
|
||||
LockHolder::kInternalFieldCount);
|
||||
isolate_data->set_lock_holder_constructor_template(tmpl);
|
||||
}
|
||||
return tmpl;
|
||||
}
|
||||
|
||||
} // namespace node::worker::locks
|
||||
|
||||
NODE_BINDING_CONTEXT_AWARE_INTERNAL(
|
||||
locks, node::worker::locks::CreatePerContextProperties)
|
||||
NODE_BINDING_PER_ISOLATE_INIT(locks,
|
||||
node::worker::locks::CreatePerIsolateProperties)
|
||||
NODE_BINDING_EXTERNAL_REFERENCE(locks,
|
||||
node::worker::locks::RegisterExternalReferences)
|
180
src/node_locks.h
Normal file
180
src/node_locks.h
Normal file
|
@ -0,0 +1,180 @@
|
|||
#ifndef SRC_NODE_LOCKS_H_
|
||||
#define SRC_NODE_LOCKS_H_
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "base_object.h"
|
||||
#include "env.h"
|
||||
#include "node_mutex.h"
|
||||
#include "v8.h"
|
||||
|
||||
namespace node::worker::locks {
|
||||
|
||||
class Lock final {
|
||||
public:
|
||||
enum class Mode { Shared, Exclusive };
|
||||
|
||||
Lock(Environment* env,
|
||||
const std::u16string& name,
|
||||
Mode mode,
|
||||
const std::string& client_id,
|
||||
v8::Local<v8::Promise::Resolver> waiting,
|
||||
v8::Local<v8::Promise::Resolver> released);
|
||||
~Lock() = default;
|
||||
|
||||
Lock(const Lock&) = delete;
|
||||
Lock& operator=(const Lock&) = delete;
|
||||
|
||||
// Resource name for this lock as DOMString
|
||||
const std::u16string& name() const { return name_; }
|
||||
// Lock mode (shared or exclusive).
|
||||
Mode mode() const { return mode_; }
|
||||
// Client identifier string.
|
||||
const std::string& client_id() const { return client_id_; }
|
||||
// Environment that owns this lock.
|
||||
Environment* env() const { return env_; }
|
||||
|
||||
// Returns true if this lock was stolen by another request.
|
||||
bool is_stolen() const { return stolen_; }
|
||||
// Marks this lock as stolen.
|
||||
void mark_stolen() { stolen_ = true; }
|
||||
|
||||
// Promise that resolves when the user callback completes.
|
||||
v8::Local<v8::Promise::Resolver> waiting_promise() {
|
||||
return waiting_promise_.Get(env_->isolate());
|
||||
}
|
||||
// Promise that resolves when the lock is finally released.
|
||||
v8::Local<v8::Promise::Resolver> released_promise() {
|
||||
return released_promise_.Get(env_->isolate());
|
||||
}
|
||||
|
||||
private:
|
||||
Environment* env_;
|
||||
std::u16string name_;
|
||||
Mode mode_;
|
||||
std::string client_id_;
|
||||
bool stolen_ = false;
|
||||
v8::Global<v8::Promise::Resolver> waiting_promise_;
|
||||
v8::Global<v8::Promise::Resolver> released_promise_;
|
||||
};
|
||||
|
||||
class LockHolder final : public BaseObject {
|
||||
public:
|
||||
LockHolder(Environment* env,
|
||||
v8::Local<v8::Object> obj,
|
||||
std::shared_ptr<Lock> lock)
|
||||
: BaseObject(env, obj), lock_(std::move(lock)) {
|
||||
MakeWeak();
|
||||
}
|
||||
|
||||
~LockHolder() = default;
|
||||
|
||||
LockHolder(const LockHolder&) = delete;
|
||||
LockHolder& operator=(const LockHolder&) = delete;
|
||||
|
||||
std::shared_ptr<Lock> lock() const { return lock_; }
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(LockHolder)
|
||||
SET_SELF_SIZE(LockHolder)
|
||||
|
||||
static BaseObjectPtr<LockHolder> Create(Environment* env,
|
||||
std::shared_ptr<Lock> lock);
|
||||
|
||||
private:
|
||||
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
|
||||
Environment* env);
|
||||
|
||||
std::shared_ptr<Lock> lock_;
|
||||
};
|
||||
|
||||
class LockRequest final {
|
||||
public:
|
||||
LockRequest(Environment* env,
|
||||
v8::Local<v8::Promise::Resolver> waiting,
|
||||
v8::Local<v8::Promise::Resolver> released,
|
||||
v8::Local<v8::Function> callback,
|
||||
const std::u16string& name,
|
||||
Lock::Mode mode,
|
||||
const std::string& client_id,
|
||||
bool steal,
|
||||
bool if_available);
|
||||
~LockRequest() = default;
|
||||
|
||||
LockRequest(const LockRequest&) = delete;
|
||||
LockRequest& operator=(const LockRequest&) = delete;
|
||||
|
||||
const std::u16string& name() const { return name_; }
|
||||
Lock::Mode mode() const { return mode_; }
|
||||
const std::string& client_id() const { return client_id_; }
|
||||
bool steal() const { return steal_; }
|
||||
// Returns true if this is an ifAvailable request.
|
||||
bool if_available() const { return if_available_; }
|
||||
Environment* env() const { return env_; }
|
||||
|
||||
v8::Local<v8::Promise::Resolver> waiting_promise() {
|
||||
return waiting_promise_.Get(env_->isolate());
|
||||
}
|
||||
v8::Local<v8::Promise::Resolver> released_promise() {
|
||||
return released_promise_.Get(env_->isolate());
|
||||
}
|
||||
v8::Local<v8::Function> callback() { return callback_.Get(env_->isolate()); }
|
||||
|
||||
private:
|
||||
Environment* env_;
|
||||
std::u16string name_;
|
||||
Lock::Mode mode_;
|
||||
std::string client_id_;
|
||||
bool steal_;
|
||||
bool if_available_;
|
||||
v8::Global<v8::Promise::Resolver> waiting_promise_;
|
||||
v8::Global<v8::Promise::Resolver> released_promise_;
|
||||
v8::Global<v8::Function> callback_;
|
||||
};
|
||||
|
||||
class LockManager final {
|
||||
public:
|
||||
static void Request(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void Query(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
void ProcessQueue(Environment* env);
|
||||
void CleanupEnvironment(Environment* env);
|
||||
|
||||
static void OnEnvironmentCleanup(void* arg);
|
||||
static LockManager* GetCurrent() { return ¤t_; }
|
||||
void ReleaseLockAndProcessQueue(Environment* env,
|
||||
std::shared_ptr<Lock> lock,
|
||||
v8::Local<v8::Value> result,
|
||||
bool was_rejected = false);
|
||||
|
||||
private:
|
||||
LockManager() = default;
|
||||
~LockManager() = default;
|
||||
|
||||
LockManager(const LockManager&) = delete;
|
||||
LockManager& operator=(const LockManager&) = delete;
|
||||
|
||||
bool IsGrantable(const LockRequest* req) const;
|
||||
void CleanupStolenLocks(Environment* env);
|
||||
void ReleaseLock(Lock* lock);
|
||||
void WakeEnvironment(Environment* env);
|
||||
|
||||
static LockManager current_;
|
||||
|
||||
mutable Mutex mutex_;
|
||||
// All entries for a given Environment* are purged in CleanupEnvironment().
|
||||
std::unordered_map<std::u16string, std::deque<std::shared_ptr<Lock>>>
|
||||
held_locks_;
|
||||
std::deque<std::unique_ptr<LockRequest>> pending_queue_;
|
||||
std::unordered_set<Environment*> registered_envs_;
|
||||
};
|
||||
|
||||
} // namespace node::worker::locks
|
||||
|
||||
#endif // NODE_WANT_INTERNALS
|
||||
#endif // SRC_NODE_LOCKS_H_
|
1
test/fixtures/wpt/README.md
vendored
1
test/fixtures/wpt/README.md
vendored
|
@ -33,6 +33,7 @@ Last update:
|
|||
- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
|
||||
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi
|
||||
- wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi
|
||||
- web-locks: https://github.com/web-platform-tests/wpt/tree/10a122a6bc/web-locks
|
||||
- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/591c95ce61/WebCryptoAPI
|
||||
- webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions
|
||||
- webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel
|
||||
|
|
45
test/fixtures/wpt/interfaces/web-locks.idl
vendored
Normal file
45
test/fixtures/wpt/interfaces/web-locks.idl
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
[SecureContext]
|
||||
interface mixin NavigatorLocks {
|
||||
readonly attribute LockManager locks;
|
||||
};
|
||||
Navigator includes NavigatorLocks;
|
||||
WorkerNavigator includes NavigatorLocks;
|
||||
|
||||
[SecureContext, Exposed=(Window,Worker)]
|
||||
interface LockManager {
|
||||
Promise<any> request(DOMString name,
|
||||
LockGrantedCallback callback);
|
||||
Promise<any> request(DOMString name,
|
||||
LockOptions options,
|
||||
LockGrantedCallback callback);
|
||||
|
||||
Promise<LockManagerSnapshot> query();
|
||||
};
|
||||
|
||||
callback LockGrantedCallback = Promise<any> (Lock? lock);
|
||||
|
||||
enum LockMode { "shared", "exclusive" };
|
||||
|
||||
dictionary LockOptions {
|
||||
LockMode mode = "exclusive";
|
||||
boolean ifAvailable = false;
|
||||
boolean steal = false;
|
||||
AbortSignal signal;
|
||||
};
|
||||
|
||||
dictionary LockManagerSnapshot {
|
||||
sequence<LockInfo> held;
|
||||
sequence<LockInfo> pending;
|
||||
};
|
||||
|
||||
dictionary LockInfo {
|
||||
DOMString name;
|
||||
LockMode mode;
|
||||
DOMString clientId;
|
||||
};
|
||||
|
||||
[SecureContext, Exposed=(Window,Worker)]
|
||||
interface Lock {
|
||||
readonly attribute DOMString name;
|
||||
readonly attribute LockMode mode;
|
||||
};
|
4
test/fixtures/wpt/versions.json
vendored
4
test/fixtures/wpt/versions.json
vendored
|
@ -91,6 +91,10 @@
|
|||
"commit": "fd1b23eeaaf9a01555d4fa29cf79ed11a4c44a50",
|
||||
"path": "wasm/webapi"
|
||||
},
|
||||
"web-locks": {
|
||||
"commit": "10a122a6bc3670a44e0fa8b9a99f6aa9012d30f1",
|
||||
"path": "web-locks"
|
||||
},
|
||||
"WebCryptoAPI": {
|
||||
"commit": "591c95ce6174690b92833cd92859ce2807714591",
|
||||
"path": "WebCryptoAPI"
|
||||
|
|
5
test/fixtures/wpt/web-locks/META.yml
vendored
Normal file
5
test/fixtures/wpt/web-locks/META.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
spec: https://w3c.github.io/web-locks/
|
||||
suggested_reviewers:
|
||||
- inexorabletash
|
||||
- pwnall
|
||||
- saschanaz
|
5
test/fixtures/wpt/web-locks/README.md
vendored
Normal file
5
test/fixtures/wpt/web-locks/README.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
This directory contains a test suite for the proposed Web Locks API.
|
||||
|
||||
Explainer: https://github.com/w3c/web-locks/
|
||||
|
||||
Spec: https://w3c.github.io/web-locks/
|
3
test/fixtures/wpt/web-locks/WEB_FEATURES.yml
vendored
Normal file
3
test/fixtures/wpt/web-locks/WEB_FEATURES.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
features:
|
||||
- name: web-locks
|
||||
files: "**"
|
136
test/fixtures/wpt/web-locks/acquire.https.any.js
vendored
Normal file
136
test/fixtures/wpt/web-locks/acquire.https.any.js
vendored
Normal file
|
@ -0,0 +1,136 @@
|
|||
// META: title=Web Locks API: navigator.locks.request method
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
await promise_rejects_js(t, TypeError, navigator.locks.request());
|
||||
await promise_rejects_js(t, TypeError, navigator.locks.request(res));
|
||||
}, 'navigator.locks.request requires a name and a callback');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
await promise_rejects_js(
|
||||
t, TypeError,
|
||||
navigator.locks.request(res, {mode: 'foo'}, lock => {}));
|
||||
await promise_rejects_js(
|
||||
t, TypeError,
|
||||
navigator.locks.request(res, {mode: null }, lock => {}));
|
||||
assert_equals(await navigator.locks.request(
|
||||
res, {mode: 'exclusive'}, lock => lock.mode), 'exclusive',
|
||||
'mode is exclusive');
|
||||
assert_equals(await navigator.locks.request(
|
||||
res, {mode: 'shared'}, lock => lock.mode), 'shared',
|
||||
'mode is shared');
|
||||
}, 'mode must be "shared" or "exclusive"');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
await promise_rejects_dom(
|
||||
t, 'NotSupportedError',
|
||||
navigator.locks.request(
|
||||
res, {steal: true, ifAvailable: true}, lock => {}),
|
||||
"A NotSupportedError should be thrown if both " +
|
||||
"'steal' and 'ifAvailable' are specified.");
|
||||
}, "The 'steal' and 'ifAvailable' options are mutually exclusive");
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
await promise_rejects_dom(
|
||||
t, 'NotSupportedError',
|
||||
navigator.locks.request(res, {mode: 'shared', steal: true}, lock => {}),
|
||||
'Request with mode=shared and steal=true should fail');
|
||||
}, "The 'steal' option must be used with exclusive locks");
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
const controller = new AbortController();
|
||||
await promise_rejects_dom(
|
||||
t, 'NotSupportedError',
|
||||
navigator.locks.request(
|
||||
res, {signal: controller.signal, steal: true}, lock => {}),
|
||||
'Request with signal and steal=true should fail');
|
||||
}, "The 'signal' and 'steal' options are mutually exclusive");
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
const controller = new AbortController();
|
||||
await promise_rejects_dom(
|
||||
t, 'NotSupportedError',
|
||||
navigator.locks.request(
|
||||
res, {signal: controller.signal, ifAvailable: true}, lock => {}),
|
||||
'Request with signal and ifAvailable=true should fail');
|
||||
}, "The 'signal' and 'ifAvailable' options are mutually exclusive");
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, undefined));
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, null));
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, 123));
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, 'abc'));
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, []));
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, {}));
|
||||
await promise_rejects_js(
|
||||
t, TypeError, navigator.locks.request(res, new Promise(r => {})));
|
||||
}, 'callback must be a function');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
let release;
|
||||
const promise = new Promise(r => { release = r; });
|
||||
|
||||
let returned = navigator.locks.request(res, lock => { return promise; });
|
||||
|
||||
const order = [];
|
||||
|
||||
returned.then(() => { order.push('returned'); });
|
||||
promise.then(() => { order.push('holding'); });
|
||||
|
||||
release();
|
||||
|
||||
await Promise.all([returned, promise]);
|
||||
|
||||
assert_array_equals(order, ['holding', 'returned']);
|
||||
|
||||
}, 'navigator.locks.request\'s returned promise resolves after' +
|
||||
' lock is released');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
const test_error = {name: 'test'};
|
||||
const p = navigator.locks.request(res, lock => {
|
||||
throw test_error;
|
||||
});
|
||||
assert_equals(Promise.resolve(p), p, 'request() result is a Promise');
|
||||
await promise_rejects_exactly(t, test_error, p, 'result should reject');
|
||||
}, 'Returned Promise rejects if callback throws synchronously');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
const test_error = {name: 'test'};
|
||||
const p = navigator.locks.request(res, async lock => {
|
||||
throw test_error;
|
||||
});
|
||||
assert_equals(Promise.resolve(p), p, 'request() result is a Promise');
|
||||
await promise_rejects_exactly(t, test_error, p, 'result should reject');
|
||||
}, 'Returned Promise rejects if callback throws asynchronously');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
let then_invoked = false;
|
||||
const test_error = { then: _ => { then_invoked = true; } };
|
||||
const p = navigator.locks.request(res, async lock => {
|
||||
throw test_error;
|
||||
});
|
||||
assert_equals(Promise.resolve(p), p, 'request() result is a Promise');
|
||||
await promise_rejects_exactly(t, test_error, p, 'result should reject');
|
||||
assert_false(then_invoked, 'then() should not be invoked');
|
||||
}, 'If callback throws a thenable, its then() should not be invoked');
|
64
test/fixtures/wpt/web-locks/bfcache/abort.tentative.https.html
vendored
Normal file
64
test/fixtures/wpt/web-locks/bfcache/abort.tentative.https.html
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<meta name="timeout" content="long">
|
||||
<title>Web Locks API: bfcache</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
|
||||
<script type="module">
|
||||
import { runWebLocksBfcacheTest } from "./helpers.js";
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
const controller = new AbortController();
|
||||
const promise = navigator.locks.request(
|
||||
uniqueNameByQuery(),
|
||||
{ signal: controller.signal },
|
||||
() => new Promise(() => { })
|
||||
);
|
||||
controller.abort();
|
||||
await promise.catch(() => { });
|
||||
},
|
||||
shouldBeCached: true
|
||||
}, "An immediately aborted lock on main thread should not prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new Worker("/web-locks/resources/worker.js");
|
||||
await postToWorkerAndWait(worker, {
|
||||
op: "request",
|
||||
name: uniqueNameByQuery(),
|
||||
abortImmediately: true
|
||||
});
|
||||
},
|
||||
shouldBeCached: true
|
||||
}, "An immediately aborted lock on a worker should not prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new Worker("/web-locks/resources/parentworker.js");
|
||||
await postToWorkerAndWait(worker, {
|
||||
op: "request",
|
||||
name: uniqueNameByQuery(),
|
||||
abortImmediately: true
|
||||
});
|
||||
},
|
||||
shouldBeCached: true
|
||||
}, "An immediately aborted lock on a nested worker should not prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new SharedWorker("/web-locks/resources/worker.js");
|
||||
worker.port.start();
|
||||
await postToWorkerAndWait(worker.port, {
|
||||
op: "request",
|
||||
name: uniqueNameByQuery(),
|
||||
abortImmediately: true
|
||||
});
|
||||
},
|
||||
shouldBeCached: true
|
||||
}, "An immediately aborted lock on a shared worker should not prevent bfcache");
|
||||
</script>
|
45
test/fixtures/wpt/web-locks/bfcache/held.tentative.https.html
vendored
Normal file
45
test/fixtures/wpt/web-locks/bfcache/held.tentative.https.html
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<meta name="timeout" content="long">
|
||||
<title>Web Locks API: bfcache</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
|
||||
<script type="module">
|
||||
import { runWebLocksBfcacheTest } from "./helpers.js";
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: () => {
|
||||
navigator.locks.request(uniqueNameByQuery(), () => new Promise(() => { }));
|
||||
},
|
||||
shouldBeCached: false
|
||||
}, "A held lock on the main thread must prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new Worker("/web-locks/resources/worker.js");
|
||||
await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() });
|
||||
},
|
||||
shouldBeCached: false
|
||||
}, "A held lock on a worker must prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new Worker("/web-locks/resources/parentworker.js");
|
||||
await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() });
|
||||
},
|
||||
shouldBeCached: false
|
||||
}, "A held lock on a nested worker must prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new SharedWorker("/web-locks/resources/worker.js");
|
||||
worker.port.start();
|
||||
await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() });
|
||||
},
|
||||
shouldBeCached: false
|
||||
}, "A held lock on a shared worker must prevent bfcache");
|
||||
</script>
|
15
test/fixtures/wpt/web-locks/bfcache/helpers.js
vendored
Normal file
15
test/fixtures/wpt/web-locks/bfcache/helpers.js
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
export function runWebLocksBfcacheTest(params, description) {
|
||||
runBfcacheTest(
|
||||
{
|
||||
scripts: ["/web-locks/resources/helpers.js"],
|
||||
openFunc: url =>
|
||||
window.open(
|
||||
url + `&prefix=${location.pathname}-${description}`,
|
||||
"_blank",
|
||||
"noopener"
|
||||
),
|
||||
...params,
|
||||
},
|
||||
description
|
||||
);
|
||||
}
|
46
test/fixtures/wpt/web-locks/bfcache/release-across-thread.tentative.https.html
vendored
Normal file
46
test/fixtures/wpt/web-locks/bfcache/release-across-thread.tentative.https.html
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<meta name="timeout" content="long">
|
||||
<title>Web Locks API: bfcache</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
|
||||
<script type="module">
|
||||
import { runWebLocksBfcacheTest } from "./helpers.js";
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
navigator.locks.request(uniqueNameByQuery(), () => new Promise(() => { }));
|
||||
window.worker = new Worker("/web-locks/resources/worker.js");
|
||||
const { lock_id } = await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() });
|
||||
await postToWorkerAndWait(worker, { op: "release", lock_id });
|
||||
},
|
||||
shouldBeCached: false,
|
||||
}, "A held lock on main thread must prevent bfcache even after worker releases locks");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
const controller = new AbortController();
|
||||
navigator.locks.request(uniqueNameByQuery(), { signal: controller.signal }, () => new Promise(() => { }));
|
||||
window.worker = new Worker("/web-locks/resources/worker.js");
|
||||
await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() });
|
||||
controller.abort();
|
||||
},
|
||||
shouldBeCached: false,
|
||||
}, "A held lock on worker must prevent bfcache even after main thread releases locks");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
const controller = new AbortController();
|
||||
navigator.locks.request(uniqueNameByQuery(), { signal: controller.signal }, () => new Promise(() => { }));
|
||||
window.worker = new SharedWorker("/web-locks/resources/worker.js");
|
||||
worker.port.start();
|
||||
await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() });
|
||||
controller.abort();
|
||||
},
|
||||
shouldBeCached: false,
|
||||
}, "A held lock on shared worker must prevent bfcache even after main thread releases locks");
|
||||
</script>
|
48
test/fixtures/wpt/web-locks/bfcache/release.tentative.https.html
vendored
Normal file
48
test/fixtures/wpt/web-locks/bfcache/release.tentative.https.html
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<meta name="timeout" content="long">
|
||||
<title>Web Locks API: bfcache</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
|
||||
<script type="module">
|
||||
import { runWebLocksBfcacheTest } from "./helpers.js";
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
await navigator.locks.request(uniqueNameByQuery(), () => { });
|
||||
},
|
||||
shouldBeCached: true,
|
||||
}, "A released lock on the main thread should not prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new Worker("/web-locks/resources/worker.js");
|
||||
const { lock_id } = await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() });
|
||||
await postToWorkerAndWait(worker, { op: "release", lock_id });
|
||||
},
|
||||
shouldBeCached: true,
|
||||
}, "A released lock on a worker should not prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new Worker("/web-locks/resources/parentworker.js");
|
||||
const { lock_id } = await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() });
|
||||
await postToWorkerAndWait(worker, { op: "release", lock_id });
|
||||
},
|
||||
shouldBeCached: true,
|
||||
}, "A released lock on a nested worker should not prevent bfcache");
|
||||
|
||||
runWebLocksBfcacheTest({
|
||||
funcBeforeNavigation: async () => {
|
||||
window.worker = new SharedWorker("/web-locks/resources/worker.js");
|
||||
worker.port.start();
|
||||
const { lock_id } = await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() });
|
||||
await postToWorkerAndWait(worker.port, { op: "release", lock_id });
|
||||
},
|
||||
shouldBeCached: true,
|
||||
}, "A released lock on a shared worker should not prevent bfcache");
|
||||
</script>
|
69
test/fixtures/wpt/web-locks/bfcache/sharedworker-multiple.tentative.https.html
vendored
Normal file
69
test/fixtures/wpt/web-locks/bfcache/sharedworker-multiple.tentative.https.html
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<meta name="timeout" content="long">
|
||||
<title>Web Locks API: bfcache</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script>
|
||||
<script>
|
||||
const connectToSharedWorker = async () => {
|
||||
await window.pageShowPromise;
|
||||
window.worker = new SharedWorker("/web-locks/resources/worker.js");
|
||||
worker.port.start();
|
||||
}
|
||||
function double_docs_test(func, description) {
|
||||
promise_test(async t => {
|
||||
const pageA1 = new RemoteContext(token());
|
||||
const pageA2 = new RemoteContext(token());
|
||||
const pageB1 = new RemoteContext(token());
|
||||
const pageB2 = new RemoteContext(token());
|
||||
|
||||
const urlA1 = executorPath + pageA1.context_id;
|
||||
const urlA2 = executorPath + pageA2.context_id;
|
||||
const urlB1 = originCrossSite + executorPath + pageB1.context_id;
|
||||
const urlB2 = originCrossSite + executorPath + pageB2.context_id;
|
||||
|
||||
window.open(urlA1, "_blank", "noopener");
|
||||
window.open(urlA2, "_blank", "noopener");
|
||||
|
||||
await func(pageA1, pageA2);
|
||||
|
||||
await Promise.all([
|
||||
navigateAndThenBack(pageA1, pageB1, urlB1),
|
||||
navigateAndThenBack(pageA2, pageB2, urlB2),
|
||||
]);
|
||||
|
||||
await assert_not_bfcached(pageA1);
|
||||
await assert_not_bfcached(pageA2);
|
||||
}, description);
|
||||
}
|
||||
|
||||
double_docs_test(async (pageA1, pageA2) => {
|
||||
await Promise.all([
|
||||
pageA1.execute_script(connectToSharedWorker),
|
||||
pageA2.execute_script(connectToSharedWorker),
|
||||
]);
|
||||
await pageA1.execute_script(async () => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "/web-locks/resources/helpers.js";
|
||||
document.head.append(script);
|
||||
await new Promise(resolve => script.onload = resolve);
|
||||
await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() });
|
||||
});
|
||||
}, "A new held lock must prevent bfcache on all connected documents");
|
||||
|
||||
double_docs_test(async (pageA1, pageA2) => {
|
||||
await pageA1.execute_script(connectToSharedWorker);
|
||||
await pageA1.execute_script(async () => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "/web-locks/resources/helpers.js";
|
||||
document.head.append(script);
|
||||
await new Promise(resolve => script.onload = resolve);
|
||||
await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() });
|
||||
});
|
||||
await pageA2.execute_script(connectToSharedWorker);
|
||||
}, "An existing held lock must prevent bfcache on all connected documents");
|
||||
</script>
|
45
test/fixtures/wpt/web-locks/clientids.https.html
vendored
Normal file
45
test/fixtures/wpt/web-locks/clientids.https.html
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>Web Locks API: Client IDs in query() vs. Service Worker</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
|
||||
<script>
|
||||
|
||||
// Returns a promise resolved by the next message event.
|
||||
function nextMessage() {
|
||||
return new Promise(resolve => {
|
||||
window.addEventListener('message', event => {
|
||||
resolve(event.data);
|
||||
}, {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const iframe_url = 'resources/sw-controlled-iframe.html';
|
||||
|
||||
// Register a service worker that will control an iframe.
|
||||
const registration = await service_worker_unregister_and_register(
|
||||
t, 'resources/service-worker.js', iframe_url);
|
||||
await wait_for_state(t, registration.installing, 'activated');
|
||||
|
||||
const iframe = await with_iframe(iframe_url);
|
||||
|
||||
iframe.contentWindow.postMessage('get_sw_client_id', '*');
|
||||
const sw_client_id = await nextMessage();
|
||||
|
||||
iframe.contentWindow.postMessage('get_lock_client_id', '*');
|
||||
const lock_client_id = await nextMessage();
|
||||
|
||||
// NOTE: Not assert_equals(), as we don't want log the randomly generated
|
||||
// clientIds, since they would not match any failure expectation files.
|
||||
assert_equals(lock_client_id, sw_client_id,
|
||||
'clientIds should match, but are different');
|
||||
|
||||
await registration.unregister();
|
||||
|
||||
}, 'Client IDs match between Locks API and Service Workers');
|
||||
|
||||
</script>
|
20
test/fixtures/wpt/web-locks/crashtests/after-worker-termination.https.html
vendored
Normal file
20
test/fixtures/wpt/web-locks/crashtests/after-worker-termination.https.html
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="test-wait">
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
const script = `
|
||||
postMessage("hi");
|
||||
// This line runs until worker.terminate() happens, which terminates this function too.
|
||||
self.reportError(new Int16Array(2147483648))
|
||||
// And thus this line runs after the termination.
|
||||
navigator.locks.request("weblock_0", () => {});
|
||||
`;
|
||||
const worker = new Worker(URL.createObjectURL(new Blob([script])));
|
||||
worker.onmessage = () => {
|
||||
worker.terminate();
|
||||
|
||||
// We want to wait for the full termination but there is no API for that
|
||||
// So, just wait for a random time
|
||||
setTimeout(() => document.documentElement.classList.remove("test-wait"), 100);
|
||||
}
|
||||
</script>
|
18
test/fixtures/wpt/web-locks/crashtests/iframe-append-2.https.html
vendored
Normal file
18
test/fixtures/wpt/web-locks/crashtests/iframe-append-2.https.html
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<iframe id="id_0"></iframe>
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
const iframe = document.getElementById("id_0")
|
||||
// Discards the previous document
|
||||
document.documentElement.appendChild(iframe)
|
||||
const xhr = new XMLHttpRequest()
|
||||
// LockManager is created after discarding
|
||||
// At this point the new document is not there yet
|
||||
iframe.contentWindow.navigator.locks.request("weblock_0", () => {
|
||||
xhr.open("GET", "FOOBAR", false)
|
||||
xhr.send()
|
||||
// Now there is a new document
|
||||
})
|
||||
})
|
||||
</script>
|
21
test/fixtures/wpt/web-locks/crashtests/iframe-append.https.html
vendored
Normal file
21
test/fixtures/wpt/web-locks/crashtests/iframe-append.https.html
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="test-wait">
|
||||
<meta charset="utf-8">
|
||||
<iframe id="id_0"></iframe>
|
||||
<script>
|
||||
/** @param {HTMLIFrameElement} iframe */
|
||||
function waitForLoad(iframe) {
|
||||
// iframe is initialized immediately on Chrome while it needs some time on Firefox
|
||||
if (iframe.contentDocument.readyState === "complete") {
|
||||
return;
|
||||
}
|
||||
return new Promise(r => iframe.onload = r);
|
||||
}
|
||||
|
||||
const iframe = document.getElementById("id_0");
|
||||
iframe.contentWindow.navigator.locks.request("weblock_0", async () => {
|
||||
await waitForLoad(iframe);
|
||||
document.body.append(iframe); // discards the document and destroys locks
|
||||
document.documentElement.classList.remove("test-wait");
|
||||
});
|
||||
</script>
|
10
test/fixtures/wpt/web-locks/crashtests/settle-after-steal.https.html
vendored
Normal file
10
test/fixtures/wpt/web-locks/crashtests/settle-after-steal.https.html
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<html class="test-wait">
|
||||
<script>
|
||||
navigator.locks.request("foo", async () => {
|
||||
await new Promise(queueMicrotask);
|
||||
document.documentElement.classList.remove("test-wait");
|
||||
});
|
||||
navigator.locks.request("foo", { steal: true }, () => {});
|
||||
</script>
|
16
test/fixtures/wpt/web-locks/crashtests/worker-termination.https.html
vendored
Normal file
16
test/fixtures/wpt/web-locks/crashtests/worker-termination.https.html
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="test-wait">
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
var worker = new Worker(URL.createObjectURL(new Blob([`
|
||||
postMessage("hi");
|
||||
(async () => {
|
||||
const abort = new AbortController()
|
||||
await navigator.locks.request("weblock_0", { signal: abort.signal }, () => {})
|
||||
})()
|
||||
`])));
|
||||
worker.onmessage = () => {
|
||||
worker.terminate();
|
||||
document.documentElement.classList.remove("test-wait");
|
||||
};
|
||||
</script>
|
265
test/fixtures/wpt/web-locks/frames.https.html
vendored
Normal file
265
test/fixtures/wpt/web-locks/frames.https.html
vendored
Normal file
|
@ -0,0 +1,265 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>Web Locks API: Frames</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="resources/helpers.js"></script>
|
||||
<style>iframe { display: none; }</style>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
const frame = await iframe('resources/iframe.html');
|
||||
t.add_cleanup(() => { frame.remove(); });
|
||||
|
||||
const lock_id = (await postToFrameAndWait(
|
||||
frame, {op: 'request', name: res, mode: 'shared'})).lock_id;
|
||||
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock => {
|
||||
await postToFrameAndWait(frame, {op: 'release', lock_id});
|
||||
});
|
||||
|
||||
}, 'Window and Frame - shared mode');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
const frame = await iframe('resources/iframe.html');
|
||||
t.add_cleanup(() => { frame.remove(); });
|
||||
|
||||
// frame acquires the lock.
|
||||
const lock_id = (await postToFrameAndWait(
|
||||
frame, {op: 'request', name: res})).lock_id;
|
||||
|
||||
// This request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = navigator.locks.request(res, lock => { lock_granted = true; });
|
||||
|
||||
// Verify that we can't get it.
|
||||
let available = undefined;
|
||||
await navigator.locks.request(
|
||||
res, {ifAvailable: true}, lock => { available = lock !== null; });
|
||||
assert_false(available);
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Ask the frame to release it.
|
||||
await postToFrameAndWait(frame, {op: 'release', lock_id});
|
||||
|
||||
await blocked;
|
||||
// Now we've got it.
|
||||
assert_true(lock_granted);
|
||||
}, 'Window and Frame - exclusive mode');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
const frame1 = await iframe('resources/iframe.html');
|
||||
const frame2 = await iframe('resources/iframe.html');
|
||||
|
||||
// frame1 acquires the lock.
|
||||
const lock_id = (await postToFrameAndWait(
|
||||
frame1, {op: 'request', name: res})).lock_id;
|
||||
|
||||
// frame2's request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = postToFrameAndWait(
|
||||
frame2, {op: 'request', name: res});
|
||||
blocked.then(f => { lock_granted = true; });
|
||||
|
||||
// Verify that frame2 can't get it.
|
||||
assert_true((await postToFrameAndWait(frame2, {
|
||||
op: 'request', name: res, ifAvailable: true
|
||||
})).failed, 'Lock request should have failed');
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Ask frame1 to release it.
|
||||
await postToFrameAndWait(frame1, {op: 'release', lock_id});
|
||||
|
||||
await blocked;
|
||||
// Now frame2 can get it.
|
||||
assert_true(lock_granted);
|
||||
frame1.parentElement.removeChild(frame1);
|
||||
frame2.parentElement.removeChild(frame2);
|
||||
}, 'Frame and Frame - exclusive mode');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
const frame = await iframe('resources/iframe.html');
|
||||
|
||||
// Frame acquires the lock.
|
||||
await postToFrameAndWait(frame, {op: 'request', name: res});
|
||||
|
||||
// This request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = navigator.locks.request(
|
||||
res, lock => { lock_granted = true; });
|
||||
|
||||
// Verify that we can't get it.
|
||||
let available = undefined;
|
||||
await navigator.locks.request(
|
||||
res, {ifAvailable: true}, lock => { available = lock !== null; });
|
||||
assert_false(available);
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Implicitly release it by terminating the frame.
|
||||
frame.remove();
|
||||
await blocked;
|
||||
// Now we've got it.
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Terminated Frame with held lock');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
const frame = await iframe('resources/iframe.html');
|
||||
|
||||
// Frame acquires the lock.
|
||||
await postToFrameAndWait(frame, {op: 'request', name: res});
|
||||
|
||||
// This request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = navigator.locks.request(
|
||||
res, lock => { lock_granted = true; });
|
||||
|
||||
// Verify that we can't get it.
|
||||
let available = undefined;
|
||||
await navigator.locks.request(
|
||||
res, {ifAvailable: true}, lock => { available = lock !== null; });
|
||||
assert_false(available);
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Implicitly release it by navigating the frame.
|
||||
frame.src = 'about:blank';
|
||||
await blocked;
|
||||
// Now we've got it.
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Navigated Frame with held lock');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
// frame1 requests and holds res - should be granted immediately.
|
||||
// frame2 requests res - should be blocked.
|
||||
// frame3 requests res - should be blocked.
|
||||
// frame2 is navigated.
|
||||
// frame1 releases res.
|
||||
// frame3's request should be granted.
|
||||
|
||||
const frame1 = await iframe('resources/iframe.html');
|
||||
const frame2 = await iframe('resources/iframe.html');
|
||||
const frame3 = await iframe('resources/iframe.html');
|
||||
t.add_cleanup(() => { frame1.remove(); });
|
||||
t.add_cleanup(() => { frame2.remove(); });
|
||||
t.add_cleanup(() => { frame3.remove(); });
|
||||
|
||||
// frame1 requests and holds res - should be granted immediately.
|
||||
const lock_id = (await postToFrameAndWait(
|
||||
frame1, {op: 'request', name: res})).lock_id;
|
||||
|
||||
// frame2 requests res - should be blocked.
|
||||
// (don't attach listeners as they will keep the frame alive)
|
||||
frame2.contentWindow.postMessage({op: 'request', name: res}, '*');
|
||||
|
||||
// frame3 requests res - should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = postToFrameAndWait(frame3, {op: 'request', name: res});
|
||||
blocked.then(f => { lock_granted = true; });
|
||||
|
||||
// Verify that frame3 can't get it.
|
||||
assert_true((await postToFrameAndWait(frame3, {
|
||||
op: 'request', name: res, ifAvailable: true
|
||||
})).failed, 'Lock request should have failed');
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Navigate frame2.
|
||||
frame2.src = 'about:blank';
|
||||
|
||||
// frame1 releases lock
|
||||
await postToFrameAndWait(frame1, {op: 'release', lock_id});
|
||||
|
||||
// frame3's request should be granted.
|
||||
await blocked;
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Navigated Frame with pending request');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
// frame1 requests and holds res - should be granted immediately.
|
||||
// frame2 requests res - should be blocked.
|
||||
// frame3 requests res - should be blocked.
|
||||
// frame2 is removed.
|
||||
// frame1 drops lock.
|
||||
// frame3's request should be granted.
|
||||
|
||||
const frame1 = await iframe('resources/iframe.html');
|
||||
const frame2 = await iframe('resources/iframe.html');
|
||||
const frame3 = await iframe('resources/iframe.html');
|
||||
t.add_cleanup(() => { frame1.remove(); });
|
||||
t.add_cleanup(() => { frame3.remove(); });
|
||||
|
||||
// frame1 requests and holds res - should be granted immediately.
|
||||
const lock_id = (await postToFrameAndWait(
|
||||
frame1, {op: 'request', name: res})).lock_id;
|
||||
|
||||
// frame2 requests res - should be blocked.
|
||||
// (don't attach listeners as they will keep the frame alive)
|
||||
frame2.contentWindow.postMessage({op: 'request', name: res}, '*');
|
||||
|
||||
// frame3 requests res - should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = postToFrameAndWait(frame3, {op: 'request', name: res});
|
||||
blocked.then(f => { lock_granted = true; });
|
||||
|
||||
// So frame3 can't get it
|
||||
assert_true((await postToFrameAndWait(frame3, {
|
||||
op: 'request', name: res, ifAvailable: true
|
||||
})).failed, 'Lock request should have failed');
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Remove frame2.
|
||||
frame2.remove();
|
||||
|
||||
// frame1 releases lock
|
||||
await postToFrameAndWait(frame1, {op: 'release', lock_id});
|
||||
|
||||
// frame3's request should be granted.
|
||||
await blocked;
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Removed Frame with pending request');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const res = uniqueName(t);
|
||||
|
||||
const frame = await iframe('about:blank');
|
||||
|
||||
// Remove a frame while it is in the process of requesting a lock.
|
||||
// The promise returned by `request` will never resolve since its frame no
|
||||
// longer exists, but the lock should still be released.
|
||||
await new Promise(resolve => {
|
||||
frame.contentWindow.navigator.locks.request(res, () => {
|
||||
frame.remove();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
assert_false((await navigator.locks.query()).held.includes(res));
|
||||
}, 'Removed Frame as lock is granted');
|
||||
|
||||
</script>
|
91
test/fixtures/wpt/web-locks/held.https.any.js
vendored
Normal file
91
test/fixtures/wpt/web-locks/held.https.any.js
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
// META: title=Web Locks API: Lock held until callback result resolves
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
// For uncaught rejections.
|
||||
setup({allow_uncaught_exception: true});
|
||||
|
||||
function snooze(t, ms) { return new Promise(r => t.step_timeout(r, ms)); }
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
const p = navigator.locks.request(res, lock => 123);
|
||||
assert_equals(Promise.resolve(p), p, 'request() result is a Promise');
|
||||
assert_equals(await p, 123, 'promise resolves to the returned value');
|
||||
}, 'callback\'s result is promisified if not async');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
// Resolved when the lock is granted.
|
||||
let granted;
|
||||
const lock_granted_promise = new Promise(r => { granted = r; });
|
||||
|
||||
// Lock is held until this is resolved.
|
||||
let resolve;
|
||||
const lock_release_promise = new Promise(r => { resolve = r; });
|
||||
|
||||
const order = [];
|
||||
|
||||
navigator.locks.request(res, lock => {
|
||||
granted(lock);
|
||||
return lock_release_promise;
|
||||
});
|
||||
await lock_granted_promise;
|
||||
|
||||
await Promise.all([
|
||||
snooze(t, 50).then(() => {
|
||||
order.push('1st lock released');
|
||||
resolve();
|
||||
}),
|
||||
navigator.locks.request(res, () => {
|
||||
order.push('2nd lock granted');
|
||||
})
|
||||
]);
|
||||
|
||||
assert_array_equals(order, ['1st lock released', '2nd lock granted']);
|
||||
}, 'lock is held until callback\'s returned promise resolves');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
// Resolved when the lock is granted.
|
||||
let granted;
|
||||
const lock_granted_promise = new Promise(r => { granted = r; });
|
||||
|
||||
// Lock is held until this is rejected.
|
||||
let reject;
|
||||
const lock_release_promise = new Promise((_, r) => { reject = r; });
|
||||
|
||||
const order = [];
|
||||
|
||||
navigator.locks.request(res, lock => {
|
||||
granted(lock);
|
||||
return lock_release_promise;
|
||||
});
|
||||
await lock_granted_promise;
|
||||
|
||||
await Promise.all([
|
||||
snooze(t, 50).then(() => {
|
||||
order.push('reject');
|
||||
reject(new Error('this uncaught rejection is expected'));
|
||||
}),
|
||||
navigator.locks.request(res, () => {
|
||||
order.push('2nd lock granted');
|
||||
})
|
||||
]);
|
||||
|
||||
assert_array_equals(order, ['reject', '2nd lock granted']);
|
||||
}, 'lock is held until callback\'s returned promise rejects');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
await navigator.locks.request(res, {ifAvailable: true}, lock => {
|
||||
callback_called = true;
|
||||
assert_equals(lock, null, 'lock request should fail if held');
|
||||
});
|
||||
});
|
||||
assert_true(callback_called, 'callback should have executed');
|
||||
}, 'held lock prevents the same client from acquiring it');
|
29
test/fixtures/wpt/web-locks/idlharness.https.any.js
vendored
Normal file
29
test/fixtures/wpt/web-locks/idlharness.https.any.js
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
// META: script=/resources/WebIDLParser.js
|
||||
// META: script=/resources/idlharness.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
// META: timeout=long
|
||||
|
||||
'use strict';
|
||||
|
||||
idl_test(
|
||||
['web-locks'],
|
||||
['html'],
|
||||
async idl_array => {
|
||||
idl_array.add_objects({
|
||||
LockManager: ['navigator.locks'],
|
||||
Lock: ['lock'],
|
||||
});
|
||||
|
||||
if (self.Window) {
|
||||
idl_array.add_objects({ Navigator: ['navigator'] });
|
||||
} else {
|
||||
idl_array.add_objects({ WorkerNavigator: ['navigator'] });
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.locks.request('name', l => { self.lock = l; });
|
||||
} catch (e) {
|
||||
// Surfaced in idlharness.js's test_object below.
|
||||
}
|
||||
}
|
||||
);
|
163
test/fixtures/wpt/web-locks/ifAvailable.https.any.js
vendored
Normal file
163
test/fixtures/wpt/web-locks/ifAvailable.https.any.js
vendored
Normal file
|
@ -0,0 +1,163 @@
|
|||
// META: title=Web Locks API: ifAvailable option
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, {ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_not_equals(lock, null, 'lock should be granted');
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Lock request with ifAvailable - lock available');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
// Request would time out if |ifAvailable| was not specified.
|
||||
const result = await navigator.locks.request(
|
||||
res, {ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_equals(lock, null, 'lock should not be granted');
|
||||
return 123;
|
||||
});
|
||||
assert_equals(result, 123, 'result should be value returned by callback');
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Lock request with ifAvailable - lock not available');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
try {
|
||||
// Request would time out if |ifAvailable| was not specified.
|
||||
await navigator.locks.request(res, {ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_equals(lock, null, 'lock should not be granted');
|
||||
throw 123;
|
||||
});
|
||||
assert_unreached('call should throw');
|
||||
} catch (ex) {
|
||||
assert_equals(ex, 123, 'ex should be value thrown by callback');
|
||||
}
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Lock request with ifAvailable - lock not available, callback throws');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
// Request with a different name - should be grantable.
|
||||
await navigator.locks.request('different', {ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_not_equals(lock, null, 'lock should be granted');
|
||||
});
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Lock request with ifAvailable - unrelated lock held');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock => {
|
||||
await navigator.locks.request(
|
||||
res, {mode: 'shared', ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_not_equals(lock, null, 'lock should be granted');
|
||||
});
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Shared lock request with ifAvailable - shared lock held');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock => {
|
||||
// Request would time out if |ifAvailable| was not specified.
|
||||
await navigator.locks.request(res, {ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_equals(lock, null, 'lock should not be granted');
|
||||
});
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Exclusive lock request with ifAvailable - shared lock held');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
// Request would time out if |ifAvailable| was not specified.
|
||||
await navigator.locks.request(
|
||||
res, {mode: 'shared', ifAvailable: true}, async lock => {
|
||||
callback_called = true;
|
||||
assert_equals(lock, null, 'lock should not be granted');
|
||||
});
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Shared lock request with ifAvailable - exclusive lock held');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
callback_called = true;
|
||||
const test_error = {name: 'test'};
|
||||
const p = navigator.locks.request(
|
||||
res, {ifAvailable: true}, lock => {
|
||||
assert_equals(lock, null, 'lock should not be available');
|
||||
throw test_error;
|
||||
});
|
||||
assert_equals(Promise.resolve(p), p, 'request() result is a Promise');
|
||||
await promise_rejects_exactly(t, test_error, p, 'result should reject');
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Returned Promise rejects if callback throws synchronously');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, async lock => {
|
||||
callback_called = true;
|
||||
const test_error = {name: 'test'};
|
||||
const p = navigator.locks.request(
|
||||
res, {ifAvailable: true}, async lock => {
|
||||
assert_equals(lock, null, 'lock should not be available');
|
||||
throw test_error;
|
||||
});
|
||||
assert_equals(Promise.resolve(p), p, 'request() result is a Promise');
|
||||
await promise_rejects_exactly(t, test_error, p, 'result should reject');
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
}, 'Returned Promise rejects if async callback yields rejected promise');
|
||||
|
||||
// Regression test for: https://crbug.com/840994
|
||||
promise_test(async t => {
|
||||
const res1 = self.uniqueName(t);
|
||||
const res2 = self.uniqueName(t);
|
||||
let callback1_called = false;
|
||||
await navigator.locks.request(res1, async lock => {
|
||||
callback1_called = true;
|
||||
let callback2_called = false;
|
||||
await navigator.locks.request(res2, async lock => {
|
||||
callback2_called = true;
|
||||
});
|
||||
assert_true(callback2_called, 'callback2 should be called');
|
||||
|
||||
let callback3_called = false;
|
||||
await navigator.locks.request(res2, {ifAvailable: true}, async lock => {
|
||||
callback3_called = true;
|
||||
// This request would fail if the "is this grantable?" test
|
||||
// failed, e.g. due to the release without a pending request
|
||||
// skipping steps.
|
||||
assert_not_equals(lock, null, 'Lock should be available');
|
||||
});
|
||||
assert_true(callback3_called, 'callback2 should be called');
|
||||
});
|
||||
assert_true(callback1_called, 'callback1 should be called');
|
||||
}, 'Locks are available once previous release is processed');
|
18
test/fixtures/wpt/web-locks/lock-attributes.https.any.js
vendored
Normal file
18
test/fixtures/wpt/web-locks/lock-attributes.https.any.js
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
// META: title=Web Locks API: Lock Attributes
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
await navigator.locks.request('resource', lock => {
|
||||
assert_equals(lock.name, 'resource');
|
||||
assert_equals(lock.mode, 'exclusive');
|
||||
});
|
||||
}, 'Lock attributes reflect requested properties (exclusive)');
|
||||
|
||||
promise_test(async t => {
|
||||
await navigator.locks.request('resource', {mode: 'shared'}, lock => {
|
||||
assert_equals(lock.name, 'resource');
|
||||
assert_equals(lock.mode, 'shared');
|
||||
});
|
||||
}, 'Lock attributes reflect requested properties (shared)');
|
34
test/fixtures/wpt/web-locks/mode-exclusive.https.any.js
vendored
Normal file
34
test/fixtures/wpt/web-locks/mode-exclusive.https.any.js
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
// META: title=Web Locks API: Exclusive Mode
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
const granted = [];
|
||||
function log_grant(n) { return () => { granted.push(n); }; }
|
||||
|
||||
await Promise.all([
|
||||
navigator.locks.request('a', log_grant(1)),
|
||||
navigator.locks.request('a', log_grant(2)),
|
||||
navigator.locks.request('a', log_grant(3))
|
||||
]);
|
||||
assert_array_equals(granted, [1, 2, 3]);
|
||||
}, 'Lock requests are granted in order');
|
||||
|
||||
promise_test(async t => {
|
||||
const granted = [];
|
||||
function log_grant(n) { return () => { granted.push(n); }; }
|
||||
|
||||
let inner_promise;
|
||||
await navigator.locks.request('a', async lock => {
|
||||
inner_promise = Promise.all([
|
||||
// This will be blocked.
|
||||
navigator.locks.request('a', log_grant(1)),
|
||||
// But this should be grantable immediately.
|
||||
navigator.locks.request('b', log_grant(2))
|
||||
]);
|
||||
});
|
||||
|
||||
await inner_promise;
|
||||
assert_array_equals(granted, [2, 1]);
|
||||
}, 'Requests for distinct resources can be granted');
|
98
test/fixtures/wpt/web-locks/mode-mixed.https.any.js
vendored
Normal file
98
test/fixtures/wpt/web-locks/mode-mixed.https.any.js
vendored
Normal file
|
@ -0,0 +1,98 @@
|
|||
// META: title=Web Locks API: Mixed Modes
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
let unblock;
|
||||
const blocked = new Promise(r => { unblock = r; });
|
||||
|
||||
const granted = [];
|
||||
|
||||
// These should be granted immediately, and held until unblocked.
|
||||
navigator.locks.request('a', {mode: 'shared'}, async lock => {
|
||||
granted.push('a-shared-1'); await blocked; });
|
||||
navigator.locks.request('a', {mode: 'shared'}, async lock => {
|
||||
granted.push('a-shared-2'); await blocked; });
|
||||
navigator.locks.request('a', {mode: 'shared'}, async lock => {
|
||||
granted.push('a-shared-3'); await blocked; });
|
||||
|
||||
// This should be blocked.
|
||||
let exclusive_lock;
|
||||
const exclusive_request = navigator.locks.request('a', async lock => {
|
||||
granted.push('a-exclusive');
|
||||
exclusive_lock = lock;
|
||||
});
|
||||
|
||||
// This should be granted immediately (different name).
|
||||
await navigator.locks.request('b', {mode: 'exclusive'}, lock => {
|
||||
granted.push('b-exclusive'); });
|
||||
|
||||
assert_array_equals(
|
||||
granted, ['a-shared-1', 'a-shared-2', 'a-shared-3', 'b-exclusive']);
|
||||
|
||||
// Release the shared locks granted above.
|
||||
unblock();
|
||||
|
||||
// Now the blocked request can be granted.
|
||||
await exclusive_request;
|
||||
assert_equals(exclusive_lock.mode, 'exclusive');
|
||||
|
||||
assert_array_equals(
|
||||
granted,
|
||||
['a-shared-1', 'a-shared-2', 'a-shared-3', 'b-exclusive', 'a-exclusive']);
|
||||
|
||||
}, 'Lock requests are granted in order');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
let [promise, resolve] = makePromiseAndResolveFunc();
|
||||
|
||||
const exclusive = navigator.locks.request(res, () => promise);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
requestLockAndHold(t, res, { mode: "shared" });
|
||||
}
|
||||
|
||||
let answer = await navigator.locks.query();
|
||||
assert_equals(answer.held.length, 1, "An exclusive lock is held");
|
||||
assert_equals(answer.pending.length, 5, "Requests for shared locks are pending");
|
||||
resolve();
|
||||
await exclusive;
|
||||
|
||||
answer = await navigator.locks.query();
|
||||
assert_equals(answer.held.length, 5, "Shared locks are held");
|
||||
assert_true(answer.held.every(l => l.mode === "shared"), "All held locks are shared ones");
|
||||
}, 'Releasing exclusive lock grants multiple shared locks');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
let [sharedPromise, sharedResolve] = makePromiseAndResolveFunc();
|
||||
let [exclusivePromise, exclusiveResolve] = makePromiseAndResolveFunc();
|
||||
|
||||
const sharedReleasedPromise = Promise.all(new Array(5).fill(0).map(
|
||||
() => navigator.locks.request(res, { mode: "shared" }, () => sharedPromise))
|
||||
);
|
||||
const exclusiveReleasedPromise = navigator.locks.request(res, () => exclusivePromise);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
requestLockAndHold(t, res, { mode: "shared" });
|
||||
}
|
||||
|
||||
let answer = await navigator.locks.query();
|
||||
assert_equals(answer.held.length, 5, "Shared locks are held");
|
||||
assert_true(answer.held.every(l => l.mode === "shared"), "All held locks are shared ones");
|
||||
sharedResolve();
|
||||
await sharedReleasedPromise;
|
||||
|
||||
answer = await navigator.locks.query();
|
||||
assert_equals(answer.held.length, 1, "An exclusive lock is held");
|
||||
assert_equals(answer.held[0].mode, "exclusive");
|
||||
exclusiveResolve();
|
||||
await exclusiveReleasedPromise;
|
||||
|
||||
answer = await navigator.locks.query();
|
||||
assert_equals(answer.held.length, 5, "The next shared locks are held");
|
||||
assert_true(answer.held.every(l => l.mode === "shared"), "All held locks are shared ones");
|
||||
}, 'An exclusive lock between shared locks');
|
38
test/fixtures/wpt/web-locks/mode-shared.https.any.js
vendored
Normal file
38
test/fixtures/wpt/web-locks/mode-shared.https.any.js
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
// META: title=Web Locks API: Shared Mode
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
const granted = [];
|
||||
function log_grant(n) { return () => { granted.push(n); }; }
|
||||
|
||||
await Promise.all([
|
||||
navigator.locks.request('a', {mode: 'shared'}, log_grant(1)),
|
||||
navigator.locks.request('b', {mode: 'shared'}, log_grant(2)),
|
||||
navigator.locks.request('c', {mode: 'shared'}, log_grant(3)),
|
||||
navigator.locks.request('a', {mode: 'shared'}, log_grant(4)),
|
||||
navigator.locks.request('b', {mode: 'shared'}, log_grant(5)),
|
||||
navigator.locks.request('c', {mode: 'shared'}, log_grant(6)),
|
||||
]);
|
||||
|
||||
assert_array_equals(granted, [1, 2, 3, 4, 5, 6]);
|
||||
}, 'Lock requests are granted in order');
|
||||
|
||||
promise_test(async t => {
|
||||
let a_acquired = false, a_acquired_again = false;
|
||||
|
||||
await navigator.locks.request('a', {mode: 'shared'}, async lock => {
|
||||
a_acquired = true;
|
||||
|
||||
// Since lock is held, this request would be blocked if the
|
||||
// lock was not 'shared', causing this test to time out.
|
||||
|
||||
await navigator.locks.request('a', {mode: 'shared'}, lock => {
|
||||
a_acquired_again = true;
|
||||
});
|
||||
});
|
||||
|
||||
assert_true(a_acquired, 'first lock acquired');
|
||||
assert_true(a_acquired_again, 'second lock acquired');
|
||||
}, 'Shared locks are not exclusive');
|
73
test/fixtures/wpt/web-locks/non-fully-active.https.html
vendored
Normal file
73
test/fixtures/wpt/web-locks/non-fully-active.https.html
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>Web Locks API: Non-fully-active documents</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="resources/helpers.js"></script>
|
||||
|
||||
<div></div>
|
||||
|
||||
<script>
|
||||
function createNonFullyActiveIframe(src) {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const { navigator, DOMException, postMessage } = iframe.contentWindow;
|
||||
iframe.remove();
|
||||
return { iframe, navigator, DOMException, postMessage };
|
||||
}
|
||||
|
||||
promise_test(async t => {
|
||||
const { navigator, DOMException } = createNonFullyActiveIframe();
|
||||
const p = navigator.locks.request("foo", t.unreached_func());
|
||||
await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Request should explicitly fail");
|
||||
}, "request() on non-fully-active document must fail");
|
||||
|
||||
promise_test(async t => {
|
||||
const { navigator, DOMException } = createNonFullyActiveIframe();
|
||||
const p = navigator.locks.query();
|
||||
await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Query should explicitly fail");
|
||||
}, "query() on a non-fully-active document must fail");
|
||||
|
||||
promise_test(async t => {
|
||||
const { navigator, DOMException, postMessage } = createNonFullyActiveIframe();
|
||||
|
||||
const p = navigator.locks.request("-", t.unreached_func());
|
||||
await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Request should explicitly fail");
|
||||
}, "request()'s fully-active check happens earlier than name validation");
|
||||
|
||||
promise_test(async t => {
|
||||
const { iframe, navigator, DOMException } = createNonFullyActiveIframe();
|
||||
document.body.append(iframe);
|
||||
t.add_cleanup(() => iframe.remove());
|
||||
|
||||
// Appending should create a new browsing context with a new Navigator object
|
||||
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element:insert-an-element-into-a-document
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object:associated-navigator
|
||||
assert_not_equals(navigator, iframe.contentWindow.navigator, "Navigator object changes");
|
||||
assert_not_equals(navigator.locks, iframe.contentWindow.navigator.locks, "LockManager object changes");
|
||||
|
||||
const p = navigator.locks.request("foo", t.unreached_func());
|
||||
await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Request on the previous LockManager still must fail");
|
||||
}, "Reactivated iframe must not reuse the previous LockManager");
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const worker = new iframe.contentWindow.Worker("resources/worker.js");
|
||||
|
||||
const name = uniqueName(t);
|
||||
await postToWorkerAndWait(worker, { op: 'request', name });
|
||||
|
||||
let query = await navigator.locks.query();
|
||||
assert_equals(query.held.length, 1, "One lock is present");
|
||||
|
||||
iframe.remove();
|
||||
|
||||
const lock = await navigator.locks.request(name, lock => lock);
|
||||
assert_equals(lock.name, name, "The following lock should be processed");
|
||||
|
||||
query = await navigator.locks.query();
|
||||
assert_equals(query.held.length, 0, "No lock is present");
|
||||
}, "Workers owned by an unloaded iframe must release their locks");
|
||||
</script>
|
14
test/fixtures/wpt/web-locks/non-secure-context.any.js
vendored
Normal file
14
test/fixtures/wpt/web-locks/non-secure-context.any.js
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
// META: title=Web Locks API: API not available in non-secure context
|
||||
// META: global=window,dedicatedworker,sharedworker
|
||||
|
||||
'use strict';
|
||||
|
||||
test(t => {
|
||||
assert_false(self.isSecureContext);
|
||||
assert_false('locks' in navigator,
|
||||
'navigator.locks is only present in secure contexts');
|
||||
assert_false('LockManager' in self,
|
||||
'LockManager is only present in secure contexts');
|
||||
assert_false('Lock' in self,
|
||||
'Lock interface is only present in secure contexts');
|
||||
}, 'API presence in non-secure contexts');
|
87
test/fixtures/wpt/web-locks/opaque-origin.https.html
vendored
Normal file
87
test/fixtures/wpt/web-locks/opaque-origin.https.html
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>Web Locks API: Opaque origins</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script>
|
||||
|
||||
function load_iframe(src, sandbox) {
|
||||
return new Promise(resolve => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.onload = () => { resolve(iframe); };
|
||||
if (sandbox)
|
||||
iframe.sandbox = sandbox;
|
||||
iframe.srcdoc = src;
|
||||
iframe.style.display = 'none';
|
||||
document.documentElement.appendChild(iframe);
|
||||
});
|
||||
}
|
||||
|
||||
function wait_for_message(iframe) {
|
||||
return new Promise(resolve => {
|
||||
self.addEventListener('message', function listener(e) {
|
||||
if (e.source === iframe.contentWindow) {
|
||||
resolve(e.data);
|
||||
self.removeEventListener('message', listener);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const script = `
|
||||
<script>
|
||||
"use strict";
|
||||
window.onmessage = async (ev) => {
|
||||
try {
|
||||
switch (ev.data) {
|
||||
case "request":
|
||||
await navigator.locks.request('name', lock => {});
|
||||
break;
|
||||
case "query":
|
||||
await navigator.locks.query();
|
||||
break;
|
||||
default:
|
||||
window.parent.postMessage({result: "unexpected message"}, "*");
|
||||
return;
|
||||
}
|
||||
window.parent.postMessage({result: "no exception"}, "*");
|
||||
} catch (ex) {
|
||||
window.parent.postMessage({result: ex.name}, "*");
|
||||
};
|
||||
};
|
||||
<\/script>
|
||||
`;
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe = await load_iframe(script);
|
||||
iframe.contentWindow.postMessage("request", '*');
|
||||
const message = await wait_for_message(iframe);
|
||||
assert_equals(message.result, 'no exception',
|
||||
'navigator.locks.request() should not throw');
|
||||
}, 'navigator.locks.request() in non-sandboxed iframe should not throw');
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe = await load_iframe(script, 'allow-scripts');
|
||||
iframe.contentWindow.postMessage("request", '*');
|
||||
const message = await wait_for_message(iframe);
|
||||
assert_equals(message.result, 'SecurityError',
|
||||
'Exception should be SecurityError');
|
||||
}, 'navigator.locks.request() in sandboxed iframe should throw SecurityError');
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe = await load_iframe(script);
|
||||
iframe.contentWindow.postMessage("query", '*');
|
||||
const message = await wait_for_message(iframe);
|
||||
assert_equals(message.result, 'no exception',
|
||||
'navigator.locks.request() should not throw');
|
||||
}, 'navigator.locks.query() in non-sandboxed iframe should not throw');
|
||||
|
||||
promise_test(async t => {
|
||||
const iframe = await load_iframe(script, 'allow-scripts');
|
||||
iframe.contentWindow.postMessage("query", '*');
|
||||
const message = await wait_for_message(iframe);
|
||||
assert_equals(message.result, 'SecurityError',
|
||||
'Exception should be SecurityError');
|
||||
}, 'navigator.locks.query() in sandboxed iframe should throw SecurityError');
|
||||
</script>
|
172
test/fixtures/wpt/web-locks/partitioned-web-locks.tentative.https.html
vendored
Normal file
172
test/fixtures/wpt/web-locks/partitioned-web-locks.tentative.https.html
vendored
Normal file
|
@ -0,0 +1,172 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Web Locks API: Partitioned WebLocks</title>
|
||||
|
||||
<!-- Pull in get_host_info() -->
|
||||
<script src="/common/get-host-info.sub.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="resources/helpers.js"></script>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
const { HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN } = get_host_info();
|
||||
// Map of lock_id => function that releases a lock.
|
||||
const held = new Map();
|
||||
let next_lock_id = 1;
|
||||
|
||||
// How this test works:
|
||||
// Step 1 (top-frame): request an exclusive web-lock and store its id
|
||||
// and release for clean-up.
|
||||
// Step 2 (top-frame): open a pop-up window and load a not-same-site
|
||||
// ./web-locks/resources/partitioned-parent.html
|
||||
// Step 3 (pop-up): load a same-site iframe inside the pop-up.
|
||||
// Step 4 (pop-up): send a web-lock request to the same-site iframe.
|
||||
// Step 5 (iframe): process the web-lock request and message the result
|
||||
// back to the pop-up.
|
||||
// Step 6 (pop-up): intercept the result message from the iframe and
|
||||
// send it to the top-frame.
|
||||
// Step 7 (top-frame): add cleanup hook.
|
||||
// Step 8 (top-frame): ensure that the same-site iframe's web-lock
|
||||
// request succeeds since it and the top-level site are successfully
|
||||
// partitioned and each can hold an exclusive lock.
|
||||
|
||||
async function third_party_test(t) {
|
||||
let target_url = HTTPS_ORIGIN + '/web-locks/resources/iframe.html';
|
||||
target_url = new URL(
|
||||
`/web-locks/resources/partitioned-parent.html?target=${encodeURIComponent(target_url)}`,
|
||||
HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
|
||||
|
||||
// Step 1.
|
||||
let lock_id = next_lock_id++;
|
||||
let [ promise, release ] = makePromiseAndResolveFunc();
|
||||
let released = navigator.locks.request('testLock', {mode: 'exclusive', ifAvailable: true},
|
||||
lock => {
|
||||
if (lock === null) {
|
||||
assert_true(false)
|
||||
return;
|
||||
}
|
||||
return promise;
|
||||
});
|
||||
held.set(lock_id, { release, released });
|
||||
|
||||
// Step 2.
|
||||
const w = window.open(target_url);
|
||||
const result = await new Promise(resolve => window.onmessage = resolve);
|
||||
|
||||
// Step 7.
|
||||
t.add_cleanup(() => {
|
||||
w.close();
|
||||
let released = [];
|
||||
for(let i = 1; i < next_lock_id; i++){
|
||||
let h = held.get(i);
|
||||
h.release();
|
||||
released.push(h.released);
|
||||
}
|
||||
return Promise.allSettled(released);
|
||||
});
|
||||
|
||||
// Step 8.
|
||||
// When 3rd party storage partitioning is enabled, the iframe should be able
|
||||
// to acquire a lock with the same name as one exclusively held by the opener
|
||||
// of its top window, even when that opener has the same origin.
|
||||
assert_equals(result.data.failed, undefined,
|
||||
'The 1p iframe failed to acquire the lock');
|
||||
}
|
||||
|
||||
promise_test(t => {
|
||||
return third_party_test(t);
|
||||
}, 'WebLocks of an iframe under a 3rd-party site are partitioned');
|
||||
|
||||
|
||||
// Optional Test: Checking for partitioned web locks in an A->B->A
|
||||
// (nested-iframe with cross-site ancestor chain) scenario.
|
||||
//
|
||||
// How this test works:
|
||||
// Nested Step 1 (top frame): request an exclusive web-lock and
|
||||
// store its id and release for clean-up.
|
||||
// Nested Step 2 (top frame): open a pop-up window and load a
|
||||
// same-site /web-locks/resources/partitioned-parent.html.
|
||||
// Nested Step 3 (pop-up): load a not-same-site "parent" iframe (A->B)
|
||||
// (/web-locks/resources/iframe-parent.html) inside the pop-up.
|
||||
// Nested Step 4 (pop-up): send a web-lock request to the parent iframe.
|
||||
// Nested Step 5 (parent iframe): load a "child" iframe (A->B->A)
|
||||
// (/web-locks/resources/iframe.html) that is same-site with the
|
||||
// pop-up inside the "parent" iframe.
|
||||
// Nested Step 6 (parent iframe): pass on the web-lock request message to
|
||||
// the "child" iframe.
|
||||
// Nested Step 7 (child iframe): process the web-lock request and message
|
||||
// the result to the parent iframe.
|
||||
// Nested Step 8 (parent iframe): intercept the result message from the
|
||||
// child iframe and send it to the pop-up.
|
||||
// Nested Step 9 (pop-up): intercept the result message from the parent
|
||||
// iframe and send it to the top frame.
|
||||
// Nested Step 10 (top frame): add cleanup hook
|
||||
// Nested Step 11 (top frame): ensure that the same-site iframe's web-lock
|
||||
// request succeeds since it and the top-level are successfully
|
||||
// partitioned and each can hold an exclusive lock.
|
||||
|
||||
// Map of lock_id => function that releases a lock.
|
||||
const held_2 = new Map();
|
||||
let next_lock_id_2 = 1;
|
||||
|
||||
async function nested_iframe_test(t) {
|
||||
// Create innermost child iframe (leaf).
|
||||
let leaf_url = HTTPS_ORIGIN + '/web-locks/resources/iframe.html';
|
||||
// Wrap the child iframe in its cross-origin parent (middle).
|
||||
let middle_url = new URL(
|
||||
`/web-locks/resources/iframe-parent.html?target=${encodeURIComponent(leaf_url)}`,
|
||||
HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
|
||||
// Embed the parent iframe in the top-level site (top).
|
||||
let top_url = new URL(
|
||||
`/web-locks/resources/partitioned-parent.html?target=${encodeURIComponent(middle_url)}`,
|
||||
HTTPS_ORIGIN + self.location.pathname);
|
||||
|
||||
// Nested Step 1.
|
||||
// Request the weblock for the top-level site.
|
||||
let lock_id = next_lock_id_2++;
|
||||
let [ promise, release ] = makePromiseAndResolveFunc();
|
||||
let released = navigator.locks.request('testLock', {mode: 'exclusive', ifAvailable: true},
|
||||
lock => {
|
||||
if (lock === null) {
|
||||
assert_true(false)
|
||||
return;
|
||||
}
|
||||
return promise;
|
||||
}).catch(error => alert(error.message));
|
||||
held_2.set(lock_id, { release, released });
|
||||
|
||||
// Nested Step 2.
|
||||
// Open the nested iframes. The script in the innermost child iframe
|
||||
// will attempt to obtain the same weblock as above.
|
||||
const w = window.open(top_url);
|
||||
const result = await new Promise(resolve => window.onmessage = resolve);
|
||||
|
||||
// Nested Step 10.
|
||||
t.add_cleanup(() => {
|
||||
w.close();
|
||||
let released = [];
|
||||
for(let i = 1; i < next_lock_id; i++){
|
||||
let h = held_2.get(i);
|
||||
h.release();
|
||||
released.push(h.released);
|
||||
}
|
||||
return Promise.allSettled(released);
|
||||
});
|
||||
|
||||
// Nested Step 11.
|
||||
// With third-party storage partitioning enabled, the same-site iframe
|
||||
// should be able to acquire the lock as it has a cross-site ancestor
|
||||
// and is partitioned separately from the top-level site.
|
||||
assert_equals(result.data.failed, undefined,
|
||||
'The 1p iframe failed to acquire the lock');
|
||||
}
|
||||
|
||||
promise_test(t => {
|
||||
return nested_iframe_test(t);
|
||||
}, 'WebLocks of a nested iframe with a cross-site ancestor are partitioned');
|
||||
</script>
|
||||
</body>
|
18
test/fixtures/wpt/web-locks/query-empty.https.any.js
vendored
Normal file
18
test/fixtures/wpt/web-locks/query-empty.https.any.js
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
// META: title=Web Locks API: navigator.locks.query method - no locks held
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
const state = await navigator.locks.query();
|
||||
|
||||
assert_own_property(state, 'pending', 'State has `pending` property');
|
||||
assert_true(Array.isArray(state.pending),
|
||||
'State `pending` property is an array');
|
||||
assert_array_equals(state.pending, [], 'Pending array is empty');
|
||||
|
||||
assert_own_property(state, 'held', 'State has `held` property');
|
||||
assert_true(Array.isArray(state.held), 'State `held` property is an array');
|
||||
assert_array_equals(state.held, [], 'Held array is empty');
|
||||
}, 'query() returns dictionary with empty arrays when no locks are held');
|
131
test/fixtures/wpt/web-locks/query-ordering.https.html
vendored
Normal file
131
test/fixtures/wpt/web-locks/query-ordering.https.html
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>Web Locks API: navigator.locks.query ordering</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="resources/helpers.js"></script>
|
||||
<style>iframe { display: none; }</style>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
// Grab a lock and hold until a release function is called. Resolves
|
||||
// to a release function.
|
||||
function getLockAndHoldUntilReleased(name, options) {
|
||||
let release;
|
||||
const promise = new Promise(resolve => { release = resolve; });
|
||||
return new Promise(resolve => {
|
||||
navigator.locks.request(name, options || {}, lock => {
|
||||
resolve(release);
|
||||
return promise;
|
||||
}).catch(_ => {});
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise resolved by the next message event.
|
||||
function nextMessage() {
|
||||
return new Promise(resolve => {
|
||||
window.addEventListener('message', event => {
|
||||
resolve(event.data);
|
||||
}, {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
// Tests the ordering constraints on the requested lock state returned by
|
||||
// navigator.locks.query(). Three separate iframes are instantiated to make
|
||||
// lock requests on the same resource, first in one order and then in another,
|
||||
// different order. For each set of requests, it is verified that the requests
|
||||
// appear in the result of navigator.locks.query() in the same order in which
|
||||
// they were made.
|
||||
//
|
||||
// It is necessary to use separate iframes here so that the lock requests have
|
||||
// distinguishable client_ids (otherwise it would not be possible to
|
||||
// distinguish the requests and thus impossible to verify ordering).
|
||||
promise_test(async testCase => {
|
||||
assert_implements(navigator.locks);
|
||||
const resourceName = uniqueName(testCase);
|
||||
|
||||
// Set up clients.
|
||||
const frame1 = await iframe('resources/iframe.html');
|
||||
const frame2 = await iframe('resources/iframe.html');
|
||||
const frame3 = await iframe('resources/iframe.html');
|
||||
testCase.add_cleanup(() => { frame1.remove(); });
|
||||
testCase.add_cleanup(() => { frame2.remove(); });
|
||||
testCase.add_cleanup(() => { frame3.remove(); });
|
||||
|
||||
// Collect the client ids.
|
||||
const clientId1 =
|
||||
(await postToFrameAndWait(frame1, {op: 'client_id',
|
||||
name: resourceName})).client_id;
|
||||
const clientId2 =
|
||||
(await postToFrameAndWait(frame2, {op: 'client_id',
|
||||
name: resourceName})).client_id;
|
||||
const clientId3 =
|
||||
(await postToFrameAndWait(frame3, {op: 'client_id',
|
||||
name: resourceName})).client_id;
|
||||
|
||||
// Preemptively take the lock.
|
||||
const firstRequestGroupReleaseFunction =
|
||||
await getLockAndHoldUntilReleased(resourceName);
|
||||
|
||||
// Queue the first group of lock requests from the different clients. These
|
||||
// will be blocked until firstRequestGroupReleaseFunction() is called.
|
||||
let lockId1;
|
||||
let lockId2;
|
||||
const lockPromise1 =
|
||||
postToFrameAndWait(frame1, {op: 'request', name: resourceName})
|
||||
.then(val => {lockId1 = val.lock_id;});
|
||||
const lockPromise2 =
|
||||
postToFrameAndWait(frame2, {op: 'request', name: resourceName})
|
||||
.then(val => {lockId2 = val.lock_id;});
|
||||
|
||||
// This third request will later be granted and held in order to block a
|
||||
// second group of requests to test a different client ordering. It is not
|
||||
// meant to be released.
|
||||
postToFrameAndWait(frame3, {op: 'request', name: resourceName});
|
||||
|
||||
// Request and wait for the release of a separate lock to ensure all previous
|
||||
// requests are processed.
|
||||
const checkpointName = uniqueName(testCase, 'checkpoint');
|
||||
const checkpointId = (await postToFrameAndWait(
|
||||
frame3,
|
||||
{op: 'request', name: checkpointName})).lock_id;
|
||||
await postToFrameAndWait(frame3, {op: 'release', lock_id: checkpointId});
|
||||
|
||||
// Query the state and test the ordering of requested locks.
|
||||
const state = await navigator.locks.query();
|
||||
const relevant_pending_ids = state.pending
|
||||
.filter(lock => [clientId1, clientId2, clientId3].includes(lock.clientId))
|
||||
.map(lock => lock.clientId);
|
||||
assert_array_equals(
|
||||
[clientId1, clientId2, clientId3],
|
||||
relevant_pending_ids,
|
||||
'Querying the state should return requested locks in the order they were '
|
||||
+ 'requested.');
|
||||
|
||||
// Add the second group of requests from the clients in a new order.
|
||||
postToFrameAndWait(frame3, {op: 'request', name: resourceName});
|
||||
postToFrameAndWait(frame1, {op: 'request', name: resourceName});
|
||||
postToFrameAndWait(frame2, {op: 'request', name: resourceName});
|
||||
|
||||
// Release locks such that only the newly added locks are requested. This
|
||||
// acts like a checkpoint for the newly queued requests.
|
||||
firstRequestGroupReleaseFunction();
|
||||
await lockPromise1;
|
||||
await postToFrameAndWait(frame1, {op: 'release', lock_id: lockId1});
|
||||
await lockPromise2;
|
||||
await postToFrameAndWait(frame2, {op: 'release', lock_id: lockId2});
|
||||
|
||||
// Query the state and test the new ordering.
|
||||
const state2 = await navigator.locks.query();
|
||||
const relevant_pending_ids2 = state2.pending
|
||||
.filter(lock => [clientId1, clientId2, clientId3].includes(lock.clientId))
|
||||
.map(lock => lock.clientId);
|
||||
assert_array_equals(
|
||||
[clientId3, clientId1, clientId2],
|
||||
relevant_pending_ids2,
|
||||
'Querying the state should return requested locks in the order they were '
|
||||
+ 'requested.');
|
||||
|
||||
}, 'Requests appear in state in order made.');
|
||||
</script>
|
227
test/fixtures/wpt/web-locks/query.https.any.js
vendored
Normal file
227
test/fixtures/wpt/web-locks/query.https.any.js
vendored
Normal file
|
@ -0,0 +1,227 @@
|
|||
// META: title=Web Locks API: navigator.locks.query method
|
||||
// META: script=resources/helpers.js
|
||||
|
||||
'use strict';
|
||||
|
||||
// Returns an array of the modes for the locks with matching name.
|
||||
function modes(list, name) {
|
||||
return list.filter(item => item.name === name).map(item => item.mode);
|
||||
}
|
||||
// Returns an array of the clientIds for the locks with matching name.
|
||||
function clients(list, name) {
|
||||
return list.filter(item => item.name === name).map(item => item.clientId);
|
||||
}
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res, async lock1 => {
|
||||
// Attempt to request this again - should be blocked.
|
||||
let lock2_acquired = false;
|
||||
navigator.locks.request(res, lock2 => { lock2_acquired = true; });
|
||||
|
||||
// Verify that it was blocked.
|
||||
await navigator.locks.request(res, {ifAvailable: true}, async lock3 => {
|
||||
assert_false(lock2_acquired, 'second request should be blocked');
|
||||
assert_equals(lock3, null, 'third request should have failed');
|
||||
|
||||
const state = await navigator.locks.query();
|
||||
|
||||
assert_own_property(state, 'pending', 'State has `pending` property');
|
||||
assert_true(Array.isArray(state.pending),
|
||||
'State `pending` property is an array');
|
||||
const pending_info = state.pending[0];
|
||||
assert_own_property(pending_info, 'name',
|
||||
'Pending info dictionary has `name` property');
|
||||
assert_own_property(pending_info, 'mode',
|
||||
'Pending info dictionary has `mode` property');
|
||||
assert_own_property(pending_info, 'clientId',
|
||||
'Pending info dictionary has `clientId` property');
|
||||
|
||||
assert_own_property(state, 'held', 'State has `held` property');
|
||||
assert_true(Array.isArray(state.held),
|
||||
'State `held` property is an array');
|
||||
const held_info = state.held[0];
|
||||
assert_own_property(held_info, 'name',
|
||||
'Held info dictionary has `name` property');
|
||||
assert_own_property(held_info, 'mode',
|
||||
'Held info dictionary has `mode` property');
|
||||
assert_own_property(held_info, 'clientId',
|
||||
'Held info dictionary has `clientId` property');
|
||||
});
|
||||
});
|
||||
}, 'query() returns dictionaries with expected properties');
|
||||
|
||||
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res, async lock1 => {
|
||||
const state = await navigator.locks.query();
|
||||
assert_array_equals(modes(state.held, res), ['exclusive'],
|
||||
'Held lock should appear once');
|
||||
});
|
||||
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock1 => {
|
||||
const state = await navigator.locks.query();
|
||||
assert_array_equals(modes(state.held, res), ['shared'],
|
||||
'Held lock should appear once');
|
||||
});
|
||||
}, 'query() reports individual held locks');
|
||||
|
||||
promise_test(async t => {
|
||||
const res1 = uniqueName(t);
|
||||
const res2 = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res1, async lock1 => {
|
||||
await navigator.locks.request(res2, {mode: 'shared'}, async lock2 => {
|
||||
const state = await navigator.locks.query();
|
||||
assert_array_equals(modes(state.held, res1), ['exclusive'],
|
||||
'Held lock should appear once');
|
||||
assert_array_equals(modes(state.held, res2), ['shared'],
|
||||
'Held lock should appear once');
|
||||
});
|
||||
});
|
||||
}, 'query() reports multiple held locks');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res, async lock1 => {
|
||||
// Attempt to request this again - should be blocked.
|
||||
let lock2_acquired = false;
|
||||
navigator.locks.request(res, lock2 => { lock2_acquired = true; });
|
||||
|
||||
// Verify that it was blocked.
|
||||
await navigator.locks.request(res, {ifAvailable: true}, async lock3 => {
|
||||
assert_false(lock2_acquired, 'second request should be blocked');
|
||||
assert_equals(lock3, null, 'third request should have failed');
|
||||
|
||||
const state = await navigator.locks.query();
|
||||
assert_array_equals(modes(state.pending, res), ['exclusive'],
|
||||
'Pending lock should appear once');
|
||||
assert_array_equals(modes(state.held, res), ['exclusive'],
|
||||
'Held lock should appear once');
|
||||
});
|
||||
});
|
||||
}, 'query() reports pending and held locks');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock1 => {
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock2 => {
|
||||
const state = await navigator.locks.query();
|
||||
assert_array_equals(modes(state.held, res), ['shared', 'shared'],
|
||||
'Held lock should appear twice');
|
||||
});
|
||||
});
|
||||
}, 'query() reports held shared locks with appropriate count');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res, async lock1 => {
|
||||
let lock2_acquired = false, lock3_acquired = false;
|
||||
navigator.locks.request(res, {mode: 'shared'},
|
||||
lock2 => { lock2_acquired = true; });
|
||||
navigator.locks.request(res, {mode: 'shared'},
|
||||
lock3 => { lock3_acquired = true; });
|
||||
|
||||
await navigator.locks.request(res, {ifAvailable: true}, async lock4 => {
|
||||
assert_equals(lock4, null, 'lock should not be available');
|
||||
assert_false(lock2_acquired, 'second attempt should be blocked');
|
||||
assert_false(lock3_acquired, 'third attempt should be blocked');
|
||||
|
||||
const state = await navigator.locks.query();
|
||||
assert_array_equals(modes(state.held, res), ['exclusive'],
|
||||
'Held lock should appear once');
|
||||
|
||||
assert_array_equals(modes(state.pending, res), ['shared', 'shared'],
|
||||
'Pending lock should appear twice');
|
||||
});
|
||||
});
|
||||
}, 'query() reports pending shared locks with appropriate count');
|
||||
|
||||
promise_test(async t => {
|
||||
const res1 = uniqueName(t);
|
||||
const res2 = uniqueName(t);
|
||||
|
||||
await navigator.locks.request(res1, async lock1 => {
|
||||
await navigator.locks.request(res2, async lock2 => {
|
||||
const state = await navigator.locks.query();
|
||||
|
||||
const res1_clients = clients(state.held, res1);
|
||||
const res2_clients = clients(state.held, res2);
|
||||
|
||||
assert_equals(res1_clients.length, 1, 'Each lock should have one holder');
|
||||
assert_equals(res2_clients.length, 1, 'Each lock should have one holder');
|
||||
|
||||
assert_array_equals(res1_clients, res2_clients,
|
||||
'Both locks should have same clientId');
|
||||
});
|
||||
});
|
||||
}, 'query() reports the same clientId for held locks from the same context');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const worker = new Worker('resources/worker.js');
|
||||
t.add_cleanup(() => { worker.terminate(); });
|
||||
|
||||
await postToWorkerAndWait(
|
||||
worker, {op: 'request', name: res, mode: 'shared'});
|
||||
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock => {
|
||||
const state = await navigator.locks.query();
|
||||
const res_clients = clients(state.held, res);
|
||||
assert_equals(res_clients.length, 2, 'Clients should have same resource');
|
||||
assert_not_equals(res_clients[0], res_clients[1],
|
||||
'Clients should have different ids');
|
||||
});
|
||||
}, 'query() reports different ids for held locks from different contexts');
|
||||
|
||||
promise_test(async t => {
|
||||
const res1 = uniqueName(t);
|
||||
const res2 = uniqueName(t);
|
||||
|
||||
const worker = new Worker('resources/worker.js');
|
||||
t.add_cleanup(() => { worker.terminate(); });
|
||||
|
||||
// Acquire 1 in the worker.
|
||||
await postToWorkerAndWait(worker, {op: 'request', name: res1})
|
||||
|
||||
// Acquire 2 here.
|
||||
await new Promise(resolve => {
|
||||
navigator.locks.request(res2, lock => {
|
||||
resolve();
|
||||
return new Promise(() => {}); // Never released.
|
||||
});
|
||||
});
|
||||
|
||||
// Request 2 in the worker.
|
||||
postToWorkerAndWait(worker, {op: 'request', name: res2});
|
||||
assert_true((await postToWorkerAndWait(worker, {
|
||||
op: 'request', name: res2, ifAvailable: true
|
||||
})).failed, 'Lock request should have failed');
|
||||
|
||||
// Request 1 here.
|
||||
navigator.locks.request(
|
||||
res1, t.unreached_func('Lock should not be acquired'));
|
||||
|
||||
// Verify that we're seeing a deadlock.
|
||||
const state = await navigator.locks.query();
|
||||
const res1_held_clients = clients(state.held, res1);
|
||||
const res2_held_clients = clients(state.held, res2);
|
||||
const res1_pending_clients = clients(state.pending, res1);
|
||||
const res2_pending_clients = clients(state.pending, res2);
|
||||
|
||||
assert_equals(res1_held_clients.length, 1);
|
||||
assert_equals(res2_held_clients.length, 1);
|
||||
assert_equals(res1_pending_clients.length, 1);
|
||||
assert_equals(res2_pending_clients.length, 1);
|
||||
|
||||
assert_equals(res1_held_clients[0], res2_pending_clients[0]);
|
||||
assert_equals(res2_held_clients[0], res1_pending_clients[0]);
|
||||
}, 'query() can observe a deadlock');
|
56
test/fixtures/wpt/web-locks/resource-names.https.any.js
vendored
Normal file
56
test/fixtures/wpt/web-locks/resource-names.https.any.js
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
// META: title=Web Locks API: Resources DOMString edge cases
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
function code_points(s) {
|
||||
return [...s]
|
||||
.map(c => '0x' + c.charCodeAt(0).toString(16).toUpperCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
[
|
||||
'', // Empty strings
|
||||
'abc\x00def', // Embedded NUL
|
||||
'\uD800', // Unpaired low surrogage
|
||||
'\uDC00', // Unpaired high surrogage
|
||||
'\uDC00\uD800', // Swapped surrogate pair
|
||||
'\uFFFF' // Non-character
|
||||
].forEach(string => {
|
||||
promise_test(async t => {
|
||||
await navigator.locks.request(string, lock => {
|
||||
assert_equals(lock.name, string,
|
||||
'Requested name matches granted name');
|
||||
});
|
||||
}, 'DOMString: ' + code_points(string));
|
||||
});
|
||||
|
||||
promise_test(async t => {
|
||||
// '\uD800' treated as a USVString would become '\uFFFD'.
|
||||
await navigator.locks.request('\uD800', async lock => {
|
||||
assert_equals(lock.name, '\uD800');
|
||||
|
||||
// |lock| is held for the duration of this name. It
|
||||
// Should not block acquiring |lock2| with a distinct
|
||||
// DOMString.
|
||||
await navigator.locks.request('\uFFFD', lock2 => {
|
||||
assert_equals(lock2.name, '\uFFFD');
|
||||
});
|
||||
|
||||
// If we did not time out, this passed.
|
||||
});
|
||||
}, 'Resource names that are not valid UTF-16 are not mangled');
|
||||
|
||||
promise_test(async t => {
|
||||
for (const name of ['-', '-foo']) {
|
||||
await promise_rejects_dom(
|
||||
t, 'NotSupportedError',
|
||||
navigator.locks.request(name, lock => {}),
|
||||
'Names starting with "-" should be rejected');
|
||||
}
|
||||
let got_lock = false;
|
||||
await navigator.locks.request('x-anything', lock => {
|
||||
got_lock = true;
|
||||
});
|
||||
assert_true(got_lock, 'Names with embedded "-" should be accepted');
|
||||
}, 'Names cannot start with "-"');
|
91
test/fixtures/wpt/web-locks/resources/helpers.js
vendored
Normal file
91
test/fixtures/wpt/web-locks/resources/helpers.js
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
// Test helpers used by multiple Web Locks API tests.
|
||||
(() => {
|
||||
|
||||
// Generate a unique resource identifier, using the script path and
|
||||
// test case name. This is useful to avoid lock interference between
|
||||
// test cases.
|
||||
let res_num = 0;
|
||||
self.uniqueName = (testCase, prefix) => {
|
||||
return `${self.location.pathname}-${prefix}-${testCase.name}-${++res_num}`;
|
||||
};
|
||||
self.uniqueNameByQuery = () => {
|
||||
const prefix = new URL(location.href).searchParams.get('prefix');
|
||||
return `${prefix}-${++res_num}`;
|
||||
}
|
||||
|
||||
// Inject an iframe showing the given url into the page, and resolve
|
||||
// the returned promise when the frame is loaded.
|
||||
self.iframe = url => new Promise(resolve => {
|
||||
const element = document.createElement('iframe');
|
||||
element.addEventListener(
|
||||
'load', () => { resolve(element); }, { once: true });
|
||||
element.src = url;
|
||||
document.documentElement.appendChild(element);
|
||||
});
|
||||
|
||||
// Post a message to the target frame, and resolve the returned
|
||||
// promise when a response comes back. The posted data is annotated
|
||||
// with unique id to track the response. This assumes the use of
|
||||
// 'iframe.html' as the frame, which implements this protocol.
|
||||
let next_request_id = 0;
|
||||
self.postToFrameAndWait = (frame, data) => {
|
||||
const iframe_window = frame.contentWindow;
|
||||
data.rqid = next_request_id++;
|
||||
iframe_window.postMessage(data, '*');
|
||||
return new Promise(resolve => {
|
||||
const listener = event => {
|
||||
if (event.source !== iframe_window || event.data.rqid !== data.rqid)
|
||||
return;
|
||||
self.removeEventListener('message', listener);
|
||||
resolve(event.data);
|
||||
};
|
||||
self.addEventListener('message', listener);
|
||||
});
|
||||
};
|
||||
|
||||
// Post a message to the target worker, and resolve the returned
|
||||
// promise when a response comes back. The posted data is annotated
|
||||
// with unique id to track the response. This assumes the use of
|
||||
// 'worker.js' as the worker, which implements this protocol.
|
||||
self.postToWorkerAndWait = (worker, data) => {
|
||||
return new Promise(resolve => {
|
||||
data.rqid = next_request_id++;
|
||||
worker.postMessage(data);
|
||||
const listener = event => {
|
||||
if (event.data.rqid !== data.rqid)
|
||||
return;
|
||||
worker.removeEventListener('message', listener);
|
||||
resolve(event.data);
|
||||
};
|
||||
worker.addEventListener('message', listener);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a lock and hold it until the subtest ends.
|
||||
* @param {*} t test runner object
|
||||
* @param {string} name lock name
|
||||
* @param {LockOptions=} options lock options
|
||||
* @returns
|
||||
*/
|
||||
self.requestLockAndHold = (t, name, options = {}) => {
|
||||
let [promise, resolve] = self.makePromiseAndResolveFunc();
|
||||
const released = navigator.locks.request(name, options, () => promise);
|
||||
// Add a cleanup function that releases the lock by resolving the promise,
|
||||
// and then waits until the lock is really released, to avoid contaminating
|
||||
// following tests with temporarily held locks.
|
||||
t.add_cleanup(() => {
|
||||
resolve();
|
||||
// Cleanup shouldn't fail if the request is aborted.
|
||||
return released.catch(() => undefined);
|
||||
});
|
||||
return released;
|
||||
};
|
||||
|
||||
self.makePromiseAndResolveFunc = () => {
|
||||
let resolve;
|
||||
const promise = new Promise(r => { resolve = r; });
|
||||
return [promise, resolve];
|
||||
};
|
||||
|
||||
})();
|
37
test/fixtures/wpt/web-locks/resources/iframe-parent.html
vendored
Normal file
37
test/fixtures/wpt/web-locks/resources/iframe-parent.html
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<title>Helper IFrame</title>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
async function onLoad() {
|
||||
// Nested Step 5: wpt/web-locks/partitioned-web-locks.tentative.https.html
|
||||
// Load the innermost child iframe and its content.
|
||||
const params = new URLSearchParams(self.location.search);
|
||||
const frame = document.createElement('iframe');
|
||||
frame.src = params.get('target');
|
||||
document.body.appendChild(frame);
|
||||
|
||||
self.addEventListener('message', evt => {
|
||||
// Nested Step 6: wpt/web-locks/partitioned-web-locks.tentative.https.html
|
||||
// Pass any operations request messages to the
|
||||
// innermost child iframe.
|
||||
if (evt.data.op){
|
||||
// Ensure that the iframe has loaded before passing
|
||||
// on the message.
|
||||
frame.addEventListener('load', function(){
|
||||
frame.contentWindow.postMessage(evt.data, '*');
|
||||
});
|
||||
}
|
||||
// Nested Step 8: wpt/web-locks/partitioned-web-locks.tentative.https.html
|
||||
else {
|
||||
// All other messages, should be sent back to the
|
||||
// top-level site.
|
||||
if (self.opener)
|
||||
self.opener.postMessage(evt.data, '*');
|
||||
else
|
||||
self.top.postMessage(evt.data, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
self.addEventListener('load', onLoad);
|
||||
</script>
|
52
test/fixtures/wpt/web-locks/resources/iframe.html
vendored
Normal file
52
test/fixtures/wpt/web-locks/resources/iframe.html
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<title>Helper IFrame</title>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
// Map of lock_id => function that releases a lock.
|
||||
|
||||
const held = new Map();
|
||||
let next_lock_id = 1;
|
||||
|
||||
self.addEventListener('message', e => {
|
||||
function respond(data) {
|
||||
parent.postMessage(Object.assign(data, {rqid: e.data.rqid}), '*');
|
||||
}
|
||||
|
||||
switch (e.data.op) {
|
||||
case 'request':
|
||||
navigator.locks.request(
|
||||
e.data.name, {
|
||||
mode: e.data.mode || 'exclusive',
|
||||
ifAvailable: e.data.ifAvailable || false
|
||||
}, lock => {
|
||||
if (lock === null) {
|
||||
respond({ack: 'request', failed: true});
|
||||
return;
|
||||
}
|
||||
let lock_id = next_lock_id++;
|
||||
let release;
|
||||
const promise = new Promise(r => { release = r; });
|
||||
held.set(lock_id, release);
|
||||
respond({ack: 'request', lock_id: lock_id});
|
||||
return promise
|
||||
});
|
||||
break;
|
||||
|
||||
case 'release':
|
||||
held.get(e.data.lock_id)();
|
||||
held.delete(e.data.lock_id);
|
||||
respond({ack: 'release', lock_id: e.data.lock_id});
|
||||
break;
|
||||
|
||||
case 'client_id':
|
||||
navigator.locks.request(e.data.name, async lock => {
|
||||
const lock_state = await navigator.locks.query();
|
||||
const held_lock =
|
||||
lock_state.held.filter(l => l.name === lock.name)[0];
|
||||
respond({ack: 'client_id', client_id: held_lock.clientId});
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
10
test/fixtures/wpt/web-locks/resources/parentworker.js
vendored
Normal file
10
test/fixtures/wpt/web-locks/resources/parentworker.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Just transparently forwards things to the child worker
|
||||
|
||||
importScripts("/web-locks/resources/helpers.js");
|
||||
const worker = new Worker("/web-locks/resources/worker.js");
|
||||
|
||||
self.addEventListener("message", async ev => {
|
||||
const data = await postToWorkerAndWait(worker, ev.data);
|
||||
data.rqid = ev.data.rqid;
|
||||
postMessage(data);
|
||||
});
|
30
test/fixtures/wpt/web-locks/resources/partitioned-parent.html
vendored
Normal file
30
test/fixtures/wpt/web-locks/resources/partitioned-parent.html
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="referrer" content="origin">
|
||||
<script>
|
||||
async function onLoad() {
|
||||
// Step 6 and Nested Step 9:
|
||||
// wpt/web-locks/partitioned-web-locks.tentative.https.html
|
||||
self.addEventListener('message', evt => {
|
||||
if (self.opener)
|
||||
self.opener.postMessage(evt.data, '*');
|
||||
else
|
||||
self.top.postMessage(evt.data, '*');
|
||||
}, { once: true });
|
||||
|
||||
// Step 3 and Nested Step 3:
|
||||
// wpt/web-locks/partitioned-web-locks.tentative.https.html
|
||||
const params = new URLSearchParams(self.location.search);
|
||||
const frame = document.createElement('iframe');
|
||||
frame.src = params.get('target');
|
||||
document.body.appendChild(frame);
|
||||
|
||||
// Step 4 and Nested Step 4:
|
||||
// wpt/web-locks/partitioned-web-locks.tentative.https.html
|
||||
frame.addEventListener('load', function(){
|
||||
frame.contentWindow.postMessage({op: 'request',
|
||||
name: 'testLock', ifAvailable: true}, '*');
|
||||
});
|
||||
}
|
||||
self.addEventListener('load', onLoad);
|
||||
</script>
|
7
test/fixtures/wpt/web-locks/resources/service-worker.js
vendored
Normal file
7
test/fixtures/wpt/web-locks/resources/service-worker.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Responds to '/clientId' with the request's clientId.
|
||||
self.addEventListener('fetch', e => {
|
||||
if (new URL(e.request.url).pathname === '/clientId') {
|
||||
e.respondWith(new Response(JSON.stringify({clientId: e.clientId})));
|
||||
return;
|
||||
}
|
||||
});
|
35
test/fixtures/wpt/web-locks/resources/sw-controlled-iframe.html
vendored
Normal file
35
test/fixtures/wpt/web-locks/resources/sw-controlled-iframe.html
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>iframe used in clientId test</title>
|
||||
<script>
|
||||
|
||||
self.onmessage = async event => {
|
||||
try {
|
||||
if (event.data === 'get_sw_client_id') {
|
||||
// Use the controlling service worker to determine
|
||||
// this client's id according to the Service Worker.
|
||||
const response = await fetch('/clientId');
|
||||
const data = await response.json();
|
||||
window.parent.postMessage(data.clientId, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data === 'get_lock_client_id') {
|
||||
// Grab a lock, then query the lock manager for state to
|
||||
// determine this client's id according to the lock manager.
|
||||
await navigator.locks.request('lock-name', async lock => {
|
||||
const lock_state = await navigator.locks.query();
|
||||
const held_lock = lock_state.held.filter(l => l.name === lock.name)[0];
|
||||
window.parent.postMessage(held_lock.clientId, '*');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.parent.postMessage(`unknown request: ${event.data}`, '*');
|
||||
} catch (ex) {
|
||||
// In case of test failure, don't leave parent window hanging.
|
||||
window.parent.postMessage(`${ex.name}: ${ex.message}`, '*');
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
56
test/fixtures/wpt/web-locks/resources/worker.js
vendored
Normal file
56
test/fixtures/wpt/web-locks/resources/worker.js
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
// Map of id => function that releases a lock.
|
||||
|
||||
const held = new Map();
|
||||
let next_lock_id = 1;
|
||||
|
||||
function processMessage(e) {
|
||||
const target = this;
|
||||
|
||||
function respond(data) {
|
||||
target.postMessage(Object.assign(data, {rqid: e.data.rqid}));
|
||||
}
|
||||
|
||||
switch (e.data.op) {
|
||||
case 'request': {
|
||||
const controller = new AbortController();
|
||||
navigator.locks.request(
|
||||
e.data.name, {
|
||||
mode: e.data.mode || 'exclusive',
|
||||
ifAvailable: e.data.ifAvailable || false,
|
||||
signal: e.data.abortImmediately ? controller.signal : undefined,
|
||||
}, lock => {
|
||||
if (lock === null) {
|
||||
respond({ack: 'request', failed: true});
|
||||
return;
|
||||
}
|
||||
let lock_id = next_lock_id++;
|
||||
let release;
|
||||
const promise = new Promise(r => { release = r; });
|
||||
held.set(lock_id, release);
|
||||
respond({ack: 'request', lock_id: lock_id});
|
||||
return promise;
|
||||
}).catch(e => {
|
||||
respond({ack: 'request', error: e.name});
|
||||
});
|
||||
if (e.data.abortImmediately) {
|
||||
controller.abort();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'release':
|
||||
held.get(e.data.lock_id)();
|
||||
held.delete(e.data.lock_id);
|
||||
respond({ack: 'release', lock_id: e.data.lock_id});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('message', processMessage);
|
||||
|
||||
self.addEventListener('connect', ev => {
|
||||
// Shared worker case
|
||||
ev.ports[0].onmessage = processMessage;
|
||||
});
|
14
test/fixtures/wpt/web-locks/secure-context.https.any.js
vendored
Normal file
14
test/fixtures/wpt/web-locks/secure-context.https.any.js
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
// META: title=Web Locks API: API requires secure context
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
test(t => {
|
||||
assert_true(self.isSecureContext);
|
||||
assert_idl_attribute(navigator, 'locks',
|
||||
'navigator.locks exists in secure context');
|
||||
assert_true('LockManager' in self,
|
||||
'LockManager is present in secure contexts');
|
||||
assert_true('Lock' in self,
|
||||
'Lock interface is present in secure contexts');
|
||||
}, 'API presence in secure contexts');
|
261
test/fixtures/wpt/web-locks/signal.https.any.js
vendored
Normal file
261
test/fixtures/wpt/web-locks/signal.https.any.js
vendored
Normal file
|
@ -0,0 +1,261 @@
|
|||
// META: title=Web Locks API: AbortSignal integration
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
// These cases should not work:
|
||||
for (const signal of ['string', 12.34, false, {}, Symbol(), () => {}, self]) {
|
||||
await promise_rejects_js(
|
||||
t, TypeError,
|
||||
navigator.locks.request(
|
||||
res, {signal}, t.unreached_func('callback should not run')),
|
||||
'Bindings should throw if the signal option is a not an AbortSignal');
|
||||
}
|
||||
}, 'The signal option must be an AbortSignal');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
await promise_rejects_dom(
|
||||
t, 'AbortError',
|
||||
navigator.locks.request(res, {signal: controller.signal},
|
||||
t.unreached_func('callback should not run')),
|
||||
'Request should reject with AbortError');
|
||||
}, 'Passing an already aborted signal aborts');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
const reason = 'My dog ate it.';
|
||||
controller.abort(reason);
|
||||
|
||||
const promise =
|
||||
navigator.locks.request(res, {signal: controller.signal},
|
||||
t.unreached_func('callback should not run'));
|
||||
|
||||
await promise_rejects_exactly(
|
||||
t, reason, promise, "Rejection should give the abort reason");
|
||||
}, 'Passing an already aborted signal rejects with the custom abort reason.');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
const promise =
|
||||
navigator.locks.request(res, {signal: controller.signal},
|
||||
t.unreached_func('callback should not run'));
|
||||
|
||||
await promise_rejects_exactly(
|
||||
t, controller.signal.reason, promise,
|
||||
"Rejection should give the abort reason");
|
||||
}, 'Passing an already aborted signal rejects with the default abort reason.');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
// Grab a lock and hold it until this subtest completes.
|
||||
requestLockAndHold(t, res);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise =
|
||||
navigator.locks.request(res, {signal: controller.signal},
|
||||
t.unreached_func('callback should not run'));
|
||||
|
||||
// Verify the request is enqueued:
|
||||
const state = await navigator.locks.query();
|
||||
assert_equals(state.held.filter(lock => lock.name === res).length, 1,
|
||||
'Number of held locks');
|
||||
assert_equals(state.pending.filter(lock => lock.name === res).length, 1,
|
||||
'Number of pending locks');
|
||||
|
||||
const rejected = promise_rejects_dom(
|
||||
t, 'AbortError', promise, 'Request should reject with AbortError');
|
||||
|
||||
controller.abort();
|
||||
|
||||
await rejected;
|
||||
|
||||
}, 'An aborted request results in AbortError');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
// Grab a lock and hold it until this subtest completes.
|
||||
requestLockAndHold(t, res);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise =
|
||||
navigator.locks.request(res, {signal: controller.signal}, lock => {});
|
||||
|
||||
// Verify the request is enqueued:
|
||||
const state = await navigator.locks.query();
|
||||
assert_equals(state.held.filter(lock => lock.name === res).length, 1,
|
||||
'Number of held locks');
|
||||
assert_equals(state.pending.filter(lock => lock.name === res).length, 1,
|
||||
'Number of pending locks');
|
||||
|
||||
const rejected = promise_rejects_dom(
|
||||
t, 'AbortError', promise, 'Request should reject with AbortError');
|
||||
|
||||
let callback_called = false;
|
||||
t.step_timeout(() => {
|
||||
callback_called = true;
|
||||
controller.abort();
|
||||
}, 10);
|
||||
|
||||
await rejected;
|
||||
assert_true(callback_called, 'timeout should have caused the abort');
|
||||
|
||||
}, 'Abort after a timeout');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
let got_lock = false;
|
||||
await navigator.locks.request(
|
||||
res, {signal: controller.signal}, async lock => { got_lock = true; });
|
||||
|
||||
assert_true(got_lock, 'Lock should be acquired if abort is not signaled.');
|
||||
|
||||
}, 'Signal that is not aborted');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
let got_lock = false;
|
||||
const p = navigator.locks.request(
|
||||
res, {signal: controller.signal}, lock => { got_lock = true; });
|
||||
|
||||
// Even though lock is grantable, this abort should be processed synchronously.
|
||||
controller.abort();
|
||||
|
||||
await promise_rejects_dom(t, 'AbortError', p, 'Request should abort');
|
||||
|
||||
assert_false(got_lock, 'Request should be aborted if signal is synchronous');
|
||||
|
||||
await navigator.locks.request(res, lock => { got_lock = true; });
|
||||
assert_true(got_lock, 'Subsequent request should not be blocked');
|
||||
|
||||
}, 'Synchronously signaled abort');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
// Make a promise that resolves when the lock is acquired.
|
||||
const [acquired_promise, acquired_func] = makePromiseAndResolveFunc();
|
||||
|
||||
// Request the lock.
|
||||
let release_func;
|
||||
const released_promise = navigator.locks.request(
|
||||
res, {signal: controller.signal}, lock => {
|
||||
acquired_func();
|
||||
|
||||
// Hold lock until release_func is called.
|
||||
const [waiting_promise, waiting_func] = makePromiseAndResolveFunc();
|
||||
release_func = waiting_func;
|
||||
return waiting_promise;
|
||||
});
|
||||
|
||||
// Wait for the lock to be acquired.
|
||||
await acquired_promise;
|
||||
|
||||
// Signal an abort.
|
||||
controller.abort();
|
||||
|
||||
// Release the lock.
|
||||
release_func('resolved ok');
|
||||
|
||||
assert_equals(await released_promise, 'resolved ok',
|
||||
'Lock released promise should not reject');
|
||||
|
||||
}, 'Abort signaled after lock granted');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
// Make a promise that resolves when the lock is acquired.
|
||||
const [acquired_promise, acquired_func] = makePromiseAndResolveFunc();
|
||||
|
||||
// Request the lock.
|
||||
let release_func;
|
||||
const released_promise = navigator.locks.request(
|
||||
res, {signal: controller.signal}, lock => {
|
||||
acquired_func();
|
||||
|
||||
// Hold lock until release_func is called.
|
||||
const [waiting_promise, waiting_func] = makePromiseAndResolveFunc();
|
||||
release_func = waiting_func;
|
||||
return waiting_promise;
|
||||
});
|
||||
|
||||
// Wait for the lock to be acquired.
|
||||
await acquired_promise;
|
||||
|
||||
// Release the lock.
|
||||
release_func('resolved ok');
|
||||
|
||||
// Signal an abort.
|
||||
controller.abort();
|
||||
|
||||
assert_equals(await released_promise, 'resolved ok',
|
||||
'Lock released promise should not reject');
|
||||
|
||||
}, 'Abort signaled after lock released');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
const first = requestLockAndHold(t, res, { signal: controller.signal });
|
||||
const next = navigator.locks.request(res, () => "resolved");
|
||||
controller.abort();
|
||||
|
||||
await promise_rejects_dom(t, "AbortError", first, "Request should abort");
|
||||
assert_equals(
|
||||
await next,
|
||||
"resolved",
|
||||
"The next request is processed after abort"
|
||||
);
|
||||
}, "Abort should process the next pending lock request");
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
const promise = requestLockAndHold(t, res, { signal: controller.signal });
|
||||
|
||||
const reason = "My cat handled it";
|
||||
controller.abort(reason);
|
||||
|
||||
await promise_rejects_exactly(t, reason, promise, "Rejection should give the abort reason");
|
||||
}, "Aborted promise should reject with the custom abort reason");
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
const controller = new AbortController();
|
||||
const promise = requestLockAndHold(t, res, { signal: controller.signal });
|
||||
|
||||
controller.abort();
|
||||
|
||||
await promise_rejects_exactly(t, controller.signal.reason, promise, "Should be the same reason");
|
||||
}, "Aborted promise should reject with the default abort reason");
|
91
test/fixtures/wpt/web-locks/steal.https.any.js
vendored
Normal file
91
test/fixtures/wpt/web-locks/steal.https.any.js
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
// META: title=Web Locks API: steal option
|
||||
// META: script=resources/helpers.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
const never_settled = new Promise(resolve => { /* never */ });
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
let callback_called = false;
|
||||
await navigator.locks.request(res, {steal: true}, lock => {
|
||||
callback_called = true;
|
||||
assert_not_equals(lock, null, 'Lock should be granted');
|
||||
});
|
||||
assert_true(callback_called, 'Callback should be called');
|
||||
}, 'Lock available');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
let callback_called = false;
|
||||
|
||||
// Grab and hold the lock.
|
||||
navigator.locks.request(res, lock => never_settled).catch(_ => {});
|
||||
|
||||
// Steal it.
|
||||
await navigator.locks.request(res, {steal: true}, lock => {
|
||||
callback_called = true;
|
||||
assert_not_equals(lock, null, 'Lock should be granted');
|
||||
});
|
||||
|
||||
assert_true(callback_called, 'Callback should be called');
|
||||
}, 'Lock not available');
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
// Grab and hold the lock.
|
||||
const promise = navigator.locks.request(res, lock => never_settled);
|
||||
const assertion = promise_rejects_dom(
|
||||
t, 'AbortError', promise, `Initial request's promise should reject`);
|
||||
|
||||
// Steal it.
|
||||
await navigator.locks.request(res, {steal: true}, lock => {});
|
||||
|
||||
await assertion;
|
||||
|
||||
}, `Broken lock's release promise rejects`);
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
// Grab and hold the lock.
|
||||
navigator.locks.request(res, lock => never_settled).catch(_ => {});
|
||||
|
||||
// Make a request for it.
|
||||
let request_granted = false;
|
||||
const promise = navigator.locks.request(res, lock => {
|
||||
request_granted = true;
|
||||
});
|
||||
|
||||
// Steal it.
|
||||
await navigator.locks.request(res, {steal: true}, lock => {
|
||||
assert_false(request_granted, 'Steal should override request');
|
||||
});
|
||||
|
||||
await promise;
|
||||
assert_true(request_granted, 'Request should eventually be granted');
|
||||
|
||||
}, `Requested lock's release promise is deferred`);
|
||||
|
||||
promise_test(async t => {
|
||||
const res = uniqueName(t);
|
||||
|
||||
// Grab and hold the lock.
|
||||
navigator.locks.request(res, lock => never_settled).catch(_ => {});
|
||||
|
||||
// Steal it.
|
||||
let saw_abort = false;
|
||||
const first_steal = navigator.locks.request(
|
||||
res, {steal: true}, lock => never_settled).catch(error => {
|
||||
saw_abort = true;
|
||||
});
|
||||
|
||||
// Steal it again.
|
||||
await navigator.locks.request(res, {steal: true}, lock => {});
|
||||
|
||||
await first_steal;
|
||||
assert_true(saw_abort, 'First steal should have aborted');
|
||||
|
||||
}, 'Last caller wins');
|
56
test/fixtures/wpt/web-locks/storage-buckets.tentative.https.any.js
vendored
Normal file
56
test/fixtures/wpt/web-locks/storage-buckets.tentative.https.any.js
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
// META: title=Web Locks API: Storage Buckets have independent lock sets
|
||||
// META: script=resources/helpers.js
|
||||
// META: script=/storage/buckets/resources/util.js
|
||||
// META: global=window,dedicatedworker,sharedworker,serviceworker
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Returns whether bucket1 and bucket2 share locks
|
||||
* @param {*} t test runner object
|
||||
* @param {*} bucket1 Storage bucket
|
||||
* @param {*} bucket2 Storage bucket
|
||||
*/
|
||||
async function locksAreShared(t, bucket1, bucket2) {
|
||||
const lock_name = self.uniqueName(t);
|
||||
let callback_called = false;
|
||||
let locks_are_shared;
|
||||
await bucket1.locks.request(lock_name, async lock => {
|
||||
await bucket2.locks.request(
|
||||
lock_name, { ifAvailable: true }, async lock => {
|
||||
callback_called = true;
|
||||
locks_are_shared = lock == null;
|
||||
});
|
||||
});
|
||||
assert_true(callback_called, 'callback should be called');
|
||||
return locks_are_shared;
|
||||
}
|
||||
|
||||
promise_test(async t => {
|
||||
await prepareForBucketTest(t);
|
||||
|
||||
const inboxBucket = await navigator.storageBuckets.open('inbox');
|
||||
const draftsBucket = await navigator.storageBuckets.open('drafts');
|
||||
|
||||
assert_true(
|
||||
await locksAreShared(t, navigator, navigator),
|
||||
'The default bucket should share locks with itself');
|
||||
|
||||
assert_true(
|
||||
await locksAreShared(t, inboxBucket, inboxBucket),
|
||||
'A non default bucket should share locks with itself');
|
||||
|
||||
assert_false(
|
||||
await locksAreShared(t, navigator, inboxBucket),
|
||||
'The default bucket shouldn\'t share locks with a non default bucket');
|
||||
|
||||
assert_false(
|
||||
await locksAreShared(t, draftsBucket, inboxBucket),
|
||||
'Two different non default buckets shouldn\'t share locks');
|
||||
|
||||
const inboxBucket2 = await navigator.storageBuckets.open('inbox');
|
||||
|
||||
assert_true(
|
||||
await self.locksAreShared(t, inboxBucket, inboxBucket2),
|
||||
'A two instances of the same non default bucket should share locks with theirselves');
|
||||
}, 'Storage buckets have independent locks');
|
122
test/fixtures/wpt/web-locks/workers.https.html
vendored
Normal file
122
test/fixtures/wpt/web-locks/workers.https.html
vendored
Normal file
|
@ -0,0 +1,122 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<title>Web Locks API: Workers</title>
|
||||
<link rel=help href="https://w3c.github.io/web-locks/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="resources/helpers.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const worker = new Worker('resources/worker.js');
|
||||
t.add_cleanup(() => { worker.terminate(); });
|
||||
|
||||
const res = 'shared resource 1';
|
||||
|
||||
const lock_id = (await postToWorkerAndWait(
|
||||
worker, {op: 'request', name: res, mode: 'shared'})).lock_id;
|
||||
|
||||
await navigator.locks.request(res, {mode: 'shared'}, async lock => {
|
||||
await postToWorkerAndWait(worker, {op: 'release', lock_id});
|
||||
});
|
||||
|
||||
}, 'Window and Worker - shared mode');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const worker = new Worker('resources/worker.js');
|
||||
t.add_cleanup(() => { worker.terminate(); });
|
||||
|
||||
const res = 'exclusive resource 1';
|
||||
|
||||
// worker acquires the lock.
|
||||
const lock_id = (await postToWorkerAndWait(
|
||||
worker, {op: 'request', name: res})).lock_id;
|
||||
|
||||
// This request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = navigator.locks.request(
|
||||
res, lock => { lock_granted = true; });
|
||||
|
||||
// Verify we can't get it.
|
||||
let available = undefined;
|
||||
await navigator.locks.request(
|
||||
res, {ifAvailable: true}, lock => { available = lock !== null; });
|
||||
assert_false(available);
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Ask the worker to release it.
|
||||
await postToWorkerAndWait(worker, {op: 'release', lock_id});
|
||||
|
||||
// Now we've got it.
|
||||
const lock2 = await blocked;
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Window and Worker - exclusive mode');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const worker1 = new Worker('resources/worker.js');
|
||||
const worker2 = new Worker('resources/worker.js');
|
||||
t.add_cleanup(() => { worker1.terminate(); worker2.terminate(); });
|
||||
|
||||
const res = 'exclusive resource 2';
|
||||
|
||||
// worker1 acquires the lock.
|
||||
const lock_id = (await postToWorkerAndWait(
|
||||
worker1, {op: 'request', name: res})).lock_id;
|
||||
|
||||
// This request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = postToWorkerAndWait(
|
||||
worker2, {op: 'request', name: res});
|
||||
blocked.then(f => { lock_granted = true; });
|
||||
|
||||
// Verify worker2 can't get it.
|
||||
assert_true((await postToWorkerAndWait(worker2, {
|
||||
op: 'request', name: res, ifAvailable: true
|
||||
})).failed, 'Lock request should have failed');
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Ask worker1 to release it.
|
||||
await postToWorkerAndWait(worker1, {op: 'release', lock_id});
|
||||
|
||||
// Now worker2 can get it.
|
||||
const lock = await blocked;
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Worker and Worker - exclusive mode');
|
||||
|
||||
promise_test(async t => {
|
||||
assert_implements(navigator.locks);
|
||||
const worker = new Worker('resources/worker.js');
|
||||
|
||||
const res = 'exclusive resource 3';
|
||||
|
||||
// Worker acquires the lock.
|
||||
await postToWorkerAndWait(worker, {op: 'request', name: res});
|
||||
|
||||
// This request should be blocked.
|
||||
let lock_granted = false;
|
||||
const blocked = navigator.locks.request(
|
||||
res, lock => { lock_granted = true; });
|
||||
|
||||
// Verify we can't get it.
|
||||
let available = undefined;
|
||||
await navigator.locks.request(
|
||||
res, {ifAvailable: true}, lock => { available = lock !== null; });
|
||||
assert_false(available);
|
||||
assert_false(lock_granted);
|
||||
|
||||
// Implicitly release it by terminating the worker.
|
||||
worker.terminate();
|
||||
|
||||
// Now we've got it.
|
||||
const lock = await blocked;
|
||||
assert_true(lock_granted);
|
||||
|
||||
}, 'Terminated Worker - exclusive mode');
|
||||
|
||||
</script>
|
|
@ -126,9 +126,11 @@ if (isMainThread) {
|
|||
].forEach(expected.beforePreExec.add.bind(expected.beforePreExec));
|
||||
} else { // Worker.
|
||||
[
|
||||
'Internal Binding locks',
|
||||
'NativeModule diagnostics_channel',
|
||||
'NativeModule internal/abort_controller',
|
||||
'NativeModule internal/error_serdes',
|
||||
'NativeModule internal/locks',
|
||||
'NativeModule internal/perf/event_loop_utilization',
|
||||
'NativeModule internal/process/worker_thread_only',
|
||||
'NativeModule internal/streams/add-abort-signal',
|
||||
|
|
92
test/parallel/test-web-locks-query.js
Normal file
92
test/parallel/test-web-locks-query.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { Worker } = require('worker_threads');
|
||||
|
||||
describe('Web Locks - query missing WPT tests', () => {
|
||||
it('should report different ids for held locks from different contexts', async () => {
|
||||
const worker = new Worker(`
|
||||
const { parentPort } = require('worker_threads');
|
||||
|
||||
navigator.locks.request('different-contexts-resource', { mode: 'shared' }, async (lock) => {
|
||||
const state = await navigator.locks.query();
|
||||
const heldLocks = state.held.filter(l => l.name === 'different-contexts-resource');
|
||||
|
||||
parentPort.postMessage({ clientId: heldLocks[0].clientId });
|
||||
|
||||
await new Promise(resolve => {
|
||||
parentPort.once('message', () => resolve());
|
||||
});
|
||||
}).catch(err => parentPort.postMessage({ error: err.message }));
|
||||
`, { eval: true });
|
||||
|
||||
const workerResult = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
|
||||
await navigator.locks.request('different-contexts-resource', { mode: 'shared' }, async (lock) => {
|
||||
const state = await navigator.locks.query();
|
||||
const heldLocks = state.held.filter((l) => l.name === 'different-contexts-resource');
|
||||
|
||||
const mainClientId = heldLocks[0].clientId;
|
||||
|
||||
assert.notStrictEqual(mainClientId, workerResult.clientId);
|
||||
|
||||
worker.postMessage('release');
|
||||
});
|
||||
|
||||
await worker.terminate();
|
||||
});
|
||||
|
||||
it('should observe a deadlock scenario', async () => {
|
||||
const worker = new Worker(`
|
||||
const { parentPort } = require('worker_threads');
|
||||
|
||||
navigator.locks.request('deadlock-resource-1', async (lock1) => {
|
||||
parentPort.postMessage({ acquired: 'resource1' });
|
||||
|
||||
await new Promise(resolve => {
|
||||
parentPort.once('message', () => resolve());
|
||||
});
|
||||
|
||||
const result = await navigator.locks.request('deadlock-resource-2',
|
||||
{ ifAvailable: true }, (lock2) => lock2 !== null);
|
||||
|
||||
parentPort.postMessage({ acquired2: result });
|
||||
|
||||
await new Promise(resolve => {
|
||||
parentPort.once('message', () => resolve());
|
||||
});
|
||||
}).catch(err => parentPort.postMessage({ error: err.message }));
|
||||
`, { eval: true });
|
||||
|
||||
const step1 = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
assert.strictEqual(step1.acquired, 'resource1');
|
||||
|
||||
await navigator.locks.request('deadlock-resource-2', async (lock2) => {
|
||||
worker.postMessage('try-resource2');
|
||||
|
||||
const step2 = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
assert.strictEqual(step2.acquired2, false);
|
||||
|
||||
const canGetResource1 = await navigator.locks.request('deadlock-resource-1',
|
||||
{ ifAvailable: true }, (lock1) => lock1 !== null);
|
||||
|
||||
assert.strictEqual(canGetResource1, false);
|
||||
|
||||
const state = await navigator.locks.query();
|
||||
const resource2Lock = state.held.find((l) => l.name === 'deadlock-resource-2');
|
||||
assert(resource2Lock);
|
||||
|
||||
worker.postMessage('release');
|
||||
});
|
||||
|
||||
await worker.terminate();
|
||||
});
|
||||
});
|
222
test/parallel/test-web-locks.js
Normal file
222
test/parallel/test-web-locks.js
Normal file
|
@ -0,0 +1,222 @@
|
|||
'use strict';
|
||||
// Flags: --expose-gc
|
||||
|
||||
const common = require('../common');
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { Worker } = require('node:worker_threads');
|
||||
const { AsyncLocalStorage } = require('node:async_hooks');
|
||||
|
||||
describe('Web Locks with worker threads', () => {
|
||||
it('should handle exclusive locks', async () => {
|
||||
const worker = new Worker(`
|
||||
const { parentPort } = require('worker_threads');
|
||||
const assert = require('node:assert');
|
||||
|
||||
navigator.locks.request('exclusive-test', async (lock) => {
|
||||
assert.strictEqual(lock.mode, 'exclusive');
|
||||
parentPort.postMessage({ success: true });
|
||||
}).catch(err => parentPort.postMessage({ error: err.message }));
|
||||
`, { eval: true });
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
|
||||
assert.strictEqual(result.success, true);
|
||||
await worker.terminate();
|
||||
|
||||
await navigator.locks.request('exclusive-test', async (lock) => {
|
||||
assert.strictEqual(lock.mode, 'exclusive');
|
||||
assert.strictEqual(lock.name, 'exclusive-test');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle shared locks', async () => {
|
||||
const worker = new Worker(`
|
||||
const { parentPort } = require('worker_threads');
|
||||
const assert = require('node:assert');
|
||||
|
||||
navigator.locks.request('shared-test', { mode: 'shared' }, async (lock) => {
|
||||
assert.strictEqual(lock.mode, 'shared');
|
||||
parentPort.postMessage({ success: true });
|
||||
}).catch(err => parentPort.postMessage({ error: err.message }));
|
||||
`, { eval: true });
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
assert.strictEqual(result.success, true);
|
||||
|
||||
await navigator.locks.request('shared-test', { mode: 'shared' }, async (lock1) => {
|
||||
await navigator.locks.request('shared-test', { mode: 'shared' }, async (lock2) => {
|
||||
assert.strictEqual(lock1.mode, 'shared');
|
||||
assert.strictEqual(lock2.mode, 'shared');
|
||||
});
|
||||
});
|
||||
|
||||
await worker.terminate();
|
||||
});
|
||||
|
||||
it('should handle steal option - no existing lock', async () => {
|
||||
await navigator.locks.request('steal-simple', { steal: true }, async (lock) => {
|
||||
assert.strictEqual(lock.name, 'steal-simple');
|
||||
assert.strictEqual(lock.mode, 'exclusive');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle steal option - existing lock', async () => {
|
||||
let originalLockRejected = false;
|
||||
|
||||
const originalLockPromise = navigator.locks.request('steal-target', async (lock) => {
|
||||
assert.strictEqual(lock.name, 'steal-target');
|
||||
return 'original-completed';
|
||||
}).catch((err) => {
|
||||
originalLockRejected = true;
|
||||
assert.strictEqual(err.name, 'AbortError');
|
||||
assert.strictEqual(err.message, 'The operation was aborted');
|
||||
return 'original-rejected';
|
||||
});
|
||||
|
||||
const stealResult = await navigator.locks.request('steal-target', { steal: true }, async (stolenLock) => {
|
||||
assert.strictEqual(stolenLock.name, 'steal-target');
|
||||
assert.strictEqual(stolenLock.mode, 'exclusive');
|
||||
return 'steal-completed';
|
||||
});
|
||||
|
||||
assert.strictEqual(stealResult, 'steal-completed');
|
||||
|
||||
const originalResult = await originalLockPromise;
|
||||
assert.strictEqual(originalLockRejected, true);
|
||||
assert.strictEqual(originalResult, 'original-rejected');
|
||||
});
|
||||
|
||||
it('should handle ifAvailable option', async () => {
|
||||
await navigator.locks.request('ifavailable-test', async () => {
|
||||
const result = await navigator.locks.request('ifavailable-test', { ifAvailable: true }, (lock) => {
|
||||
return lock; // should be null
|
||||
});
|
||||
|
||||
assert.strictEqual(result, null);
|
||||
|
||||
const availableResult = await navigator.locks.request('ifavailable-different-resource',
|
||||
{ ifAvailable: true }, (lock) => {
|
||||
return lock !== null;
|
||||
});
|
||||
|
||||
assert.strictEqual(availableResult, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle AbortSignal', async () => {
|
||||
const worker = new Worker(`
|
||||
const { parentPort } = require('worker_threads');
|
||||
const assert = require('node:assert');
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
navigator.locks.request('signal-after-grant', { signal: controller.signal }, async (lock) => {
|
||||
parentPort.postMessage({ acquired: true });
|
||||
|
||||
setTimeout(() => controller.abort(), 50);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return 'completed successfully';
|
||||
}).then(result => {
|
||||
parentPort.postMessage({ resolved: result });
|
||||
}).catch(err => {
|
||||
parentPort.postMessage({ rejected: err.name });
|
||||
});
|
||||
`, { eval: true });
|
||||
|
||||
const acquired = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
assert.strictEqual(acquired.acquired, true);
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
assert.strictEqual(result.resolved, 'completed successfully');
|
||||
|
||||
await worker.terminate();
|
||||
});
|
||||
|
||||
it('should handle many concurrent locks without hanging', async () => {
|
||||
if (global.gc) global.gc();
|
||||
const before = process.memoryUsage().rss;
|
||||
|
||||
let callbackCount = 0;
|
||||
let resolveCount = 0;
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const promise = navigator.locks.request(`test-${i}`, async (lock) => {
|
||||
callbackCount++;
|
||||
const innerPromise = navigator.locks.request(`inner-${i}`, async () => {
|
||||
resolveCount++;
|
||||
return 'done';
|
||||
});
|
||||
await innerPromise;
|
||||
return `completed-${lock.name}`;
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (global.gc) global.gc();
|
||||
|
||||
const after = process.memoryUsage().rss;
|
||||
|
||||
assert.strictEqual(callbackCount, 100);
|
||||
assert.strictEqual(resolveCount, 100);
|
||||
assert(after < before * 3);
|
||||
});
|
||||
|
||||
it('should preserve AsyncLocalStorage context across lock callback', async () => {
|
||||
const als = new AsyncLocalStorage();
|
||||
const store = { id: 'lock' };
|
||||
|
||||
als.run(store, () => {
|
||||
navigator.locks
|
||||
.request('als-context-test', async () => {
|
||||
assert.strictEqual(als.getStore(), store);
|
||||
})
|
||||
.then(common.mustCall());
|
||||
});
|
||||
});
|
||||
|
||||
it('should clean up when worker is terminated with a pending lock', async () => {
|
||||
// Acquire the lock in the main thread so that the worker's request will be pending
|
||||
await navigator.locks.request('cleanup-test', async () => {
|
||||
// Launch a worker that requests the same lock
|
||||
const worker = new Worker(`
|
||||
const { parentPort } = require('worker_threads');
|
||||
|
||||
parentPort.postMessage({ requesting: true });
|
||||
|
||||
navigator.locks.request('cleanup-test', async () => {
|
||||
return 'should-not-complete';
|
||||
}).catch(err => {
|
||||
parentPort.postMessage({ error: err.name });
|
||||
});
|
||||
`, { eval: true });
|
||||
|
||||
const requestSignal = await new Promise((resolve) => {
|
||||
worker.once('message', resolve);
|
||||
});
|
||||
|
||||
assert.strictEqual(requestSignal.requesting, true);
|
||||
|
||||
await worker.terminate();
|
||||
|
||||
});
|
||||
|
||||
// Request the lock again to make sure cleanup succeeded
|
||||
await navigator.locks.request('cleanup-test', async (lock) => {
|
||||
assert.strictEqual(lock.name, 'cleanup-test');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -71,6 +71,7 @@ const { getSystemErrorName } = require('util');
|
|||
delete providers.QUIC_ENDPOINT;
|
||||
delete providers.QUIC_SESSION;
|
||||
delete providers.QUIC_STREAM;
|
||||
delete providers.LOCKS;
|
||||
|
||||
const objKeys = Object.keys(providers);
|
||||
if (objKeys.length > 0)
|
||||
|
|
45
test/wpt/status/web-locks.json
Normal file
45
test/wpt/status/web-locks.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"idlharness.https.any.js": {
|
||||
"fail": {
|
||||
"expected": [
|
||||
"LockManager interface: existence and properties of interface object",
|
||||
"LockManager interface object length",
|
||||
"LockManager interface object name",
|
||||
"LockManager interface: existence and properties of interface prototype object",
|
||||
"LockManager interface: existence and properties of interface prototype object's \"constructor\" property",
|
||||
"LockManager interface: existence and properties of interface prototype object's @@unscopables property",
|
||||
"LockManager interface: operation request(DOMString, LockGrantedCallback)",
|
||||
"LockManager interface: operation request(DOMString, LockOptions, LockGrantedCallback)",
|
||||
"LockManager interface: operation query()",
|
||||
"LockManager must be primary interface of navigator.locks",
|
||||
"Lock interface: existence and properties of interface object",
|
||||
"Lock interface object length",
|
||||
"Lock interface object name",
|
||||
"Lock interface: existence and properties of interface prototype object",
|
||||
"Lock interface: existence and properties of interface prototype object's \"constructor\" property",
|
||||
"Lock interface: existence and properties of interface prototype object's @@unscopables property",
|
||||
"Lock interface: attribute name",
|
||||
"Lock interface: attribute mode",
|
||||
"Lock must be primary interface of lock"
|
||||
]
|
||||
}
|
||||
},
|
||||
"non-secure-context.any.js": {
|
||||
"skip": "navigator.locks is only present in secure contexts"
|
||||
},
|
||||
"query.https.any.js": {
|
||||
"fail": {
|
||||
"expected": [
|
||||
"query() reports different ids for held locks from different contexts",
|
||||
"query() can observe a deadlock"
|
||||
],
|
||||
"note": "Browser-specific test"
|
||||
}
|
||||
},
|
||||
"secure-context.https.any.js": {
|
||||
"skip": "Different secure context behavior in Node.js"
|
||||
},
|
||||
"storage-buckets.tentative.https.any.js": {
|
||||
"skip": "Node.js does not implement Storage Buckets API"
|
||||
}
|
||||
}
|
9
test/wpt/test-web-locks.js
Normal file
9
test/wpt/test-web-locks.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const { WPTRunner } = require('../common/wpt');
|
||||
|
||||
// Run serially to avoid cross-test interference on the shared LockManager.
|
||||
const runner = new WPTRunner('web-locks', { concurrency: 1 });
|
||||
|
||||
runner.pretendGlobalThisAs('Window');
|
||||
runner.runJsTests();
|
|
@ -333,6 +333,10 @@ const customTypesMap = {
|
|||
'quic.OnHeadersCallback': 'quic.html#callback-onheaderscallback',
|
||||
'quic.OnTrailersCallback': 'quic.html#callback-ontrailerscallback',
|
||||
'quic.OnPullCallback': 'quic.html#callback-onpullcallback',
|
||||
|
||||
'Lock': 'worker_threads.html#class-lock',
|
||||
'LockManager': 'worker_threads.html#class-lockmanager',
|
||||
'LockManagerSnapshot': 'https://developer.mozilla.org/en-US/docs/Web/API/LockManagerSnapshot',
|
||||
};
|
||||
|
||||
const arrayPart = /(?:\[])+$/;
|
||||
|
|
2
typings/globals.d.ts
vendored
2
typings/globals.d.ts
vendored
|
@ -7,6 +7,7 @@ import { HttpParserBinding } from './internalBinding/http_parser';
|
|||
import { InspectorBinding } from './internalBinding/inspector';
|
||||
import { FsBinding } from './internalBinding/fs';
|
||||
import { FsDirBinding } from './internalBinding/fs_dir';
|
||||
import { LocksBinding } from './internalBinding/locks';
|
||||
import { MessagingBinding } from './internalBinding/messaging';
|
||||
import { OptionsBinding } from './internalBinding/options';
|
||||
import { OSBinding } from './internalBinding/os';
|
||||
|
@ -33,6 +34,7 @@ interface InternalBindingMap {
|
|||
fs_dir: FsDirBinding;
|
||||
http_parser: HttpParserBinding;
|
||||
inspector: InspectorBinding;
|
||||
locks: LocksBinding;
|
||||
messaging: MessagingBinding;
|
||||
modules: ModulesBinding;
|
||||
options: OptionsBinding;
|
||||
|
|
31
typings/internalBinding/locks.d.ts
vendored
Normal file
31
typings/internalBinding/locks.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
declare namespace InternalLocksBinding {
|
||||
interface LockInfo {
|
||||
readonly name: string;
|
||||
readonly mode: 'shared' | 'exclusive';
|
||||
readonly clientId: string;
|
||||
}
|
||||
|
||||
interface LockManagerSnapshot {
|
||||
readonly held: LockInfo[];
|
||||
readonly pending: LockInfo[];
|
||||
}
|
||||
|
||||
type LockGrantedCallback = (lock: LockInfo | null) => Promise<any> | any;
|
||||
}
|
||||
|
||||
export interface LocksBinding {
|
||||
readonly LOCK_MODE_SHARED: 'shared';
|
||||
readonly LOCK_MODE_EXCLUSIVE: 'exclusive';
|
||||
readonly LOCK_STOLEN_ERROR: 'LOCK_STOLEN';
|
||||
|
||||
request(
|
||||
name: string,
|
||||
clientId: string,
|
||||
mode: string,
|
||||
steal: boolean,
|
||||
ifAvailable: boolean,
|
||||
callback: InternalLocksBinding.LockGrantedCallback
|
||||
): Promise<any>;
|
||||
|
||||
query(): Promise<InternalLocksBinding.LockManagerSnapshot>;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue