1. 介绍
本节是非规范性的。
脚本对特定的锁请求发起操作, 请求特定的资源名称和 模式。调度算法查看当前和之前请求的状态, 最终授予锁请求。锁是一个已授予的请求; 它有一个资源名称和 模式。它作为一个返回给脚本的对象表示。 只要锁被持有,它可能阻止其他锁请求被授予(取决于名称和模式)。脚本可以释放锁, 此时它可能允许其他锁请求被授予。
该 API 提供了一些可按需使用的可选功能,包括:
-
从异步任务中返回值,
-
共享和独占锁模式,
-
条件获取,
-
查询锁状态的诊断功能,
-
防止死锁的应急措施。
合作协调在共享代理的 存储桶的范围内进行; 这可能跨越多个代理集群。
1.1. 使用概述
该 API 的使用步骤如下:
-
请求锁。
-
在异步任务中持有锁时执行工作。
-
任务完成时自动释放锁。
1.2. 动机和使用案例
基于 Web 的文档编辑器将状态存储在内存中以便快速访问,并将更改(作为一系列记录)持久化到存储 API, 例如 Indexed Database API, 以提高弹性和离线使用能力,同时同步到服务器以支持跨设备使用。当同一文档在两个标签页中打开进行编辑时,必须在这些标签页之间进行协调, 例如确保一次只能有一个标签页对文档进行修改或同步。需要在活动标签页离开(导航、关闭、崩溃)时使其他标签页可以变为活动状态。
在数据同步服务中,会指定一个“主标签页”。这个标签页是唯一执行某些操作的页面(例如网络同步、清理排队数据等)。它持有一个锁并且从不释放。 其他标签页可以尝试获取该锁,所有尝试将排队进行。如果“主标签页”崩溃或关闭,则其他标签页中的一个将获得该锁并成为新的主标签页。
Indexed Database API 定义了一个事务模型, 允许在原点内的多个命名存储分区之间进行共享读取和独占写入访问。将此概念作为一个原语公开,可以基于资源的可用性调度任何 Web 平台活动, 例如允许为其他存储类型(例如 Caches [Service-Workers])、 跨存储类型,甚至跨非存储 API(例如网络获取)组合事务。
2. 概念
本规范的目的如下:
-
浏览器中的不同用户配置文件被视为不同的用户代理。
-
每个隐私模式浏览会话被视为一个单独的用户代理。
一个用户代理具有一个 锁任务队列,它是 启动新的并行队列的结果。
2.1. 资源名称
资源名称是由 web 应用程序选择的表示抽象资源的 JavaScript 字符串。
资源名称在调度算法之外没有外部含义,但在共享 代理的 存储桶中是全局的。 Web 应用程序可以自由使用任何资源命名方案。
以 U+002D HYPHEN-MINUS(-)开头的资源名称是保留的;请求这些资源名称将引发异常。
2.2. 锁管理器
锁管理器封装了 锁和 锁请求的状态。 每个存储桶 都通过与 Web 锁 API 相关联的存储瓶包含一个 锁管理器。
注意:页面和 worker(代理) 共享一个存储桶, 即使它们处于不相关的浏览上下文中, 但它们仍共享同一个锁管理器。
这里需要完善与 [存储]的集成, 包括如何从给定环境中正确获取锁管理器。
2.3. 模式和调度
模式可以是
"exclusive
"
或 "shared
"。
模式可以用于模拟常见的
读写锁模式。
如果持有 "exclusive
"
锁,
则无法授予该名称的其他锁。如果持有 "shared
"
锁,
则可以授予其他具有相同名称的 "shared
"
锁,
但不能授予任何 "exclusive
"
锁。
API 中的默认模式是 "exclusive
"。
其他属性可能会影响调度,例如超时、公平性等。
2.4. 锁
锁表示对共享资源的独占访问。
一个锁具有一个clientId,它是一个不透明的字符串。
一个锁具有一个模式,它是"exclusive
"
或 "shared
"中的一个。
一个锁具有一个等待的 promise,它是一个 Promise。
一个锁具有一个释放的 promise,它是一个 Promise。
当锁 lock的 等待的 promise 完成(成功或失败)时,排队以下步骤到 锁任务队列中:
-
释放锁 lock。
-
解决 lock的 释放的 promise, 使用lock的等待的 promise。
2.5. 锁请求
锁请求表示一个待处理的锁请求。
每个锁管理器都有一个锁请求队列映射,它是一个有序映射, 将资源名称映射到锁请求队列。
当以下步骤返回 true 时,表示锁请求 request是可授予的:
2.6. 锁的终止
每当运行卸载文档清理步骤时,对其文档使用终止剩余锁和请求的过程,其对应的代理。
目前这仅适用于工作线程,并且定义得较为模糊,因为没有规范的方法来在工作线程终止时运行步骤。
3. API
3.1. Navigator 混入
[SecureContext ]interface mixin {
NavigatorLocks readonly attribute LockManager locks ; };Navigator includes NavigatorLocks ;WorkerNavigator includes NavigatorLocks ;
每个环境设置对象都有一个 LockManager
对象。
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
locks
获取器的步骤是返回 此对象的 相关设置对象的 LockManager
对象。
3.2. LockManager
类
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
[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 = "exclusive";
mode boolean =
ifAvailable false ;boolean =
steal false ;AbortSignal ; };
signal dictionary {
LockManagerSnapshot sequence <LockInfo >;
held sequence <LockInfo >; };
pending dictionary {
LockInfo DOMString ;
name LockMode ;
mode DOMString ; };
clientId
一个 LockManager
实例允许脚本发起 锁请求 并查询
锁管理器 的状态。
3.2.1.
方法 request()
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
- promise = navigator.locks.
request
(name, callback) - promise = navigator.locks.
request
(name, options, callback) -
调用
request()
方法来请求一个锁。name(初始参数)是一个 资源名称字符串。
callback(最后一个参数)是一个当锁被授予时调用的 回调函数。这是由脚本指定的,通常是一个
async
函数。锁在回调函数完成之前一直被持有。如果传递了一个非异步的回调函数,它将自动包裹在一个立即解析的 Promise 中,因此锁只会在同步回调的持续时间内持有。
返回的 promise 在锁释放后解析(或拒绝)回调的结果,如果请求被中止则拒绝。
示例:
try { const result= await navigator. locks. request( 'resource' , async lock=> { // 此处持有锁。 await do_something(); await do_something_else(); return "ok" ; // 现在锁将被释放。 }); // |result| 是回调的返回值。 } catch ( ex) { // 如果回调抛出错误,将在此处捕获。 }
当回调因任何原因退出时,无论是代码返回还是抛出,锁都将被释放。
可以将 options 字典作为第二个参数指定;callback 参数始终在最后。
- options.mode
-
mode
选项可以是 "exclusive
"(如果未指定,默认为此)或 "shared
"。多个标签页或工作者可以以 "shared
" 模式持有对同一资源的锁,但在 "exclusive
" 模式下只能有一个标签页或工作者持有对资源的锁。最常见的用途是允许多个读取器同时访问资源,但防止更改。一旦读取器锁被释放,单个排他写入器可以获取锁进行更改,随后可以是另一个排他写入器或更多共享读取器。
await navigator. locks. request( 'resource' , { mode: 'shared' }, async lock=> { // 锁在此处持有。其他上下文也可能以共享模式持有锁, // 但没有其他上下文会以排他模式持有锁。 });
- options.ifAvailable
-
如果
ifAvailable
选项为true
,则只有在无需额外等待的情况下才会授予锁。请注意,这仍然不是同步的;在许多用户代理中,这将需要跨进程通信以查看锁是否可以被授予。如果锁无法被授予,则回调会用null
调用。(由于这是预期的,故请求不会被拒绝。)
await navigator. locks. request( 'resource' , { ifAvailable: true }, async lock=> { if ( ! lock) { // 没有获得锁。可能需要采取适当的行动。 return ; } // 此处持有锁。 });
- options.signal
-
signal
选项可以设置为一个AbortSignal
。这允许中止锁请求,例如如果在及时的情况下请求未被授予:
const controller= new AbortController(); setTimeout(() => controller. abort(), 200 ); // 最多等待 200ms。 try { await navigator. locks. request( 'resource' , { signal: controller. signal}, async lock=> { // 此处持有锁。 }); // 在此完成锁的使用。 } catch ( ex) { // 如果定时器触发,|ex| 将是一个名称为 "AbortError" 的 DOMException。 }
如果在授予锁之前发出中止信号,则请求的 Promise 将以 AbortError
被拒绝。一旦锁被授予,信号将被忽略。
- options.steal
-
如果
steal
选项为true
,则将释放对该资源持有的任何锁(并且此类锁的 释放的 Promise 将以AbortError
解析),并授予请求,抢占任何排队的请求。如果 Web 应用程序检测到不可恢复的状态 — 例如,一些协调点(如 Service Worker)确定持有锁的标签页不再响应 — 那么可以使用此选项 "窃取" 锁。
方法 request(name, callback)
和
request(name, options, callback)
的步骤如下:
-
如果没有传递 options,则让 options 为一个新的
LockOptions
字典,其成员使用默认值。 -
如果 environment 的 相关全局对象 的 关联的文档 不是 完全激活的,则返回 一个被拒绝的 Promise,带有一个 "
InvalidStateError
"DOMException
。 -
让 manager 为 获取锁管理器 的结果,给定 environment。如果返回失败,则返回 一个被拒绝的 Promise,带有一个 "
SecurityError
"DOMException
。 -
如果 name 以 U+002D HYPHEN-MINUS (-) 开头,则返回 一个被拒绝的 Promise,带有一个 "
NotSupportedError
"DOMException
。 -
如果 options["
steal
"] 和 options["ifAvailable
"] 都为 true,则返回 一个被拒绝的 Promise,带有一个 "NotSupportedError
"DOMException
。 -
如果 options["
steal
"] 为 true 且 options["mode
"] 不是 "exclusive
",则返回 一个被拒绝的 Promise,带有一个 "NotSupportedError
"DOMException
。 -
如果 options["
signal
"] 存在,且 options["steal
"] 或 options["ifAvailable
"] 为 true,则返回 一个被拒绝的 Promise,带有一个 "NotSupportedError
"DOMException
。 -
如果 options["
signal
"] 存在 且已 中止,则返回 一个被拒绝的 Promise,包含 options["signal
"] 的 中止原因。 -
让 promise 为 一个新的 Promise。
-
请求一个锁,传入 promise、当前的 代理、environment 的 id、manager、callback、name、options["
mode
"]、options["ifAvailable
"]、options["steal
"] 和 options["signal
"]。 -
返回 promise。
3.2.2.
方法 query()
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
- state = await navigator.locks.
query
() -
可以使用
query()
方法为一个源生成 锁管理器状态的快照,这允许 Web 应用程序检查其锁的使用情况,用于日志记录或调试目的。返回的 Promise 解析为 state,这是一个普通的数据结构(即类似 JSON 的数据),其形式如下:
{ held: [ { name: "resource1" , mode: "exclusive" , clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, { name: "resource2" , mode: "shared" , clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, { name: "resource2" , mode: "shared" , clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, ], pending: [ { name: "resource1" , mode: "exclusive" , clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, { name: "resource1" , mode: "exclusive" , clientId: "d341a5d0-1d8d-4224-be10-704d1ef92a15" }, ] }
方法 query()
的步骤如下:
-
如果 environment 的 相关全局对象 的 关联的文档 不是 完全激活的,则返回 一个被拒绝的 Promise,带有一个 "
InvalidStateError
"DOMException
。 -
让 manager 为 获取锁管理器 的结果,给定 environment。如果返回失败,则返回 一个被拒绝的 Promise,带有一个 "
SecurityError
"DOMException
。 -
让 promise 为 一个新的 Promise。
-
返回 promise。
3.3. Lock
类
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
[SecureContext ,Exposed =(Window ,Worker )]interface {
Lock readonly attribute DOMString name ;readonly attribute LockMode mode ; };
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
在所有当前的引擎中可用。
Opera?Edge79+
Edge (旧版)?IE无
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?三星 Internet?Opera Mobile?
4. 算法
4.1. 请求锁
-
让 request 成为一个新的 锁请求 (agent,clientId,manager,name,mode,callback,promise,signal)。
-
如果存在 signal,则将算法 信号中止请求 request 使用 signal 添加到 signal 中。
-
返回 request。
4.2. 释放锁
4.3. 中止请求
4.4. 处理给定资源名称的锁请求队列
-
对 queue 中的每个 request:
-
如果 request 不是 可授予的,则返回。
注意:队列中只有第一个项目是可授予的。因此,如果某个项目不可授予,那么后面的所有项目也自动不可授予。
-
从 queue 中移除 request。
-
让 agent 为 request 的 代理。
-
让 manager 为 request 的 管理器。
-
让 clientId 为 request 的 客户端ID。
-
让 name 为 request 的 名称。
-
让 mode 为 request 的 模式。
-
让 callback 为 request 的 回调函数。
-
让 p 为 request 的 承诺。
-
让 signal 为 request 的 信号。
-
让 waiting 为 一个新的承诺。
-
让 lock 成为一个新的 锁,具有 代理 agent,客户端ID clientId,管理器 manager,模式 mode,名称 name,释放承诺 p,以及 等待承诺 waiting。
-
4.5. 快照锁状态
5. 使用注意事项
本节为非规范性内容。
5.1. 死锁
死锁是并发计算中的一个概念,通过此 API 可以引入限制在特定 锁管理器的死锁。
防止死锁需要小心。一种方法是始终按照严格的顺序获取多个锁。
6. 安全和隐私注意事项
6.1. 锁的作用域
锁管理器的作用域定义很重要,因为它定义了一个隐私边界。锁可以用作临时状态保留机制,像存储 API 一样,也可以用作通信机制,并且其权限不能高于存储设施。对某些服务施加更细粒度的用户代理也必须对其他服务施加相同的粒度;例如,出于隐私原因,对顶级页面(第一方)和同源的跨域 iframe(第三方)暴露不同存储分区的用户代理也必须同样对锁进行分区。
这也为 Web 应用程序作者提供了合理的期望;如果对存储资源获取锁,所有同源浏览上下文必须观察到相同的状态。
6.2. 隐私浏览
每个私密模式浏览会话在此 API 中被视为一个独立的用户代理。也就是说,在此类会话外请求/持有的锁不会影响此类会话内请求/持有的锁,反之亦然。这防止了网站确定会话是否为“隐私模式”,同时也不允许会话之间通过此类锁进行通信。
6.3. 实现风险
实现时必须确保锁不会跨越不同的来源。如果不这样做,将为在两个不同来源中运行的脚本提供通信侧信道,或允许一个来源中的脚本破坏另一个来源的行为(例如拒绝服务)。
6.4. 检查清单
W3C TAG 为规范编辑者开发了安全与隐私自我评审问卷以提供信息性答案。回顾以下问题:
-
本规范不涉及可识别个人身份的信息或高价值数据。
-
未引入跨浏览会话持续的任何新来源状态。
-
未向 Web 暴露任何新的跨源持久状态。
-
未向一个来源暴露其当前无法访问的任何新数据(例如,通过轮询 [IndexedDB-2])。
-
未启用任何新的脚本执行/加载机制。
-
本规范不允许来源访问以下任何内容:
-
用户的位置。
-
用户设备上的传感器。
-
用户本地计算环境的各个方面。
-
访问其他设备。
-
对用户代理原生 UI 的任何控制。
-
-
未向 Web 暴露任何临时标识符。所有资源名称由 Web 应用程序本身提供。
-
如果区分存储,则用户代理在第一方和第三方上下文中的行为应区分。请参见§ 6.1 锁的作用域。
-
在用户代理“隐私模式”上下文中的行为在§ 6.2 隐私浏览中描述。
-
此 API 不会将数据持久化到用户的本地设备中。
-
此 API 不允许降低默认的安全特性。
7. 致谢
特别感谢 Alex Russell、Andreas Butler、Anne van Kesteren、Boris Zbarsky、Chris Messina、Darin Fisher、Domenic Denicola、Gus Caplan、Harald Alvestrand、Jake Archibald、Kagami Sascha Rosylight、L. David Baron、Luciano Pacheco、Marcos Caceres、Ralph Chelala、Raymond Toy、Ryan Fioravanti 和 Victor Costan 对本提案的帮助。
特别感谢 Tab Atkins, Jr. 创建和维护了用于编写本规范的Bikeshed工具,并提供了总体的编写建议。