@salesforce/core 5.3.8 → 6.0.1-crdt.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.
Files changed (36) hide show
  1. package/lib/config/config.d.ts +4 -4
  2. package/lib/config/config.js +6 -9
  3. package/lib/config/configFile.d.ts +4 -3
  4. package/lib/config/configFile.js +63 -14
  5. package/lib/config/configStackTypes.d.ts +14 -0
  6. package/lib/config/configStackTypes.js +3 -0
  7. package/lib/config/configStore.d.ts +8 -19
  8. package/lib/config/configStore.js +36 -37
  9. package/lib/config/lwwMap.d.ts +25 -0
  10. package/lib/config/lwwMap.js +93 -0
  11. package/lib/config/lwwRegister.d.ts +19 -0
  12. package/lib/config/lwwRegister.js +39 -0
  13. package/lib/config/orgUsersConfig.d.ts +5 -1
  14. package/lib/config/tokensConfig.d.ts +1 -1
  15. package/lib/config/ttlConfig.js +4 -1
  16. package/lib/exported.d.ts +2 -1
  17. package/lib/org/authInfo.d.ts +3 -1
  18. package/lib/org/authInfo.js +2 -0
  19. package/lib/org/org.js +2 -2
  20. package/lib/org/orgConfigProperties.d.ts +1 -1
  21. package/lib/org/user.js +1 -2
  22. package/lib/sfProject.d.ts +3 -3
  23. package/lib/sfProject.js +5 -19
  24. package/lib/stateAggregator/accessors/aliasAccessor.d.ts +2 -2
  25. package/lib/stateAggregator/accessors/aliasAccessor.js +3 -7
  26. package/lib/stateAggregator/accessors/orgAccessor.d.ts +2 -1
  27. package/lib/stateAggregator/accessors/orgAccessor.js +1 -0
  28. package/lib/stateAggregator/accessors/tokenAccessor.d.ts +1 -1
  29. package/lib/stateAggregator/accessors/tokenAccessor.js +1 -1
  30. package/lib/testSetup.d.ts +3 -20
  31. package/lib/testSetup.js +24 -58
  32. package/lib/util/lockRetryOptions.d.ts +11 -0
  33. package/lib/util/lockRetryOptions.js +16 -0
  34. package/lib/util/uniqid.d.ts +14 -0
  35. package/lib/util/uniqid.js +34 -0
  36. package/package.json +5 -5
@@ -1,7 +1,7 @@
1
- import { JsonPrimitive, Nullable } from '@salesforce/ts-types';
1
+ import { JsonCollection, JsonPrimitive, Nullable } from '@salesforce/ts-types';
2
2
  import { OrgConfigProperties } from '../org/orgConfigProperties';
3
3
  import { ConfigFile } from './configFile';
4
- import { ConfigContents, ConfigValue } from './configStore';
4
+ import { ConfigContents, ConfigValue } from './configStackTypes';
5
5
  /**
6
6
  * Interface for meta information about config properties
7
7
  */
@@ -241,7 +241,7 @@ export declare class Config extends ConfigFile<ConfigFile.Options, ConfigPropert
241
241
  *
242
242
  * @param newContents The new Config value to persist.
243
243
  */
244
- write(newContents?: ConfigProperties): Promise<ConfigProperties>;
244
+ write(): Promise<ConfigProperties>;
245
245
  /**
246
246
  * DO NOT CALL - The config file needs to encrypt values which can only be done asynchronously.
247
247
  * Call {@link SfdxConfig.write} instead.
@@ -260,7 +260,7 @@ export declare class Config extends ConfigFile<ConfigFile.Options, ConfigPropert
260
260
  * @param key The property to set.
261
261
  * @param value The value of the property.
262
262
  */
263
- set(key: string, value: JsonPrimitive): ConfigProperties;
263
+ set(key: string, value: JsonPrimitive | JsonCollection): ConfigProperties;
264
264
  /**
265
265
  * Unsets a value for a property.
266
266
  *
@@ -281,14 +281,14 @@ class Config extends configFile_1.ConfigFile {
281
281
  */
