w-serv-orm 1.1.16 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/index.html CHANGED
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WServOrm.html">WServOrm</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WServOrm.html">WServOrm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#buildBaseline">buildBaseline</a></li><li><a href="global.html#checkContract">checkContract</a></li><li><a href="global.html#compareBaseline">compareBaseline</a></li><li><a href="global.html#cyrb53">cyrb53</a></li><li><a href="global.html#digestRow">digestRow</a></li><li><a href="global.html#getRejectKey">getRejectKey</a></li><li><a href="global.html#isBaselineValid">isBaselineValid</a></li><li><a href="global.html#isOrmFail">isOrmFail</a></li><li><a href="global.html#pickWriteRow">pickWriteRow</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Oct 20 2026 11:25:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 14:34:16 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
@@ -0,0 +1,102 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>isOrmFail.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WServOrm.html">WServOrm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#buildBaseline">buildBaseline</a></li><li><a href="global.html#checkContract">checkContract</a></li><li><a href="global.html#compareBaseline">compareBaseline</a></li><li><a href="global.html#cyrb53">cyrb53</a></li><li><a href="global.html#digestRow">digestRow</a></li><li><a href="global.html#getRejectKey">getRejectKey</a></li><li><a href="global.html#isBaselineValid">isBaselineValid</a></li><li><a href="global.html#isOrmFail">isOrmFail</a></li><li><a href="global.html#pickWriteRow">pickWriteRow</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">isOrmFail.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import size from 'lodash-es/size.js'
49
+ import getItemsFail from 'w-data-collector/src/getItemsFail.mjs'
50
+
51
+
52
+ /**
53
+ * 判斷ORM寫入類函數(insert、save、del、delAll)之回傳結果是否回報失敗
54
+ *
55
+ * 依w-orm系列規格,insert與delAll回傳聚合物件且ok恆為1,save與del回傳逐筆陣列,錯誤皆整批reject;單筆失敗者(save、del)於該筆以ok為0回報。
56
+ * 本函數不論函數與形狀一律判讀:回傳為聚合物件(非null、非陣列之物件)而ok為0者(ORM違約或他種後端)視為失敗;
57
+ * 回傳為逐筆陣列而任一筆ok為0者(同getItemsFail之判讀)視為失敗。
58
+ * 其餘(undefined、null、無ok鍵之物件、空陣列、元素皆ok非0或非物件之陣列、ok為1含略過既有主鍵或未命中之n為0)不視為失敗,維持舊行為。
59
+ * 讀取函數(select)之回傳為數據列,列內欄位可能名為ok,不得以本函數判讀。
60
+ * 供procOrm之寫入類各模式、mix之各步驟、備份還原與測試數據寫入共用。
61
+ *
62
+ * @param {*} r 輸入ORM寫入類函數之回傳結果
63
+ * @returns {Boolean} 回傳是否回報失敗
64
+ */
65
+ function isOrmFail(r) {
66
+
67
+ //check, 逐筆陣列
68
+ if (Array.isArray(r)) {
69
+ return size(getItemsFail(r, [])) > 0
70
+ }
71
+
72
+ //check, 聚合物件
73
+ return r !== null &amp;&amp; typeof r === 'object' &amp;&amp; r.ok === 0
74
+ }
75
+
76
+
77
+ export default isOrmFail
78
+ </code></pre>
79
+ </article>
80
+ </section>
81
+
82
+
83
+
84
+
85
+
86
+
87
+ </div>
88
+
89
+ <br class="clear">
90
+
91
+ <footer>
92
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 14:34:16 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
93
+ </footer>
94
+
95
+ <script>prettyPrint();</script>
96
+ <script src="scripts/polyfill.js"></script>
97
+ <script src="scripts/linenumber.js"></script>
98
+
99
+
100
+
101
+ </body>
102
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w-serv-orm",
3
- "version": "1.1.16",
3
+ "version": "1.1.18",
4
4
  "main": "dist/w-serv-orm.umd.js",
