@henrychong-ai/mcp-neo4j-knowledge-graph 2.4.1 → 2.6.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 +177 -0
- package/dist/KnowledgeGraphManager.d.ts +18 -0
- package/dist/KnowledgeGraphManager.js +52 -1
- package/dist/KnowledgeGraphManager.js.map +1 -1
- package/dist/cli/generate-embeddings.js +12 -0
- package/dist/cli/generate-embeddings.js.map +1 -1
- package/dist/embeddings/EmbeddingJobManager.js +4 -26
- package/dist/embeddings/EmbeddingJobManager.js.map +1 -1
- package/dist/embeddings/EmbeddingServiceFactory.d.ts +35 -0
- package/dist/embeddings/EmbeddingServiceFactory.js +122 -18
- package/dist/embeddings/EmbeddingServiceFactory.js.map +1 -1
- package/dist/embeddings/OpenAIEmbeddingService.d.ts +6 -0
- package/dist/embeddings/OpenAIEmbeddingService.js +1 -1
- package/dist/embeddings/OpenAIEmbeddingService.js.map +1 -1
- package/dist/embeddings/entityText.d.ts +12 -0
- package/dist/embeddings/entityText.js +39 -0
- package/dist/embeddings/entityText.js.map +1 -0
- package/dist/index.js +65 -53
- package/dist/index.js.map +1 -1
- package/dist/retrieval/RerankerService.d.ts +57 -0
- package/dist/retrieval/RerankerService.js +117 -0
- package/dist/retrieval/RerankerService.js.map +1 -0
- package/dist/server/setup.d.ts +10 -0
- package/dist/server/setup.js +16 -1
- package/dist/server/setup.js.map +1 -1
- package/dist/storage/neo4j/Neo4jStorageProvider.d.ts +10 -0
- package/dist/storage/neo4j/Neo4jStorageProvider.js +56 -9
- package/dist/storage/neo4j/Neo4jStorageProvider.js.map +1 -1
- package/example.env +61 -0
- package/package.json +16 -14
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
function intEnv(name, fallback) {
|
|
4
|
+
const raw = process.env[name];
|
|
5
|
+
if (!raw)
|
|
6
|
+
return fallback;
|
|
7
|
+
const parsed = Number.parseInt(raw, 10);
|
|
8
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Thin client for a cross-encoder reranker (default: Cloudflare Workers AI
|
|
12
|
+
* `@cf/baai/bge-reranker-base`). OpenAI-style/native HTTP; reuses `axios`.
|
|
13
|
+
*
|
|
14
|
+
* The service itself MAY throw (transport/parse errors) — callers are expected
|
|
15
|
+
* to fail open (return the pre-rerank order). It never enables itself without a
|
|
16
|
+
* resolved endpoint + API key.
|
|
17
|
+
*/
|
|
18
|
+
export class RerankerService {
|
|
19
|
+
cfg;
|
|
20
|
+
constructor(cfg) {
|
|
21
|
+
this.cfg = cfg;
|
|
22
|
+
}
|
|
23
|
+
get enabled() {
|
|
24
|
+
return this.cfg.enabled;
|
|
25
|
+
}
|
|
26
|
+
get topN() {
|
|
27
|
+
return this.cfg.topN;
|
|
28
|
+
}
|
|
29
|
+
get topK() {
|
|
30
|
+
return this.cfg.topK;
|
|
31
|
+
}
|
|
32
|
+
get model() {
|
|
33
|
+
return this.cfg.model;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build a reranker from environment variables. Returns a DISABLED instance
|
|
37
|
+
* unless `RERANK_ENABLED=true` AND an endpoint + API key resolve.
|
|
38
|
+
*
|
|
39
|
+
* Env: RERANK_ENABLED, RERANK_MODEL (default @cf/baai/bge-reranker-base),
|
|
40
|
+
* RERANK_ENDPOINT (or derived from RERANK_ACCOUNT_ID/CF_ACCOUNT_ID + model),
|
|
41
|
+
* RERANK_API_KEY (falls back to EMBEDDING_API_KEY), RERANK_TOP_N (20),
|
|
42
|
+
* RERANK_TOP_K (10), RERANK_MAX_PASSAGE_CHARS (2000), RERANK_TIMEOUT_MS (5000).
|
|
43
|
+
*/
|
|
44
|
+
static fromEnv() {
|
|
45
|
+
const requested = process.env.RERANK_ENABLED === 'true';
|
|
46
|
+
const model = process.env.RERANK_MODEL || '@cf/baai/bge-reranker-base';
|
|
47
|
+
const apiKey = process.env.RERANK_API_KEY || process.env.EMBEDDING_API_KEY || '';
|
|
48
|
+
let endpoint = process.env.RERANK_ENDPOINT || '';
|
|
49
|
+
const accountId = process.env.RERANK_ACCOUNT_ID || process.env.CF_ACCOUNT_ID;
|
|
50
|
+
if (!endpoint && accountId) {
|
|
51
|
+
endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`;
|
|
52
|
+
}
|
|
53
|
+
const enabled = requested && endpoint.length > 0 && apiKey.length > 0;
|
|
54
|
+
if (requested && !enabled) {
|
|
55
|
+
logger.warn('RerankerService: RERANK_ENABLED=true but endpoint or API key could not be resolved — ' +
|
|
56
|
+
'reranking DISABLED (fail-open). Set RERANK_ENDPOINT (or RERANK_ACCOUNT_ID) and RERANK_API_KEY.');
|
|
57
|
+
}
|
|
58
|
+
else if (enabled) {
|
|
59
|
+
logger.info(`RerankerService: enabled (model ${model})`);
|
|
60
|
+
}
|
|
61
|
+
return new RerankerService({
|
|
62
|
+
enabled,
|
|
63
|
+
endpoint,
|
|
64
|
+
model,
|
|
65
|
+
apiKey,
|
|
66
|
+
topN: intEnv('RERANK_TOP_N', 20),
|
|
67
|
+
topK: intEnv('RERANK_TOP_K', 10),
|
|
68
|
+
maxPassageChars: intEnv('RERANK_MAX_PASSAGE_CHARS', 2000),
|
|
69
|
+
timeoutMs: intEnv('RERANK_TIMEOUT_MS', 5000),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rerank `passages` against `query`. Returns candidate indices reordered
|
|
74
|
+
* descending by relevance, length <= topK. THROWS on transport/parse error —
|
|
75
|
+
* the caller fail-opens.
|
|
76
|
+
*
|
|
77
|
+
* @param query - Search query
|
|
78
|
+
* @param passages - Candidate passages (index-aligned with the caller's results)
|
|
79
|
+
* @returns Indices into `passages`, best-first
|
|
80
|
+
*/
|
|
81
|
+
async rerank(query, passages) {
|
|
82
|
+
const contexts = passages
|
|
83
|
+
.slice(0, this.cfg.topN)
|
|
84
|
+
.map(text => ({ text: (text || '').slice(0, this.cfg.maxPassageChars) }));
|
|
85
|
+
const response = await axios.post(this.cfg.endpoint, { query: query.slice(0, this.cfg.maxPassageChars), contexts }, {
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${this.cfg.apiKey}`,
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
},
|
|
90
|
+
timeout: this.cfg.timeoutMs,
|
|
91
|
+
});
|
|
92
|
+
const ranked = response.data?.result?.response;
|
|
93
|
+
if (!Array.isArray(ranked)) {
|
|
94
|
+
throw new Error('Reranker returned a malformed response (missing result.response array)');
|
|
95
|
+
}
|
|
96
|
+
// Defensive: keep only integer, in-range, UNIQUE ids (CF returns request-array indices).
|
|
97
|
+
// Fractional / duplicate / out-of-range ids are dropped so a malformed-but-arrayed response
|
|
98
|
+
// can't corrupt ordering or duplicate results — the caller still fully fail-opens on a throw.
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const indices = [];
|
|
101
|
+
for (const row of ranked) {
|
|
102
|
+
const id = row?.id;
|
|
103
|
+
if (typeof id === 'number' &&
|
|
104
|
+
Number.isInteger(id) &&
|
|
105
|
+
id >= 0 &&
|
|
106
|
+
id < contexts.length &&
|
|
107
|
+
!seen.has(id)) {
|
|
108
|
+
seen.add(id);
|
|
109
|
+
indices.push(id);
|
|
110
|
+
if (indices.length >= this.cfg.topK)
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return indices;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=RerankerService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RerankerService.js","sourceRoot":"","sources":["../../src/retrieval/RerankerService.ts"],"names":[],"mappings":"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"}
|
package/dist/server/setup.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the package version from package.json at runtime — the single source
|
|
4
|
+
* of truth (no hardcoded literal to drift). Runtime read (not a static JSON
|
|
5
|
+
* import) because tsconfig rootDir=src excludes package.json from the build;
|
|
6
|
+
* `../../package.json` resolves identically from src/server/ (dev) and
|
|
7
|
+
* dist/server/ (published package).
|
|
8
|
+
*
|
|
9
|
+
* @returns The package.json version string
|
|
10
|
+
*/
|
|
11
|
+
export declare function getPackageVersion(): string;
|
|
2
12
|
/**
|
|
3
13
|
* Sets up and configures the MCP server with the appropriate request handlers.
|
|
4
14
|
*
|
package/dist/server/setup.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import { handleCallToolRequest } from './handlers/callToolHandler.js';
|
|
4
5
|
import { handleListToolsRequest } from './handlers/listToolsHandler.js';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the package version from package.json at runtime — the single source
|
|
9
|
+
* of truth (no hardcoded literal to drift). Runtime read (not a static JSON
|
|
10
|
+
* import) because tsconfig rootDir=src excludes package.json from the build;
|
|
11
|
+
* `../../package.json` resolves identically from src/server/ (dev) and
|
|
12
|
+
* dist/server/ (published package).
|
|
13
|
+
*
|
|
14
|
+
* @returns The package.json version string
|
|
15
|
+
*/
|
|
16
|
+
export function getPackageVersion() {
|
|
17
|
+
const { version } = require('../../package.json');
|
|
18
|
+
return version;
|
|
19
|
+
}
|
|
5
20
|
/**
|
|
6
21
|
* Sets up and configures the MCP server with the appropriate request handlers.
|
|
7
22
|
*
|
|
@@ -13,7 +28,7 @@ export function setupServer(knowledgeGraphManager) {
|
|
|
13
28
|
// Create server instance
|
|
14
29
|
const server = new Server({
|
|
15
30
|
name: 'mcp-neo4j-knowledge-graph',
|
|
16
|
-
version:
|
|
31
|
+
version: getPackageVersion(),
|
|
17
32
|
}, {
|
|
18
33
|
capabilities: {
|
|
19
34
|
tools: {},
|
package/dist/server/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/server/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE;;;;;GAKG;AACH,8DAA8D;AAC9D,MAAM,UAAU,WAAW,CAAC,qBAA0B;IACpD,yBAAyB;IACzB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB;QACE,IAAI,EAAE,2BAA2B;QACjC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/server/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,oBAAoB,CAAwB,CAAC;IACzE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,8DAA8D;AAC9D,MAAM,UAAU,WAAW,CAAC,qBAA0B;IACpD,yBAAyB;IACzB,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB;QACE,IAAI,EAAE,2BAA2B;QACjC,OAAO,EAAE,iBAAiB,EAAE;KAC7B,EACD;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,EAAE;SACZ;KACF,CACF,CAAC;IAEF,4BAA4B;IAC5B,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,EAAC,QAAQ,EAAC,EAAE;QAChE,OAAO,MAAM,sBAAsB,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QAC9D,OAAO,MAAM,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -237,6 +237,16 @@ export declare class Neo4jStorageProvider implements StorageProvider {
|
|
|
237
237
|
* @returns Array of entity names that need an embedding job scheduled.
|
|
238
238
|
*/
|
|
239
239
|
getEntityNamesMissingEmbeddings(): Promise<string[]>;
|
|
240
|
+
/**
|
|
241
|
+
* Guard: reject embedding vectors whose length does not match the configured
|
|
242
|
+
* vector index dimension (`NEO4J_VECTOR_DIMENSIONS`). A mismatched write —
|
|
243
|
+
* e.g. a 1536-dim vector into a 1024-dim index — can never be indexed and
|
|
244
|
+
* silently corrupts semantic search. Throwing turns silent corruption into a
|
|
245
|
+
* loud failed job. Inert when `vectorDimensions` is unset.
|
|
246
|
+
*
|
|
247
|
+
* @param vector The embedding vector about to be persisted
|
|
248
|
+
*/
|
|
249
|
+
private assertEmbeddingDimension;
|
|
240
250
|
/**
|
|
241
251
|
* Store or update the embedding vector for an entity
|
|
242
252
|
* @param entityName The name of the entity to update
|
|
@@ -40,23 +40,38 @@ export class Neo4jStorageProvider {
|
|
|
40
40
|
this.connectionManager = options?.connectionManager || new Neo4jConnectionManager(this.config);
|
|
41
41
|
// Set up schema manager
|
|
42
42
|
this.schemaManager = new Neo4jSchemaManager(this.connectionManager, this.config, false);
|
|
43
|
-
// Set up vector store
|
|
43
|
+
// Set up vector store — dimensions MUST follow the configured index dimension
|
|
44
|
+
// (was hardcoded 1536, which would create a wrong-sized index on non-1536 deployments)
|
|
44
45
|
this.vectorStore = new Neo4jVectorStore({
|
|
45
46
|
connectionManager: this.connectionManager,
|
|
46
47
|
indexName: this.config.vectorIndexName,
|
|
47
|
-
dimensions:
|
|
48
|
+
dimensions: this.config.vectorDimensions,
|
|
48
49
|
similarityFunction: 'cosine',
|
|
49
50
|
entityNodeLabel: 'Entity',
|
|
50
51
|
});
|
|
51
52
|
logger.debug('Neo4jStorageProvider: Initializing embedding service');
|
|
52
53
|
try {
|
|
53
|
-
// Set up embedding service
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
// Set up embedding service — same provider/production gates as index.ts, so the
|
|
55
|
+
// storage provider's DIRECT write paths (createEntities/createEntitiesBatch)
|
|
56
|
+
// can never generate random/mock vectors when no real provider is configured.
|
|
57
|
+
if (!EmbeddingServiceFactory.hasEmbeddingProvider()) {
|
|
58
|
+
logger.info('Neo4jStorageProvider: no embedding provider configured — entity writes will persist with NULL embeddings');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const service = EmbeddingServiceFactory.createFromEnvironment();
|
|
62
|
+
if (!EmbeddingServiceFactory.shouldWriteEmbeddings(service)) {
|
|
63
|
+
logger.error('Neo4jStorageProvider: refusing random/mock embedding service under NODE_ENV=production — ' +
|
|
64
|
+
'entity writes will persist with NULL embeddings');
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.embeddingService = service;
|
|
68
|
+
logger.debug('Neo4jStorageProvider: Embedding service initialized successfully', {
|
|
69
|
+
provider: this.embeddingService.getProviderInfo().provider,
|
|
70
|
+
model: this.embeddingService.getProviderInfo().model,
|
|
71
|
+
dimensions: this.embeddingService.getProviderInfo().dimensions,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
76
|
catch (error) {
|
|
62
77
|
logger.error('Neo4jStorageProvider: Failed to initialize embedding service', error);
|
|
@@ -595,11 +610,15 @@ export class Neo4jStorageProvider {
|
|
|
595
610
|
: '';
|
|
596
611
|
// Generate embedding using the instance's embedding service
|
|
597
612
|
embedding = await this.embeddingService.generateEmbedding(text);
|
|
613
|
+
// Wrong-dimension vectors must never be persisted — throw into the
|
|
614
|
+
// catch below so the entity is still created, with embedding=NULL.
|
|
615
|
+
this.assertEmbeddingDimension(embedding);
|
|
598
616
|
logger.info(`Generated embedding for entity: ${entity.name}`);
|
|
599
617
|
}
|
|
600
618
|
catch (error) {
|
|
601
619
|
logger.error(`Failed to generate embedding for entity: ${entity.name}`, error);
|
|
602
620
|
// Continue without embedding if generation fails
|
|
621
|
+
embedding = null;
|
|
603
622
|
}
|
|
604
623
|
}
|
|
605
624
|
else {
|
|
@@ -1682,6 +1701,29 @@ export class Neo4jStorageProvider {
|
|
|
1682
1701
|
`, {});
|
|
1683
1702
|
return result.records.map(r => String(r.get('name')));
|
|
1684
1703
|
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Guard: reject embedding vectors whose length does not match the configured
|
|
1706
|
+
* vector index dimension (`NEO4J_VECTOR_DIMENSIONS`). A mismatched write —
|
|
1707
|
+
* e.g. a 1536-dim vector into a 1024-dim index — can never be indexed and
|
|
1708
|
+
* silently corrupts semantic search. Throwing turns silent corruption into a
|
|
1709
|
+
* loud failed job. Inert when `vectorDimensions` is unset.
|
|
1710
|
+
*
|
|
1711
|
+
* @param vector The embedding vector about to be persisted
|
|
1712
|
+
*/
|
|
1713
|
+
assertEmbeddingDimension(vector) {
|
|
1714
|
+
const expected = this.config.vectorDimensions;
|
|
1715
|
+
// Number.isFinite (not truthiness): parseInt of a malformed env value yields
|
|
1716
|
+
// NaN, which is falsy and would otherwise silently disable the guard.
|
|
1717
|
+
if (typeof expected === 'number' &&
|
|
1718
|
+
Number.isFinite(expected) &&
|
|
1719
|
+
expected > 0 &&
|
|
1720
|
+
Array.isArray(vector) &&
|
|
1721
|
+
vector.length !== expected) {
|
|
1722
|
+
throw new Error(`Embedding dimension mismatch: got ${vector.length}, vector index expects ${expected} ` +
|
|
1723
|
+
`(NEO4J_VECTOR_DIMENSIONS). Ensure EMBEDDING_MODEL's native output dimension and ` +
|
|
1724
|
+
`EMBEDDING_DIMENSIONS both match the index — refusing to write a corrupt vector.`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1685
1727
|
/**
|
|
1686
1728
|
* Store or update the embedding vector for an entity
|
|
1687
1729
|
* @param entityName The name of the entity to update
|
|
@@ -1689,6 +1731,7 @@ export class Neo4jStorageProvider {
|
|
|
1689
1731
|
*/
|
|
1690
1732
|
async updateEntityEmbedding(entityName, embedding) {
|
|
1691
1733
|
try {
|
|
1734
|
+
this.assertEmbeddingDimension(embedding.vector);
|
|
1692
1735
|
// Verify that the entity exists
|
|
1693
1736
|
const entity = await this.getEntity(entityName);
|
|
1694
1737
|
if (!entity) {
|
|
@@ -2342,9 +2385,13 @@ export class Neo4jStorageProvider {
|
|
|
2342
2385
|
? entity.observations.join('\n')
|
|
2343
2386
|
: '';
|
|
2344
2387
|
embedding = await this.embeddingService.generateEmbedding(text);
|
|
2388
|
+
// Same write-path dimension guard as createEntities: a mismatched
|
|
2389
|
+
// vector throws into the catch and the entity persists with NULL.
|
|
2390
|
+
this.assertEmbeddingDimension(embedding);
|
|
2345
2391
|
}
|
|
2346
2392
|
catch (error) {
|
|
2347
2393
|
logger.warn(`Failed to generate embedding for entity: ${entity.name}`, error);
|
|
2394
|
+
embedding = null;
|
|
2348
2395
|
}
|
|
2349
2396
|
}
|
|
2350
2397
|
const now = Date.now();
|