tigerbeetle-node 0.14.178 → 0.14.179

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/index.ts CHANGED
@@ -5,8 +5,10 @@ import {
5
5
  CreateAccountsError,
6
6
  CreateTransfersError,
7
7
  Operation,
8
- GetAccountTransfers,
8
+ AccountFilter,
9
+ AccountBalance,
9
10
  } from './bindings'
11
+ import { randomFillSync } from 'node:crypto'
10
12
 
11
13
  const binding: Binding = (() => {
12
14
  const { arch, platform } = process
@@ -66,8 +68,8 @@ const binding: Binding = (() => {
66
68
  export type Context = object // tb_client
67
69
  export type AccountID = bigint // u128
68
70
  export type TransferID = bigint // u128
69
- export type Event = Account | Transfer | AccountID | TransferID | GetAccountTransfers
70
- export type Result = CreateAccountsError | CreateTransfersError | Account | Transfer
71
+ export type Event = Account | Transfer | AccountID | TransferID | AccountFilter
72
+ export type Result = CreateAccountsError | CreateTransfersError | Account | Transfer | AccountBalance
71
73
  export type ResultCallback = (error: Error | null, results: Result[] | null) => void
72
74
 
73
75
  interface BindingInitArgs {
@@ -93,7 +95,8 @@ export interface Client {
93
95
  createTransfers: (batch: Transfer[]) => Promise<CreateTransfersError[]>
94
96
  lookupAccounts: (batch: AccountID[]) => Promise<Account[]>
95
97
  lookupTransfers: (batch: TransferID[]) => Promise<Transfer[]>
96
- getAccountTransfers: (filter: GetAccountTransfers) => Promise<Transfer[]>
98
+ getAccountTransfers: (filter: AccountFilter) => Promise<Transfer[]>
99
+ getAccountHistory: (filter: AccountFilter) => Promise<AccountBalance[]>
97
100
  destroy: () => void
98
101
  }
99
102
 
@@ -129,6 +132,49 @@ export function createClient (args: ClientInitArgs): Client {
129
132
  lookupAccounts(batch) { return request(Operation.lookup_accounts, batch) },
130
133
  lookupTransfers(batch) { return request(Operation.lookup_transfers, batch) },
131
134
  getAccountTransfers(filter) { return request(Operation.get_account_transfers, [filter]) },
135
+ getAccountHistory(filter) { return request(Operation.get_account_history, [filter]) },
132
136
  destroy() { binding.deinit(context) },
133
137
  }
134
138
  }
139
+
140
+ let idLastTimestamp = 0;
141
+ let idLastBuffer = new DataView(new ArrayBuffer(16));
142
+
143
+ /**
144
+ * Generates a Universally Unique and Sortable Identifier as a u128 bigint.
145
+ *
146
+ * @remarks
147
+ * Based on {@link https://github.com/ulid/spec}, IDs returned are guaranteed to be monotonically
148
+ * increasing.
149
+ */
150
+ export function id(): bigint {
151
+ // Ensure timestamp monotonically increases and generate a new random on each new timestamp.
152
+ let timestamp = Date.now()
153
+ if (timestamp <= idLastTimestamp) {
154
+ timestamp = idLastTimestamp
155
+ } else {
156
+ idLastTimestamp = timestamp
157
+ randomFillSync(idLastBuffer)
158
+ }
159
+
160
+ // Increment the u80 in idLastBuffer using carry arithmetic on u32s (as JS doesn't have fast u64).
161
+ const littleEndian = true
162
+ const randomLo32 = idLastBuffer.getUint32(0, littleEndian) + 1
163
+ const randomHi32 = idLastBuffer.getUint32(4, littleEndian) + (randomLo32 > 0xFFFFFFFF ? 1 : 0)
164
+ const randomHi16 = idLastBuffer.getUint16(8, littleEndian) + (randomHi32 > 0xFFFFFFFF ? 1 : 0)
165
+ if (randomHi16 > 0xFFFF) {
166
+ throw new Error('random bits overflow on monotonic increment')
167
+ }
168
+
169
+ // Store the incremented random monotonic and the timestamp into the buffer.
170
+ idLastBuffer.setUint32(0, randomLo32 & 0xFFFFFFFF, littleEndian)
171
+ idLastBuffer.setUint32(4, randomHi32 & 0xFFFFFFFF, littleEndian)
172
+ idLastBuffer.setUint16(8, randomHi16, littleEndian) // No need to mask since checked above.
173
+ idLastBuffer.setUint16(10, timestamp & 0xFFFF, littleEndian) // timestamp lo.
174
+ idLastBuffer.setUint32(12, (timestamp >>> 16) & 0xFFFFFFFF, littleEndian) // timestamp hi.
175
+
176
+ // Then return the buffer's contents as a little-endian u128 bigint.
177
+ const lo = idLastBuffer.getBigUint64(0, littleEndian)
178
+ const hi = idLastBuffer.getBigUint64(8, littleEndian)
179
+ return (hi << 64n) | lo
180
+ }
package/src/node.zig CHANGED
@@ -15,8 +15,9 @@ const Transfer = tb.Transfer;
15
15
  const TransferFlags = tb.TransferFlags;
16
16
  const CreateAccountsResult = tb.CreateAccountsResult;
17
17
  const CreateTransfersResult = tb.CreateTransfersResult;
18
- const GetAccountTransfers = tb.GetAccountTransfers;
19
- const GetAccountTransfersFlags = tb.GetAccountTransfersFlags;
18
+ const AccountFilter = tb.AccountFilter;
19
+ const AccountFilterFlags = tb.AccountFilterFlags;
20
+ const AccountBalance = tb.AccountBalance;
20
21
 
21
22
  const Storage = @import("../../../storage.zig").Storage;
22
23
  const StateMachine = @import("../../../state_machine.zig").StateMachineType(Storage, constants.state_machine_config);
@@ -367,7 +368,7 @@ fn decode_array(comptime Event: type, env: c.napi_env, array: c.napi_value, even
367
368
  for (events, 0..) |*event, i| {
368
369
  const object = try translate.array_element(env, array, @intCast(i));
369
370
  switch (Event) {
370
- Account, Transfer, GetAccountTransfers => {
371
+ Account, Transfer, AccountFilter, AccountBalance => {
371
372
  inline for (std.meta.fields(Event)) |field| {
372
373
  const value: field.type = switch (@typeInfo(field.type)) {
373
374
  .Struct => |info| @bitCast(try @field(
@@ -419,6 +420,8 @@ fn encode_array(comptime Result: type, env: c.napi_env, results: []const Result)
419
420
  const FieldInt = switch (@typeInfo(field.type)) {
420
421
  .Struct => |info| info.backing_integer.?,
421
422
  .Enum => |info| info.tag_type,
423
+ // Arrays are only used for padding/reserved fields.
424
+ .Array => continue,
422
425
  else => field.type,
423
426
  };
424
427
 
@@ -474,9 +477,7 @@ fn BufferType(comptime op: Operation) type {
474
477
  // Allocate enough bytes to hold memory for the Events and the Results.
475
478
  const max_bytes = @max(
476
479
  @sizeOf(Event) * count,
477
- @sizeOf(Result) *
478
- // Ad-hoc hack, event and result sizes are not the same size.
479
- if (op == .get_account_transfers) 8190 else count,
480
+ @sizeOf(Result) * event_count(op, count),
480
481
  );
481
482
  if (@sizeOf(vsr.Header) + max_bytes > constants.message_size_max) {
482
483
  return translate.throw(env, "Batch is larger than the maximum message size.");
@@ -496,9 +497,7 @@ fn BufferType(comptime op: Operation) type {
496
497
  fn free(buffer: Buffer) void {
497
498
  const max_bytes = @max(
498
499
  @sizeOf(Event) * buffer.count,
499
- @sizeOf(Result) *
500
- //TODO(batiati): Refine the way we handle events with asymmetric results.
501
- if (op == .get_account_transfers) 8190 else buffer.count,
500
+ @sizeOf(Result) * event_count(op, buffer.count),
502
501
  );
503
502
  const bytes: []align(max_align) u8 = @alignCast(buffer.ptr[0..max_bytes]);
504
503
  allocator.free(bytes);
@@ -510,10 +509,16 @@ fn BufferType(comptime op: Operation) type {
510
509
  }
511
510
 
512
511
  fn results(buffer: Buffer) []Result {
513
- const result_bytes = buffer.ptr[0 .. @sizeOf(Result) *
514
- // Ad-hoc hack, event and result sizes are not the same size.
515
- if (op == .get_account_transfers) 8190 else buffer.count];
512
+ const result_bytes = buffer.ptr[0 .. @sizeOf(Result) * event_count(op, buffer.count)];
516
513
  return @alignCast(std.mem.bytesAsSlice(Result, result_bytes));
517
514
  }
515
+
516
+ fn event_count(operation: Operation, count: usize) usize {
517
+ // TODO(batiati): Refine the way we handle events with asymmetric results.
518
+ return switch (operation) {
519
+ .get_account_transfers, .get_account_history => 8190,
520
+ else => count,
521
+ };
522
+ }
518
523
  };
519
524
  }
package/src/test.ts CHANGED
@@ -6,8 +6,10 @@ import {
6
6
  TransferFlags,
7
7
  CreateAccountError,
8
8
  CreateTransferError,
9
- GetAccountTransfers,
10
- GetAccountTransfersFlags,
9
+ AccountFilter,
10
+ AccountFilterFlags,
11
+ AccountFlags,
12
+ id,
11
13
  } from '.'
12
14
 
13
15
  const client = createClient({
@@ -57,6 +59,20 @@ test.skip = (name: string, fn: () => Promise<void>) => {
57
59
  console.log(name + ': SKIPPED')
58
60
  }
59
61
 
62
+ test('id() monotonically increasing', async (): Promise<void> => {
63
+ let idA = id();
64
+ for (let i = 0; i < 10_000_000; i++) {
65
+ // Ensure ID is monotonic between milliseconds if the loop executes too fast.
66
+ if (i % 10_000 == 0) {
67
+ await new Promise(resolve => setTimeout(resolve, 1))
68
+ }
69
+
70
+ const idB = id();
71
+ assert.ok(idB > idA, 'id() returned an id that did not monotonically increase');
72
+ idA = idB;
73
+ }
74
+ })
75
+
60
76
  test('range check `code` on Account to be u16', async (): Promise<void> => {
61
77
  const account = { ...accountA, id: 0n }
62
78
 
@@ -404,7 +420,7 @@ test('can get account transfers', async (): Promise<void> => {
404
420
  reserved: 0,
405
421
  ledger: 1,
406
422
  code: 718,
407
- flags: 0,
423
+ flags: AccountFlags.history,
408
424
  timestamp: 0n
409
425
  }
410
426
  const account_errors = await client.createAccounts([accountC])
@@ -434,19 +450,26 @@ test('can get account transfers', async (): Promise<void> => {
434
450
  assert.strictEqual(transfers_created_result.length, 0)
435
451
 
436
452
  // Query all transfers for accountC:
437
- var filter: GetAccountTransfers = {
453
+ var filter: AccountFilter = {
438
454
  account_id: accountC.id,
439
455
  timestamp_min: 0n,
440
456
  timestamp_max: 0n,
441
457
  limit: 8190,
442
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
458
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
443
459
  }
444
460
  var transfers = await client.getAccountTransfers(filter)
461
+ var history = await client.getAccountHistory(filter)
445
462
  assert.strictEqual(transfers.length, transfers_created.length)
463
+ assert.strictEqual(history.length, transfers.length)
464
+
446
465
  var timestamp = 0n;
466
+ var i = 0;
447
467
  for (var transfer of transfers) {
448
468
  assert.ok(timestamp < transfer.timestamp);
449
469
  timestamp = transfer.timestamp;
470
+
471
+ assert.ok(history[i].timestamp == transfer.timestamp);
472
+ i++;
450
473
  }
451
474
 
452
475
  // Query only the debit transfers for accountC, descending:
@@ -455,14 +478,22 @@ test('can get account transfers', async (): Promise<void> => {
455
478
  timestamp_min: 0n,
456
479
  timestamp_max: 0n,
457
480
  limit: 8190,
458
- flags: GetAccountTransfersFlags.debits | GetAccountTransfersFlags.reversed,
481
+ flags: AccountFilterFlags.debits | AccountFilterFlags.reversed,
459
482
  }
460
483
  transfers = await client.getAccountTransfers(filter)
484
+ history = await client.getAccountHistory(filter)
485
+
461
486
  assert.strictEqual(transfers.length, transfers_created.length / 2)
487
+ assert.strictEqual(history.length, transfers.length)
488
+
462
489
  timestamp = 1n << 64n;
490
+ i = 0;
463
491
  for (var transfer of transfers) {
464
492
  assert.ok(transfer.timestamp < timestamp);
465
493
  timestamp = transfer.timestamp;
494
+
495
+ assert.ok(history[i].timestamp == transfer.timestamp);
496
+ i++;
466
497
  }
467
498
 
468
499
  // Query only the credit transfers for accountC, descending:
@@ -471,14 +502,22 @@ test('can get account transfers', async (): Promise<void> => {
471
502
  timestamp_min: 0n,
472
503
  timestamp_max: 0n,
473
504
  limit: 8190,
474
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.reversed,
505
+ flags: AccountFilterFlags.credits | AccountFilterFlags.reversed,
475
506
  }
476
507
  transfers = await client.getAccountTransfers(filter)
508
+ history = await client.getAccountHistory(filter)
509
+
477
510
  assert.strictEqual(transfers.length, transfers_created.length / 2)
511
+ assert.strictEqual(history.length, transfers.length)
512
+
478
513
  timestamp = 1n << 64n;
514
+ i = 0;
479
515
  for (var transfer of transfers) {
480
516
  assert.ok(transfer.timestamp < timestamp);
481
517
  timestamp = transfer.timestamp;
518
+
519
+ assert.ok(history[i].timestamp == transfer.timestamp);
520
+ i++;
482
521
  }
483
522
 
484
523
  // Query the first 5 transfers for accountC:
@@ -487,14 +526,22 @@ test('can get account transfers', async (): Promise<void> => {
487
526
  timestamp_min: 0n,
488
527
  timestamp_max: 0n,
489
528
  limit: transfers_created.length / 2,
490
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
529
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
491
530
  }
492
531
  transfers = await client.getAccountTransfers(filter)
532
+ history = await client.getAccountHistory(filter)
533
+
493
534
  assert.strictEqual(transfers.length, transfers_created.length / 2)
535
+ assert.strictEqual(history.length, transfers.length)
536
+
494
537
  timestamp = 0n;
538
+ i = 0;
495
539
  for (var transfer of transfers) {
496
540
  assert.ok(timestamp < transfer.timestamp);
497
541
  timestamp = transfer.timestamp;
542
+
543
+ assert.ok(history[i].timestamp == transfer.timestamp);
544
+ i++;
498
545
  }
499
546
 
500
547
  // Query the next 5 transfers for accountC, with pagination:
@@ -503,13 +550,21 @@ test('can get account transfers', async (): Promise<void> => {
503
550
  timestamp_min: timestamp + 1n,
504
551
  timestamp_max: 0n,
505
552
  limit: transfers_created.length / 2,
506
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
553
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
507
554
  }
508
555
  transfers = await client.getAccountTransfers(filter)
556
+ history = await client.getAccountHistory(filter)
557
+
509
558
  assert.strictEqual(transfers.length, transfers_created.length / 2)
559
+ assert.strictEqual(history.length, transfers.length)
560
+
561
+ i = 0;
510
562
  for (var transfer of transfers) {
511
563
  assert.ok(timestamp < transfer.timestamp);
512
564
  timestamp = transfer.timestamp;
565
+
566
+ assert.ok(history[i].timestamp == transfer.timestamp);
567
+ i++;
513
568
  }
514
569
 
515
570
  // Query again, no more transfers should be found:
@@ -518,10 +573,13 @@ test('can get account transfers', async (): Promise<void> => {
518
573
  timestamp_min: timestamp + 1n,
519
574
  timestamp_max: 0n,
520
575
  limit: transfers_created.length / 2,
521
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
576
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
522
577
  }
523
578
  transfers = await client.getAccountTransfers(filter)
579
+ history = await client.getAccountHistory(filter)
580
+
524
581
  assert.strictEqual(transfers.length, 0)
582
+ assert.strictEqual(history.length, transfers.length)
525
583
 
526
584
  // Query the first 5 transfers for accountC ORDER BY DESC:
527
585
  filter = {
@@ -529,14 +587,22 @@ test('can get account transfers', async (): Promise<void> => {
529
587
  timestamp_min: 0n,
530
588
  timestamp_max: 0n,
531
589
  limit: transfers_created.length / 2,
532
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits | GetAccountTransfersFlags.reversed,
590
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits | AccountFilterFlags.reversed,
533
591
  }
534
592
  transfers = await client.getAccountTransfers(filter)
593
+ history = await client.getAccountHistory(filter)
594
+
535
595
  assert.strictEqual(transfers.length, transfers_created.length / 2)
596
+ assert.strictEqual(history.length, transfers.length)
597
+
536
598
  timestamp = 1n << 64n;
599
+ i = 0;
537
600
  for (var transfer of transfers) {
538
601
  assert.ok(timestamp > transfer.timestamp);
539
602
  timestamp = transfer.timestamp;
603
+
604
+ assert.ok(history[i].timestamp == transfer.timestamp);
605
+ i++;
540
606
  }
541
607
 
542
608
  // Query the next 5 transfers for accountC, with pagination:
@@ -545,13 +611,21 @@ test('can get account transfers', async (): Promise<void> => {
545
611
  timestamp_min: 0n,
546
612
  timestamp_max: timestamp - 1n,
547
613
  limit: transfers_created.length / 2,
548
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits | GetAccountTransfersFlags.reversed,
614
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits | AccountFilterFlags.reversed,
549
615
  }
550
616
  transfers = await client.getAccountTransfers(filter)
617
+ history = await client.getAccountHistory(filter)
618
+
551
619
  assert.strictEqual(transfers.length, transfers_created.length / 2)
620
+ assert.strictEqual(history.length, transfers.length)
621
+
622
+ i = 0;
552
623
  for (var transfer of transfers) {
553
624
  assert.ok(timestamp > transfer.timestamp);
554
625
  timestamp = transfer.timestamp;
626
+
627
+ assert.ok(history[i].timestamp == transfer.timestamp);
628
+ i++;
555
629
  }
556
630
 
557
631
  // Query again, no more transfers should be found:
@@ -560,73 +634,90 @@ test('can get account transfers', async (): Promise<void> => {
560
634
  timestamp_min: 0n,
561
635
  timestamp_max: timestamp - 1n,
562
636
  limit: transfers_created.length / 2,
563
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits | GetAccountTransfersFlags.reversed,
637
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits | AccountFilterFlags.reversed,
564
638
  }
565
639
  transfers = await client.getAccountTransfers(filter)
566
- assert.strictEqual(transfers.length, 0)
640
+ history = await client.getAccountHistory(filter)
641
+
642
+ assert.strictEqual(transfers.length, 0)
643
+ assert.strictEqual(history.length, transfers.length)
567
644
 
568
645
  // Invalid account:
569
- assert.strictEqual((await client.getAccountTransfers({
646
+ filter = {
570
647
  account_id: 0n,
571
648
  timestamp_min: 0n,
572
649
  timestamp_max: 0n,
573
650
  limit: 8190,
574
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
575
- })).length, 0)
651
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
652
+ }
653
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
654
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
576
655
 
577
656
  // Invalid timestamp min:
578
- assert.strictEqual((await client.getAccountTransfers({
657
+ filter = {
579
658
  account_id: accountC.id,
580
659
  timestamp_min: (1n << 64n) - 1n, // ulong max value
581
660
  timestamp_max: 0n,
582
661
  limit: 8190,
583
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
584
- })).length, 0)
662
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
663
+ }
664
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
665
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
585
666
 
586
667
  // Invalid timestamp max:
587
- assert.strictEqual((await client.getAccountTransfers({
668
+ filter = {
588
669
  account_id: accountC.id,
589
670
  timestamp_min: 0n,
590
671
  timestamp_max: (1n << 64n) - 1n, // ulong max value
591
672
  limit: 8190,
592
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
593
- })).length, 0)
673
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
674
+ }
675
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
676
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
594
677
 
595
678
  // Invalid timestamp range:
596
- assert.strictEqual((await client.getAccountTransfers({
679
+ filter = {
597
680
  account_id: accountC.id,
598
681
  timestamp_min: (1n << 64n) - 2n, // ulong max - 1
599
682
  timestamp_max: 1n,
600
683
  limit: 8190,
601
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
602
- })).length, 0)
684
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
685
+ }
686
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
687
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
603
688
 
604
689
  // Zero limit:
605
- assert.strictEqual((await client.getAccountTransfers({
690
+ filter = {
606
691
  account_id: accountC.id,
607
692
  timestamp_min: 0n,
608
693
  timestamp_max: 0n,
609
694
  limit: 0,
610
- flags: GetAccountTransfersFlags.credits | GetAccountTransfersFlags.debits,
611
- })).length, 0)
695
+ flags: AccountFilterFlags.credits | AccountFilterFlags.debits,
696
+ }
697
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
698
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
612
699
 
613
700
  // Empty flags:
614
- assert.strictEqual((await client.getAccountTransfers({
701
+ filter = {
615
702
  account_id: accountC.id,
616
703
  timestamp_min: 0n,
617
704
  timestamp_max: 0n,
618
705
  limit: 8190,
619
- flags: GetAccountTransfersFlags.none,
620
- })).length, 0)
706
+ flags: AccountFilterFlags.none,
707
+ }
708
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
709
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
621
710
 
622
711
  // Invalid flags:
623
- assert.strictEqual((await client.getAccountTransfers({
712
+ filter = {
624
713
  account_id: accountC.id,
625
714
  timestamp_min: 0n,
626
715
  timestamp_max: 0n,
627
716
  limit: 8190,
628
717
  flags: 0xFFFF,
629
- })).length, 0)
718
+ }
719
+ assert.strictEqual((await client.getAccountTransfers(filter)).length, 0)
720
+ assert.strictEqual((await client.getAccountHistory(filter)).length, 0)
630
721
 
631
722
  })
632
723