memcache 1.2.0 → 1.4.0

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [<img src="https://jaredwray.com/images/memcache.svg" alt="Memcache Logo" align="center">](https://memcachejs.org)
1
+ [<img src="https://jaredwray.com/images/memcache.svg" width="80%" height="80%" align="center" alt="Memcache Logo" align="center">](https://memcachejs.org)
2
2
 
3
3
  [![codecov](https://codecov.io/gh/jaredwray/memcache/graph/badge.svg?token=4DUANNWiIE)](https://codecov.io/gh/jaredwray/memcache)
4
4
  [![tests](https://github.com/jaredwray/memcache/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/memcache/actions/workflows/tests.yaml)
@@ -44,6 +44,27 @@ Nodejs Memcache Client
44
44
  - [decr(key, value?)](#decrkey-value)
45
45
  - [touch(key, exptime)](#touchkey-exptime)
46
46
  - [Hook Examples](#hook-examples)
47
+ - [Distribution Algorithms](#distribution-algorithms)
48
+ - [KetamaHash (Default)](#ketamahash-default)
49
+ - [ModulaHash](#modulahash)
50
+ - [Choosing an Algorithm](#choosing-an-algorithm)
51
+ - [Retry Configuration](#retry-configuration)
52
+ - [Basic Retry Setup](#basic-retry-setup)
53
+ - [Backoff Strategies](#backoff-strategies)
54
+ - [Idempotent Safety](#idempotent-safety)
55
+ - [Methods Without Retry Support](#methods-without-retry-support)
56
+ - [SASL Authentication](#sasl-authentication)
57
+ - [Enabling SASL Authentication](#enabling-sasl-authentication)
58
+ - [SASL Options](#sasl-options)
59
+ - [Per-Node SASL Configuration](#per-node-sasl-configuration)
60
+ - [Authentication Events](#authentication-events)
61
+ - [Server Configuration](#server-configuration)
62
+ - [Auto Discovery](#auto-discovery)
63
+ - [Enabling Auto Discovery](#enabling-auto-discovery)
64
+ - [Auto Discovery Options](#auto-discovery-options)
65
+ - [Auto Discovery Events](#auto-discovery-events)
66
+ - [Legacy Command Support](#legacy-command-support)
67
+ - [IPv6 Support](#ipv6-support)
47
68
  - [Contributing](#contributing)
48
69
  - [License and Copyright](#license-and-copyright)
49
70
 
@@ -179,6 +200,11 @@ const client = new Memcache({
179
200
  - `keepAlive?: boolean` - Keep connection alive (default: true)
180
201
  - `keepAliveDelay?: number` - Keep alive delay in milliseconds (default: 1000)
181
202
  - `hash?: HashProvider` - Hash provider for consistent hashing (default: KetamaHash)
203
+ - `retries?: number` - Number of retry attempts for failed commands (default: 0)
204
+ - `retryDelay?: number` - Base delay in milliseconds between retries (default: 100)
205
+ - `retryBackoff?: RetryBackoffFunction` - Function to calculate backoff delay (default: fixed delay)
206
+ - `retryOnlyIdempotent?: boolean` - Only retry commands marked as idempotent (default: true)
207
+ - `autoDiscover?: AutoDiscoverOptions` - AWS ElastiCache Auto Discovery configuration (see [Auto Discovery](#auto-discovery))
182
208
 
183
209
  ## Properties
184
210
 
@@ -200,6 +226,18 @@ Get or set the keepAlive setting. Updates all existing nodes. Requires `reconnec
200
226
  ### `keepAliveDelay: number`
201
227
  Get or set the keep alive delay in milliseconds. Updates all existing nodes. Requires `reconnect()` to apply changes.
202
228
 
229
+ ### `retries: number`
230
+ Get or set the number of retry attempts for failed commands (default: 0).
231
+
232
+ ### `retryDelay: number`
233
+ Get or set the base delay in milliseconds between retry attempts (default: 100).
234
+
235
+ ### `retryBackoff: RetryBackoffFunction`
236
+ Get or set the backoff function for calculating retry delays.
237
+
238
+ ### `retryOnlyIdempotent: boolean`
239
+ Get or set whether retries are restricted to idempotent commands only (default: true).
240
+
203
241
  ## Connection Management
204
242
 
205
243
  ### `connect(nodeId?: string): Promise<void>`
@@ -373,6 +411,9 @@ client.on('miss', (key) => {
373
411
  - `quit` - Emitted when quit command is sent
374
412
  - `warn` - Emitted for warning messages
375
413
  - `info` - Emitted for informational messages
414
+ - `autoDiscover` - Emitted on initial auto discovery with the cluster config
415
+ - `autoDiscoverUpdate` - Emitted when auto discovery detects a topology change
416
+ - `autoDiscoverError` - Emitted when auto discovery encounters an error
376
417
 
377
418
  ## Hooks
378
419
 
@@ -473,9 +514,534 @@ client.onHook('after:set', async (context) => {
473
514
  });
474
515
  ```
475
516
 
517
+ # Distribution Algorithms
518
+
519
+ Memcache supports pluggable distribution algorithms to determine how keys are distributed across nodes. You can configure the algorithm using the `hash` option.
520
+
521
+ ## KetamaHash (Default)
522
+
523
+ KetamaHash uses the Ketama consistent hashing algorithm, which minimizes key redistribution when nodes are added or removed. This is the default and recommended algorithm for production environments with dynamic scaling.
524
+
525
+ ```javascript
526
+ import { Memcache } from 'memcache';
527
+
528
+ // KetamaHash is used by default
529
+ const client = new Memcache({
530
+ nodes: ['server1:11211', 'server2:11211', 'server3:11211']
531
+ });
532
+ ```
533
+
534
+ **Characteristics:**
535
+ - Minimal key redistribution (~1/n keys move when adding/removing nodes)
536
+ - Uses virtual nodes for better distribution
537
+ - Supports weighted nodes
538
+ - Best for production environments with dynamic scaling
539
+
540
+ ## ModulaHash
541
+
542
+ ModulaHash uses a simple modulo-based hashing algorithm (`hash(key) % nodeCount`). This is a simpler algorithm that may redistribute all keys when nodes change.
543
+
544
+ ```javascript
545
+ import { Memcache, ModulaHash } from 'memcache';
546
+
547
+ // Use ModulaHash for distribution
548
+ const client = new Memcache({
549
+ nodes: ['server1:11211', 'server2:11211', 'server3:11211'],
550
+ hash: new ModulaHash()
551
+ });
552
+
553
+ // With a custom hash algorithm (default is sha1)
554
+ const client2 = new Memcache({
555
+ nodes: ['server1:11211', 'server2:11211'],
556
+ hash: new ModulaHash('md5')
557
+ });
558
+ ```
559
+
560
+ **Characteristics:**
561
+ - Simple and fast algorithm
562
+ - All keys may be redistributed when nodes are added or removed
563
+ - Supports weighted nodes (nodes with higher weight appear more in the distribution)
564
+ - Best for fixed-size clusters or testing environments
565
+
566
+ ### Weighted Nodes with ModulaHash
567
+
568
+ ModulaHash supports weighted nodes, where nodes with higher weights receive proportionally more keys:
569
+
570
+ ```javascript
571
+ import { Memcache, ModulaHash, createNode } from 'memcache';
572
+
573
+ // Create nodes with different weights
574
+ const node1 = createNode('server1', 11211, { weight: 3 }); // 3x traffic
575
+ const node2 = createNode('server2', 11211, { weight: 1 }); // 1x traffic
576
+
577
+ const client = new Memcache({
578
+ nodes: [node1, node2],
579
+ hash: new ModulaHash()
580
+ });
581
+
582
+ // server1 will receive approximately 75% of keys
583
+ // server2 will receive approximately 25% of keys
584
+ ```
585
+
586
+ ## Choosing an Algorithm
587
+
588
+ | Feature | KetamaHash | ModulaHash |
589
+ |---------|------------|------------|
590
+ | Key redistribution on node change | Minimal (~1/n keys) | All keys may move |
591
+ | Complexity | Higher (virtual nodes) | Lower (simple modulo) |
592
+ | Performance | Slightly slower | Faster |
593
+ | Best for | Dynamic scaling | Fixed clusters |
594
+ | Weighted nodes | Yes | Yes |
595
+
596
+ **Use KetamaHash (default) when:**
597
+ - Your cluster size may change dynamically
598
+ - You want to minimize cache invalidation during scaling
599
+ - You're running in production
600
+
601
+ **Use ModulaHash when:**
602
+ - Your cluster size is fixed
603
+ - You prefer simplicity over minimal redistribution
604
+ - You're in a testing or development environment
605
+
606
+ # Retry Configuration
607
+
608
+ The Memcache client supports automatic retry of failed commands with configurable backoff strategies.
609
+
610
+ ## Basic Retry Setup
611
+
612
+ Enable retries by setting the `retries` option:
613
+
614
+ ```javascript
615
+ import { Memcache } from 'memcache';
616
+
617
+ const client = new Memcache({
618
+ nodes: ['localhost:11211'],
619
+ retries: 3, // Retry up to 3 times
620
+ retryDelay: 100 // 100ms between retries
621
+ });
622
+ ```
623
+
624
+ You can also modify retry settings at runtime:
625
+
626
+ ```javascript
627
+ client.retries = 5;
628
+ client.retryDelay = 200;
629
+ ```
630
+
631
+ ## Backoff Strategies
632
+
633
+ The client includes two built-in backoff functions:
634
+
635
+ ### Fixed Delay (Default)
636
+
637
+ ```javascript
638
+ import { Memcache, defaultRetryBackoff } from 'memcache';
639
+
640
+ const client = new Memcache({
641
+ retries: 3,
642
+ retryDelay: 100,
643
+ retryBackoff: defaultRetryBackoff // 100ms, 100ms, 100ms
644
+ });
645
+ ```
646
+
647
+ ### Exponential Backoff
648
+
649
+ ```javascript
650
+ import { Memcache, exponentialRetryBackoff } from 'memcache';
651
+
652
+ const client = new Memcache({
653
+ retries: 3,
654
+ retryDelay: 100,
655
+ retryBackoff: exponentialRetryBackoff // 100ms, 200ms, 400ms
656
+ });
657
+ ```
658
+
659
+ ### Custom Backoff Function
660
+
661
+ You can provide your own backoff function:
662
+
663
+ ```javascript
664
+ const client = new Memcache({
665
+ retries: 3,
666
+ retryDelay: 100,
667
+ retryBackoff: (attempt, baseDelay) => {
668
+ // Exponential backoff with jitter
669
+ const delay = baseDelay * Math.pow(2, attempt);
670
+ return delay + Math.random() * delay * 0.1;
671
+ }
672
+ });
673
+ ```
674
+
675
+ The backoff function receives:
676
+ - `attempt` - The current attempt number (0-indexed)
677
+ - `baseDelay` - The configured `retryDelay` value
678
+
679
+ ## Idempotent Safety
680
+
681
+ **Important:** By default, retries are only performed for commands explicitly marked as idempotent. This prevents accidental double-execution of non-idempotent operations like `incr`, `decr`, `append`, and `prepend`.
682
+
683
+ ### Why This Matters
684
+
685
+ If a network timeout occurs after the server applies a mutation but before the client receives the response, retrying would apply the mutation twice:
686
+ - Counter incremented twice instead of once
687
+ - Data appended twice instead of once
688
+
689
+ ### Safe Usage Patterns
690
+
691
+ **For read operations (always safe to retry):**
692
+
693
+ ```javascript
694
+ // Mark read operations as idempotent
695
+ await client.execute('get mykey', nodes, { idempotent: true });
696
+ ```
697
+
698
+ **For idempotent writes (safe to retry):**
699
+
700
+ ```javascript
701
+ // SET with the same value is idempotent
702
+ await client.execute('set mykey 0 0 5\r\nhello', nodes, { idempotent: true });
703
+ ```
704
+
705
+ **Disable safety for all commands (use with caution):**
706
+
707
+ ```javascript
708
+ const client = new Memcache({
709
+ retries: 3,
710
+ retryOnlyIdempotent: false // Allow retries for ALL commands
711
+ });
712
+ ```
713
+
714
+ ### Behavior Summary
715
+
716
+ | `retryOnlyIdempotent` | `idempotent` flag | Retries enabled? |
717
+ |-----------------------|-------------------|------------------|
718
+ | `true` (default) | `false` (default) | No |
719
+ | `true` (default) | `true` | Yes |
720
+ | `false` | (any) | Yes |
721
+
722
+ ### Methods Without Retry Support
723
+
724
+ The following methods do not use the retry mechanism and have their own error handling:
725
+
726
+ - `get()` - Returns `undefined` on failure
727
+ - `gets()` - Returns partial results on node failure
728
+ - `flush()` - Operates directly on nodes
729
+ - `stats()` - Operates directly on nodes
730
+ - `version()` - Operates directly on nodes
731
+
732
+ To use retries with read operations, use the `execute()` method directly:
733
+
734
+ ```javascript
735
+ const nodes = await client.getNodesByKey('mykey');
736
+ const results = await client.execute('get mykey', nodes, { idempotent: true });
737
+ ```
738
+
739
+ # SASL Authentication
740
+
741
+ The Memcache client supports SASL (Simple Authentication and Security Layer) authentication using the PLAIN mechanism. This allows you to connect to memcached servers that require authentication.
742
+
743
+ ## Enabling SASL Authentication
744
+
745
+ ```javascript
746
+ import { Memcache } from 'memcache';
747
+
748
+ const client = new Memcache({
749
+ nodes: ['localhost:11211'],
750
+ sasl: {
751
+ username: 'myuser',
752
+ password: 'mypassword',
753
+ },
754
+ });
755
+
756
+ await client.connect();
757
+ // Client is now authenticated and ready to use
758
+ ```
759
+
760
+ ## SASL Options
761
+
762
+ The `sasl` option accepts an object with the following properties:
763
+
764
+ - `username: string` - The username for authentication (required)
765
+ - `password: string` - The password for authentication (required)
766
+ - `mechanism?: 'PLAIN'` - The SASL mechanism to use (default: 'PLAIN')
767
+
768
+ Currently, only the PLAIN mechanism is supported.
769
+
770
+ ## Binary Protocol Methods
771
+
772
+ **Important:** Memcached servers with SASL enabled (`-S` flag) require the binary protocol for all operations after authentication. The standard text-based methods (`client.get()`, `client.set()`, etc.) will not work on SASL-enabled servers.
773
+
774
+ Use the `binary*` methods on nodes for SASL-enabled servers:
775
+
776
+ ```javascript
777
+ import { Memcache } from 'memcache';
778
+
779
+ const client = new Memcache({
780
+ nodes: ['localhost:11211'],
781
+ sasl: { username: 'user', password: 'pass' },
782
+ });
783
+
784
+ await client.connect();
785
+
786
+ // Access the node directly for binary operations
787
+ const node = client.nodes[0];
788
+
789
+ // Binary protocol operations
790
+ await node.binarySet('mykey', 'myvalue', 3600); // Set with 1 hour expiry
791
+ const value = await node.binaryGet('mykey'); // Get value
792
+ await node.binaryDelete('mykey'); // Delete key
793
+
794
+ // Other binary operations
795
+ await node.binaryAdd('newkey', 'value'); // Add (only if not exists)
796
+ await node.binaryReplace('existingkey', 'newvalue'); // Replace (only if exists)
797
+ await node.binaryIncr('counter', 1); // Increment
798
+ await node.binaryDecr('counter', 1); // Decrement
799
+ await node.binaryAppend('mykey', '-suffix'); // Append to value
800
+ await node.binaryPrepend('mykey', 'prefix-'); // Prepend to value
801
+ await node.binaryTouch('mykey', 7200); // Update expiration
802
+ await node.binaryFlush(); // Flush all
803
+ const version = await node.binaryVersion(); // Get server version
804
+ const stats = await node.binaryStats(); // Get server stats
805
+ ```
806
+
807
+ ## Per-Node SASL Configuration
808
+
809
+ You can also configure SASL credentials when creating individual nodes:
810
+
811
+ ```javascript
812
+ import { createNode } from 'memcache';
813
+
814
+ // Create a node with SASL credentials
815
+ const node = createNode('localhost', 11211, {
816
+ sasl: { username: 'user', password: 'pass' },
817
+ });
818
+
819
+ // Connect and use binary methods
820
+ await node.connect();
821
+ await node.binarySet('mykey', 'hello');
822
+ const value = await node.binaryGet('mykey');
823
+ ```
824
+
825
+ ## Authentication Events
826
+
827
+ You can listen for authentication events on both nodes and the client:
828
+
829
+ ```javascript
830
+ import { Memcache, MemcacheNode } from 'memcache';
831
+
832
+ // Node-level events
833
+ const node = new MemcacheNode('localhost', 11211, {
834
+ sasl: { username: 'user', password: 'pass' },
835
+ });
836
+
837
+ node.on('authenticated', () => {
838
+ console.log('Node authenticated successfully');
839
+ });
840
+
841
+ node.on('error', (error) => {
842
+ if (error.message.includes('SASL authentication failed')) {
843
+ console.error('Authentication failed:', error.message);
844
+ }
845
+ });
846
+
847
+ await node.connect();
848
+
849
+ // Client-level events (forwarded from nodes)
850
+ const client = new Memcache({
851
+ nodes: ['localhost:11211'],
852
+ sasl: { username: 'user', password: 'pass' },
853
+ });
854
+
855
+ client.on('authenticated', () => {
856
+ console.log('Client authenticated');
857
+ });
858
+
859
+ await client.connect();
860
+ ```
861
+
862
+ ### Node Properties
863
+
864
+ - `node.hasSaslCredentials` - Returns `true` if SASL credentials are configured
865
+ - `node.isAuthenticated` - Returns `true` if the node has successfully authenticated
866
+
867
+ ## Server Configuration
868
+
869
+ To use SASL authentication, your memcached server must be configured with SASL support:
870
+
871
+ 1. **Build memcached with SASL support** - Ensure memcached was compiled with `--enable-sasl`
872
+
873
+ 2. **Create SASL users** - Use `saslpasswd2` to create users:
874
+ ```bash
875
+ saslpasswd2 -a memcached -c username
876
+ ```
877
+
878
+ 3. **Configure SASL mechanism** - Create `/etc/sasl2/memcached.conf`:
879
+ ```
880
+ mech_list: plain
881
+ ```
882
+
883
+ 4. **Start memcached with SASL** - Use the `-S` flag:
884
+ ```bash
885
+ memcached -S -m 64 -p 11211
886
+ ```
887
+
888
+ For more details, see the [memcached SASL documentation](https://github.com/memcached/memcached/wiki/SASLHowto).
889
+
890
+ # Auto Discovery
891
+
892
+ The Memcache client supports AWS ElastiCache Auto Discovery, which automatically detects cluster topology changes and adds or removes nodes as needed. When enabled, the client connects to a configuration endpoint, retrieves the current list of cache nodes, and periodically polls for changes.
893
+
894
+ ## Enabling Auto Discovery
895
+
896
+ ```javascript
897
+ import { Memcache } from 'memcache';
898
+
899
+ const client = new Memcache({
900
+ nodes: [],
901
+ autoDiscover: {
902
+ enabled: true,
903
+ configEndpoint: 'my-cluster.cfg.use1.cache.amazonaws.com:11211',
904
+ },
905
+ });
906
+
907
+ await client.connect();
908
+ // The client automatically discovers and connects to all cluster nodes
909
+ ```
910
+
911
+ If you omit `configEndpoint`, the first node in the `nodes` array is used as the configuration endpoint:
912
+
913
+ ```javascript
914
+ const client = new Memcache({
915
+ nodes: ['my-cluster.cfg.use1.cache.amazonaws.com:11211'],
916
+ autoDiscover: {
917
+ enabled: true,
918
+ },
919
+ });
920
+ ```
921
+
922
+ ## Auto Discovery Options
923
+
924
+ The `autoDiscover` option accepts an object with the following properties:
925
+
926
+ - `enabled: boolean` - Enable auto discovery of cluster nodes (required)
927
+ - `pollingInterval?: number` - How often to poll for topology changes, in milliseconds (default: 60000)
928
+ - `configEndpoint?: string` - The configuration endpoint to use for discovery. This is typically the `.cfg` endpoint from ElastiCache. If not specified, the first node in the `nodes` array will be used
929
+ - `useLegacyCommand?: boolean` - Use the legacy `get AmazonElastiCache:cluster` command instead of `config get cluster` (default: false)
930
+
931
+ ## Auto Discovery Events
932
+
933
+ The client emits events during the auto discovery lifecycle:
934
+
935
+ ```javascript
936
+ const client = new Memcache({
937
+ nodes: [],
938
+ autoDiscover: {
939
+ enabled: true,
940
+ configEndpoint: 'my-cluster.cfg.use1.cache.amazonaws.com:11211',
941
+ },
942
+ });
943
+
944
+ // Emitted on initial discovery with the full cluster config
945
+ client.on('autoDiscover', (config) => {
946
+ console.log('Discovered nodes:', config.nodes);
947
+ console.log('Config version:', config.version);
948
+ });
949
+
950
+ // Emitted when polling detects a topology change
951
+ client.on('autoDiscoverUpdate', (config) => {
952
+ console.log('Cluster topology changed:', config.nodes);
953
+ });
954
+
955
+ // Emitted when discovery encounters an error (non-fatal, retries on next poll)
956
+ client.on('autoDiscoverError', (error) => {
957
+ console.error('Discovery error:', error.message);
958
+ });
959
+
960
+ await client.connect();
961
+ ```
962
+
963
+ ## Legacy Command Support
964
+
965
+ For ElastiCache engine versions older than 1.4.14, use the legacy discovery command:
966
+
967
+ ```javascript
968
+ const client = new Memcache({
969
+ nodes: [],
970
+ autoDiscover: {
971
+ enabled: true,
972
+ configEndpoint: 'my-cluster.cfg.use1.cache.amazonaws.com:11211',
973
+ useLegacyCommand: true, // Uses 'get AmazonElastiCache:cluster' instead of 'config get cluster'
974
+ },
975
+ });
976
+ ```
977
+
978
+ # IPv6 Support
979
+
980
+ The Memcache client fully supports IPv6 addresses using standard bracket notation in URIs.
981
+
982
+ ## Connecting to IPv6 Nodes
983
+
984
+ ```javascript
985
+ import { Memcache } from 'memcache';
986
+
987
+ // IPv6 loopback
988
+ const client = new Memcache('[::1]:11211');
989
+
990
+ // Multiple IPv6 nodes
991
+ const client = new Memcache({
992
+ nodes: [
993
+ '[::1]:11211',
994
+ '[2001:db8::1]:11211',
995
+ 'memcache://[2001:db8::2]:11212',
996
+ ],
997
+ });
998
+
999
+ await client.connect();
1000
+ ```
1001
+
1002
+ ## IPv6 in Auto Discovery
1003
+
1004
+ When auto discovery returns IPv6 node addresses, the client automatically brackets them for correct URI handling:
1005
+
1006
+ ```javascript
1007
+ const client = new Memcache({
1008
+ nodes: [],
1009
+ autoDiscover: {
1010
+ enabled: true,
1011
+ configEndpoint: '[2001:db8::1]:11211',
1012
+ },
1013
+ });
1014
+
1015
+ await client.connect();
1016
+ // Discovered IPv6 nodes are added as [host]:port automatically
1017
+ ```
1018
+
1019
+ ## IPv6 Node IDs
1020
+
1021
+ Node IDs for IPv6 addresses use bracket notation to avoid ambiguity:
1022
+
1023
+ ```javascript
1024
+ const client = new Memcache({
1025
+ nodes: ['[::1]:11211', '[2001:db8::1]:11212'],
1026
+ });
1027
+
1028
+ console.log(client.nodeIds);
1029
+ // ['[::1]:11211', '[2001:db8::1]:11212']
1030
+ ```
1031
+
1032
+ # Benchmarks
1033
+
1034
+ These are provided to show a simple benchmark against current libraries. This is not robust but it is something we update regularly to make sure we are keeping performant.
1035
+
1036
+ | name | summary | ops/sec | time/op | margin | samples |
1037
+ |------------------------------|:---------:|----------:|----------:|:--------:|----------:|
1038
+ | memcache set/get (v1.4.0) | 🥇 | 3K | 350µs | ±0.19% | 10K |
1039
+ | memcached set/get (v2.2.2) | -2.9% | 3K | 361µs | ±0.16% | 10K |
1040
+ | memjs set/get (v1.3.2) | -12% | 3K | 398µs | ±0.17% | 10K |
1041
+
476
1042
  # Contributing
477
1043
 
478
- Please read our [Contributing Guidelines](./CONTRIBUTING.md) and also our [Code of Conduct](./CODE_OF_CONDUCT.md).
1044
+ Please read our [Contributing Guidelines](./CONTRIBUTING.md) and also our [Code of Conduct](./CODE_OF_CONDUCT.md).
479
1045
 
480
1046
  # License and Copyright
481
1047