@atlaspack/cache 3.1.1-canary.36 → 3.1.1-canary.360

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.
@@ -1,5 +1,3 @@
1
- // @flow strict-local
2
-
3
1
  import {
4
2
  deserialize,
5
3
  registerSerializableClass,
@@ -10,15 +8,15 @@ import {Lmdb} from '@atlaspack/rust';
10
8
  import type {FilePath} from '@atlaspack/types';
11
9
  import type {Cache} from './types';
12
10
  import type {Readable, Writable} from 'stream';
13
- import fs from 'fs';
11
+ // @ts-expect-error TS7016
14
12
  import ncp from 'ncp';
15
13
  import {promisify} from 'util';
16
14
  import stream from 'stream';
17
15
  import path from 'path';
18
16
  import {NodeFS} from '@atlaspack/fs';
19
- // $FlowFixMe
20
17
  import packageJson from '../package.json';
21
18
  import {FSCache} from './FSCache';
19
+ import {instrumentAsync} from '@atlaspack/logger';
22
20
 
23
21
  const ncpAsync = promisify(ncp);
24
22
 
@@ -35,11 +33,6 @@ export class LmdbWrapper {
35
33
 
36
34
  constructor(lmdb: Lmdb) {
37
35
  this.lmdb = lmdb;
38
-
39
- // $FlowFixMe
40
- this[Symbol.dispose] = () => {
41
- this.lmdb.close();
42
- };
43
36
  }
44
37
 
45
38
  has(key: string): boolean {
@@ -94,23 +87,28 @@ export function open(
94
87
  );
95
88
  }
96
89
 
97
- const pipeline: (Readable, Writable) => Promise<void> = promisify(
90
+ const pipeline: (arg1: Readable, arg2: Writable) => Promise<void> = promisify(
98
91
  stream.pipeline,
99
92
  );
100
93
 
101
- export type SerLMDBLiteCache = {|
102
- dir: FilePath,
103
- |};
94
+ export type SerLMDBLiteCache = {
95
+ dir: FilePath;
96
+ };
104
97
 
105
98
  export class LMDBLiteCache implements Cache {
106
99
  fs: NodeFS;
107
100
  dir: FilePath;
108
101
  store: LmdbWrapper;
109
102
  fsCache: FSCache;
103
+ /**
104
+ * Directory where we store raw files.
105
+ */
106
+ cacheFilesDirectory: FilePath;
110
107
 
111
108
  constructor(cacheDir: FilePath) {
112
109
  this.fs = new NodeFS();
113
110
  this.dir = cacheDir;
111
+ this.cacheFilesDirectory = path.join(cacheDir, 'files');
114
112
  this.fsCache = new FSCache(this.fs, cacheDir);
115
113
 
116
114
  this.store = open(cacheDir, {
@@ -131,6 +129,7 @@ export class LMDBLiteCache implements Cache {
131
129
  if (!getFeatureFlag('cachePerformanceImprovements')) {
132
130
  await this.fsCache.ensure();
133
131
  }
132
+ await this.fs.mkdirp(this.cacheFilesDirectory);
134
133
  return Promise.resolve();
135
134
  }
136
135
 
@@ -148,7 +147,7 @@ export class LMDBLiteCache implements Cache {
148
147
  return Promise.resolve(this.store.has(key));
149
148
  }
150
149
 
151
- get<T>(key: string): Promise<?T> {
150
+ get<T>(key: string): Promise<T | null | undefined> {
152
151
  let data = this.store.get(key);
153
152
  if (data == null) {
154
153
  return Promise.resolve(null);
@@ -157,19 +156,29 @@ export class LMDBLiteCache implements Cache {
157
156
  return Promise.resolve(deserialize(data));
158
157
  }
159
158
 
160
- async set(key: string, value: mixed): Promise<void> {
159
+ async set(key: string, value: unknown): Promise<void> {
161
160
  await this.setBlob(key, serialize(value));
162
161
  }
163
162
 
164
163
  getStream(key: string): Readable {
165
- return this.fs.createReadStream(path.join(this.dir, key));
164
+ if (!getFeatureFlag('cachePerformanceImprovements')) {
165
+ return this.fs.createReadStream(path.join(this.dir, key));
166
+ }
167
+
168
+ return this.fs.createReadStream(this.getFileKey(key));
166
169
  }
167
170
 
168
- setStream(key: string, stream: Readable): Promise<void> {
169
- return pipeline(
170
- stream,
171
- this.fs.createWriteStream(path.join(this.dir, key)),
172
- );
171
+ async setStream(key: string, stream: Readable): Promise<void> {
172
+ if (!getFeatureFlag('cachePerformanceImprovements')) {
173
+ return pipeline(
174
+ stream,
175
+ this.fs.createWriteStream(path.join(this.dir, key)),
176
+ );
177
+ }
178
+
179
+ const filePath = this.getFileKey(key);
180
+ await this.fs.mkdirp(path.dirname(filePath));
181
+ return pipeline(stream, this.fs.createWriteStream(filePath));
173
182
  }
174
183
 
175
184
  // eslint-disable-next-line require-await
@@ -189,43 +198,39 @@ export class LMDBLiteCache implements Cache {
189
198
  await this.store.put(key, contents);
190
199
  }
191
200
 
192
- getBuffer(key: string): Promise<?Buffer> {
201
+ getBuffer(key: string): Promise<Buffer | null | undefined> {
193
202
  return Promise.resolve(this.store.get(key));
194
203
  }
195
204
 
196
- #getFilePath(key: string, index: number): string {
197
- return path.join(this.dir, `${key}-${index}`);
198
- }
199
-
200
205
  hasLargeBlob(key: string): Promise<boolean> {
201
206
  if (!getFeatureFlag('cachePerformanceImprovements')) {
202
207
  return this.fsCache.hasLargeBlob(key);
203
208
  }
204
- return this.has(key);
209
+
210
+ return this.fs.exists(this.getFileKey(key));
205
211
  }
206
212
 
207
- /**
208
- * @deprecated Use getBlob instead.
209
- */
210
213
  getLargeBlob(key: string): Promise<Buffer> {
211
214
  if (!getFeatureFlag('cachePerformanceImprovements')) {
212
215
  return this.fsCache.getLargeBlob(key);
213
216
  }
214
- return Promise.resolve(this.getBlobSync(key));
217
+ return this.fs.readFile(this.getFileKey(key));
215
218
  }
216
219
 
217
- /**
218
- * @deprecated Use setBlob instead.
219
- */
220
- setLargeBlob(
220
+ async setLargeBlob(
221
221
  key: string,
222
222
  contents: Buffer | string,
223
- options?: {|signal?: AbortSignal|},
223
+ options?: {
224
+ signal?: AbortSignal;
225
+ },
224
226
  ): Promise<void> {
225
227
  if (!getFeatureFlag('cachePerformanceImprovements')) {
226
228
  return this.fsCache.setLargeBlob(key, contents, options);
227
229
  }
228
- return this.setBlob(key, contents);
230
+
231
+ const targetPath = this.getFileKey(key);
232
+ await this.fs.mkdirp(path.dirname(targetPath));
233
+ return this.fs.writeFile(targetPath, contents);
229
234
  }
230
235
 
231
236
  /**
@@ -244,9 +249,9 @@ export class LMDBLiteCache implements Cache {
244
249
  }
245
250
 
246
251
  async compact(targetPath: string): Promise<void> {
247
- await fs.promises.mkdir(targetPath, {recursive: true});
252
+ await this.fs.mkdirp(targetPath);
248
253
 
249
- const files = await fs.promises.readdir(this.dir);
254
+ const files = await this.fs.readdir(this.dir);
250
255
  // copy all files except data.mdb and lock.mdb to the target path (recursive)
251
256
  for (const file of files) {
252
257
  const filePath = path.join(this.dir, file);
@@ -262,6 +267,42 @@ export class LMDBLiteCache implements Cache {
262
267
  }
263
268
 
264
269
  refresh(): void {}
270
+
271
+ /**
272
+ * Streams, packages are stored in files instead of LMDB.
273
+ *
274
+ * On this case, if a cache key happens to have a parent traversal, ../..
275
+ * it is treated specially
276
+ *
277
+ * That is, something/../something and something are meant to be different
278
+ * keys.
279
+ *
280
+ * Plus we do not want to store values outside of the cache directory.
281
+ */
282
+ getFileKey(key: string): string {
283
+ const cleanKey = key
284
+ .split('/')
285
+ .map((part) => {
286
+ if (part === '..') {
287
+ return '$$__parent_dir$$';
288
+ }
289
+ return part;
290
+ })
291
+ .join('/');
292
+ return path.join(this.cacheFilesDirectory, cleanKey);
293
+ }
294
+
295
+ async clear(): Promise<void> {
296
+ await instrumentAsync('LMDBLiteCache::clear', async () => {
297
+ const keys = await this.keys();
298
+ for (const key of keys) {
299
+ await this.store.delete(key);
300
+ }
301
+
302
+ await this.fs.rimraf(this.cacheFilesDirectory);
303
+ await this.fs.mkdirp(this.cacheFilesDirectory);
304
+ });
305
+ }
265
306
  }
266
307
 
267
308
  registerSerializableClass(
@@ -1,4 +1,2 @@
1
- // @flow strict-local
2
-
3
1
  // Node has a file size limit of 2 GB
4
2
  export const WRITE_LIMIT_CHUNK = 2 * 1024 ** 3;
@@ -1,5 +1,3 @@
1
- // @flow
2
-
3
1
  export * from './FSCache';
4
2
  export * from './IDBCache';
5
3
  export * from './LMDBLiteCache';
@@ -1,4 +1,3 @@
1
- // @flow
2
1
  import type {Cache} from '@atlaspack/types';
3
2
 
4
3
  export type {Cache};
@@ -0,0 +1,241 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import {tmpdir} from 'os';
4
+ import {LMDBLiteCache} from '../src/index';
5
+ import {deserialize, serialize} from 'v8';
6
+ import assert from 'assert';
7
+ import {Worker} from 'worker_threads';
8
+ import {initializeMonitoring} from '@atlaspack/rust';
9
+
10
+ const cacheDir = path.join(tmpdir(), 'lmdb-lite-cache-tests');
11
+
12
+ describe('LMDBLiteCache', () => {
13
+ let cache: any;
14
+
15
+ beforeEach(async () => {
16
+ await fs.promises.rm(cacheDir, {recursive: true, force: true});
17
+ });
18
+
19
+ it('can be constructed', async () => {
20
+ cache = new LMDBLiteCache(cacheDir);
21
+ await cache.ensure();
22
+ });
23
+
24
+ it('can retrieve keys', async () => {
25
+ cache = new LMDBLiteCache(cacheDir);
26
+ await cache.ensure();
27
+ await cache.setBlob('key', Buffer.from(serialize({value: 42})));
28
+ const buffer = await cache.getBlob('key');
29
+ const result = deserialize(buffer);
30
+ assert.equal(result.value, 42);
31
+ });
32
+
33
+ it('can retrieve keys synchronously', async () => {
34
+ cache = new LMDBLiteCache(path.join(cacheDir, 'retrieve_keys_test'));
35
+ await cache.ensure();
36
+ await cache.setBlob('key', Buffer.from(serialize({value: 42})));
37
+ const buffer = cache.getBlobSync('key');
38
+ const result = deserialize(buffer);
39
+ assert.equal(result.value, 42);
40
+ });
41
+
42
+ it('can iterate over keys', async () => {
43
+ cache = new LMDBLiteCache(path.join(cacheDir, 'keys_test'));
44
+ await cache.ensure();
45
+ await cache.setBlob('key1', Buffer.from(serialize({value: 42})));
46
+ await cache.setBlob('key2', Buffer.from(serialize({value: 43})));
47
+ const keys = cache.keys();
48
+ assert.deepEqual(Array.from(keys), ['key1', 'key2']);
49
+ });
50
+
51
+ it('can compact databases', async () => {
52
+ cache = new LMDBLiteCache(path.join(cacheDir, 'compact_test'));
53
+ await cache.ensure();
54
+ await cache.setBlob('key1', Buffer.from(serialize({value: 42})));
55
+ await cache.setBlob('key2', Buffer.from(serialize({value: 43})));
56
+ await cache.compact(path.join(cacheDir, 'compact_test_compacted'));
57
+
58
+ cache = new LMDBLiteCache(path.join(cacheDir, 'compact_test_compacted'));
59
+ await cache.ensure();
60
+ const keys = cache.keys();
61
+ assert.deepEqual(Array.from(keys), ['key1', 'key2']);
62
+ });
63
+
64
+ describe('getFileKey', () => {
65
+ it('should return the correct key', () => {
66
+ const target = path.join(cacheDir, 'test-file-keys');
67
+ const cache = new LMDBLiteCache(target);
68
+ const key = cache.getFileKey('key');
69
+ assert.equal(key, path.join(target, 'files', 'key'));
70
+ });
71
+
72
+ it('should return the correct key for a key with a parent traversal', () => {
73
+ const target = path.join(cacheDir, 'test-parent-keys');
74
+ cache = new LMDBLiteCache(target);
75
+ const key = cache.getFileKey('../../key');
76
+ assert.equal(
77
+ key,
78
+ path.join(target, 'files', '$$__parent_dir$$/$$__parent_dir$$/key'),
79
+ );
80
+ });
81
+ });
82
+
83
+ it('can be closed and re-opened', async () => {
84
+ cache = new LMDBLiteCache(path.join(cacheDir, 'close_and_reopen_test'));
85
+ await cache.ensure();
86
+ await cache.setBlob('key', Buffer.from(serialize({value: 42})));
87
+ cache = new LMDBLiteCache(path.join(cacheDir, 'close_and_reopen_test'));
88
+ await cache.ensure();
89
+ const buffer = await cache.getBlob('key');
90
+ const result = deserialize(buffer);
91
+ assert.equal(result.value, 42);
92
+ });
93
+
94
+ it('should NOT fail when trying to open the same database twice', async () => {
95
+ const testDir = path.join(cacheDir, 'double_open_test');
96
+ const cache1 = new LMDBLiteCache(testDir);
97
+ await cache1.ensure();
98
+
99
+ assert.doesNotThrow(() => {
100
+ new LMDBLiteCache(testDir);
101
+ });
102
+ });
103
+
104
+ it('should NOT fail when trying to open after GC', async () => {
105
+ const testDir = path.join(cacheDir, 'gc_test');
106
+
107
+ let cache1 = new LMDBLiteCache(testDir);
108
+ await cache1.ensure();
109
+ await cache1.setBlob('key', Buffer.from(serialize({value: 42})));
110
+
111
+ cache1 = null;
112
+
113
+ if (global.gc) {
114
+ global.gc();
115
+ }
116
+
117
+ assert.doesNotThrow(() => {
118
+ new LMDBLiteCache(testDir);
119
+ });
120
+ });
121
+
122
+ it('should handle rapid open/close cycles', async () => {
123
+ const testDir = path.join(cacheDir, 'rapid_cycles_test');
124
+
125
+ for (let i = 0; i < 10; i++) {
126
+ const cache = new LMDBLiteCache(testDir);
127
+ await cache.ensure();
128
+ await cache.setBlob(`key${i}`, Buffer.from(serialize({value: i})));
129
+
130
+ await new Promise((resolve: any) => setTimeout(resolve, 10));
131
+ }
132
+
133
+ const finalCache = new LMDBLiteCache(testDir);
134
+ await finalCache.ensure();
135
+ const buffer = await finalCache.getBlob('key9');
136
+ const result = deserialize(buffer);
137
+ assert.equal(result.value, 9);
138
+ });
139
+
140
+ it('should work when there are multiple node.js worker threads accessing the same database', async function () {
141
+ this.timeout(40000);
142
+
143
+ try {
144
+ initializeMonitoring();
145
+ } catch (error: any) {
146
+ /* empty */
147
+ }
148
+
149
+ const testDir = path.join(cacheDir, 'worker_threads_test');
150
+
151
+ let cache = new LMDBLiteCache(testDir);
152
+ await cache.set('main_thread_key', {
153
+ mainThreadId: 0,
154
+ hello: 'world',
155
+ });
156
+ setTimeout(() => {
157
+ cache = null;
158
+
159
+ if (global.gc) {
160
+ global.gc();
161
+ }
162
+ }, Math.random() * 300);
163
+
164
+ const numWorkers = 10;
165
+
166
+ const workers: Array<any> = [];
167
+ const responsePromises: Array<any> = [];
168
+ for (let i = 0; i < numWorkers; i++) {
169
+ const worker = new Worker(path.join(__dirname, 'workerThreadsTest.js'), {
170
+ workerData: {
171
+ cacheDir: testDir,
172
+ },
173
+ });
174
+ workers.push(worker);
175
+
176
+ const responsePromise = new Promise((resolve: any, reject: any) => {
177
+ worker.addListener('error', (error: Error) => {
178
+ reject(error);
179
+ });
180
+ worker.addListener('message', (message: any) => {
181
+ resolve(message);
182
+ });
183
+ });
184
+
185
+ worker.addListener('message', (message: any) => {
186
+ // eslint-disable-next-line no-console
187
+ console.log('Worker message', message);
188
+ });
189
+ worker.addListener('online', () => {
190
+ worker.postMessage({
191
+ type: 'go',
192
+ });
193
+ });
194
+
195
+ responsePromises.push(responsePromise);
196
+ }
197
+
198
+ // eslint-disable-next-line no-console
199
+ console.log('Waiting for responses');
200
+ const responses = await Promise.all(responsePromises);
201
+
202
+ // eslint-disable-next-line no-console
203
+ console.log('Responses received');
204
+ for (const [index, response] of responses.entries()) {
205
+ const worker = workers[index];
206
+
207
+ assert.deepEqual(
208
+ response,
209
+ {
210
+ mainThreadData: {
211
+ mainThreadId: 0,
212
+ hello: 'world',
213
+ },
214
+ workerId: worker.threadId,
215
+ },
216
+ `worker_${index} - Worker ${worker.threadId} should have received the correct data`,
217
+ );
218
+ }
219
+
220
+ // eslint-disable-next-line no-console
221
+ console.log('Getting main thread key');
222
+ cache = new LMDBLiteCache(testDir);
223
+ const data = await cache?.get('main_thread_key');
224
+ assert.deepEqual(data, {
225
+ mainThreadId: 0,
226
+ hello: 'world',
227
+ });
228
+
229
+ // eslint-disable-next-line no-console
230
+ console.log('Getting worker keys');
231
+ for (const worker of workers) {
232
+ const data = await cache?.get(`worker_key/${worker.threadId}`);
233
+ assert.deepEqual(data, {
234
+ workerId: worker.threadId,
235
+ });
236
+
237
+ await new Promise((resolve: any) => setTimeout(resolve, 500));
238
+ worker.terminate();
239
+ }
240
+ });
241
+ });
@@ -0,0 +1,42 @@
1
+ /* eslint-disable no-inner-declarations */
2
+
3
+ require('@atlaspack/babel-register');
4
+ const {
5
+ workerData,
6
+ threadId,
7
+ parentPort,
8
+ isMainThread,
9
+ } = require('worker_threads');
10
+ const {LMDBLiteCache} = require('../src/index');
11
+
12
+ if (!isMainThread) {
13
+ const cache = new LMDBLiteCache(workerData.cacheDir);
14
+
15
+ async function onMessage() {
16
+ try {
17
+ cache.set(`worker_key/${threadId}`, {
18
+ workerId: threadId,
19
+ });
20
+
21
+ const data = await cache.get('main_thread_key');
22
+
23
+ parentPort.postMessage({
24
+ mainThreadData: data,
25
+ workerId: threadId,
26
+ });
27
+
28
+ setTimeout(() => {
29
+ parentPort.postMessage({
30
+ type: 'close',
31
+ workerId: threadId,
32
+ });
33
+ }, Math.random() * 200);
34
+ } catch (error) {
35
+ parentPort.postMessage({
36
+ error: error.message,
37
+ });
38
+ }
39
+ }
40
+
41
+ parentPort.on('message', onMessage);
42
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["./src/", "./package.json"],
4
+ "compilerOptions": {
5
+ "composite": true
6
+ },
7
+ "references": [
8
+ {
9
+ "path": "../build-cache/tsconfig.json"
10
+ },
11
+ {
12
+ "path": "../feature-flags/tsconfig.json"
13
+ },
14
+ {
15
+ "path": "../fs/tsconfig.json"
16
+ },
17
+ {
18
+ "path": "../logger/tsconfig.json"
19
+ },
20
+ {
21
+ "path": "../rust/tsconfig.json"
22
+ },
23
+ {
24
+ "path": "../utils/tsconfig.json"
25
+ }
26
+ ]
27
+ }