@netlify/plugin-nextjs 5.14.0 → 5.14.2
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/build/templates/handler-monorepo.tmpl.js +4 -4
- package/dist/build/templates/handler.tmpl.js +4 -4
- package/dist/run/handlers/cache.cjs +13 -13
- package/dist/run/handlers/server.js +7 -7
- package/dist/run/handlers/tags-handler.cjs +14 -2
- package/dist/run/handlers/tracer.cjs +728 -641
- package/dist/run/handlers/use-cache-handler.js +50 -24
- package/dist/run/next.cjs +1 -2
- package/dist/run/storage/regional-blob-store.cjs +87 -43
- package/dist/run/storage/storage.cjs +5 -5
- package/package.json +1 -1
|
@@ -1385,7 +1385,7 @@ import {
|
|
|
1385
1385
|
isAnyTagStaleOrExpired,
|
|
1386
1386
|
markTagsAsStaleAndPurgeEdgeCache
|
|
1387
1387
|
} from "./tags-handler.cjs";
|
|
1388
|
-
import { getTracer } from "./tracer.cjs";
|
|
1388
|
+
import { getTracer, withActiveSpan } from "./tracer.cjs";
|
|
1389
1389
|
var LRU_CACHE_GLOBAL_KEY = Symbol.for("nf-use-cache-handler-lru-cache");
|
|
1390
1390
|
var PENDING_SETS_GLOBAL_KEY = Symbol.for("nf-use-cache-handler-pending-sets");
|
|
1391
1391
|
var cacheHandlersSymbol = Symbol.for("@next/cache-handlers");
|
|
@@ -1415,11 +1415,12 @@ var tmpResolvePendingBeforeCreatingAPromise = () => {
|
|
|
1415
1415
|
};
|
|
1416
1416
|
var NetlifyDefaultUseCacheHandler = {
|
|
1417
1417
|
get(cacheKey) {
|
|
1418
|
-
return
|
|
1418
|
+
return withActiveSpan(
|
|
1419
|
+
getTracer(),
|
|
1419
1420
|
"DefaultUseCacheHandler.get",
|
|
1420
1421
|
async (span) => {
|
|
1421
1422
|
getLogger().withFields({ cacheKey }).debug(`[NetlifyDefaultUseCacheHandler] get`);
|
|
1422
|
-
span
|
|
1423
|
+
span?.setAttributes({
|
|
1423
1424
|
cacheKey
|
|
1424
1425
|
});
|
|
1425
1426
|
const pendingPromise = getPendingSets().get(cacheKey);
|
|
@@ -1429,7 +1430,7 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1429
1430
|
const privateEntry = getLRUCache().get(cacheKey);
|
|
1430
1431
|
if (!privateEntry) {
|
|
1431
1432
|
getLogger().withFields({ cacheKey, status: "MISS" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1432
|
-
span
|
|
1433
|
+
span?.setAttributes({
|
|
1433
1434
|
cacheStatus: "miss"
|
|
1434
1435
|
});
|
|
1435
1436
|
return void 0;
|
|
@@ -1438,41 +1439,47 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1438
1439
|
const ttl = (entry.timestamp + entry.revalidate * 1e3 - Date.now()) / 1e3;
|
|
1439
1440
|
if (ttl < 0) {
|
|
1440
1441
|
getLogger().withFields({ cacheKey, ttl, status: "STALE" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1441
|
-
span
|
|
1442
|
+
span?.setAttributes({
|
|
1442
1443
|
cacheStatus: "expired, discarded",
|
|
1443
1444
|
ttl
|
|
1444
1445
|
});
|
|
1445
1446
|
return void 0;
|
|
1446
1447
|
}
|
|
1447
|
-
const { stale } = await isAnyTagStaleOrExpired(entry.tags, entry.timestamp);
|
|
1448
|
-
if (
|
|
1449
|
-
getLogger().withFields({ cacheKey, ttl, status: "
|
|
1450
|
-
span
|
|
1451
|
-
cacheStatus: "
|
|
1448
|
+
const { stale, expired } = await isAnyTagStaleOrExpired(entry.tags, entry.timestamp);
|
|
1449
|
+
if (expired) {
|
|
1450
|
+
getLogger().withFields({ cacheKey, ttl, status: "EXPIRED BY TAG" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1451
|
+
span?.setAttributes({
|
|
1452
|
+
cacheStatus: "expired tag, discarded",
|
|
1452
1453
|
ttl
|
|
1453
1454
|
});
|
|
1454
1455
|
return void 0;
|
|
1455
1456
|
}
|
|
1456
|
-
|
|
1457
|
+
let { revalidate, value } = entry;
|
|
1458
|
+
if (stale) {
|
|
1459
|
+
revalidate = -1;
|
|
1460
|
+
}
|
|
1461
|
+
const [returnStream, newSaved] = value.tee();
|
|
1457
1462
|
entry.value = newSaved;
|
|
1458
|
-
getLogger().withFields({ cacheKey, ttl, status: "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1459
|
-
span
|
|
1460
|
-
cacheStatus: "hit",
|
|
1463
|
+
getLogger().withFields({ cacheKey, ttl, status: stale ? "STALE" : "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1464
|
+
span?.setAttributes({
|
|
1465
|
+
cacheStatus: stale ? "stale" : "hit",
|
|
1461
1466
|
ttl
|
|
1462
1467
|
});
|
|
1463
1468
|
return {
|
|
1464
1469
|
...entry,
|
|
1470
|
+
revalidate,
|
|
1465
1471
|
value: returnStream
|
|
1466
1472
|
};
|
|
1467
1473
|
}
|
|
1468
1474
|
);
|
|
1469
1475
|
},
|
|
1470
1476
|
set(cacheKey, pendingEntry) {
|
|
1471
|
-
return
|
|
1477
|
+
return withActiveSpan(
|
|
1478
|
+
getTracer(),
|
|
1472
1479
|
"DefaultUseCacheHandler.set",
|
|
1473
1480
|
async (span) => {
|
|
1474
1481
|
getLogger().withFields({ cacheKey }).debug(`[NetlifyDefaultUseCacheHandler]: set`);
|
|
1475
|
-
span
|
|
1482
|
+
span?.setAttributes({
|
|
1476
1483
|
cacheKey
|
|
1477
1484
|
});
|
|
1478
1485
|
let resolvePending = tmpResolvePendingBeforeCreatingAPromise;
|
|
@@ -1482,7 +1489,7 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1482
1489
|
const pendingSets = getPendingSets();
|
|
1483
1490
|
pendingSets.set(cacheKey, pendingPromise);
|
|
1484
1491
|
const entry = await pendingEntry;
|
|
1485
|
-
span
|
|
1492
|
+
span?.setAttributes({
|
|
1486
1493
|
cacheKey
|
|
1487
1494
|
});
|
|
1488
1495
|
let size = 0;
|
|
@@ -1493,7 +1500,7 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1493
1500
|
for (let chunk; !(chunk = await reader.read()).done; ) {
|
|
1494
1501
|
size += Buffer.from(chunk.value).byteLength;
|
|
1495
1502
|
}
|
|
1496
|
-
span
|
|
1503
|
+
span?.setAttributes({
|
|
1497
1504
|
tags: entry.tags,
|
|
1498
1505
|
timestamp: entry.timestamp,
|
|
1499
1506
|
revalidate: entry.revalidate,
|
|
@@ -1514,33 +1521,52 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1514
1521
|
},
|
|
1515
1522
|
async refreshTags() {
|
|
1516
1523
|
},
|
|
1517
|
-
getExpiration: function(...
|
|
1518
|
-
return
|
|
1524
|
+
getExpiration: function(...notNormalizedTags) {
|
|
1525
|
+
return withActiveSpan(
|
|
1526
|
+
getTracer(),
|
|
1519
1527
|
"DefaultUseCacheHandler.getExpiration",
|
|
1520
1528
|
async (span) => {
|
|
1521
|
-
|
|
1529
|
+
const tags = notNormalizedTags.flat();
|
|
1530
|
+
span?.setAttributes({
|
|
1522
1531
|
tags
|
|
1523
1532
|
});
|
|
1524
1533
|
const expiration = await getMostRecentTagExpirationTimestamp(tags);
|
|
1525
1534
|
getLogger().withFields({ tags, expiration }).debug(`[NetlifyDefaultUseCacheHandler] getExpiration`);
|
|
1526
|
-
span
|
|
1535
|
+
span?.setAttributes({
|
|
1527
1536
|
expiration
|
|
1528
1537
|
});
|
|
1529
1538
|
return expiration;
|
|
1530
1539
|
}
|
|
1531
1540
|
);
|
|
1532
1541
|
},
|
|
1542
|
+
// this is for CacheHandlerV2
|
|
1533
1543
|
expireTags(...tags) {
|
|
1534
|
-
return
|
|
1544
|
+
return withActiveSpan(
|
|
1545
|
+
getTracer(),
|
|
1535
1546
|
"DefaultUseCacheHandler.expireTags",
|
|
1536
1547
|
async (span) => {
|
|
1537
1548
|
getLogger().withFields({ tags }).debug(`[NetlifyDefaultUseCacheHandler] expireTags`);
|
|
1538
|
-
span
|
|
1549
|
+
span?.setAttributes({
|
|
1539
1550
|
tags
|
|
1540
1551
|
});
|
|
1541
1552
|
await markTagsAsStaleAndPurgeEdgeCache(tags);
|
|
1542
1553
|
}
|
|
1543
1554
|
);
|
|
1555
|
+
},
|
|
1556
|
+
// this is for CacheHandlerV3 / Next 16
|
|
1557
|
+
updateTags(tags, durations) {
|
|
1558
|
+
return withActiveSpan(
|
|
1559
|
+
getTracer(),
|
|
1560
|
+
"DefaultUseCacheHandler.updateTags",
|
|
1561
|
+
async (span) => {
|
|
1562
|
+
getLogger().withFields({ tags, durations }).debug(`[NetlifyDefaultUseCacheHandler] updateTags`);
|
|
1563
|
+
span?.setAttributes({
|
|
1564
|
+
tags,
|
|
1565
|
+
durations: JSON.stringify(durations)
|
|
1566
|
+
});
|
|
1567
|
+
await markTagsAsStaleAndPurgeEdgeCache(tags, durations);
|
|
1568
|
+
}
|
|
1569
|
+
);
|
|
1544
1570
|
}
|
|
1545
1571
|
};
|
|
1546
1572
|
function configureUseCacheHandlers() {
|
package/dist/run/next.cjs
CHANGED
|
@@ -545,8 +545,7 @@ ResponseCache.prototype.get = function get(...getArgs) {
|
|
|
545
545
|
async function getMockedRequestHandler(nextConfig, ...args) {
|
|
546
546
|
const initContext = { initializingServer: true };
|
|
547
547
|
const initAsyncLocalStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
548
|
-
|
|
549
|
-
return tracer.withActiveSpan("mocked request handler", async () => {
|
|
548
|
+
return (0, import_tracer.withActiveSpan)((0, import_tracer.getTracer)(), "mocked request handler", async () => {
|
|
550
549
|
const ofs = { ...import_promises.default };
|
|
551
550
|
async function readFileFallbackBlobStore(...fsargs) {
|
|
552
551
|
const [path, options] = fsargs;
|
|
@@ -25,40 +25,12 @@ __export(regional_blob_store_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(regional_blob_store_exports);
|
|
27
27
|
|
|
28
|
-
// node_modules/@netlify/blobs/dist/
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (res.headers.has(NF_REQUEST_ID)) {
|
|
35
|
-
details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`;
|
|
36
|
-
}
|
|
37
|
-
super(`Netlify Blobs has generated an internal error (${details})`);
|
|
38
|
-
this.name = "BlobsInternalError";
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
var collectIterator = async (iterator) => {
|
|
42
|
-
const result = [];
|
|
43
|
-
for await (const item of iterator) {
|
|
44
|
-
result.push(item);
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
};
|
|
48
|
-
var base64Decode = (input) => {
|
|
49
|
-
const { Buffer: Buffer2 } = globalThis;
|
|
50
|
-
if (Buffer2) {
|
|
51
|
-
return Buffer2.from(input, "base64").toString();
|
|
52
|
-
}
|
|
53
|
-
return atob(input);
|
|
54
|
-
};
|
|
55
|
-
var base64Encode = (input) => {
|
|
56
|
-
const { Buffer: Buffer2 } = globalThis;
|
|
57
|
-
if (Buffer2) {
|
|
58
|
-
return Buffer2.from(input).toString("base64");
|
|
59
|
-
}
|
|
60
|
-
return btoa(input);
|
|
61
|
-
};
|
|
28
|
+
// node_modules/@netlify/blobs/node_modules/@netlify/runtime-utils/dist/main.js
|
|
29
|
+
var getString = (input) => typeof input === "string" ? input : JSON.stringify(input);
|
|
30
|
+
var base64Decode = globalThis.Buffer ? (input) => Buffer.from(input, "base64").toString() : (input) => atob(input);
|
|
31
|
+
var base64Encode = globalThis.Buffer ? (input) => Buffer.from(getString(input)).toString("base64") : (input) => btoa(getString(input));
|
|
32
|
+
|
|
33
|
+
// node_modules/@netlify/blobs/dist/chunk-HN33TXZT.js
|
|
62
34
|
var getEnvironment = () => {
|
|
63
35
|
const { Deno, Netlify, process: process2 } = globalThis;
|
|
64
36
|
return Netlify?.env ?? Deno?.env ?? {
|
|
@@ -111,7 +83,7 @@ var encodeMetadata = (metadata) => {
|
|
|
111
83
|
return payload;
|
|
112
84
|
};
|
|
113
85
|
var decodeMetadata = (header) => {
|
|
114
|
-
if (!header
|
|
86
|
+
if (!header?.startsWith(BASE64_PREFIX)) {
|
|
115
87
|
return {};
|
|
116
88
|
}
|
|
117
89
|
const encodedData = header.slice(BASE64_PREFIX.length);
|
|
@@ -132,6 +104,25 @@ var getMetadataFromResponse = (response) => {
|
|
|
132
104
|
);
|
|
133
105
|
}
|
|
134
106
|
};
|
|
107
|
+
var NF_ERROR = "x-nf-error";
|
|
108
|
+
var NF_REQUEST_ID = "x-nf-request-id";
|
|
109
|
+
var BlobsInternalError = class extends Error {
|
|
110
|
+
constructor(res) {
|
|
111
|
+
let details = res.headers.get(NF_ERROR) || `${res.status} status code`;
|
|
112
|
+
if (res.headers.has(NF_REQUEST_ID)) {
|
|
113
|
+
details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`;
|
|
114
|
+
}
|
|
115
|
+
super(`Netlify Blobs has generated an internal error (${details})`);
|
|
116
|
+
this.name = "BlobsInternalError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var collectIterator = async (iterator) => {
|
|
120
|
+
const result = [];
|
|
121
|
+
for await (const item of iterator) {
|
|
122
|
+
result.push(item);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
135
126
|
var BlobsConsistencyError = class extends Error {
|
|
136
127
|
constructor() {
|
|
137
128
|
super(
|
|
@@ -283,6 +274,7 @@ var Client = class {
|
|
|
283
274
|
}
|
|
284
275
|
async makeRequest({
|
|
285
276
|
body,
|
|
277
|
+
conditions = {},
|
|
286
278
|
consistency,
|
|
287
279
|
headers: extraHeaders,
|
|
288
280
|
key,
|
|
@@ -306,6 +298,11 @@ var Client = class {
|
|
|
306
298
|
if (method === "put") {
|
|
307
299
|
headers["cache-control"] = "max-age=0, stale-while-revalidate=60";
|
|
308
300
|
}
|
|
301
|
+
if ("onlyIfMatch" in conditions && conditions.onlyIfMatch) {
|
|
302
|
+
headers["if-match"] = conditions.onlyIfMatch;
|
|
303
|
+
} else if ("onlyIfNew" in conditions && conditions.onlyIfNew) {
|
|
304
|
+
headers["if-none-match"] = "*";
|
|
305
|
+
}
|
|
309
306
|
const options = {
|
|
310
307
|
body,
|
|
311
308
|
headers,
|
|
@@ -344,6 +341,8 @@ var getClientOptions = (options, contextOverride) => {
|
|
|
344
341
|
var DEPLOY_STORE_PREFIX = "deploy:";
|
|
345
342
|
var LEGACY_STORE_INTERNAL_PREFIX = "netlify-internal/legacy-namespace/";
|
|
346
343
|
var SITE_STORE_PREFIX = "site:";
|
|
344
|
+
var STATUS_OK = 200;
|
|
345
|
+
var STATUS_PRE_CONDITION_FAILED = 412;
|
|
347
346
|
var Store = class _Store {
|
|
348
347
|
constructor(options) {
|
|
349
348
|
this.client = options.client;
|
|
@@ -468,36 +467,56 @@ var Store = class _Store {
|
|
|
468
467
|
)
|
|
469
468
|
);
|
|
470
469
|
}
|
|
471
|
-
async set(key, data,
|
|
470
|
+
async set(key, data, options = {}) {
|
|
472
471
|
_Store.validateKey(key);
|
|
472
|
+
const conditions = _Store.getConditions(options);
|
|
473
473
|
const res = await this.client.makeRequest({
|
|
474
|
+
conditions,
|
|
474
475
|
body: data,
|
|
475
476
|
key,
|
|
476
|
-
metadata,
|
|
477
|
+
metadata: options.metadata,
|
|
477
478
|
method: "put",
|
|
478
479
|
storeName: this.name
|
|
479
480
|
});
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
const etag = res.headers.get("etag") ?? "";
|
|
482
|
+
if (conditions) {
|
|
483
|
+
return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true };
|
|
484
|
+
}
|
|
485
|
+
if (res.status === STATUS_OK) {
|
|
486
|
+
return {
|
|
487
|
+
etag,
|
|
488
|
+
modified: true
|
|
489
|
+
};
|
|
482
490
|
}
|
|
491
|
+
throw new BlobsInternalError(res);
|
|
483
492
|
}
|
|
484
|
-
async setJSON(key, data,
|
|
493
|
+
async setJSON(key, data, options = {}) {
|
|
485
494
|
_Store.validateKey(key);
|
|
495
|
+
const conditions = _Store.getConditions(options);
|
|
486
496
|
const payload = JSON.stringify(data);
|
|
487
497
|
const headers = {
|
|
488
498
|
"content-type": "application/json"
|
|
489
499
|
};
|
|
490
500
|
const res = await this.client.makeRequest({
|
|
501
|
+
...conditions,
|
|
491
502
|
body: payload,
|
|
492
503
|
headers,
|
|
493
504
|
key,
|
|
494
|
-
metadata,
|
|
505
|
+
metadata: options.metadata,
|
|
495
506
|
method: "put",
|
|
496
507
|
storeName: this.name
|
|
497
508
|
});
|
|
498
|
-
|
|
499
|
-
|
|
509
|
+
const etag = res.headers.get("etag") ?? "";
|
|
510
|
+
if (conditions) {
|
|
511
|
+
return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true };
|
|
500
512
|
}
|
|
513
|
+
if (res.status === STATUS_OK) {
|
|
514
|
+
return {
|
|
515
|
+
etag,
|
|
516
|
+
modified: true
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
throw new BlobsInternalError(res);
|
|
501
520
|
}
|
|
502
521
|
static formatListResultBlob(result) {
|
|
503
522
|
if (!result.key) {
|
|
@@ -508,6 +527,31 @@ var Store = class _Store {
|
|
|
508
527
|
key: result.key
|
|
509
528
|
};
|
|
510
529
|
}
|
|
530
|
+
static getConditions(options) {
|
|
531
|
+
if ("onlyIfMatch" in options && "onlyIfNew" in options) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`The 'onlyIfMatch' and 'onlyIfNew' options are mutually exclusive. Using 'onlyIfMatch' will make the write succeed only if there is an entry for the key with the given content, while 'onlyIfNew' will make the write succeed only if there is no entry for the key.`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if ("onlyIfMatch" in options && options.onlyIfMatch) {
|
|
537
|
+
if (typeof options.onlyIfMatch !== "string") {
|
|
538
|
+
throw new Error(`The 'onlyIfMatch' property expects a string representing an ETag.`);
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
onlyIfMatch: options.onlyIfMatch
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if ("onlyIfNew" in options && options.onlyIfNew) {
|
|
545
|
+
if (typeof options.onlyIfNew !== "boolean") {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`The 'onlyIfNew' property expects a boolean indicating whether the write should fail if an entry for the key already exists.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
onlyIfNew: true
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
511
555
|
static validateKey(key) {
|
|
512
556
|
if (key === "") {
|
|
513
557
|
throw new Error("Blob key must not be empty.");
|
|
@@ -55,11 +55,11 @@ var getMemoizedKeyValueStoreBackedByRegionalBlobStore = (...args) => {
|
|
|
55
55
|
return memoizedValue;
|
|
56
56
|
}
|
|
57
57
|
const blobKey = await encodeBlobKey(key);
|
|
58
|
-
const getPromise =
|
|
59
|
-
span
|
|
58
|
+
const getPromise = (0, import_tracer.withActiveSpan)(tracer, otelSpanTitle, async (span) => {
|
|
59
|
+
span?.setAttributes({ key, blobKey });
|
|
60
60
|
const blob = await store.get(blobKey, { type: "json" });
|
|
61
61
|
inMemoryCache.set(key, blob);
|
|
62
|
-
span
|
|
62
|
+
span?.addEvent(blob ? "Hit" : "Miss");
|
|
63
63
|
return blob;
|
|
64
64
|
});
|
|
65
65
|
inMemoryCache.set(key, getPromise);
|
|
@@ -69,8 +69,8 @@ var getMemoizedKeyValueStoreBackedByRegionalBlobStore = (...args) => {
|
|
|
69
69
|
const inMemoryCache = (0, import_request_scoped_in_memory_cache.getRequestScopedInMemoryCache)();
|
|
70
70
|
inMemoryCache.set(key, value);
|
|
71
71
|
const blobKey = await encodeBlobKey(key);
|
|
72
|
-
return
|
|
73
|
-
span
|
|
72
|
+
return (0, import_tracer.withActiveSpan)(tracer, otelSpanTitle, async (span) => {
|
|
73
|
+
span?.setAttributes({ key, blobKey });
|
|
74
74
|
return await store.setJSON(blobKey, value);
|
|
75
75
|
});
|
|
76
76
|
}
|