282
282
  static async update(isGlobal, propertyName, value) {
283
283
  const config = await Config.create({ isGlobal });
284
- const content = await config.read();
285
- if (value == null) {
286
- delete content[propertyName];
284
+ await config.read();
285
+ if (value == null || value === undefined) {
286
+ config.unset(propertyName);
287
287
  }
288
288
  else {
289
- (0, kit_1.set)(content, propertyName, value);
289
+ config.set(propertyName, value);
290
290
  }
291
- return config.write(content);
291
+ return config.write();
292
292
  }
293
293
  /**
294
294
  * Clear all the configured properties both local and global.
@@ -339,10 +339,7 @@ class Config extends configFile_1.ConfigFile {
339
339
  *
340
340
  * @param newContents The new Config value to persist.
341
341
  */
342
- async write(newContents) {
343
- if (newContents != null) {
344
- this.setContents(newContents);
345
- }
342
+ async write() {
346
343
  await this.cryptProperties(true);
347
344
  await super.write();
348
345
  if (global_1.Global.SFDX_INTEROPERABILITY)
@@ -1,7 +1,8 @@
1
1
  /// <reference types="node" />
2
2
  import { Stats as fsStats } from 'fs';
3
3
  import { Logger } from '../logger/logger';
4
- import { BaseConfigStore, ConfigContents } from './configStore';
4
+ import { BaseConfigStore } from './configStore';
5
+ import { ConfigContents } from './configStackTypes';
5
6
  /**
6
7
  * Represents a json config file used to manage settings and state. Global config
7
8
  * files are stored in the home directory hidden state folder (.sfdx) and local config
@@ -100,14 +101,14 @@ export declare class ConfigFile<T extends ConfigFile.Options = ConfigFile.Option
100
101
  *
101
102
  * @param newContents The new contents of the file.
102
103
  */
103
- write(newContents?: P): Promise<P>;
104
+ write(): Promise<P>;
104
105
  /**
105
106
  * Write the config file with new contents. If no new contents are provided it will write the existing config
106
107
  * contents that were set from {@link ConfigFile.read}, or an empty file if {@link ConfigFile.read} was not called.
107
108
  *
108
109
  * @param newContents The new contents of the file.
109
110
  */
110
- writeSync(newContents?: P): P;
111
+ writeSync(): P;
111
112
  /**
112
113
  * Check to see if the config file exists. Returns `true` if the config file exists and has access, false otherwise.
113
114
  */
@@ -11,13 +11,15 @@ const fs = require("fs");
11
11
  const fs_1 = require("fs");
12
12
  const os_1 = require("os");
13
13
  const path_1 = require("path");
14
- const ts_types_1 = require("@salesforce/ts-types");
14
+ const proper_lockfile_1 = require("proper-lockfile");
15
15
  const kit_1 = require("@salesforce/kit");
16
16
  const global_1 = require("../global");
17
17
  const logger_1 = require("../logger/logger");
18
18
  const sfError_1 = require("../sfError");
19
19
  const internal_1 = require("../util/internal");
20
+ const lockRetryOptions_1 = require("../util/lockRetryOptions");
20
21
  const configStore_1 = require("./configStore");
22
+ const lwwMap_1 = require("./lwwMap");
21
23
  /**
22
24
  * Represents a json config file used to manage settings and state. Global config
23
25
  * files are stored in the home directory hidden state folder (.sfdx) and local config
@@ -146,9 +148,9 @@ class ConfigFile extends configStore_1.BaseConfigStore {
146
148
  // Only need to read config files once. They are kept up to date
147
149
  // internally and updated persistently via write().
148
150
  if (!this.hasRead || force) {
149
- this.logger.info(`Reading config file: ${this.getPath()}`);
151
+ this.logger.info(`Reading config file: ${this.getPath()} because ${!this.hasRead ? 'hasRead is false' : 'force parameter is true'}`);
150
152
  const obj = (0, kit_1.parseJsonMap)(await fs.promises.readFile(this.getPath(), 'utf8'), this.getPath());
151
- this.setContentsFromObject(obj);
153
+ this.setContentsFromFileContents(obj, (await fs.promises.stat(this.getPath(), { bigint: true })).mtimeNs);
152
154
  }
153
155
  // Necessarily set this even when an error happens to avoid infinite re-reading.
154
156
  // To attempt another read, pass `force=true`.
@@ -159,7 +161,7 @@ class ConfigFile extends configStore_1.BaseConfigStore {
159
161
  this.hasRead = true;
160
162
  if (err.code === 'ENOENT') {
161
163
  if (!throwOnNotFound) {
162
- this.setContents();
164
+ this.setContentsFromFileContents({}, process.hrtime.bigint());
163
165
  return this.getContents();
164
166
  }
165
167
  }
@@ -184,14 +186,18 @@ class ConfigFile extends configStore_1.BaseConfigStore {
184
186
  if (!this.hasRead || force) {
185
187
  this.logger.info(`Reading config file: ${this.getPath()}`);
186
188
  const obj = (0, kit_1.parseJsonMap)(fs.readFileSync(this.getPath(), 'utf8'));
187
- this.setContentsFromObject(obj);
189
+ this.setContentsFromFileContents(obj, fs.statSync(this.getPath(), { bigint: true }).mtimeNs);
188
190
  }
191
+ // Necessarily set this even when an error happens to avoid infinite re-reading.
192
+ // To attempt another read, pass `force=true`.
193
+ this.hasRead = true;
189
194
  return this.getContents();
190
195
  }
191
196
  catch (err) {
197
+ this.hasRead = true;
192
198
  if (err.code === 'ENOENT') {
193
199
  if (!throwOnNotFound) {
194
- this.setContents();
200
+ this.setContentsFromFileContents({}, process.hrtime.bigint());
195
201
  return this.getContents();
196
202
  }
197
203
  }
@@ -209,18 +215,40 @@ class ConfigFile extends configStore_1.BaseConfigStore {
209
215
  *
210
216
  * @param newContents The new contents of the file.
211
217
  */
212
- async write(newContents) {
213
- if (newContents) {
214
- this.setContents(newContents);
215
- }
218
+ async write() {
219
+ // make sure we can write to the directory
216
220
  try {
217
221
  await fs.promises.mkdir((0, path_1.dirname)(this.getPath()), { recursive: true });
218
222
  }
219
223
  catch (err) {
220
224
  throw sfError_1.SfError.wrap(err);
221
225
  }
226
+ let unlockFn;
227
+ // lock the file. Returns an unlock function to call when done.
228
+ try {
229
+ unlockFn = await (0, proper_lockfile_1.lock)(this.getPath(), lockRetryOptions_1.lockRetryOptions);
230
+ // get the file modstamp. Do this after the lock acquisition in case the file is being written to.
231
+ const fileTimestamp = (await fs.promises.stat(this.getPath(), { bigint: true })).mtimeNs;
232
+ // read the file contents into a LWWMap using the modstamp
233
+ const stateFromFile = (0, lwwMap_1.stateFromContents)((0, kit_1.parseJsonMap)(await fs.promises.readFile(this.getPath(), 'utf8'), this.getPath()), fileTimestamp, this.getPath());
234
+ // merge the new contents into the in-memory LWWMap
235
+ this.contents.merge(stateFromFile);
236
+ }
237
+ catch (err) {
238
+ if (err instanceof Error && err.message.includes('ENOENT')) {
239
+ this.logger.debug(`No file found at ${this.getPath()}. Write will create it.`);
240
+ }
241
+ else {
242
+ throw err;
243
+ }
244
+ }
245
+ // write the merged LWWMap to file
222
246
  this.logger.info(`Writing to config file: ${this.getPath()}`);
223
247
  await fs.promises.writeFile(this.getPath(), JSON.stringify(this.toObject(), null, 2));
248
+ // unlock the file
249
+ if (typeof unlockFn !== 'undefined') {
250
+ await unlockFn();
251
+ }
224
252
  return this.getContents();
225
253
  }
226
254
  /**
@@ -229,18 +257,39 @@ class ConfigFile extends configStore_1.BaseConfigStore {
229
257
  *
230
258
  * @param newContents The new contents of the file.
231
259
  */
232
- writeSync(newContents) {
233
- if ((0, ts_types_1.isPlainObject)(newContents)) {
234
- this.setContents(newContents);
235
- }
260
+ writeSync() {
236
261
  try {
237
262
  fs.mkdirSync((0, path_1.dirname)(this.getPath()), { recursive: true });
238
263
  }
239
264
  catch (err) {
240
265
  throw sfError_1.SfError.wrap(err);
241
266
  }
267
+ let unlockFn;
268
+ try {
269
+ // lock the file. Returns an unlock function to call when done.
270
+ unlockFn = (0, proper_lockfile_1.lockSync)(this.getPath(), lockRetryOptions_1.lockOptions);
271
+ // get the file modstamp. Do this after the lock acquisition in case the file is being written to.
272
+ const fileTimestamp = fs.statSync(this.getPath(), { bigint: true }).mtimeNs;
273
+ // read the file contents into a LWWMap using the modstamp
274
+ const stateFromFile = (0, lwwMap_1.stateFromContents)((0, kit_1.parseJsonMap)(fs.readFileSync(this.getPath(), 'utf8'), this.getPath()), fileTimestamp, this.getPath());
275
+ // merge the new contents into the in-memory LWWMap
276
+ this.contents.merge(stateFromFile);
277
+ }
278
+ catch (err) {
279
+ if (err instanceof Error && err.message.includes('ENOENT')) {
280
+ this.logger.debug(`No file found at ${this.getPath()}. Write will create it.`);
281
+ }
282
+ else {
283
+ throw err;
284
+ }
285
+ }
286
+ // write the merged LWWMap to file
242
287
  this.logger.info(`Writing to config file: ${this.getPath()}`);
243
288
  fs.writeFileSync(this.getPath(), JSON.stringify(this.toObject(), null, 2));
289
+ if (typeof unlockFn !== 'undefined') {
290
+ // unlock the file
291
+ unlockFn();
292
+ }
244
293
  return this.getContents();
245
294
  }
246
295
  /**
@@ -0,0 +1,14 @@
1
+ import { Dictionary, AnyJson } from '@salesforce/ts-types';
2
+ export type Key<P extends ConfigContents> = Extract<keyof P, string>;
3
+ /**
4
+ * The allowed types stored in a config store.
5
+ */
6
+ export type ConfigValue = AnyJson;
7
+ /**
8
+ * The type of entries in a config store defined by the key and value type of {@link ConfigContents}.
9
+ */
10
+ export type ConfigEntry = [string, ConfigValue];
11
+ /**
12
+ * The type of content a config stores.
13
+ */
14
+ export type ConfigContents<T = ConfigValue> = Dictionary<T>;
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=configStackTypes.js.map
@@ -1,19 +1,8 @@
1
1
  import { AsyncOptionalCreatable } from '@salesforce/kit';
2
- import { AnyJson, Dictionary, JsonMap, Optional } from '@salesforce/ts-types';
2
+ import { JsonMap, Optional } from '@salesforce/ts-types';
3
3
  import { Crypto } from '../crypto/crypto';
4
- /**
5
- * The allowed types stored in a config store.
6
- */
7
- export type ConfigValue = AnyJson;
8
- /**
9
- * The type of entries in a config store defined by the key and value type of {@link ConfigContents}.
10
- */
11
- export type ConfigEntry = [string, ConfigValue];
12
- /**
13
- * The type of content a config stores.
14
- */
15
- export type ConfigContents<T = ConfigValue> = Dictionary<T>;
16
- export type Key<P extends ConfigContents> = Extract<keyof P, string>;
4
+ import { LWWMap } from './lwwMap';
5
+ import { ConfigContents, ConfigEntry, ConfigValue, Key } from './configStackTypes';
17
6
  /**
18
7
  * An interface for a config object with a persistent store.
19
8
  */
@@ -47,7 +36,7 @@ export declare abstract class BaseConfigStore<T extends BaseConfigStore.Options
47
36
  protected static encryptedKeys: Array<string | RegExp>;
48
37
  protected options: T;
49
38
  protected crypto?: Crypto;
50
- private contents;
39
+ protected contents: LWWMap<P>;
51
40
  private statics;
52
41
  /**
53
42
  * Constructor.
@@ -78,7 +67,6 @@ export declare abstract class BaseConfigStore<T extends BaseConfigStore.Options
78
67
  /**
79
68
  * Returns a boolean asserting whether a value has been associated to the key in the config object or not.
80
69
  *
81
- * @param key The key. Supports query key like `a.b[0]`.
82
70
  */
83
71
  has(key: string): boolean;
84
72
  /**
@@ -89,7 +77,7 @@ export declare abstract class BaseConfigStore<T extends BaseConfigStore.Options
89
77
  * Sets the value for the key in the config object. This will override the existing value.
90
78
  * To do a partial update, use {@link BaseConfigStore.update}.
91
79
  *
92
- * @param key The key. Supports query key like `a.b[0]`.
80
+ * @param key The key.
93
81
  * @param value The value.
94
82
  */
95
83
  set<K extends Key<P>>(key: K, value: P[K]): void;
@@ -107,7 +95,7 @@ export declare abstract class BaseConfigStore<T extends BaseConfigStore.Options
107
95
  * Returns `true` if an element in the config object existed and has been removed, or `false` if the element does not
108
96
  * exist. {@link BaseConfigStore.has} will return false afterwards.
109
97
  *
110
- * @param key The key. Supports query key like `a.b[0]`.
98
+ * @param key The key
111
99
  */
112
100
  unset(key: string): boolean;
113
101
  /**
@@ -166,7 +154,8 @@ export declare abstract class BaseConfigStore<T extends BaseConfigStore.Options
166
154
  *
167
155
  * @param obj The object.
168
156
  */
169
- setContentsFromObject<U extends JsonMap>(obj: U): void;
157
+ setContentsFromObject<U extends P>(obj: U): void;
158
+ protected setContentsFromFileContents(contents: P, timestamp: bigint): void;
170
159
  protected getEncryptedKeys(): Array<string | RegExp>;
171
160
  /**
172
161
  * This config file has encrypted keys and it should attempt to encrypt them.
@@ -12,6 +12,7 @@ const ts_types_1 = require("@salesforce/ts-types");
12
12
  const ts_types_2 = require("@salesforce/ts-types");
13
13
  const crypto_1 = require("../crypto/crypto");
14
14
  const sfError_1 = require("../sfError");
15
+ const lwwMap_1 = require("./lwwMap");
15
16
  /**
16
17
  * An abstract class that implements all the config management functions but
17
18
  * none of the storage functions.
@@ -27,6 +28,8 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
27
28
  */
28
29
  constructor(options) {
29
30
  super(options);
31
+ // Initialized in setContents
32
+ this.contents = new lwwMap_1.LWWMap();
30
33
  this.statics = this.constructor;
31
34
  this.options = options ?? {};
32
35
  this.setContents(this.initialContents());
@@ -35,11 +38,11 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
35
38
  * Returns an array of {@link ConfigEntry} for each element in the config.
36
39
  */
37
40
  entries() {
38
- return (0, ts_types_2.definiteEntriesOf)(this.contents);
41
+ return (0, ts_types_2.definiteEntriesOf)(this.contents.value ?? {});
39
42
  }
40
43
  get(key, decrypt = false) {
41
44
  const k = key;
42
- let value = this.getMethod(this.contents, k);
45
+ let value = this.getMethod(this.contents.value ?? {}, k);
43
46
  if (this.hasEncryption() && decrypt) {
44
47
  if ((0, ts_types_2.isJsonMap)(value)) {
45
48
  value = this.recursiveDecrypt((0, kit_1.cloneJson)(value), k);
@@ -63,16 +66,15 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
63
66
  /**
64
67
  * Returns a boolean asserting whether a value has been associated to the key in the config object or not.
65
68
  *
66
- * @param key The key. Supports query key like `a.b[0]`.
67
69
  */
68
70
  has(key) {
69
- return !!this.getMethod(this.contents, key);
71
+ return this.contents.has(key) ?? false;
70
72
  }
71
73
  /**
72
74
  * Returns an array that contains the keys for each element in the config object.
73
75
  */
74
76
  keys() {
75
- return Object.keys(this.contents);
77
+ return Object.keys(this.contents.value ?? {});
76
78
  }
77
79
  set(key, value) {
78
80
  if (this.hasEncryption()) {
@@ -83,7 +85,14 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
83
85
  value = this.encrypt(value);
84
86
  }
85
87
  }
86
- this.setMethod(this.contents, key, value);
88
+ // undefined value means unset
89
+ if (value === undefined) {
90
+ this.unset(key);
91
+ }
92
+ else {
93
+ // @ts-expect-error TODO: why isn't key guaranteed to be a string
94
+ this.contents.set(key, value);
95
+ }
87
96
  }
88
97
  update(key, value) {
89
98
  const existingValue = this.get(key, true);
@@ -96,17 +105,11 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
96
105
  * Returns `true` if an element in the config object existed and has been removed, or `false` if the element does not
97
106
  * exist. {@link BaseConfigStore.has} will return false afterwards.
98
107
  *
99
- * @param key The key. Supports query key like `a.b[0]`.
108
+ * @param key The key
100
109
  */
101
110
  unset(key) {
102
111
  if (this.has(key)) {
103
- if (this.contents[key]) {
104
- delete this.contents[key];
105
- }
106
- else {
107
- // It is a query key, so just set it to undefined
108
- this.setMethod(this.contents, key, undefined);
109
- }
112
+ this.contents.delete(key);
110
113
  return true;
111
114
  }
112
115
  return false;
@@ -118,19 +121,19 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
118
121
  * @param keys The keys. Supports query keys like `a.b[0]`.
119
122
  */
120
123
  unsetAll(keys) {
121
- return keys.reduce((val, key) => val && this.unset(key), true);
124
+ return keys.map((key) => this.unset(key)).every(Boolean);
122
125
  }
123
126
  /**
124
127
  * Removes all key/value pairs from the config object.
125
128
  */
126
129
  clear() {
127
- this.contents = {};
130
+ this.keys().map((key) => this.unset(key));
128
131
  }
129
132
  /**
130
133
  * Returns an array that contains the values for each element in the config object.
131
134
  */
132
135
  values() {
133
- return (0, ts_types_2.definiteValuesOf)(this.contents);
136
+ return (0, ts_types_2.definiteValuesOf)(this.contents.value ?? {});
134
137
  }
135
138
  /**
136
139
  * Returns the entire config contents.
@@ -143,13 +146,10 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
143
146
  *
144
147
  */
145
148
  getContents(decrypt = false) {
146
- if (!this.contents) {
147
- this.setContents();
148
- }
149
149
  if (this.hasEncryption() && decrypt) {
150
- return this.recursiveDecrypt((0, kit_1.cloneJson)(this.contents));
150
+ return this.recursiveDecrypt((0, kit_1.cloneJson)(this.contents?.value ?? {}));
151
151
  }
152
- return this.contents;
152
+ return this.contents?.value ?? {};
153
153
  }
154
154
  /**
155
155
  * Sets the entire config contents.
@@ -160,7 +160,9 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
160
160
  if (this.hasEncryption()) {
161
161
  contents = this.recursiveEncrypt(contents);
162
162
  }
163
- this.contents = contents;
163
+ (0, ts_types_1.entriesOf)(contents).map(([key, value]) => {
164
+ this.contents.set(key, value);
165
+ });
164
166
  }
165
167
  /**
166
168
  * Invokes `actionFn` once for each key-value pair present in the config object.
@@ -168,10 +170,7 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
168
170
  * @param {function} actionFn The function `(key: string, value: ConfigValue) => void` to be called for each element.
169
171
  */
170
172
  forEach(actionFn) {
171
- const entries = this.entries();
172
- for (const entry of entries) {
173
- actionFn(entry[0], entry[1]);
174
- }
173
+ this.entries().map((entry) => actionFn(entry[0], entry[1]));
175
174
  }
176
175
  /**
177
176
  * Asynchronously invokes `actionFn` once for each key-value pair present in the config object.
@@ -182,18 +181,14 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
182
181
  */
183
182
  async awaitEach(actionFn) {
184
183
  const entries = this.entries();
185
- for (const entry of entries) {
186
- // prevent ConfigFile collision bug
187
- // eslint-disable-next-line no-await-in-loop
188
- await actionFn(entry[0], entry[1]);
189
- }
184
+ await Promise.all(entries.map((entry) => actionFn(entry[0], entry[1])));
190
185
  }
191
186
  /**
192
187
  * Convert the config object to a JSON object. Returns the config contents.
193
188
  * Same as calling {@link ConfigStore.getContents}
194
189
  */
195
190
  toObject() {
196
- return this.contents;
191
+ return this.contents.value ?? {};
197
192
  }
198
193
  /**
199
194
  * Convert an object to a {@link ConfigContents} and set it as the config contents.
@@ -201,13 +196,17 @@ class BaseConfigStore extends kit_1.AsyncOptionalCreatable {
201
196
  * @param obj The object.
202
197
  */
203
198
  setContentsFromObject(obj) {
204
- this.contents = (this.hasEncryption() ? this.recursiveEncrypt(obj) : {});
205
- Object.entries(obj).forEach(([key, value]) => {
206
- this.setMethod(this.contents, key, value);
199
+ const objForWrite = this.hasEncryption() ? this.recursiveEncrypt(obj) : obj;
200
+ (0, ts_types_1.entriesOf)(objForWrite).map(([key, value]) => {
201
+ this.set(key, value);
207
202
  });
208
203
  }
204
+ setContentsFromFileContents(contents, timestamp) {
205
+ const state = (0, lwwMap_1.stateFromContents)(contents, timestamp, this.contents.id);
206
+ this.contents = new lwwMap_1.LWWMap(this.contents.id, state);
207
+ }
209
208
  getEncryptedKeys() {
210
- return [...(this.options?.encryptedKeys ?? []), ...(this.statics?.encryptedKeys || [])];
209
+ return [...(this.options?.encryptedKeys ?? []), ...(this.statics?.encryptedKeys ?? [])];
211
210
  }
212
211
  /**
213
212
  * This config file has encrypted keys and it should attempt to encrypt them.
@@ -0,0 +1,25 @@
1
+ import { LWWRegister } from './lwwRegister';
2
+ import { ConfigContents, Key } from './configStackTypes';
3
+ export declare const SYMBOL_FOR_DELETED: "UNIQUE_IDENTIFIER_FOR_DELETED";
4
+ export type LWWState<P> = {
5
+ [Property in keyof P]: LWWRegister<P[Property] | typeof SYMBOL_FOR_DELETED>['state'];
6
+ };
7
+ /**
8
+ * @param contents object aligning with ConfigContents
9
+ * @param timestamp a bigInt that sets the timestamp. Defaults to the current time
10
+ * construct a LWWState from an object
11
+ *
12
+ * */
13
+ export declare const stateFromContents: <P extends ConfigContents<import("@salesforce/ts-types").AnyJson>>(contents: P, timestamp?: bigint, id?: string) => LWWState<P>;
14
+ export declare class LWWMap<P extends ConfigContents> {
15
+ #private;
16
+ readonly id: string;
17
+ constructor(id?: string, state?: LWWState<P>);
18
+ get value(): P;
19
+ get state(): LWWState<P>;
20
+ merge(state: LWWState<P>): LWWState<P>;
21
+ set<K extends Key<P>>(key: K, value: P[K]): void;
22
+ get<K extends Key<P>>(key: K): P[K] | undefined;
23
+ delete(key: string): void;
24
+ has(key: string): boolean;
25
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, salesforce.com, inc.
4
+ * All rights reserved.
5
+ * Licensed under the BSD 3-Clause license.
6
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7
+ */
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _LWWMap_data;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.LWWMap = exports.stateFromContents = exports.SYMBOL_FOR_DELETED = void 0;
16
+ const ts_types_1 = require("@salesforce/ts-types");
17
+ const uniqid_1 = require("../util/uniqid");
18
+ const lwwRegister_1 = require("./lwwRegister");
19
+ exports.SYMBOL_FOR_DELETED = 'UNIQUE_IDENTIFIER_FOR_DELETED';
20
+ /**
21
+ * @param contents object aligning with ConfigContents
22
+ * @param timestamp a bigInt that sets the timestamp. Defaults to the current time
23
+ * construct a LWWState from an object
24
+ *
25
+ * */
26
+ const stateFromContents = (contents, timestamp = process.hrtime.bigint(), id) => Object.fromEntries((0, ts_types_1.entriesOf)(contents).map(([key, value]) => [
27
+ key,
28
+ new lwwRegister_1.LWWRegister(id ?? (0, uniqid_1.uniqid)(), { peer: 'file', timestamp, value }),
29
+ ]));
30
+ exports.stateFromContents = stateFromContents;
31
+ class LWWMap {
32
+ constructor(id, state) {
33
+ /** map of key to LWWRegister. Used for managing conflicts */
34
+ _LWWMap_data.set(this, new Map());
35
+ this.id = id ?? (0, uniqid_1.uniqid)();
36
+ // create a new register for each key in the initial state
37
+ for (const [key, register] of (0, ts_types_1.entriesOf)(state ?? {})) {
38
+ __classPrivateFieldGet(this, _LWWMap_data, "f").set(key, new lwwRegister_1.LWWRegister(this.id, register));
39
+ }
40
+ }
41
+ get value() {
42
+ return Object.fromEntries(Array.from(__classPrivateFieldGet(this, _LWWMap_data, "f").entries())
43
+ .filter(([, register]) => register.value !== exports.SYMBOL_FOR_DELETED)
44
+ .map(([key, register]) => [key, register.value]));
45
+ }
46
+ get state() {
47
+ return Object.fromEntries(Array.from(__classPrivateFieldGet(this, _LWWMap_data, "f").entries())
48
+ // .filter(([, register]) => Boolean(register))
49
+ .map(([key, register]) => [key, register.state]));
50
+ }
51
+ merge(state) {
52
+ // recursively merge each key's register with the incoming state for that key
53
+ for (const [key, remote] of (0, ts_types_1.entriesOf)(state)) {
54
+ const local = __classPrivateFieldGet(this, _LWWMap_data, "f").get(key);
55
+ // if the register already exists, merge it with the incoming state
56
+ if (local)
57
+ local.merge(remote);
58
+ // otherwise, instantiate a new `LWWRegister` with the incoming state
59
+ else
60
+ __classPrivateFieldGet(this, _LWWMap_data, "f").set(key, new lwwRegister_1.LWWRegister(this.id, remote));
61
+ }
62
+ return this.state;
63
+ }
64
+ set(key, value) {
65
+ // get the register at the given key
66
+ const register = __classPrivateFieldGet(this, _LWWMap_data, "f").get(key);
67
+ // if the register already exists, set the value
68
+ if (register)
69
+ register.set(value);
70
+ // otherwise, instantiate a new `LWWRegister` with the value
71
+ else
72
+ __classPrivateFieldGet(this, _LWWMap_data, "f").set(key, new lwwRegister_1.LWWRegister(this.id, { peer: this.id, timestamp: process.hrtime.bigint(), value }));
73
+ }
74
+ // TODO: how to handle the deep `get` that is currently allowed ex: get('foo.bar.baz')
75
+ get(key) {
76
+ // map loses the typing
77
+ const value = __classPrivateFieldGet(this, _LWWMap_data, "f").get(key)?.value;
78
+ if (value === exports.SYMBOL_FOR_DELETED)
79
+ return undefined;
80
+ return value;
81
+ }
82
+ delete(key) {
83
+ // set the register to null, if it exists
84
+ __classPrivateFieldGet(this, _LWWMap_data, "f").get(key)?.set(exports.SYMBOL_FOR_DELETED);
85
+ }
86
+ has(key) {
87
+ // if a register doesn't exist or its value is null, the map doesn't contain the key
88
+ return __classPrivateFieldGet(this, _LWWMap_data, "f").has(key) && __classPrivateFieldGet(this, _LWWMap_data, "f").get(key)?.value !== exports.SYMBOL_FOR_DELETED;
89
+ }
90
+ }
91
+ exports.LWWMap = LWWMap;
92
+ _LWWMap_data = new WeakMap();
93
+ //# sourceMappingURL=lwwMap.js.map
@@ -0,0 +1,19 @@
1
+ type LWWRegisterState<T> = {
2
+ peer: string;
3
+ timestamp: bigint;
4
+ value: T;
5
+ };
6
+ /** a CRDT implementation. Uses timestamps to resolve conflicts when updating the value (last write wins)
7
+ * mostly based on https://jakelazaroff.com/words/an-interactive-intro-to-crdts/
8
+ *
9
+ * @param T the type of the value stored in the register
10
+ */
11
+ export declare class LWWRegister<T> {
12
+ readonly id: string;
13
+ state: LWWRegisterState<T>;
14
+ constructor(id: string, state: LWWRegisterState<T>);
15
+ get value(): T;
16
+ set(value: T): void;
17
+ merge(incoming: LWWRegisterState<T>): LWWRegisterState<T>;
18
+ }
19
+ export {};