5
5
  "dependencies": {
6
6
  "json5": "^2.2.3",
package/src/WServOrm.mjs CHANGED
@@ -1,12 +1,43 @@
1
1
  import pmSeries from 'wsemi/src/pmSeries.mjs'
2
+ import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
2
3
  import isfun from 'wsemi/src/isfun.mjs'
3
4
  import checkOptRemoved from './checkOptRemoved.mjs'
5
+ import { checkContract, compareBaseline } from './baseline.mjs'
4
6
  import getDbBackupAndRecover from './getDbBackupAndRecover.mjs'
5
7
  import getWoItems from './getWoItems.mjs'
6
8
  import getMapOrm from './getMapOrm.mjs'
7
9
  import getProc from './getProc.mjs'
8
10
 
9
11
 
12
+ function getTabsLock(woItems, tableNames, fn) {
13
+ //withTabLocks之輸入檢查並回傳去重且依表名碼元序排序之表名: 表名須為本實例之資料表(拼錯之表名會鎖到無人使用之鍵而靜默失去互斥), fn須為函數
14
+ if (!Array.isArray(tableNames) || tableNames.length === 0) {
15
+ throw new Error('invalid tableNames of withTabLocks, need a non-empty array of table names')
16
+ }
17
+ for (let t of tableNames) {
18
+ if (typeof t !== 'string' || !Object.prototype.hasOwnProperty.call(woItems, t)) {
19
+ throw new Error('invalid tableNames of withTabLocks, need own table names of this instance')
20
+ }
21
+ }
22
+ if (!isfun(fn)) {
23
+ throw new Error('invalid fn of withTabLocks, need a function')
24
+ }
25
+ return [...new Set(tableNames)].sort() //預設排序為UTF-16碼元序, 不用localeCompare
26
+ }
27
+
28
+
29
+ function mkErrBaseline(c) {
30
+ //baseline不符之reject: message與key皆為固定英文拒收鍵, 另附差異供使用端診斷
31
+ let e = new Error(c.reason)
32
+ e.key = c.reason
33
+ e.added = c.added
34
+ e.removed = c.removed
35
+ e.changed = c.changed
36
+ e.reordered = c.reordered
37
+ return e
38
+ }
39
+
40
+
10
41
  /**
11
42
  * 產生伺服器ORM的支援物件
12
43
  *
@@ -14,6 +45,8 @@ import getProc from './getProc.mjs'
14
45
  *
15
46
  * procOrm之save、del與mix,於ORM回傳之逐筆結果有任一筆ok為0(單筆失敗)時reject,訊息列出失敗筆之主鍵,如'failed to save data: id[b]'(超過100字元者截斷顯示;無有效主鍵者以data[k]列其於輸入內之位置,k自0起;超過10筆者只列前10筆並附總筆數),錯誤原文另由console.log輸出。
16
47
  * 單筆失敗不中斷整批,故reject時其餘筆數已寫入。
48
+ * 寫入類函數之回傳凡可表達失敗者一律檢查,失敗即reject或拋錯,不當成功(規則帳本R-ORM-RESULT-01):save、del與mix之del、diff若ORM改回聚合物件而ok為0(非契約形狀,無從逐筆列出)亦視為失敗,以固定字串'failed to save data'、'failed to delete data'、'failed to update data'reject,mix之del、diff遇此視為整批性錯誤而立即reject,後續步驟不執行;delAll依契約回傳聚合物件且ok恆為1(錯誤皆整批reject),ok為0(或回傳逐筆陣列而任一筆ok為0)以固定字串'failed to delete data'reject;讀取類select之回傳為數據列(列內欄位可能名為ok),失敗只以reject表達,不檢查ok。
49
+ * insert(含mix之新增)依ORM契約回傳聚合物件且ok恆為1(錯誤皆整批reject);ORM若resolve聚合物件而ok為0,或回傳逐筆陣列(非insert契約形狀)而任一筆ok為0(違約或他種後端),亦視為失敗,以固定字串'failed to insert data'reject(錯誤原文另由console.log輸出),已寫入之筆無從回復;mix之新增遇此視為整批性錯誤而立即reject,後續del、diff步驟不執行。回傳undefined、null、無ok鍵之物件、空陣列或逐筆皆ok非0之陣列者不檢查,維持舊行為;略過既有主鍵(n為0、ok為1)為正常結果。備份還原(recover)之逐表insert同此判準,失敗時於該表停止並以Error reject(訊息為'failed to recover data into table[表名]: '接ORM回傳原文,逐筆陣列則列失敗筆{k,id,err},至多前10筆並附總筆數,另列先前已寫入之表),之後之表不寫入,先前之表與該表已被ORM寫入之筆無從回復。
17
50
  * mix之各步驟遇單筆失敗不中斷後續步驟,待全部步驟完成後一併reject;遇整批性錯誤則立即reject,後續步驟不執行。mix於resolve時回傳各步驟所處理之列之id(依add、del、diff之序串接,del之列不分是否命中)。
18
51
  *
19
52
  * insert、save與mix之新增、修改,數據未帶有效主鍵(未給、null、空字串,或非字串非數字之值,含NaN與±Infinity)者,若資料表之主鍵欄為id且為字串型(或型別未知;型別如STRING(36)者以括號前之字判定),由本套件產生主鍵(同各ORM之genIDSeq)並於resolve時回傳;單一空物件與只含一個空物件之陣列不補(ORM視為無效輸入而不寫入),多筆批次中之空物件列則補。
@@ -30,6 +63,12 @@ import getProc from './getProc.mjs'
30
63
  * insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi之clearXSS清除(白名單外之標籤轉義,白名單內之標籤刪除白名單外之屬性與危險網址,標籤外之文字只轉義會開始標籤之<,故'a < b'等數學式不改寫);select與del不經清除。
31
64
  * 字串主鍵若會被清除改寫(含HTML標記,如<後緊接英文字母),整批reject且不寫入任何一筆(mix於任何步驟之前檢查新增與修改),訊息列其位置而不回顯主鍵,如'failed to save data, id contains HTML markup: data[1]';否則改寫後之主鍵與呼叫端所持者不同,以原主鍵查詢與刪除皆找不到。
32
65
  *
66
+ * 多人同時編輯之寫入契約(規格見opensrcOper之dspec;規則帳本R-BASELINE-01、R-LOCK-01、R-ONLYMOD-01):
67
+ * (1)procOrm之第5參數opt(選用,既有四參數呼叫行為不變),有兩個布林鍵:{ mustExist: true }只適用save與del,{ keepUserId: true }只適用insert(見下);用於其餘mode、其餘鍵、非布林者於任何讀寫之前(含早於擴充函數check與前處理)reject,如'invalid opt of procOrm, mustExist supports only save and del'、'invalid opt of procOrm, keepUserId supports only insert'、'invalid opt of procOrm, keepUserId needs a boolean'。以下先述mustExist。save:寫入前以select(id $in)分批(每批至多1000個主鍵,避開各w-orm-*共用條件檢核之$in上限10000)檢查全部列(含補主鍵後)已存在,任一列不存在(含未帶主鍵者)即整批reject固定字串'rowNotFound'且零寫入,並轉傳autoInsert:false給ORM之save,使已刪除之列不復活;del:同樣先檢查,任一列不存在即整批零刪除並reject 'rowNotFound',同批重複之主鍵整批reject 'invalid input of del, duplicate id: data[k]'(列其位置,零刪除)(未帶mustExist之del仍回ORM之逐筆結果,未命中為{n:0,nDeleted:0,ok:1}不視為錯誤)。檢查之後ORM仍回報逐筆未命中者(表鎖紀律被破壞)亦reject 'rowNotFound',已寫入或刪除之筆無從回復。檢查與寫入非原子,呼叫端須已持有該表之表鎖(withTabLocks或guardWrite之write內);檢查之select經ORM,ORM開啟useCache時可能非新鮮,故須同表只由同一ORM實例寫入且不開useCache,或於非快取之ORM實例執行。擴充函數(check、前後處理)之參數物件不含opt。w-orm-mdb之Jet比對不分大小寫且忽略尾端空白,而存在檢查以嚴格字串相等比對,於mdb可能把「ORM會命中」之列判為不存在而reject 'rowNotFound'(零寫入,為收嚴方向,非放寬)。keepUserId:insert預設把列上之userId(schema之keys含者)一律設為procOrm之第1參數(操作者);當userId之語意為擁有者而非建立者(如sso之tokens.userId),給{ keepUserId: true }則逐列保留自有、可列舉之資料屬性userId為非空字串者(單純空白字串亦屬之),缺欄、空字串、null、非字串、繼承或不可列舉者仍以操作者補,保留之值仍經clearXSS;userIdUpdate、timeCreate、timeUpdate照現行(userIdUpdate恆為操作者);表之keys不含userId者無效果;判定依據為前處理改寫後進入ORM之列;save現行即不覆寫列上之userId(只補userIdUpdate、timeUpdate)故不適用,mix之新增為內部步驟不開放;{ keepUserId: false }於各mode無效果。
68
+ * (2)withTabLocks(tableNames, fn):每個實例一個鎖管理器(wsemi之pmKeyMutex),每表一鍵;tableNames為本實例資料表名之非空陣列(含不存在之表名者拋Error,避免拼錯而鎖到無人使用之鍵),去重後依表名碼元序逐一取鎖(多表呼叫之取鎖順序固定,不死結),取齊後執行fn(無參數)並回傳其結果,成敗皆釋放;fn之reject原樣轉出。不可重入:fn內不得再對同表呼叫withTabLocks或guardWrite(同鍵排隊等待自己,永不返回),既有自行取鎖之函式不得再外包同鍵鎖;**巢狀取不同表亦有死結風險**:每次呼叫內雖依碼元序取鎖,跨層(fn內再呼叫)之次序不受控,一個呼叫先持b再等a、另一個先持a再等b即互等;故須一次宣告全部要用之表(如['users','tokens']),不得於fn內再取其他表之鎖(若必須巢狀,所有呼叫路徑須依同一全域遞增次序);逐列實體之業務鎖與表鎖並用時,先取表鎖、後取實體鎖。同表之所有寫入者(後台存檔、逐列寫入、外部同步、系統直寫雙方欄位)須共用同一實例之withTabLocks,鎖只在同一行程之同一實例內有效(多行程部署不在支援範圍)。
69
+ * (3)guardWrite({ tables, read, baseline, contract, write }):withTabLocks(tables)內依序read()(呼叫端之新鮮讀取,須resolve陣列,不得經ORM快取)、compareBaseline(rows, baseline, contract)、write(rows),回傳write之結果。baseline為undefined(不帶基準之寫入)時跳過比對但仍取鎖與讀取;null與其他非undefined之值皆比對,格式不合者reject 'baselineFormat'(不降級為不帶基準)。不符時reject一個Error:message與key皆為固定英文拒收鍵(baselineMismatch、rowNotFound、baselineFormat),另附added、removed、changed(id陣列)與reordered(布林)供診斷,write不被呼叫。contract.table若有,須在tables內。write自身之reject(如procOrm之字串)原樣轉出,使用端以baseline.mjs之getRejectKey(err)取得拒收鍵(字串原樣、Error取其key、其餘為null)。同樣不可重入:write內勿再取鎖(同表重入、巢狀取他表之限制同withTabLocks),須寫入時直接呼叫procOrm或woItems。write內寫入時,只可寫契約之pk與fields(客戶端欄位與雙方欄位),伺服器專有欄位與系統欄位不得出現於寫入列(否則客戶端持舊列整列送出時會把系統已改之值回寫成舊值,規格V5),可用baseline.mjs之pickWriteRow取白名單;整表存檔之刪除集合須為「基準內且在scope內之id − 送出之id」,不得用「庫內全部 − 送出」(會刪掉他人於載入後新增者,規格V8)。
70
+ * (4)純函數子模組src/baseline.mjs(零import,可由瀏覽器端以w-serv-orm/src/baseline.mjs深層匯入):digestRow、buildBaseline、compareBaseline、checkContract、isBaselineValid、cyrb53、pickWriteRow、getRejectKey,規則見該檔與README;不提供雜湊快取。
71
+ *
33
72
  * 擴充函數(addFunCheck、addFunPreProcessing、addFunPostProcessing)屬於各實例,重建實例後須重新註冊;註冊時須傳入函數,否則立即拋錯(addFunCheck另令該實例之procOrm一律reject);註冊後約1毫秒(以計時器)才生效,在此之前之procOrm呼叫不會套用。
34
73
  * 呼叫契約:check以reject或拋錯阻擋,回傳值不影響結果,且早於前處理執行;前處理逐一呼叫,各收新建之參數物件{woName,userId,mode,input},input為前一個回傳之input,回傳須為含input之物件否則reject;後處理逐一呼叫,各收新建之參數物件且其output為前一個之回傳值,最後之回傳值即為procOrm之回傳值。
35
74
  * 後處理只在check、前處理與ORM操作皆成功後執行,逐筆失敗(含del)或mix部分寫入後reject者皆不執行;後處理自身亦可reject。後處理之input為前處理後之數據(無前處理者即呼叫端傳入之數據),不含本套件之補值與主鍵轉型,主鍵見output。
@@ -45,7 +84,7 @@ import getProc from './getProc.mjs'
45
84
  * @param {String} url 輸入資料庫連線位址字串
46
85
  * @param {String} db 輸入資料庫名稱字串
47
86
  * @param {Object} [opt={}] 輸入設定物件,預設{};目前無設定鍵,保留供日後擴充,已移除之設定見上述
48
- * @returns {Object} 回傳伺服器ORM支援物件,含woItems(各資料表ORM實例)、procOrm(資料操作函數,輸入為userId、woName、mode、input)、addFunCheck、addFunPreProcessing、addFunPostProcessing、closeAll(逐一關閉各資料表ORM之連線),以及backup(woItems)與recover(fp, woItems, genModels, needCreateStorage);後兩者不綁定本實例,須自行傳入woItems(須為非空物件,否則reject),其參數順序與getDb之recover(woItems, fp, opt)不同;recover於寫入前驗證備份內容(須為非空陣列且各筆之name為woItems內之資料表),失敗時reject,逐表寫入時中途失敗則先前之表已寫入
87
+ * @returns {Object} 回傳伺服器ORM支援物件,含woItems(各資料表ORM實例)、procOrm(資料操作函數,輸入為userId、woName、mode、input,及選用之opt)、withTabLocks(tableNames, fn)與guardWrite({ tables, read, baseline, contract, write })(見上述多人同時編輯之寫入契約)、addFunCheck、addFunPreProcessing、addFunPostProcessing、closeAll(逐一關閉各資料表ORM之連線),以及backup(woItems)與recover(fp, woItems, genModels, needCreateStorage);後兩者不綁定本實例,須自行傳入woItems(須為非空物件,否則reject),其參數順序與getDb之recover(woItems, fp, opt)不同;recover於寫入前驗證備份內容(須為非空陣列且各筆之name為woItems內之資料表),失敗時reject,逐表寫入時中途失敗則先前之表已寫入
49
88
  * @example
50
89
  *
51
90
  * // import WOrm from 'w-orm-mongodb/src/WOrmMongodb.mjs' //自行選擇引用ORM
@@ -127,11 +166,65 @@ function WServOrm(ds, WOrm, url, db, opt = {}) {
127
166
  })
128
167
  }
129
168
 
169
+ //kmx, 每實例一個鎖管理器(每表一鍵), 同表之寫入者須共用(R-LOCK-01)
170
+ let kmx = pmKeyMutex()
171
+
172
+ //withTabLocks, 依表名碼元序逐一取鎖後執行fn; 不可重入(說明見上方JSDoc)
173
+ let withTabLocks = async (tableNames, fn) => {
174
+ let ts = getTabsLock(woItems, tableNames, fn)
175
+ let run = fn
176
+ for (let k = ts.length - 1; k >= 0; k--) {
177
+ let inner = run
178
+ let key = ts[k]
179
+ run = () => kmx(key, inner)
180
+ }
181
+ return run()
182
+ }
183
+
184
+ //guardWrite, 取鎖 -> 新鮮讀取 -> 比對 -> 寫入, 同一表鎖內(說明見上方JSDoc)
185
+ let guardWrite = async (p) => {
186
+ if (p === null || typeof p !== 'object') {
187
+ throw new Error('invalid input of guardWrite, need an object')
188
+ }
189
+ let { tables, read, baseline, contract, write } = p
190
+ if (!isfun(read)) {
191
+ throw new Error('invalid read of guardWrite, need a function')
192
+ }
193
+ if (!isfun(write)) {
194
+ throw new Error('invalid write of guardWrite, need a function')
195
+ }
196
+
197
+ //contract, 帶基準時必須有; 未帶基準而給了契約者仍驗證(及早發現使用端之錯誤)
198
+ let useBaseline = baseline !== undefined
199
+ if (useBaseline || contract !== undefined) {
200
+ checkContract(contract)
201
+ if (typeof contract.table === 'string' && !(Array.isArray(tables) && tables.indexOf(contract.table) >= 0)) {
202
+ throw new Error('invalid tables of guardWrite, need to contain contract.table')
203
+ }
204
+ }
205
+
206
+ return withTabLocks(tables, async () => {
207
+ let rows = await read()
208
+ if (!Array.isArray(rows)) {
209
+ throw new Error('invalid rows of guardWrite, read needs to resolve an array')
210
+ }
211
+ if (useBaseline) {
212
+ let c = compareBaseline(rows, baseline, contract)
213
+ if (!c.ok) {
214
+ throw mkErrBaseline(c)
215
+ }
216
+ }
217
+ return await write(rows)
218
+ })
219
+ }
220
+
130
221
  return {
131
222
  ...getDbBackupAndRecover,
132
223
  woItems,
133
224
  // mapOrm,
134
225
  closeAll,
226
+ withTabLocks,
227
+ guardWrite,
135
228
  ...proc,
136
229
  }
137
230
  }