layercache 1.2.9 → 1.3.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/dist/index.cjs CHANGED
@@ -2306,29 +2306,35 @@ var JsonSerializer = class {
2306
2306
  };
2307
2307
 
2308
2308
  // src/stampede/StampedeGuard.ts
2309
- var import_async_mutex2 = require("async-mutex");
2310
2309
  var StampedeGuard = class {
2311
- mutexes = /* @__PURE__ */ new Map();
2310
+ inFlight = /* @__PURE__ */ new Map();
2312
2311
  async execute(key, task) {
2313
- const entry = this.getMutexEntry(key);
2312
+ const existing = this.inFlight.get(key);
2313
+ if (existing) {
2314
+ existing.references += 1;
2315
+ try {
2316
+ return await existing.promise;
2317
+ } finally {
2318
+ this.releaseEntry(key, existing);
2319
+ }
2320
+ }
2321
+ const entry = {
2322
+ promise: Promise.resolve().then(task),
2323
+ references: 1
2324
+ };
2325
+ this.inFlight.set(key, entry);
2314
2326
  try {
2315
- return await entry.mutex.runExclusive(task);
2327
+ return await entry.promise;
2316
2328
  } finally {
2317
- entry.references -= 1;
2318
- const current = this.mutexes.get(key);
2319
- if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
2320
- this.mutexes.delete(key);
2321
- }
2329
+ this.releaseEntry(key, entry);
2322
2330
  }
2323
2331
  }
2324
- getMutexEntry(key) {
2325
- let entry = this.mutexes.get(key);
2326
- if (!entry) {
2327
- entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
2328
- this.mutexes.set(key, entry);
2332
+ releaseEntry(key, entry) {
2333
+ entry.references -= 1;
2334
+ const current = this.inFlight.get(key);
2335
+ if (current === entry && entry.references === 0) {
2336
+ this.inFlight.delete(key);
2329
2337
  }
2330
- entry.references += 1;
2331
- return entry;
2332
2338
  }
2333
2339
  };
2334
2340
 
@@ -2543,7 +2549,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2543
2549
  if (!fetcher) {
2544
2550
  return null;
2545
2551
  }
2546
- return this.fetchWithGuards(normalizedKey, fetcher, options);
2552
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2547
2553
  }
