Skip to content

Commit 44532ff

Browse files
committed
worker: implement web locks api
1 parent 0740394 commit 44532ff

File tree

9 files changed

+690
-3
lines changed

9 files changed

+690
-3
lines changed

doc/api/worker_threads.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,106 @@ added: v10.5.0
9292
An arbitrary JavaScript value that contains a clone of the data passed
9393
to this thread’s `Worker` constructor.
9494

95+
## worker.locks
96+
<!-- YAML
97+
added: REPLACEME
98+
-->
99+
100+
* {LockManager}
101+
102+
An instance of a [`LockManager`][].
103+
104+
## Class: Lock
105+
<!-- YAML
106+
added: REPLACEME
107+
-->
108+
109+
The Lock interface provides the name and mode of a previously requested lock,
110+
which is received in the callback to [`LockManager.request()`][].
111+
112+
### lock.name
113+
<!-- YAML
114+
added: REPLACEME
115+
-->
116+
117+
* {string}
118+
119+
The name of this lock.
120+
121+
### lock.mode
122+
<!-- YAML
123+
added: REPLACEME
124+
-->
125+
126+
* {string}
127+
128+
The mode of this lock. Either `shared` or `exclusive`.
129+
130+
## Class: LockManager
131+
<!-- YAML
132+
added: REPLACEME
133+
-->
134+
135+
The `LockManager` interface provides methods for requesting a new [`Lock`][]
136+
object and querying for an existing `Lock` object. To get an instance of
137+
`LockManager`, call `worker_threads.locks`.
138+
139+
With the exception of `AbortController` support, this implementation matches
140+
the [browser `LockManager`][] API.
141+
142+
### locks.request(name[, options], callback)
143+
<!-- YAML
144+
added: REPLACEME
145+
-->
146+
147+
* `name` {string}
148+
* `options` {Object}
149+
* `mode` {string} Either `'exclusive'` or `'shared'`. **Default:**
150+
`'exclusive'`.
151+
* `ifAvailable` {boolean} If `true`, the lock request will only be
152+
granted if it is not already held. If it cannot be granted, the
153+
callback will be invoked with `null` instead of a `Lock` instance.
154+
**Default:** `false`.
155+
* `steal` {boolean} If `true`, then any held locks with the same name will be
156+
released, and the request will be granted, preempting any queued requests
157+
for it. **Default:** `false`.
158+
* `callback` {Function} The function to be invoked while the lock is acquired.
159+
The lock will be released when the function ends, or if the function returns
160+
a promise, when that promise settles.
161+
* Returns: {Promise}
162+
163+
Requests a [`Lock`][] object with parameters specifying its name and
164+
characteristics.
165+
166+
```js
167+
worker_threads.locks.request('my_resource', async (lock) => {
168+
// The lock was granted.
169+
}).then(() => {
170+
// The lock is released here.
171+
});
172+
```
173+
174+
### locks.query()
175+
<!-- YAML
176+
added: REPLACEME
177+
-->
178+
179+
* Returns: {Promise}
180+
181+
Returns a Promise that resolves with a [`LockManagerSnapshot`][] which contains
182+
information about held and pending locks.
183+
184+
```js
185+
worker_threads.locks.query().then((state) => {
186+
state.held.forEach((lock) => {
187+
console.log(`held lock: name ${lock.name}, mode ${lock.mode}`);
188+
});
189+
state.pending.forEach((request) => {
190+
console.log(`requested lock: name ${request.name}, mode ${request.mode}`);
191+
});
192+
});
193+
```
194+
95195
## Class: MessageChannel
96196
<!-- YAML
97197
added: v10.5.0
@@ -483,10 +583,14 @@ active handle in the event system. If the worker is already `unref()`ed calling
483583
[`require('worker_threads').threadId`]: #worker_threads_worker_threadid
484584
[`cluster` module]: cluster.html
485585
[`inspector`]: inspector.html
586+
[`Lock`]: #worker_threads_class_lock
587+
[`LockManager`]: #worker_threads_class_lockmanager
588+
[`LockManagerSnapshot`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManagerSnapshot
486589
[v8.serdes]: v8.html#v8_serialization_api
487590
[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
488591
[Signals events]: process.html#process_signal_events
489592
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
593+
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
490594
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
491595
[child processes]: child_process.html
492596
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

lib/internal/worker.js

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ const {
1515
const { internalBinding } = require('internal/bootstrap/loaders');
1616
const { MessagePort, MessageChannel } = internalBinding('messaging');
1717
const { handle_onclose } = internalBinding('symbols');
18+
const locks = internalBinding('locks');
1819
const { clearAsyncIdStack } = require('internal/async_hooks');
1920
const { serializeError, deserializeError } = require('internal/error-serdes');
21+
const DOMException = require('internal/domexception');
2022

2123
util.inherits(MessagePort, EventEmitter);
2224

@@ -44,6 +46,7 @@ const kStdioWantsMoreDataCallback = Symbol('kStdioWantsMoreDataCallback');
4446
const kStartedReading = Symbol('kStartedReading');
4547
const kWaitingStreams = Symbol('kWaitingStreams');
4648
const kIncrementsPortRef = Symbol('kIncrementsPortRef');
49+
const kMode = Symbol('mode');
4750

4851
const debug = util.debuglog('worker');
4952

@@ -505,12 +508,159 @@ function pipeWithoutWarning(source, dest) {
505508
dest._maxListeners = destMaxListeners;
506509
}
507510

511+
// https://wicg.github.io/web-locks/#api-lock
512+
class Lock {
513+
constructor() {
514+
// eslint-disable-next-line no-restricted-syntax
515+
throw new TypeError('Illegal constructor');
516+
}
517+
518+
get name() {
519+
return this[kName];
520+
}
521+
522+
get mode() {
523+
return this[kMode];
524+
}
525+
}
526+
527+
Object.defineProperties(Lock.prototype, {
528+
name: { enumerable: true },
529+
mode: { enumerable: true },
530+
[Symbol.toStringTag]: {
531+
value: 'Lock',
532+
writable: false,
533+
enumerable: false,
534+
configurable: true,
535+
},
536+
});
537+
538+
// https://wicg.github.io/web-locks/#api-lock-manager
539+
class LockManager {
540+
constructor() {
541+
// eslint-disable-next-line no-restricted-syntax
542+
throw new TypeError('Illegal constructor');
543+
}
544+
545+
// https://wicg.github.io/web-locks/#api-lock-manager-request
546+
request(name, options, callback) {
547+
if (callback === undefined) {
548+
callback = options;
549+
options = undefined;
550+
}
551+
552+
// Let promise be a new promise.
553+
let reject;
554+
let resolve;
555+
const promise = new Promise((res, rej) => {
556+
resolve = res;
557+
reject = rej;
558+
});
559+
560+
// If options was not passed, then let options be a new LockOptions
561+
// dictionary with default members.
562+
if (options === undefined) {
563+
options = {
564+
mode: 'exclusive',
565+
ifAvailable: false,
566+
steal: false,
567+
};
568+
}
569+
570+
if (name.startsWith('-')) {
571+
// if name starts with U+002D HYPHEN-MINUS (-), then reject promise with a
572+
// "NotSupportedError" DOMException.
573+
reject(new DOMException('NotSupportedError'));
574+
} else if (options.ifAvailable === true && options.steal === true) {
575+
// Otherwise, if both options' steal dictionary member and option's
576+
// ifAvailable dictionary member are true, then reject promise with a
577+
// "NotSupportedError" DOMException.
578+
reject(new DOMException('NotSupportedError'));
579+
} else if (options.steal === true && options.mode !== 'exclusive') {
580+
// Otherwise, if options' steal dictionary member is true and option's
581+
// mode dictionary member is not "exclusive", then reject promise with a
582+
// "NotSupportedError" DOMException.
583+
reject(new DOMException('NotSupportedError'));
584+
} else {
585+
// Otherwise, run these steps:
586+
587+
// Let request be the result of running the steps to request a lock with
588+
// promise, the current agent, environment's id, origin, callback, name,
589+
// options' mode dictionary member, options' ifAvailable dictionary
590+
// member, and option's steal dictionary member.
591+
process.nextTick(() => {
592+
locks.request(
593+
promise,
594+
(name, mode, waitingPromise, release) => {
595+
const lock = Object.create(Lock.prototype, {
596+
[kName]: {
597+
value: name,
598+
writable: false,
599+
enumerable: false,
600+
configurable: false,
601+
},
602+
[kMode]: {
603+
value: mode === 0 ? 'shared' : 'exclusive',
604+
writable: false,
605+
enumerable: false,
606+
configurable: false,
607+
},
608+
});
609+
610+
// When lock lock's waiting promise settles (fulfills or rejects),
611+
// enqueue the following steps on the lock task queue:
612+
waitingPromise
613+
.finally(() => undefined)
614+
.then(() => {
615+
// Release the lock lock.
616+
release();
617+
618+
// Resolve lock's released promise with lock's waiting promise.
619+
resolve(waitingPromise);
620+
});
621+
622+
return callback(lock);
623+
},
624+
name,
625+
options.mode === 'shared' ? 0 : 1,
626+
options.ifAvailable,
627+
options.steal);
628+
});
629+
}
630+
631+
// Return promise.
632+
return promise;
633+
}
634+
635+
// https://wicg.github.io/web-locks/#api-lock-manager-query
636+
query() {
637+
return new Promise((resolve) => {
638+
process.nextTick(() => {
639+
const snapshot = locks.snapshot();
640+
resolve(snapshot);
641+
});
642+
});
643+
}
644+
}
645+
646+
Object.defineProperties(LockManager.prototype, {
647+
request: { enumerable: true },
648+
query: { enumerable: true },
649+
[Symbol.toStringTag]: {
650+
value: 'LockManager',
651+
writable: false,
652+
enumerable: false,
653+
configurable: true,
654+
},
655+
});
656+
508657
module.exports = {
509658
MessagePort,
510659
MessageChannel,
511660
threadId,
512661
Worker,
513662
setupChild,
514663
isMainThread,
515-
workerStdio
664+
workerStdio,
665+
LockManager,
516666
};

lib/worker_threads.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ const {
55
MessagePort,
66
MessageChannel,
77
threadId,
8-
Worker
8+
Worker,
9+
LockManager,
910
} = require('internal/worker');
1011

1112
module.exports = {
@@ -14,5 +15,6 @@ module.exports = {
1415
MessageChannel,
1516
threadId,
1617
Worker,
17-
parentPort: null
18+
parentPort: null,
19+
locks: Object.create(LockManager.prototype),
1820
};

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
'src/node_file.cc',
351351
'src/node_http2.cc',
352352
'src/node_http_parser.cc',
353+
'src/node_locks.cc',
353354
'src/node_messaging.cc',
354355
'src/node_options.cc',
355356
'src/node_os.cc',
@@ -413,6 +414,7 @@
413414
'src/node_http2_state.h',
414415
'src/node_internals.h',
415416
'src/node_javascript.h',
417+
'src/node_locks.h',
416418
'src/node_messaging.h',
417419
'src/node_mutex.h',
418420
'src/node_options.h',

src/env.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ struct PackageConfig {
153153
V(duration_string, "duration") \
154154
V(emit_warning_string, "emitWarning") \
155155
V(exchange_string, "exchange") \
156+
V(exclusive_string, "exclusive") \
156157
V(encoding_string, "encoding") \
157158
V(entries_string, "entries") \
158159
V(entry_type_string, "entryType") \
@@ -178,6 +179,7 @@ struct PackageConfig {
178179
V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \
179180
V(gid_string, "gid") \
180181
V(handle_string, "handle") \
182+
V(held_string, "held") \
181183
V(help_text_string, "helpText") \
182184
V(homedir_string, "homedir") \
183185
V(host_string, "host") \
@@ -201,6 +203,7 @@ struct PackageConfig {
201203
V(message_port_string, "messagePort") \
202204
V(message_port_constructor_string, "MessagePort") \
203205
V(minttl_string, "minttl") \
206+
V(mode_string, "mode") \
204207
V(modulus_string, "modulus") \
205208
V(name_string, "name") \
206209
V(netmask_string, "netmask") \
@@ -242,6 +245,7 @@ struct PackageConfig {
242245
V(parse_error_string, "Parse Error") \
243246
V(password_string, "password") \
244247
V(path_string, "path") \
248+
V(pending_string, "pending") \
245249
V(pending_handle_string, "pendingHandle") \
246250
V(pid_string, "pid") \
247251
V(pipe_string, "pipe") \
@@ -270,6 +274,7 @@ struct PackageConfig {
270274
V(service_string, "service") \
271275
V(servername_string, "servername") \
272276
V(session_id_string, "sessionId") \
277+
V(shared_string, "shared") \
273278
V(shell_string, "shell") \
274279
V(signal_string, "signal") \
275280
V(sink_string, "sink") \

src/node_internals.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ struct sockaddr;
117117
V(http_parser) \
118118
V(inspector) \
119119
V(js_stream) \
120+
V(locks) \
120121
V(messaging) \
121122
V(module_wrap) \
122123
V(options) \

0 commit comments

Comments
 (0)