w-orm-lmdb 1.0.12 → 1.0.14

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/src/WOrmLmdb.mjs CHANGED
@@ -1,765 +1,813 @@
1
- import { open } from 'lmdb'
2
- // import mingo from 'mingo' //mingo內未更新import寫法, 會導致ERR_UNSUPPORTED_DIR_IMPORT, 故須改用require引入使用
3
- import mingo from './reqMingo.js'
4
- import size from 'lodash-es/size.js'
5
- import get from 'lodash-es/get.js'
6
- import each from 'lodash-es/each.js'
7
- import map from 'lodash-es/map.js'
8
- import merge from 'lodash-es/merge.js'
9
- import isEqual from 'lodash-es/isEqual.js'
10
- import cloneDeep from 'lodash-es/cloneDeep.js'
11
- import isestr from 'wsemi/src/isestr.mjs'
12
- import isarr from 'wsemi/src/isarr.mjs'
13
- import isearr from 'wsemi/src/isearr.mjs'
14
- import iseobj from 'wsemi/src/iseobj.mjs'
15
- import isbol from 'wsemi/src/isbol.mjs'
16
- import haskey from 'wsemi/src/haskey.mjs'
17
- import evem from 'wsemi/src/evem.mjs'
18
- import genIDSeq from 'wsemi/src/genIDSeq.mjs'
19
- import pmSeries from 'wsemi/src/pmSeries.mjs'
20
- import waitFun from 'wsemi/src/waitFun.mjs'
21
-
22
-
23
- /**
24
- * 操作資料庫(LMDB)
25
- *
26
- * @class
27
- * @param {Object} [opt={}] 輸入設定物件,預設{}
28
- * @param {String} [opt.url='_db'] 輸入資料庫用資料夾字串,預設'_db'
29
- * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
30
- * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
31
- * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
32
- * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
33
- */
34
- function WOrmLmdb(opt = {}) {
35
-
36
- //_cache
37
- let _cache = null
38
-
39
- //url
40
- let url = get(opt, 'url')
41
- if (!isestr(url)) {
42
- url = './_db'
43
- }
44
-
45
- //db
46
- let db = get(opt, 'db')
47
- if (!isestr(db)) {
48
- db = 'worm'
49
- }
50
-
51
- //cl
52
- let cl = get(opt, 'cl')
53
- if (!isestr(cl)) {
54
- cl = 'test'
55
- }
56
-
57
- //useCache
58
- let useCache = get(opt, 'useCache')
59
- if (!isbol(useCache)) {
60
- useCache = false
61
- }
62
-
63
- //storage
64
- let storage = `${url}/${db}/${cl}`
65
- // console.log('storage',storage)
66
-
67
- // client
68
- let client = open({
69
- path: storage,
70
- compression: true,
71
- useVersions: true,
72
- })
73
-
74
- //ee
75
- let ee = evem()
76
-
77
- //getData
78
- let getData = async() => {
79
-
80
- //check
81
- if (useCache && isarr(_cache)) {
82
- return cloneDeep(_cache) //與外部使用數據脫勾
83
- }
84
-
85
- //waitOpen
86
- await waitOpen()
87
- // console.log('client.status',client.status)
88
-
89
- //ltdt
90
- let ltdt = []
91
- for await (let { value: dt } of client.getRange()) {
92
- // console.log('dt',dt)
93
- ltdt.push(dt)
94
- }
95
-
96
- if (useCache) {
97
- _cache = ltdt
98
- return cloneDeep(ltdt) //與外部使用數據脫勾
99
- }
100
- return ltdt
101
- }
102
-
103
- //waitOpen, 等待client開啟, 因getRange與get於client關閉時會拋錯
104
- let waitOpen = async () => {
105
- await waitFun(() => {
106
- if (client.status === 'closed') {
107
- console.log(`client.status[${client.status}], level is closed`)
108
- }
109
- return client.status === 'open'
110
- })
111
- }
112
-
113
- //getValue
114
- let getValue = async (key) => {
115
- let value = null
116
- try {
117
- value = await client.get(key)
118
- }
119
- catch (err) {
120
- // if (err.notFound) {
121
- // console.log('資料不存在')
122
- // }
123
- // else {
124
- // console.log('其他錯誤:', err)
125
- // }
126
- }
127
- return value
128
- }
129
-
130
- /**
131
- * 查詢數據
132
- *
133
- * @memberOf WOrmLmdb
134
- * @param {Object} [find={}] 輸入查詢條件物件
135
- * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
136
- */
137
- async function select(find = {}) {
138
- let isErr = false
139
- let res = null
140
-
141
- try {
142
-
143
- //ltdt
144
- let ltdt = await getData()
145
- // console.log('select ltdt',ltdt)
146
-
147
- //filter
148
- if (iseobj(find)) {
149
-
150
- //q
151
- let q = new mingo.Query(find)
152
- // console.log('q', q)
153
-
154
- //find
155
- res = q.find(ltdt).all()
156
- // console.log('select(find) ltdt',res)
157
-
158
- }
159
- else {
160
- res = ltdt
161
- }
162
-
163
- //check
164
- if (!isarr(res)) {
165
- isErr = true
166
- res = `can not select by find[${JSON.stringify(find)}]`
167
- }
168
-
169
- }
170
- catch (err) {
171
- isErr = true
172
- res = err
173
- }
174
- // console.log('res', res)
175
-
176
- //check
177
- if (isErr) {
178
- return Promise.reject(res)
179
- }
180
-
181
- return res
182
- }
183
-
184
- /**
185
- * 由主鍵id查詢單筆數據,因直接由LMDB取值,不需如select提取全表數據再過濾,故數據量大時效能較佳
186
- *
187
- * @memberOf WOrmLmdb
188
- * @param {String} id 輸入主鍵字串
189
- * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此id則回傳null,reject回傳錯誤訊息
190
- */
191
- async function selectById(id) {
192
- let isErr = false
193
- let res = null
194
-
195
- try {
196
-
197
- //check
198
- if (!isestr(id)) {
199
- //未給有效id視為查無數據
200
- return null
201
- }
202
-
203
- //waitOpen
204
- await waitOpen()
205
-
206
- //查找資料表內id
207
- let v = await getValue(id) //不會有catch
208
-
209
- //check, 判定基準與insert、save、del內對既有數據之認定一致
210
- if (iseobj(v)) {
211
- res = v
212
- }
213
- else {
214
- //不存在id, 回傳null
215
- res = null
216
- }
217
-
218
- }
219
- catch (err) {
220
- isErr = true
221
- res = err
222
- }
223
-
224
- //check
225
- if (isErr) {
226
- return Promise.reject(res)
227
- }
228
-
229
- return res
230
- }
231
-
232
- /**
233
- * 插入數據
234
- *
235
- * @memberOf WOrmLmdb
236
- * @param {Object|Array} data 輸入數據物件或陣列
237
- * @returns {Promise} 回傳Promise,resolve回傳插入結果,reject回傳錯誤訊息
238
- */
239
- async function insert(data) {
240
- let isErr = false
241
-
242
- //check
243
- if (!iseobj(data) && !isearr(data)) {
244
- return {
245
- n: 0,
246
- nInserted: 0,
247
- ok: 1,
248
- }
249
- }
250
-
251
- //cloneDeep, 與外部數據脫勾
252
- data = cloneDeep(data)
253
-
254
- //res
255
- let res = null
256
- try {
257
-
258
- //check
259
- if (!isarr(data)) {
260
- data = [data]
261
- }
262
-
263
- //check id
264
- data = map(data, function(v) {
265
- if (!isestr(v.id)) {
266
- v.id = genIDSeq()
267
- }
268
- return v
269
- })
270
-
271
- //each
272
- //ifNoExists, 由LMDB於同一寫交易內原子完成[檢查v.id未存在]與[寫入], 併發時同一v.id僅有一次成功,
273
- //同批含重複v.id時亦僅首筆成功, 故不逐筆await而一次送出, 由LMDB寫執行緒依序處理並合併為批次寫交易
274
- //注意: 回調函數須為同步函數, 且client.put須於回調函數內直接呼叫,
275
- //若將client.put置於await之後, 該寫入會逸出條件批次而變成無條件寫入, 並使回傳值與實際狀態不一致
276
- let nAll = size(data)
277
- let ltdone = await Promise.all(map(data, function(v) {
278
- // console.log(v)
279
- return client.ifNoExists(v.id, () => {
280
- client.put(v.id, v)
281
- })
282
- }))
283
-
284
- //nPush
285
- let nPush = 0
286
- each(ltdone, function(done) {
287
- if (done) {
288
- //未存在v.id, 已寫入
289
- nPush++
290
- }
291
- else {
292
- //已存在v.id則不push
293
- }
294
- })
295
-
296
- //res
297
- res = {
298
- n: nAll,
299
- nInserted: nPush,
300
- ok: 1,
301
- }
302
-
303
- }
304
- catch (err) {
305
- isErr = true
306
- res = err
307
- }
308
-
309
- //update, 不能保證插入多少, 一律重設快取
310
- _cache = null
311
-
312
- //emit, 於change可能須使用select, 故須放在重設快取之後
313
- if (!isErr) {
314
- try {
315
-
316
- //emit
317
- ee.emit('change', 'insert', data, res)
318
-
319
- }
320
- catch (err) {
321
- console.log(err)
322
- }
323
- }
324
-
325
- //check
326
- if (isErr) {
327
- return Promise.reject(res)
328
- }
329
-
330
- return res
331
- }
332
-
333
- /**
334
- * 儲存數據
335
- *
336
- * @memberOf WOrmLmdb
337
- * @param {Object|Array} data 輸入數據物件或陣列
338
- * @param {Object} [option={}] 輸入設定物件,預設為{}
339
- * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
340
- * @returns {Promise} 回傳Promise,resolve回傳儲存結果,reject回傳錯誤訊息
341
- */
342
- async function save(data, option = {}) {
343
- let isErr = false
344
-
345
- //check
346
- if (!iseobj(data) && !isearr(data)) {
347
- return []
348
- }
349
-
350
- //cloneDeep, 與外部數據脫勾
351
- data = cloneDeep(data)
352
-
353
- //autoInsert
354
- let autoInsert = get(option, 'autoInsert', true)
355
-
356
- //res
357
- let res = null
358
- try {
359
-
360
- //check
361
- if (!isarr(data)) {
362
- data = [data]
363
- }
364
-
365
- //check id
366
- data = map(data, function(v) {
367
- if (!isestr(v.id)) {
368
- v.id = genIDSeq()
369
- }
370
- return v
371
- })
372
-
373
- //pmSeries
374
- res = await pmSeries(data, async(v) => {
375
-
376
- //rest
377
- let rest = null
378
-
379
- //快速路徑, 先不開寫交易直接預讀, 內容相同者本就不須寫入, 可省去開啟寫交易之成本
380
- //預讀值縱使已被其他寫入者更動而過期亦不影響正確性, 因內容相同時本次save等價於無操作,
381
- //無操作可視為於預讀當下即已完成, 其後他人之寫入結果與[本次save先執行再輪到他人]相同
382
- let vp = await getValue(v.id) //不會有catch
383
- if (iseobj(vp) && isEqual(v, vp)) {
384
- //內容相同不更新
385
- return {
386
- n: 0,
387
- nModified: 0,
388
- ok: 1,
389
- }
390
- }
391
-
392
- //existed, updated, inserted
393
- let existed = false
394
- let updated = false
395
- let inserted = false
396
-
397
- //transaction, 由LMDB於同一寫交易內原子完成[查找v.id]與[合併寫入],
398
- //併發時不會有兩方各自讀到同一舊值再各自寫入而導致遺失更新
399
- //注意: 回調函數須為同步函數, client.get於寫交易內為同步取值, 不可使用await,
400
- //若將client.get或client.put置於await之後, 該操作會逸出本交易而失去原子性
401
- await client.transaction(() => {
402
-
403
- //查找資料表內v.id
404
- let vv = client.get(v.id)
405
-
406
- //existed
407
- existed = iseobj(vv)
408
-
409
- //check
410
- if (existed) {
411
- //已存在v.id
412
- if (isEqual(v, vv)) {
413
- //內容相同不更新
414
- }
415
- else {
416
- //內容不同須更新
417
-
418
- //merge and put
419
- client.put(v.id, merge(vv, v))
420
-
421
- updated = true
422
- }
423
- }
424
- else {
425
- //內容不存在, 若autoInsert則須插入
426
- if (autoInsert) {
427
-
428
- //put
429
- client.put(v.id, v)
430
-
431
- inserted = true
432
- }
433
- }
434
-
435
- })
436
-
437
- //rest
438
- if (inserted) {
439
- //內容不存在而自動插入, 回傳形狀同insert
440
- rest = {
441
- n: 1,
442
- nInserted: 1,
443
- ok: 1,
444
- }
445
- }
446
- else if (updated) {
447
- rest = {
448
- n: 1,
449
- nModified: 1,
450
- ok: 1,
451
- }
452
- }
453
- else {
454
- //rest.n === 0:
455
- // 內容相同不更新, 不須update
456
- // 內容不存在且未開啟autoInsert
457
- rest = {
458
- n: 0,
459
- nModified: 0,
460
- ok: 1,
461
- }
462
- }
463
-
464
- //emit, 原autoInsert係轉呼叫insert故會發出insert事件, 此處維持既有行為,
465
- //須於交易外發出, 避免外部訂閱函數於寫交易內執行
466
- if (inserted) {
467
-
468
- //update, 不能保證插入多少, 一律重設快取
469
- _cache = null
470
-
471
- try {
472
-
473
- //emit
474
- ee.emit('change', 'insert', [v], rest)
475
-
476
- }
477
- catch (err) {
478
- console.log(err)
479
- }
480
-
481
- }
482
-
483
- return rest
484
- })
485
-
486
- }
487
- catch (err) {
488
- isErr = true
489
- res = err
490
- }
491
-
492
- //update, 不能保證變更多少, 一律重設快取
493
- _cache = null
494
-
495
- //emit, 於change可能須使用select, 故須放在重設快取之後
496
- if (!isErr) {
497
- try {
498
-
499
- //emit
500
- ee.emit('change', 'save', data, res)
501
-
502
- }
503
- catch (err) {
504
- console.log(err)
505
- }
506
- }
507
-
508
- //check
509
- if (isErr) {
510
- return Promise.reject(res)
511
- }
512
-
513
- return res
514
- }
515
-
516
- /**
517
- * 刪除數據
518
- *
519
- * @memberOf WOrmLmdb
520
- * @param {Object|Array} data 輸入數據物件或陣列
521
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
522
- */
523
- async function del(data) {
524
- let isErr = false
525
-
526
- //check
527
- if (!iseobj(data) && !isearr(data)) {
528
- return []
529
- }
530
-
531
- //cloneDeep, 與外部數據脫勾
532
- data = cloneDeep(data)
533
-
534
- //res
535
- let res = null
536
- try {
537
-
538
- //check
539
- if (!isarr(data)) {
540
- data = [data]
541
- }
542
-
543
- //pmSeries
544
- res = await pmSeries(data, async(v) => {
545
-
546
- //rest
547
- let rest = null
548
-
549
- //id
550
- let id = get(v, 'id', '')
551
-
552
- //check
553
- if (isestr(id)) {
554
-
555
- //查找資料表內v.id
556
- let vv = await getValue(v.id) //不會有catch
557
-
558
- //check
559
- if (iseobj(vv)) {
560
- //已存在v.id則須刪除
561
-
562
- //del
563
- await client.del(v.id)
564
-
565
- //rest
566
- rest = {
567
- n: 1,
568
- nDeleted: 1,
569
- ok: 1,
570
- }
571
-
572
- }
573
- else {
574
- //不存在v.id則不刪除
575
-
576
- //rest
577
- rest = {
578
- n: 1,
579
- nDeleted: 0,
580
- ok: 1,
581
- }
582
-
583
- }
584
-
585
- }
586
- else {
587
- //未給v.id則不刪除
588
-
589
- //rest
590
- rest = {
591
- n: 1,
592
- nDeleted: 0,
593
- ok: 0, //未給v.id視為有問題數據, 故ok給0
594
- }
595
-
596
- }
597
-
598
- return rest
599
- })
600
-
601
- }
602
- catch (err) {
603
- isErr = true
604
- res = err
605
- }
606
-
607
- //update, 不能保證刪除多少, 一律重設快取
608
- _cache = null
609
-
610
- //emit, 於change可能須使用select, 故須放在重設快取之後
611
- if (!isErr) {
612
- try {
613
-
614
- //emit
615
- ee.emit('change', 'del', data, res)
616
-
617
- }
618
- catch (err) {
619
- console.log(err)
620
- }
621
- }
622
-
623
- //check
624
- if (isErr) {
625
- return Promise.reject(res)
626
- }
627
-
628
- return res
629
- }
630
-
631
- /**
632
- * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
633
- *
634
- * @memberOf WOrmLmdb
635
- * @param {Object} [find={}] 輸入刪除條件物件
636
- * @returns {Promise} 回傳Promise,resolve回傳刪除結果,reject回傳錯誤訊息
637
- */
638
- async function delAll(find = {}) {
639
- let isErr = false
640
-
641
- //res
642
- let res = null
643
- try {
644
-
645
- //ltdt
646
- let ltdt = await getData()
647
-
648
- //filter
649
- let nAll = size(ltdt)
650
- let nDel = 0
651
- if (iseobj(find)) {
652
-
653
- //q
654
- let q = new mingo.Query(find)
655
- // console.log('q', q)
656
-
657
- //find
658
- let _res = q.find(ltdt).all()
659
- // console.log('_res', _res)
660
-
661
- //nDel
662
- nDel = size(_res)
663
- // console.log('nDel', nDel)
664
-
665
- if (nDel === 0) {
666
- //未有find結果等於不刪除
667
- }
668
- else if (nAll === nDel) {
669
- //全在find結果內等於全部刪除
670
-
671
- //empty
672
- for (let v of ltdt) {
673
- await client.del(v.id)
674
- }
675
-
676
- }
677
- else {
678
- //部份在find結果內
679
-
680
- //_kp
681
- let _kp = {}
682
- each(_res, (v, k) => {
683
- _kp[v.id] = { k, v }
684
- })
685
-
686
- //del
687
- for (let v of ltdt) {
688
- if (haskey(_kp, v.id)) {
689
- //在find結果內代表須刪除
690
- await client.del(v.id)
691
- }
692
- }
693
-
694
- }
695
- }
696
- else {
697
-
698
- //nDel
699
- nDel = nAll
700
-
701
- //empty
702
- for (let v of ltdt) {
703
- await client.del(v.id)
704
- }
705
-
706
- }
707
-
708
- //res
709
- res = {
710
- n: nAll,
711
- nDeleted: nDel,
712
- ok: 1,
713
- }
714
-
715
- }
716
- catch (err) {
717
- isErr = true
718
- res = err
719
- }
720
-
721
- //update, 不能保證刪除多少, 一律重設快取
722
- _cache = null
723
-
724
- //emit, 於change可能須使用select, 故須放在重設快取之後
725
- if (!isErr) {
726
- try {
727
-
728
- //emit
729
- ee.emit('change', 'delAll', null, res)
730
-
731
- }
732
- catch (err) {
733
- console.log(err)
734
- }
735
- }
736
-
737
- //check
738
- if (isErr) {
739
- return Promise.reject(res)
740
- }
741
-
742
- return res
743
- }
744
-
745
- //close, 關閉內部LMDB env並釋放檔案鎖, close後該實例即報廢, 須重新new WOrmLmdb
746
- let close = async () => {
747
- if (client && client.status !== 'closed') {
748
- await client.close() //lmdb root database之close(), 回傳Promise
749
- }
750
- }
751
-
752
- //save
753
- ee.select = select
754
- ee.selectById = selectById
755
- ee.insert = insert
756
- ee.save = save
757
- ee.del = del
758
- ee.delAll = delAll
759
- ee.close = close
760
-
761
- return ee
762
- }
763
-
764
-
765
- export default WOrmLmdb
1
+ import { open } from 'lmdb'
2
+ // import mingo from 'mingo' //mingo內未更新import寫法, 會導致ERR_UNSUPPORTED_DIR_IMPORT, 故須改用require引入使用
3
+ import mingo from './reqMingo.js'
4
+ import size from 'lodash-es/size.js'
5
+ import get from 'lodash-es/get.js'
6
+ import each from 'lodash-es/each.js'
7
+ import map from 'lodash-es/map.js'
8
+ import merge from 'lodash-es/merge.js'
9
+ import isEqual from 'lodash-es/isEqual.js'
10
+ import cloneDeep from 'lodash-es/cloneDeep.js'
11
+ import isestr from 'wsemi/src/isestr.mjs'
12
+ import isarr from 'wsemi/src/isarr.mjs'
13
+ import isearr from 'wsemi/src/isearr.mjs'
14
+ import iseobj from 'wsemi/src/iseobj.mjs'
15
+ import isbol from 'wsemi/src/isbol.mjs'
16
+ import haskey from 'wsemi/src/haskey.mjs'
17
+ import evem from 'wsemi/src/evem.mjs'
18
+ import genIDSeq from 'wsemi/src/genIDSeq.mjs'
19
+ import pmSeries from 'wsemi/src/pmSeries.mjs'
20
+ import waitFun from 'wsemi/src/waitFun.mjs'
21
+
22
+
23
+ /**
24
+ * 操作資料庫(LMDB)
25
+ *
26
+ * @class
27
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
28
+ * @param {String} [opt.url='_db'] 輸入資料庫用資料夾字串,預設'_db'
29
+ * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
30
+ * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
31
+ * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
32
+ * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
33
+ */
34
+ function WOrmLmdb(opt = {}) {
35
+
36
+ //_cache
37
+ let _cache = null
38
+
39
+ //url
40
+ let url = get(opt, 'url')
41
+ if (!isestr(url)) {
42
+ url = './_db'
43
+ }
44
+
45
+ //db
46
+ let db = get(opt, 'db')
47
+ if (!isestr(db)) {
48
+ db = 'worm'
49
+ }
50
+
51
+ //cl
52
+ let cl = get(opt, 'cl')
53
+ if (!isestr(cl)) {
54
+ cl = 'test'
55
+ }
56
+
57
+ //useCache
58
+ let useCache = get(opt, 'useCache')
59
+ if (!isbol(useCache)) {
60
+ useCache = false
61
+ }
62
+
63
+ //storage
64
+ let storage = `${url}/${db}/${cl}`
65
+ // console.log('storage',storage)
66
+
67
+ // client
68
+ let client = open({
69
+ path: storage,
70
+ compression: true,
71
+ useVersions: true,
72
+ })
73
+
74
+ //ee
75
+ let ee = evem()
76
+
77
+ //getData
78
+ let getData = async() => {
79
+
80
+ //check
81
+ if (useCache && isarr(_cache)) {
82
+ return cloneDeep(_cache) //與外部使用數據脫勾
83
+ }
84
+
85
+ //waitOpen
86
+ await waitOpen()
87
+ // console.log('client.status',client.status)
88
+
89
+ //ltdt
90
+ let ltdt = []
91
+ for await (let { value: dt } of client.getRange()) {
92
+ // console.log('dt',dt)
93
+ ltdt.push(dt)
94
+ }
95
+
96
+ if (useCache) {
97
+ _cache = ltdt
98
+ return cloneDeep(ltdt) //與外部使用數據脫勾
99
+ }
100
+ return ltdt
101
+ }
102
+
103
+ //waitOpen, 等待client開啟, 因getRange與get於client關閉時會拋錯
104
+ let waitOpen = async () => {
105
+ await waitFun(() => {
106
+ if (client.status === 'closed') {
107
+ console.log(`client.status[${client.status}], level is closed`)
108
+ }
109
+ return client.status === 'open'
110
+ })
111
+ }
112
+
113
+ //getErrMsg, 取錯誤訊息字串, 供逐筆結果之err欄位使用
114
+ let getErrMsg = (err) => {
115
+ let m = get(err, 'message')
116
+ if (isestr(m)) {
117
+ return m
118
+ }
119
+ return String(err)
120
+ }
121
+
122
+ //getValue
123
+ let getValue = async (key) => {
124
+ let value = null
125
+ try {
126
+ value = await client.get(key)
127
+ }
128
+ catch (err) {
129
+ // if (err.notFound) {
130
+ // console.log('資料不存在')
131
+ // }
132
+ // else {
133
+ // console.log('其他錯誤:', err)
134
+ // }
135
+ }
136
+ return value
137
+ }
138
+
139
+ /**
140
+ * 查詢數據
141
+ *
142
+ * @memberOf WOrmLmdb
143
+ * @param {Object} [find={}] 輸入查詢條件物件
144
+ * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
145
+ */
146
+ async function select(find = {}) {
147
+ let isErr = false
148
+ let res = null
149
+
150
+ try {
151
+
152
+ //ltdt
153
+ let ltdt = await getData()
154
+ // console.log('select ltdt',ltdt)
155
+
156
+ //filter
157
+ if (iseobj(find)) {
158
+
159
+ //q
160
+ let q = new mingo.Query(find)
161
+ // console.log('q', q)
162
+
163
+ //find
164
+ res = q.find(ltdt).all()
165
+ // console.log('select(find) ltdt',res)
166
+
167
+ }
168
+ else {
169
+ res = ltdt
170
+ }
171
+
172
+ //check
173
+ if (!isarr(res)) {
174
+ isErr = true
175
+ res = `can not select by find[${JSON.stringify(find)}]`
176
+ }
177
+
178
+ }
179
+ catch (err) {
180
+ isErr = true
181
+ res = err
182
+ }
183
+ // console.log('res', res)
184
+
185
+ //check
186
+ if (isErr) {
187
+ return Promise.reject(res)
188
+ }
189
+
190
+ return res
191
+ }
192
+
193
+ /**
194
+ * 由主鍵查詢單筆數據,因直接由LMDB取值,不需如select提取全表數據再過濾,故數據量大時效能較佳
195
+ * 註: 本套件之主鍵欄位固定為id,尚未支援指定其他欄位為主鍵
196
+ *
197
+ * @memberOf WOrmLmdb
198
+ * @param {String} pk 輸入主鍵值字串,本套件之主鍵欄位為id
199
+ * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵則回傳null,reject回傳錯誤訊息
200
+ */
201
+ async function selectByPk(pk) {
202
+ let isErr = false
203
+ let res = null
204
+
205
+ try {
206
+
207
+ //check
208
+ if (!isestr(pk)) {
209
+ //未給有效主鍵值視為查無數據
210
+ return null
211
+ }
212
+
213
+ //waitOpen
214
+ await waitOpen()
215
+
216
+ //查找資料表內pk
217
+ let v = await getValue(pk) //不會有catch
218
+
219
+ //check, 判定基準與insert、save、del內對既有數據之認定一致
220
+ if (iseobj(v)) {
221
+ res = v
222
+ }
223
+ else {
224
+ //不存在此主鍵, 回傳null
225
+ res = null
226
+ }
227
+
228
+ }
229
+ catch (err) {
230
+ isErr = true
231
+ res = err
232
+ }
233
+
234
+ //check
235
+ if (isErr) {
236
+ return Promise.reject(res)
237
+ }
238
+
239
+ return res
240
+ }
241
+
242
+ /**
243
+ * 插入數據
244
+ *
245
+ * @memberOf WOrmLmdb
246
+ * @param {Object|Array} data 輸入數據物件或陣列
247
+ * @returns {Promise} 回傳Promise,resolve回傳插入結果物件{n,nInserted,ok},n為輸入筆數、nInserted為實際插入筆數,reject回傳錯誤訊息
248
+ */
249
+ async function insert(data) {
250
+ let isErr = false
251
+
252
+ //check
253
+ if (!iseobj(data) && !isearr(data)) {
254
+ return {
255
+ n: 0,
256
+ nInserted: 0,
257
+ ok: 1,
258
+ }
259
+ }
260
+
261
+ //cloneDeep, 與外部數據脫勾
262
+ data = cloneDeep(data)
263
+
264
+ //res
265
+ let res = null
266
+ try {
267
+
268
+ //check
269
+ if (!isarr(data)) {
270
+ data = [data]
271
+ }
272
+
273
+ //check id
274
+ data = map(data, function(v) {
275
+ if (!isestr(v.id)) {
276
+ v.id = genIDSeq()
277
+ }
278
+ return v
279
+ })
280
+
281
+ //each
282
+ //ifNoExists, 由LMDB於同一寫交易內原子完成[檢查v.id未存在]與[寫入], 併發時同一v.id僅有一次成功,
283
+ //同批含重複v.id時亦僅首筆成功, 故不逐筆await而一次送出, 由LMDB寫執行緒依序處理並合併為批次寫交易
284
+ //注意: 回調函數須為同步函數, 且client.put須於回調函數內直接呼叫,
285
+ //若將client.put置於await之後, 該寫入會逸出條件批次而變成無條件寫入, 並使回傳值與實際狀態不一致
286
+ let nAll = size(data)
287
+ let ltdone = await Promise.all(map(data, function(v) {
288
+ // console.log(v)
289
+ return client.ifNoExists(v.id, () => {
290
+ client.put(v.id, v)
291
+ })
292
+ }))
293
+
294
+ //nPush
295
+ let nPush = 0
296
+ each(ltdone, function(done) {
297
+ if (done) {
298
+ //未存在v.id, 已寫入
299
+ nPush++
300
+ }
301
+ else {
302
+ //已存在v.id則不push
303
+ }
304
+ })
305
+
306
+ //res
307
+ res = {
308
+ n: nAll,
309
+ nInserted: nPush,
310
+ ok: 1,
311
+ }
312
+
313
+ }
314
+ catch (err) {
315
+ isErr = true
316
+ res = err
317
+ }
318
+
319
+ //update, 不能保證插入多少, 一律重設快取
320
+ _cache = null
321
+
322
+ //emit, 於change可能須使用select, 故須放在重設快取之後
323
+ if (!isErr) {
324
+ try {
325
+
326
+ //emit
327
+ ee.emit('change', 'insert', data, res)
328
+
329
+ }
330
+ catch (err) {
331
+ console.log(err)
332
+ }
333
+ }
334
+
335
+ //check
336
+ if (isErr) {
337
+ return Promise.reject(res)
338
+ }
339
+
340
+ return res
341
+ }
342
+
343
+ /**
344
+ * 儲存數據
345
+ *
346
+ * @memberOf WOrmLmdb
347
+ * @param {Object|Array} data 輸入數據物件或陣列
348
+ * @param {Object} [option={}] 輸入設定物件,預設為{}
349
+ * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
350
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之儲存結果陣列,各筆為{n,nInserted,nModified,ok},n為主鍵命中筆數,單筆失敗者ok為0並附err,reject回傳錯誤訊息
351
+ */
352
+ async function save(data, option = {}) {
353
+ let isErr = false
354
+
355
+ //check
356
+ if (!iseobj(data) && !isearr(data)) {
357
+ return []
358
+ }
359
+
360
+ //cloneDeep, 與外部數據脫勾
361
+ data = cloneDeep(data)
362
+
363
+ //autoInsert
364
+ let autoInsert = get(option, 'autoInsert', true)
365
+
366
+ //res
367
+ let res = null
368
+ try {
369
+
370
+ //check
371
+ if (!isarr(data)) {
372
+ data = [data]
373
+ }
374
+
375
+ //check id
376
+ data = map(data, function(v) {
377
+ if (!isestr(v.id)) {
378
+ v.id = genIDSeq()
379
+ }
380
+ return v
381
+ })
382
+
383
+ //pmSeries
384
+ res = await pmSeries(data, async(v) => {
385
+
386
+ //rest
387
+ let rest = null
388
+
389
+ //inserted, 供emit使用
390
+ let inserted = false
391
+
392
+ try {
393
+
394
+ //快速路徑, 先不開寫交易直接預讀, 內容相同者本就不須寫入, 可省去開啟寫交易之成本
395
+ //預讀值縱使已被其他寫入者更動而過期亦不影響正確性, 因內容相同時本次save等價於無操作,
396
+ //無操作可視為於預讀當下即已完成, 其後他人之寫入結果與[本次save先執行再輪到他人]相同
397
+ //註: 預讀僅用於判斷是否略過寫入, 不用於決定寫入內容, 寫入內容一律由交易內讀到之現值決定
398
+ let vp = await getValue(v.id) //不會有catch
399
+ if (iseobj(vp) && isEqual(merge({}, vp, v), vp)) {
400
+ //合併後與現值相同不須寫入, 惟已命中v.id故n為1
401
+ return {
402
+ n: 1,
403
+ nInserted: 0,
404
+ nModified: 0,
405
+ ok: 1,
406
+ }
407
+ }
408
+
409
+ //existed, updated
410
+ let existed = false
411
+ let updated = false
412
+
413
+ //transaction, 由LMDB於同一寫交易內原子完成[查找v.id]與[合併寫入],
414
+ //併發時不會有兩方各自讀到同一舊值再各自寫入而導致遺失更新
415
+ //注意: 回調函數須為同步函數, client.get於寫交易內為同步取值, 不可使用await,
416
+ //若將client.get或client.put置於await之後, 該操作會逸出本交易而失去原子性
417
+ await client.transaction(() => {
418
+
419
+ //查找資料表內v.id
420
+ let vv = client.get(v.id)
421
+
422
+ //existed
423
+ existed = iseobj(vv)
424
+
425
+ //check
426
+ if (existed) {
427
+ //已存在v.id
428
+
429
+ //vm, 合併後之結果
430
+ let vm = merge({}, vv, v)
431
+
432
+ //判定基準為[合併後結果與現值相同], 而非[待寫入物件與現值全等],
433
+ //令nModified忠實反映資料庫端是否真的寫入, 僅給部份欄位且值皆相同者不應回報已修改
434
+ if (isEqual(vm, vv)) {
435
+ //合併後與現值相同不須寫入
436
+ }
437
+ else {
438
+ //合併後與現值不同須更新
439
+
440
+ //put
441
+ client.put(v.id, vm)
442
+
443
+ updated = true
444
+ }
445
+ }
446
+ else {
447
+ //內容不存在, 若autoInsert則須插入
448
+ if (autoInsert) {
449
+
450
+ //put
451
+ client.put(v.id, v)
452
+
453
+ inserted = true
454
+ }
455
+ }
456
+
457
+ })
458
+
459
+ //rest, nInserted與nModified恆存在, 無對應行為者給0
460
+ if (inserted) {
461
+ rest = {
462
+ n: 1,
463
+ nInserted: 1,
464
+ nModified: 0,
465
+ ok: 1,
466
+ }
467
+ }
468
+ else if (updated) {
469
+ rest = {
470
+ n: 1,
471
+ nInserted: 0,
472
+ nModified: 1,
473
+ ok: 1,
474
+ }
475
+ }
476
+ else {
477
+ //未寫入, 有二:
478
+ // 已存在v.id而內容相同不更新, 已命中故n為1
479
+ // 不存在v.id且未開啟autoInsert, 未命中故n為0
480
+ //n為v.id之命中筆數, 與nModified[實際更新筆數]分屬不同語義, 不可混用
481
+ rest = {
482
+ n: existed ? 1 : 0,
483
+ nInserted: 0,
484
+ nModified: 0,
485
+ ok: 1,
486
+ }
487
+ }
488
+
489
+ }
490
+ catch (err) {
491
+
492
+ //本筆失敗不中斷整批, 以ok為0並附err回報, 由呼叫端逐筆檢查
493
+ rest = {
494
+ n: 1,
495
+ nInserted: 0,
496
+ nModified: 0,
497
+ ok: 0,
498
+ err: getErrMsg(err),
499
+ }
500
+
501
+ }
502
+
503
+ //emit, 原autoInsert係轉呼叫insert故會發出insert事件, 此處維持既有行為,
504
+ //須於交易外發出, 避免外部訂閱函數於寫交易內執行
505
+ if (inserted) {
506
+
507
+ //update, 不能保證插入多少, 一律重設快取
508
+ _cache = null
509
+
510
+ try {
511
+
512
+ //emit
513
+ ee.emit('change', 'insert', [v], rest)
514
+
515
+ }
516
+ catch (err) {
517
+ console.log(err)
518
+ }
519
+
520
+ }
521
+
522
+ return rest
523
+ })
524
+
525
+ }
526
+ catch (err) {
527
+ isErr = true
528
+ res = err
529
+ }
530
+
531
+ //update, 不能保證變更多少, 一律重設快取
532
+ _cache = null
533
+
534
+ //emit, 於change可能須使用select, 故須放在重設快取之後
535
+ if (!isErr) {
536
+ try {
537
+
538
+ //emit
539
+ ee.emit('change', 'save', data, res)
540
+
541
+ }
542
+ catch (err) {
543
+ console.log(err)
544
+ }
545
+ }
546
+
547
+ //check
548
+ if (isErr) {
549
+ return Promise.reject(res)
550
+ }
551
+
552
+ return res
553
+ }
554
+
555
+ /**
556
+ * 刪除數據
557
+ *
558
+ * @memberOf WOrmLmdb
559
+ * @param {Object|Array} data 輸入數據物件或陣列
560
+ * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列,各筆為{n,nDeleted,ok},n為主鍵命中筆數,單筆失敗或未給有效id者ok為0並附err,reject回傳錯誤訊息
561
+ */
562
+ async function del(data) {
563
+ let isErr = false
564
+
565
+ //check
566
+ if (!iseobj(data) && !isearr(data)) {
567
+ return []
568
+ }
569
+
570
+ //cloneDeep, 與外部數據脫勾
571
+ data = cloneDeep(data)
572
+
573
+ //res
574
+ let res = null
575
+ try {
576
+
577
+ //check
578
+ if (!isarr(data)) {
579
+ data = [data]
580
+ }
581
+
582
+ //pmSeries
583
+ res = await pmSeries(data, async(v) => {
584
+
585
+ //rest
586
+ let rest = null
587
+
588
+ try {
589
+
590
+ //id
591
+ let id = get(v, 'id', '')
592
+
593
+ //check
594
+ if (!isestr(id)) {
595
+ //未給有效v.id視為該筆數據有問題而無法處理, 以ok為0並附err回報,
596
+ //與[已給v.id但查無數據]之ok為1有別, 二者須可由ok分辨
597
+ return {
598
+ n: 0,
599
+ nDeleted: 0,
600
+ ok: 0,
601
+ err: `can not delete by invalid id[${id}]`,
602
+ }
603
+ }
604
+
605
+ //查找資料表內v.id
606
+ let vv = await getValue(id) //不會有catch
607
+
608
+ //check
609
+ if (iseobj(vv)) {
610
+ //已存在v.id則須刪除
611
+
612
+ //del
613
+ await client.del(id)
614
+
615
+ //rest
616
+ rest = {
617
+ n: 1,
618
+ nDeleted: 1,
619
+ ok: 1,
620
+ }
621
+
622
+ }
623
+ else {
624
+ //不存在v.id則不刪除, 未命中故n為0
625
+ rest = {
626
+ n: 0,
627
+ nDeleted: 0,
628
+ ok: 1,
629
+ }
630
+
631
+ }
632
+
633
+ }
634
+ catch (err) {
635
+
636
+ //本筆失敗不中斷整批, 以ok為0並附err回報, 由呼叫端逐筆檢查
637
+ rest = {
638
+ n: 1,
639
+ nDeleted: 0,
640
+ ok: 0,
641
+ err: getErrMsg(err),
642
+ }
643
+
644
+ }
645
+
646
+ return rest
647
+ })
648
+
649
+ }
650
+ catch (err) {
651
+ isErr = true
652
+ res = err
653
+ }
654
+
655
+ //update, 不能保證刪除多少, 一律重設快取
656
+ _cache = null
657
+
658
+ //emit, 於change可能須使用select, 故須放在重設快取之後
659
+ if (!isErr) {
660
+ try {
661
+
662
+ //emit
663
+ ee.emit('change', 'del', data, res)
664
+
665
+ }
666
+ catch (err) {
667
+ console.log(err)
668
+ }
669
+ }
670
+
671
+ //check
672
+ if (isErr) {
673
+ return Promise.reject(res)
674
+ }
675
+
676
+ return res
677
+ }
678
+
679
+ /**
680
+ * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
681
+ *
682
+ * @memberOf WOrmLmdb
683
+ * @param {Object} [find={}] 輸入刪除條件物件
684
+ * @returns {Promise} 回傳Promise,resolve回傳刪除結果物件{n,nDeleted,ok},n與nDeleted同為實際刪除筆數,reject回傳錯誤訊息
685
+ */
686
+ async function delAll(find = {}) {
687
+ let isErr = false
688
+
689
+ //res
690
+ let res = null
691
+ try {
692
+
693
+ //ltdt
694
+ let ltdt = await getData()
695
+
696
+ //filter
697
+ let nAll = size(ltdt)
698
+ let nDel = 0
699
+ if (iseobj(find)) {
700
+
701
+ //q
702
+ let q = new mingo.Query(find)
703
+ // console.log('q', q)
704
+
705
+ //find
706
+ let _res = q.find(ltdt).all()
707
+ // console.log('_res', _res)
708
+
709
+ //nDel
710
+ nDel = size(_res)
711
+ // console.log('nDel', nDel)
712
+
713
+ if (nDel === 0) {
714
+ //未有find結果等於不刪除
715
+ }
716
+ else if (nAll === nDel) {
717
+ //全在find結果內等於全部刪除
718
+
719
+ //empty
720
+ for (let v of ltdt) {
721
+ await client.del(v.id)
722
+ }
723
+
724
+ }
725
+ else {
726
+ //部份在find結果內
727
+
728
+ //_kp
729
+ let _kp = {}
730
+ each(_res, (v, k) => {
731
+ _kp[v.id] = { k, v }
732
+ })
733
+
734
+ //del
735
+ for (let v of ltdt) {
736
+ if (haskey(_kp, v.id)) {
737
+ //在find結果內代表須刪除
738
+ await client.del(v.id)
739
+ }
740
+ }
741
+
742
+ }
743
+ }
744
+ else {
745
+
746
+ //nDel
747
+ nDel = nAll
748
+
749
+ //empty
750
+ for (let v of ltdt) {
751
+ await client.del(v.id)
752
+ }
753
+
754
+ }
755
+
756
+ //res, n為實際刪除筆數而非全表筆數, 故與nDeleted同值
757
+ res = {
758
+ n: nDel,
759
+ nDeleted: nDel,
760
+ ok: 1,
761
+ }
762
+
763
+ }
764
+ catch (err) {
765
+ isErr = true
766
+ res = err
767
+ }
768
+
769
+ //update, 不能保證刪除多少, 一律重設快取
770
+ _cache = null
771
+
772
+ //emit, 於change可能須使用select, 故須放在重設快取之後
773
+ if (!isErr) {
774
+ try {
775
+
776
+ //emit
777
+ ee.emit('change', 'delAll', null, res)
778
+
779
+ }
780
+ catch (err) {
781
+ console.log(err)
782
+ }
783
+ }
784
+
785
+ //check
786
+ if (isErr) {
787
+ return Promise.reject(res)
788
+ }
789
+
790
+ return res
791
+ }
792
+
793
+ //close, 關閉內部LMDB env並釋放檔案鎖, close後該實例即報廢, 須重新new WOrmLmdb
794
+ let close = async () => {
795
+ if (client && client.status !== 'closed') {
796
+ await client.close() //lmdb root database之close(), 回傳Promise
797
+ }
798
+ }
799
+
800
+ //save
801
+ ee.select = select
802
+ ee.selectByPk = selectByPk
803
+ ee.insert = insert
804
+ ee.save = save
805
+ ee.del = del
806
+ ee.delAll = delAll
807
+ ee.close = close
808
+
809
+ return ee
810
+ }
811
+
812
+
813
+ export default WOrmLmdb