2548
2554
  /**
2549
2555
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
@@ -3034,12 +3040,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
3034
3040
  await this.handleInvalidationMessage(message);
3035
3041
  });
3036
3042
  }
3037
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3043
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
3038
3044
  const fetchTask = async () => {
3039
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
3040
- if (secondHit.found) {
3041
- this.metricsCollector.increment("hits");
3042
- return secondHit.value;
3045
+ const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
3046
+ if (shouldRecheckFreshLayers) {
3047
+ const secondHit = await this.readFromLayers(key, options, "fresh-only");
3048
+ if (secondHit.found) {
3049
+ this.metricsCollector.increment("hits");
3050
+ return secondHit.value;
3051
+ }
3043
3052
  }
3044
3053
  return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3045
3054
  };
@@ -3047,12 +3056,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
3047
3056
  if (!this.options.singleFlightCoordinator) {
3048
3057
  return fetchTask();
3049
3058
  }
3050
- return this.options.singleFlightCoordinator.execute(
3051
- key,
3052
- this.resolveSingleFlightOptions(),
3053
- fetchTask,
3054
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3055
- );
3059
+ try {
3060
+ return await this.options.singleFlightCoordinator.execute(
3061
+ key,
3062
+ this.resolveSingleFlightOptions(),
3063
+ fetchTask,
3064
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3065
+ );
3066
+ } catch (error) {
3067
+ if (!this.isGracefulDegradationEnabled()) {
3068
+ throw error;
3069
+ }
3070
+ this.metricsCollector.increment("degradedOperations");
3071
+ this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
3072
+ this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
3073
+ return fetchTask();
3074
+ }
3056
3075
  };
3057
3076
  if (this.options.stampedePrevention === false) {
3058
3077
  return singleFlightTask();
@@ -4393,6 +4412,7 @@ var RedisLayer = class {
4393
4412
  compression;
4394
4413
  compressionThreshold;
4395
4414
  decompressionMaxBytes;
4415
+ commandTimeoutMs;
4396
4416
  disconnectOnDispose;
4397
4417
  constructor(options) {
4398
4418
  this.client = options.client;
@@ -4405,6 +4425,7 @@ var RedisLayer = class {
4405
4425
  this.compression = options.compression;
4406
4426
  this.compressionThreshold = options.compressionThreshold ?? 1024;
4407
4427
  this.decompressionMaxBytes = options.decompressionMaxBytes ?? 64 * 1024 * 1024;
4428
+ this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
4408
4429
  this.disconnectOnDispose = options.disconnectOnDispose ?? false;
4409
4430
  }
4410
4431
  async get(key) {
@@ -4412,7 +4433,7 @@ var RedisLayer = class {
4412
4433
  return unwrapStoredValue(payload);
4413
4434
  }
4414
4435
  async getEntry(key) {
4415
- const payload = await this.client.getBuffer(this.withPrefix(key));
4436
+ const payload = await this.runCommand(`get("${key}")`, () => this.client.getBuffer(this.withPrefix(key)));
4416
4437
  if (payload === null) {
4417
4438
  return null;
4418
4439
  }
@@ -4426,7 +4447,7 @@ var RedisLayer = class {
4426
4447
  for (const key of keys) {
4427
4448
  pipeline.getBuffer(this.withPrefix(key));
4428
4449
  }
4429
- const results = await pipeline.exec();
4450
+ const results = await this.runCommand(`mget(${keys.length})`, () => pipeline.exec());
4430
4451
  if (results === null) {
4431
4452
  return keys.map(() => null);
4432
4453
  }
@@ -4455,33 +4476,36 @@ var RedisLayer = class {
4455
4476
  pipeline.set(normalizedKey, payload);
4456
4477
  }
4457
4478
  }
4458
- await pipeline.exec();
4479
+ await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
4459
4480
  }
4460
4481
  async set(key, value, ttl = this.defaultTtl) {
4461
4482
  const serialized = this.primarySerializer().serialize(value);
4462
4483
  const payload = await this.encodePayload(serialized);
4463
4484
  const normalizedKey = this.withPrefix(key);
4464
4485
  if (ttl && ttl > 0) {
4465
- await this.client.set(normalizedKey, payload, "EX", ttl);
4486
+ await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload, "EX", ttl));
4466
4487
  return;
4467
4488
  }
4468
- await this.client.set(normalizedKey, payload);
4489
+ await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload));
4469
4490
  }
4470
4491
  async delete(key) {
4471
- await this.client.del(this.withPrefix(key));
4492
+ await this.runCommand(`delete("${key}")`, () => this.client.del(this.withPrefix(key)));
4472
4493
  }
4473
4494
  async deleteMany(keys) {
4474
4495
  if (keys.length === 0) {
4475
4496
  return;
4476
4497
  }
4477
- await this.client.del(...keys.map((key) => this.withPrefix(key)));
4498
+ await this.runCommand(
4499
+ `deleteMany(${keys.length})`,
4500
+ () => this.client.del(...keys.map((key) => this.withPrefix(key)))
4501
+ );
4478
4502
  }
4479
4503
  async has(key) {
4480
- const exists = await this.client.exists(this.withPrefix(key));
4504
+ const exists = await this.runCommand(`has("${key}")`, () => this.client.exists(this.withPrefix(key)));
4481
4505
  return exists > 0;
4482
4506
  }
4483
4507
  async ttl(key) {
4484
- const remaining = await this.client.ttl(this.withPrefix(key));
4508
+ const remaining = await this.runCommand(`ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
4485
4509
  if (remaining < 0) {
4486
4510
  return null;
4487
4511
  }
@@ -4489,13 +4513,16 @@ var RedisLayer = class {
4489
4513
  }
4490
4514
  async size() {
4491
4515
  if (!this.prefix) {
4492
- return this.client.dbsize();
4516
+ return this.runCommand("dbsize()", () => this.client.dbsize());
4493
4517
  }
4494
4518
  const pattern = `${this.prefix}*`;
4495
4519
  let cursor = "0";
4496
4520
  let count = 0;
4497
4521
  do {
4498
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4522
+ const [nextCursor, keys] = await this.runCommand(
4523
+ `scan("${pattern}")`,
4524
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4525
+ );
4499
4526
  cursor = nextCursor;
4500
4527
  count += keys.length;
4501
4528
  } while (cursor !== "0");
@@ -4503,7 +4530,7 @@ var RedisLayer = class {
4503
4530
  }
4504
4531
  async ping() {
4505
4532
  try {
4506
- return await this.client.ping() === "PONG";
4533
+ return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
4507
4534
  } catch {
4508
4535
  return false;
4509
4536
  }
@@ -4526,14 +4553,17 @@ var RedisLayer = class {
4526
4553
  const pattern = `${this.prefix}*`;
4527
4554
  let cursor = "0";
4528
4555
  do {
4529
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4556
+ const [nextCursor, keys] = await this.runCommand(
4557
+ `scan("${pattern}")`,
4558
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4559
+ );
4530
4560
  cursor = nextCursor;
4531
4561
  if (keys.length === 0) {
4532
4562
  continue;
4533
4563
  }
4534
4564
  for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
4535
4565
  const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
4536
- await this.client.del(...batch);
4566
+ await this.runCommand(`clear-del(${batch.length})`, () => this.client.del(...batch));
4537
4567
  }
4538
4568
  } while (cursor !== "0");
4539
4569
  }
@@ -4549,7 +4579,10 @@ var RedisLayer = class {
4549
4579
  const pattern = `${this.prefix}*`;
4550
4580
  let cursor = "0";
4551
4581
  do {
4552
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4582
+ const [nextCursor, keys] = await this.runCommand(
4583
+ `scan("${pattern}")`,
4584
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4585
+ );
4553
4586
  cursor = nextCursor;
4554
4587
  for (const key of keys) {
4555
4588
  await visitor(this.prefix ? key.slice(this.prefix.length) : key);
@@ -4560,7 +4593,10 @@ var RedisLayer = class {
4560
4593
  const matches = [];
4561
4594
  let cursor = "0";
4562
4595
  do {
4563
- const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
4596
+ const [nextCursor, keys] = await this.runCommand(
4597
+ `scan("${pattern}")`,
4598
+ () => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
4599
+ );
4564
4600
  cursor = nextCursor;
4565
4601
  matches.push(...keys);
4566
4602
  } while (cursor !== "0");
@@ -4592,7 +4628,7 @@ var RedisLayer = class {
4592
4628
  }
4593
4629
  async deleteCorruptedKey(key) {
4594
4630
  try {
4595
- await this.client.del(this.withPrefix(key));
4631
+ await this.runCommand(`deleteCorrupted("${key}")`, () => this.client.del(this.withPrefix(key)));
4596
4632
  } catch (deleteError) {
4597
4633
  console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
4598
4634
  }
@@ -4600,12 +4636,15 @@ var RedisLayer = class {
4600
4636
  async rewriteWithPrimarySerializer(key, value) {
4601
4637
  const serialized = this.primarySerializer().serialize(value);
4602
4638
  const payload = await this.encodePayload(serialized);
4603
- const ttl = await this.client.ttl(this.withPrefix(key));
4639
+ const ttl = await this.runCommand(`rewrite-ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
4604
4640
  if (ttl > 0) {
4605
- await this.client.set(this.withPrefix(key), payload, "EX", ttl);
4641
+ await this.runCommand(
4642
+ `rewrite-set("${key}")`,
4643
+ () => this.client.set(this.withPrefix(key), payload, "EX", ttl)
4644
+ );
4606
4645
  return;
4607
4646
  }
4608
- await this.client.set(this.withPrefix(key), payload);
4647
+ await this.runCommand(`rewrite-set("${key}")`, () => this.client.set(this.withPrefix(key), payload));
4609
4648
  }
4610
4649
  primarySerializer() {
4611
4650
  const serializer = this.serializers[0];
@@ -4701,6 +4740,35 @@ var RedisLayer = class {
4701
4740
  source.pipe(decompressor);
4702
4741
  });
4703
4742
  }
4743
+ normalizeCommandTimeoutMs(value) {
4744
+ if (value === void 0) {
4745
+ return void 0;
4746
+ }
4747
+ if (!Number.isFinite(value) || value <= 0) {
4748
+ throw new Error("RedisLayer.commandTimeoutMs must be a positive number.");
4749
+ }
4750
+ return value;
4751
+ }
4752
+ async runCommand(operation, command) {
4753
+ const promise = command();
4754
+ if (!this.commandTimeoutMs) {
4755
+ return promise;
4756
+ }
4757
+ let timer;
4758
+ return Promise.race([
4759
+ promise,
4760
+ new Promise((_, reject) => {
4761
+ timer = setTimeout(() => {
4762
+ reject(new Error(`RedisLayer command ${operation} timed out after ${this.commandTimeoutMs}ms.`));
4763
+ }, this.commandTimeoutMs);
4764
+ timer.unref?.();
4765
+ })
4766
+ ]).finally(() => {
4767
+ if (timer) {
4768
+ clearTimeout(timer);
4769
+ }
4770
+ });
4771
+ }
4704
4772
  };
4705
4773
 
4706
4774
  // src/layers/DiskLayer.ts
@@ -5151,14 +5219,19 @@ return 0
5151
5219
  var RedisSingleFlightCoordinator = class {
5152
5220
  client;
5153
5221
  prefix;
5222
+ commandTimeoutMs;
5154
5223
  constructor(options) {
5155
5224
  this.client = options.client;
5156
5225
  this.prefix = options.prefix ?? "layercache:singleflight";
5226
+ this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
5157
5227
  }
5158
5228
  async execute(key, options, worker, waiter) {
5159
5229
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
5160
5230
  const token = (0, import_node_crypto3.randomUUID)();
5161
- const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
5231
+ const acquired = await this.runCommand(
5232
+ `acquire("${key}")`,
5233
+ () => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
5234
+ );
5162
5235
  if (acquired === "OK") {
5163
5236
  const renewTimer = this.startLeaseRenewal(lockKey, token, options);
5164
5237
  try {
@@ -5167,7 +5240,7 @@ var RedisSingleFlightCoordinator = class {
5167
5240
  if (renewTimer) {
5168
5241
  clearInterval(renewTimer);
5169
5242
  }
5170
- await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
5243
+ await this.runCommand(`release("${key}")`, () => this.client.eval(RELEASE_SCRIPT, 1, lockKey, token));
5171
5244
  }
5172
5245
  }
5173
5246
  return waiter();
@@ -5178,11 +5251,45 @@ var RedisSingleFlightCoordinator = class {
5178
5251
  return void 0;
5179
5252
  }
5180
5253
  const timer = setInterval(() => {
5181
- void this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs)).catch(() => void 0);
5254
+ void this.runCommand(
5255
+ `renew("${lockKey}")`,
5256
+ () => this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs))
5257
+ ).catch(() => void 0);
5182
5258
  }, renewIntervalMs);
5183
5259
  timer.unref?.();
5184
5260
  return timer;
5185
5261
  }
5262
+ normalizeCommandTimeoutMs(value) {
5263
+ if (value === void 0) {
5264
+ return void 0;
5265
+ }
5266
+ if (!Number.isFinite(value) || value <= 0) {
5267
+ throw new Error("RedisSingleFlightCoordinator.commandTimeoutMs must be a positive number.");
5268
+ }
5269
+ return value;
5270
+ }
5271
+ async runCommand(operation, command) {
5272
+ const promise = command();
5273
+ if (!this.commandTimeoutMs) {
5274
+ return promise;
5275
+ }
5276
+ let timer;
5277
+ return Promise.race([
5278
+ promise,
5279
+ new Promise((_, reject) => {
5280
+ timer = setTimeout(() => {
5281
+ reject(
5282
+ new Error(`RedisSingleFlightCoordinator command ${operation} timed out after ${this.commandTimeoutMs}ms.`)
5283
+ );
5284
+ }, this.commandTimeoutMs);
5285
+ timer.unref?.();
5286
+ })
5287
+ ]).finally(() => {
5288
+ if (timer) {
5289
+ clearTimeout(timer);
5290
+ }
5291
+ });
5292
+ }
5186
5293
  };
5187
5294
 
5188
5295
  // src/metrics/PrometheusExporter.ts
package/dist/index.d.cts CHANGED
@@ -203,6 +203,11 @@ interface RedisLayerOptions {
203
203
  * Prevents decompression bomb attacks. Defaults to 64 MiB.
204
204
  */
205
205
  decompressionMaxBytes?: number;
206
+ /**
207
+ * Per-command timeout in milliseconds for Redis round-trips.
208
+ * Slow commands reject so CacheStack can treat the layer as degraded.
209
+ */
210
+ commandTimeoutMs?: number;
206
211
  disconnectOnDispose?: boolean;
207
212
  }
208
213
  declare class RedisLayer implements CacheLayer {
@@ -217,6 +222,7 @@ declare class RedisLayer implements CacheLayer {
217
222
  private readonly compression?;
218
223
  private readonly compressionThreshold;
219
224
  private readonly decompressionMaxBytes;
225
+ private readonly commandTimeoutMs;
220
226
  private readonly disconnectOnDispose;
221
227
  constructor(options: RedisLayerOptions);
222
228
  get<T>(key: string): Promise<T | null>;
@@ -256,6 +262,8 @@ declare class RedisLayer implements CacheLayer {
256
262
  */
257
263
  private decodePayload;
258
264
  private decompressWithLimit;
265
+ private normalizeCommandTimeoutMs;
266
+ private runCommand;
259
267
  }
260
268
 
261
269
  interface DiskLayerOptions {
@@ -408,19 +416,23 @@ declare class MsgpackSerializer implements CacheSerializer {
408
416
  interface RedisSingleFlightCoordinatorOptions {
409
417
  client: Redis;
410
418
  prefix?: string;
419
+ commandTimeoutMs?: number;
411
420
  }
412
421
  declare class RedisSingleFlightCoordinator implements CacheSingleFlightCoordinator {
413
422
  private readonly client;
414
423
  private readonly prefix;
424
+ private readonly commandTimeoutMs;
415
425
  constructor(options: RedisSingleFlightCoordinatorOptions);
416
426
  execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
417
427
  private startLeaseRenewal;
428
+ private normalizeCommandTimeoutMs;
429
+ private runCommand;
418
430
  }
419
431
 
420
432
  declare class StampedeGuard {
421
- private readonly mutexes;
433
+ private readonly inFlight;
422
434
  execute<T>(key: string, task: () => Promise<T>): Promise<T>;
423
- private getMutexEntry;
435
+ private releaseEntry;
424
436
  }
425
437
 
426
438
  /**
package/dist/index.d.ts CHANGED
@@ -203,6 +203,11 @@ interface RedisLayerOptions {
203
203
  * Prevents decompression bomb attacks. Defaults to 64 MiB.
204
204
  */
205
205
  decompressionMaxBytes?: number;
206
+ /**
207
+ * Per-command timeout in milliseconds for Redis round-trips.
208
+ * Slow commands reject so CacheStack can treat the layer as degraded.
209
+ */
210
+ commandTimeoutMs?: number;
206
211
  disconnectOnDispose?: boolean;
207
212
  }
208
213
  declare class RedisLayer implements CacheLayer {
@@ -217,6 +222,7 @@ declare class RedisLayer implements CacheLayer {
217
222
  private readonly compression?;
218
223
  private readonly compressionThreshold;
219
224
  private readonly decompressionMaxBytes;
225
+ private readonly commandTimeoutMs;
220
226
  private readonly disconnectOnDispose;
221
227
  constructor(options: RedisLayerOptions);
222
228
  get<T>(key: string): Promise<T | null>;
@@ -256,6 +262,8 @@ declare class RedisLayer implements CacheLayer {
256
262
  */
257
263
  private decodePayload;
258
264
  private decompressWithLimit;
265
+ private normalizeCommandTimeoutMs;
266
+ private runCommand;
259
267
  }
260
268
 
261
269
  interface DiskLayerOptions {
@@ -408,19 +416,23 @@ declare class MsgpackSerializer implements CacheSerializer {
408
416
  interface RedisSingleFlightCoordinatorOptions {
409
417
  client: Redis;
410
418
  prefix?: string;
419
+ commandTimeoutMs?: number;
411
420
  }
412
421
  declare class RedisSingleFlightCoordinator implements CacheSingleFlightCoordinator {
413
422
  private readonly client;
414
423
  private readonly prefix;
424
+ private readonly commandTimeoutMs;
415
425
  constructor(options: RedisSingleFlightCoordinatorOptions);
416
426
  execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
417
427
  private startLeaseRenewal;
428
+ private normalizeCommandTimeoutMs;
429
+ private runCommand;
418
430
  }
419
431
 
420
432
  declare class StampedeGuard {
421
- private readonly mutexes;
433
+ private readonly inFlight;
422
434
  execute<T>(key: string, task: () => Promise<T>): Promise<T>;
423
- private getMutexEntry;
435
+ private releaseEntry;
424
436
  }
425
437
 
426
438
  /**