@teambit/objects 0.0.683 → 0.0.685
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.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/objects/index.d.ts +1 -0
- package/dist/objects/index.js.map +1 -1
- package/dist/objects/object.d.ts +19 -0
- package/dist/objects/object.js +43 -0
- package/dist/objects/object.js.map +1 -1
- package/dist/objects/repository.d.ts +50 -2
- package/dist/objects/repository.js +122 -4
- package/dist/objects/repository.js.map +1 -1
- package/dist/objects/repository.spec.d.ts +1 -0
- package/dist/objects/repository.spec.js +159 -0
- package/dist/objects/repository.spec.js.map +1 -0
- package/objects/index.ts +1 -0
- package/objects/object.ts +40 -1
- package/objects/repository.spec.ts +92 -0
- package/objects/repository.ts +119 -7
- package/package.json +14 -14
- /package/dist/{preview-1790183760587.js → preview-1790261426827.js} +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import zlib from 'zlib';
|
|
6
|
+
import { ScopeJson } from '@teambit/legacy.scope';
|
|
7
|
+
import { ModelComponent, Source } from '../models';
|
|
8
|
+
import Ref from './ref';
|
|
9
|
+
import Repository from './repository';
|
|
10
|
+
import { IndexType } from './scope-index';
|
|
11
|
+
|
|
12
|
+
const SCOPE_NAME = 'my-scope';
|
|
13
|
+
const SOURCES_COUNT = 5;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `Repository.list(types)` is what rebuilds a missing `index.json`, and it's asked for a handful of
|
|
17
|
+
* object types in a scope that is mostly file contents. the type is in each file's header, so the
|
|
18
|
+
* rest of a file should only be read when its type is one that was asked for.
|
|
19
|
+
*/
|
|
20
|
+
describe('Repository.list', () => {
|
|
21
|
+
let scopePath: string;
|
|
22
|
+
let scopeJson: ScopeJson;
|
|
23
|
+
let repository: Repository;
|
|
24
|
+
let contentReads: number;
|
|
25
|
+
const original = {
|
|
26
|
+
onPostObjectRead: Repository.onPostObjectRead,
|
|
27
|
+
hasPostObjectReadTransformer: Repository.hasPostObjectReadTransformer,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
scopePath = await fs.mkdtemp(path.join(os.tmpdir(), 'bit-repository-spec-'));
|
|
32
|
+
scopeJson = new ScopeJson(
|
|
33
|
+
{ name: SCOPE_NAME, version: '1.0.0', groupName: null },
|
|
34
|
+
path.join(scopePath, 'scope.json')
|
|
35
|
+
);
|
|
36
|
+
repository = await Repository.create({ scopePath, scopeJson });
|
|
37
|
+
const modelComponent = ModelComponent.from({
|
|
38
|
+
name: 'bar/foo',
|
|
39
|
+
scope: SCOPE_NAME,
|
|
40
|
+
lang: 'javascript',
|
|
41
|
+
deprecated: false,
|
|
42
|
+
bindingPrefix: '@bit',
|
|
43
|
+
versions: {},
|
|
44
|
+
});
|
|
45
|
+
const sources = Array.from({ length: SOURCES_COUNT }, (_, i) => Source.from(Buffer.from(`file ${i}`)));
|
|
46
|
+
const objects = [modelComponent, ...sources];
|
|
47
|
+
objects.forEach((object) => {
|
|
48
|
+
object.validateBeforePersist = false;
|
|
49
|
+
});
|
|
50
|
+
await repository.writeObjectsToTheFS(objects);
|
|
51
|
+
|
|
52
|
+
contentReads = 0;
|
|
53
|
+
// every full read of an object file goes through this hook, while a header read doesn't
|
|
54
|
+
Repository.onPostObjectRead = (content) => {
|
|
55
|
+
contentReads += 1;
|
|
56
|
+
return content;
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
Repository.onPostObjectRead = original.onPostObjectRead;
|
|
62
|
+
Repository.hasPostObjectReadTransformer = original.hasPostObjectReadTransformer;
|
|
63
|
+
await fs.remove(scopePath);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should rebuild a missing index.json reading the contents of the wanted objects only', async () => {
|
|
67
|
+
Repository.hasPostObjectReadTransformer = () => false;
|
|
68
|
+
await Repository.reset(scopePath);
|
|
69
|
+
const loaded = await Repository.load({ scopePath, scopeJson });
|
|
70
|
+
expect(loaded.scopeIndex.getHashes(IndexType.components)).to.have.lengthOf(1);
|
|
71
|
+
expect(contentReads).to.equal(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should read every object once when a transformer is registered, as the header is transformed too', async () => {
|
|
75
|
+
Repository.hasPostObjectReadTransformer = () => true;
|
|
76
|
+
const objects = await repository.list([ModelComponent]);
|
|
77
|
+
expect(objects).to.have.lengthOf(1);
|
|
78
|
+
expect(contentReads).to.equal(SOURCES_COUNT + 1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should skip an object whose header names a type nothing is registered under, as the full read does', async () => {
|
|
82
|
+
Repository.hasPostObjectReadTransformer = () => false;
|
|
83
|
+
const hash = 'a'.repeat(40);
|
|
84
|
+
const content = 'whatever';
|
|
85
|
+
const unknownObject = zlib.deflateSync(Buffer.from(`NotARealType ${hash} ${content.length}\0${content}`));
|
|
86
|
+
const objectPath = repository.objectPath(Ref.from(hash));
|
|
87
|
+
await fs.outputFile(objectPath, unknownObject);
|
|
88
|
+
const objects = await repository.list([ModelComponent]);
|
|
89
|
+
expect(objects).to.have.lengthOf(1);
|
|
90
|
+
expect(objects[0]).to.be.instanceOf(ModelComponent);
|
|
91
|
+
});
|
|
92
|
+
});
|
package/objects/repository.ts
CHANGED
|
@@ -14,7 +14,13 @@ import { glob } from 'glob';
|
|
|
14
14
|
import { removeEmptyDir } from '@teambit/toolbox.fs.remove-empty-dir';
|
|
15
15
|
import { concurrentIOLimit } from '@teambit/harmony.modules.concurrency';
|
|
16
16
|
import type { Types, ScopeJson } from '@teambit/legacy.scope';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
HashNotFound,
|
|
19
|
+
OutdatedIndexJson,
|
|
20
|
+
UnknownObjectType,
|
|
21
|
+
UnmergedComponents,
|
|
22
|
+
RemoteLanes,
|
|
23
|
+
} from '@teambit/legacy.scope';
|
|
18
24
|
import type { IndexType, IndexItem } from './scope-index';
|
|
19
25
|
import { ScopeIndex } from './scope-index';
|
|
20
26
|
import BitObject from './object';
|
|
@@ -29,6 +35,18 @@ import { ScopeMeta, Lane, ModelComponent } from '../models';
|
|
|
29
35
|
type ContentTransformer = (content: Buffer) => Buffer;
|
|
30
36
|
const OBJECTS_BACKUP_DIR = `${OBJECTS_DIR}.bak`;
|
|
31
37
|
const TRASH_DIR = 'trash';
|
|
38
|
+
/**
|
|
39
|
+
* how much of an object file to read when all that's needed is its header. the header holds the
|
|
40
|
+
* type and is a few dozen bytes, which 512 compressed bytes always cover - and every byte past it
|
|
41
|
+
* is inflated for nothing (4KB inflates ten times as much, across every object in the scope). a
|
|
42
|
+
* chunk that falls short anyway is read in full.
|
|
43
|
+
*/
|
|
44
|
+
const OBJECT_HEADER_CHUNK_SIZE = 512;
|
|
45
|
+
|
|
46
|
+
/** `mtimeMs` lets a caller tell an object that has been here a while from one just written */
|
|
47
|
+
export type ObjectWithType = { ref: Ref; type: string; size: number; mtimeMs: number };
|
|
48
|
+
/** `unreadable` holds the objects that couldn't be classified, so a caller can refuse to act on a partial inventory */
|
|
49
|
+
export type ObjectsWithType = { objects: ObjectWithType[]; unreadable: Ref[] };
|
|
32
50
|
|
|
33
51
|
export default class Repository {
|
|
34
52
|
objects: { [key: string]: BitObject } = {};
|
|
@@ -94,14 +112,23 @@ export default class Repository {
|
|
|
94
112
|
* Note: This function cannot be async because it's used by the synchronous `loadSync` method
|
|
95
113
|
* which needs to maintain sync behavior for compatibility with existing code.
|
|
96
114
|
*/
|
|
97
|
-
static onPreObjectPersist
|
|
115
|
+
static onPreObjectPersist?: (content: Buffer) => Buffer;
|
|
98
116
|
|
|
99
117
|
/**
|
|
100
118
|
* Hook for transforming content after objects are read from the filesystem.
|
|
101
119
|
* Note: This function cannot be async because it's used by the synchronous `loadSync` method
|
|
102
120
|
* which needs to maintain sync behavior for compatibility with existing code.
|
|
103
121
|
*/
|
|
104
|
-
static onPostObjectRead
|
|
122
|
+
static onPostObjectRead?: (content: Buffer) => Buffer;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* whether `onPostObjectRead` actually transforms anything.
|
|
126
|
+
*
|
|
127
|
+
* the scope aspect installs the hook above unconditionally - it's a reduce over a slot that is
|
|
128
|
+
* usually empty - so the hook being set says nothing about whether object content is transformed.
|
|
129
|
+
* only a caller that can skip reading content altogether needs to tell the two apart.
|
|
130
|
+
*/
|
|
131
|
+
static hasPostObjectReadTransformer?: () => boolean;
|
|
105
132
|
|
|
106
133
|
async reLoadScopeIndex() {
|
|
107
134
|
this.scopeIndex = await this.loadOptionallyCreateScopeIndex();
|
|
@@ -259,6 +286,79 @@ export default class Repository {
|
|
|
259
286
|
return objects;
|
|
260
287
|
}
|
|
261
288
|
|
|
289
|
+
/**
|
|
290
|
+
* classify every object in the scope by its type, along with its size on the filesystem.
|
|
291
|
+
*
|
|
292
|
+
* needed by the garbage collector, which must know the type of each object but has no use for
|
|
293
|
+
* its content. inflating every object would mean inflating gigabytes of file-content (`Source`)
|
|
294
|
+
* objects, so only the first chunk of each file is read - enough to hold the header, which is
|
|
295
|
+
* where the type is. when a content transformer is registered (`onPostObjectRead`), it applies
|
|
296
|
+
* to the whole buffer, so in that case the file is read in full.
|
|
297
|
+
*/
|
|
298
|
+
async listObjectsWithType(): Promise<ObjectsWithType> {
|
|
299
|
+
const refs = await this.listRefs();
|
|
300
|
+
const concurrency = concurrentIOLimit();
|
|
301
|
+
logger.debug(`Repository.listObjectsWithType, classifying ${refs.length} objects`);
|
|
302
|
+
const results = await pMapPool(refs, (ref) => this.getObjectTypeGracefully(ref), { concurrency });
|
|
303
|
+
const objects = compact(results.map((result) => result.object));
|
|
304
|
+
const unreadable = compact(results.map((result) => result.unreadable));
|
|
305
|
+
if (unreadable.length) {
|
|
306
|
+
logger.warn(`Repository.listObjectsWithType, unable to classify ${unreadable.length} objects`);
|
|
307
|
+
}
|
|
308
|
+
return { objects, unreadable };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* the objects that couldn't be classified are reported rather than skipped. a caller that acts on
|
|
313
|
+
* this inventory (the garbage collector) has to know that it's incomplete, because an object it
|
|
314
|
+
* can't see is one it can't reason about.
|
|
315
|
+
*/
|
|
316
|
+
private async getObjectTypeGracefully(ref: Ref): Promise<{ object?: ObjectWithType; unreadable?: Ref }> {
|
|
317
|
+
const objectPath = this.objectPath(ref);
|
|
318
|
+
try {
|
|
319
|
+
const stat = await fs.stat(objectPath);
|
|
320
|
+
const type = await this.readObjectType(objectPath);
|
|
321
|
+
return { object: { ref, type, size: stat.size, mtimeMs: stat.mtimeMs } };
|
|
322
|
+
} catch (err: any) {
|
|
323
|
+
logger.warn(`Repository.listObjectsWithType, failed reading ${objectPath}. Error: ${err.message}`);
|
|
324
|
+
return { unreadable: ref };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async readObjectType(objectPath: string): Promise<string> {
|
|
329
|
+
const readInFull = async () => BitObject.parseObjectType(this.onRead(await fs.readFile(objectPath)), objectPath);
|
|
330
|
+
if (this.isContentTransformed()) return readInFull();
|
|
331
|
+
return (await this.readTypeFromHeader(objectPath)) || readInFull();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* whether object files have to go through `onPostObjectRead` before their header can be read.
|
|
336
|
+
* when there's no way to tell, assume they do. it's the slow answer, never the wrong one.
|
|
337
|
+
*/
|
|
338
|
+
private isContentTransformed(): boolean {
|
|
339
|
+
return Repository.hasPostObjectReadTransformer
|
|
340
|
+
? Repository.hasPostObjectReadTransformer()
|
|
341
|
+
: Boolean(Repository.onPostObjectRead);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* the type an object file names in its header, read from the first chunk of the file only.
|
|
346
|
+
* null when that chunk doesn't hold the whole header, in which case the file has to be read in
|
|
347
|
+
* full. throws `UnknownObjectType` when the header names a type nothing is registered under.
|
|
348
|
+
*/
|
|
349
|
+
private async readTypeFromHeader(objectPath: string): Promise<string | null> {
|
|
350
|
+
// `Uint8Array` rather than `Buffer` - see `parseObjectTypeFromChunk`
|
|
351
|
+
const chunk = new Uint8Array(OBJECT_HEADER_CHUNK_SIZE);
|
|
352
|
+
const fileDescriptor = await fs.open(objectPath, 'r');
|
|
353
|
+
let bytesRead: number;
|
|
354
|
+
try {
|
|
355
|
+
({ bytesRead } = await fs.read(fileDescriptor, chunk, 0, chunk.length, 0));
|
|
356
|
+
} finally {
|
|
357
|
+
await fs.close(fileDescriptor);
|
|
358
|
+
}
|
|
359
|
+
return BitObject.parseObjectTypeFromChunk(chunk.subarray(0, bytesRead));
|
|
360
|
+
}
|
|
361
|
+
|
|
262
362
|
async loadRefDeleteIfInvalid(ref: Ref) {
|
|
263
363
|
try {
|
|
264
364
|
return await this.load(ref, true);
|
|
@@ -276,11 +376,23 @@ export default class Repository {
|
|
|
276
376
|
|
|
277
377
|
async loadRefOnlyIfType(ref: Ref, types: Types): Promise<BitObject | null> {
|
|
278
378
|
const objectPath = this.objectPath(ref);
|
|
279
|
-
const fileContentsRaw = await fs.readFile(objectPath);
|
|
280
|
-
const fileContents = this.onRead(fileContentsRaw);
|
|
281
379
|
const typeNames = types.map((type) => type.name);
|
|
282
|
-
|
|
283
|
-
|
|
380
|
+
// most of a scope is file contents, which callers of this never ask for. the header says
|
|
381
|
+
// whether an object is wanted, so the rest of the file is only read and inflated when it is.
|
|
382
|
+
// transformed content has no header to read on its own, so it keeps to the single full read.
|
|
383
|
+
if (!this.isContentTransformed()) {
|
|
384
|
+
let type: string | null;
|
|
385
|
+
try {
|
|
386
|
+
type = await this.readTypeFromHeader(objectPath);
|
|
387
|
+
} catch (err: any) {
|
|
388
|
+
// an unregistered type is not one of the types asked for - the same answer the full read gives
|
|
389
|
+
if (err instanceof UnknownObjectType) return null;
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
if (type && !typeNames.includes(type)) return null;
|
|
393
|
+
}
|
|
394
|
+
const fileContents = this.onRead(await fs.readFile(objectPath));
|
|
395
|
+
return BitObject.parseObjectOnlyIfType(fileContents, typeNames, objectPath);
|
|
284
396
|
}
|
|
285
397
|
|
|
286
398
|
async listRefs(cwd = this.getPath()): Promise<Array<Ref>> {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teambit/objects",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.685",
|
|
4
4
|
"homepage": "https://bit.cloud/teambit/scope/objects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "teambit.scope",
|
|
8
8
|
"name": "objects",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.685"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"lodash": "4.17.21",
|
|
@@ -19,36 +19,36 @@
|
|
|
19
19
|
"semver": "7.7.1",
|
|
20
20
|
"uuid": "8.3.2",
|
|
21
21
|
"@teambit/harmony": "0.4.12",
|
|
22
|
+
"@teambit/cli": "0.0.1396",
|
|
22
23
|
"@teambit/component-id": "1.2.4",
|
|
24
|
+
"@teambit/component.snap-distance": "0.0.168",
|
|
23
25
|
"@teambit/graph.cleargraph": "0.0.11",
|
|
24
26
|
"@teambit/harmony.modules.concurrency": "0.0.46",
|
|
25
27
|
"@teambit/legacy.logger": "0.0.62",
|
|
28
|
+
"@teambit/legacy.scope": "0.0.167",
|
|
26
29
|
"@teambit/toolbox.promise.map-pool": "0.0.21",
|
|
27
30
|
"@teambit/legacy.constants": "0.0.45",
|
|
31
|
+
"@teambit/legacy.utils": "0.0.52",
|
|
28
32
|
"@teambit/toolbox.crypto.sha1": "0.0.22",
|
|
29
33
|
"@teambit/bit-error": "0.0.404",
|
|
30
34
|
"@teambit/component-version": "1.0.4",
|
|
31
35
|
"@teambit/harmony.modules.in-memory-cache": "0.0.49",
|
|
32
36
|
"@teambit/toolbox.fs.remove-empty-dir": "0.0.20",
|
|
33
37
|
"@teambit/lane-id": "0.0.312",
|
|
38
|
+
"@teambit/harmony.modules.get-basic-log": "0.0.168",
|
|
39
|
+
"@teambit/config-store": "0.0.277",
|
|
34
40
|
"@teambit/legacy.cli.error": "0.0.62",
|
|
41
|
+
"@teambit/component.sources": "0.0.219",
|
|
35
42
|
"@teambit/legacy-bit-id": "1.1.3",
|
|
36
43
|
"@teambit/legacy-component-log": "0.0.432",
|
|
44
|
+
"@teambit/legacy.consumer-component": "0.0.168",
|
|
45
|
+
"@teambit/legacy.consumer-config": "0.0.167",
|
|
46
|
+
"@teambit/legacy.extension-data": "0.0.169",
|
|
37
47
|
"@teambit/pkg.modules.semver-helper": "0.0.36",
|
|
38
48
|
"@teambit/toolbox.array.duplications-finder": "0.0.16",
|
|
39
49
|
"@teambit/bit.get-bit-version": "0.0.29",
|
|
40
|
-
"@teambit/
|
|
41
|
-
"@teambit/
|
|
42
|
-
"@teambit/graph": "1.0.1176",
|
|
43
|
-
"@teambit/legacy.scope": "0.0.166",
|
|
44
|
-
"@teambit/legacy.utils": "0.0.52",
|
|
45
|
-
"@teambit/harmony.modules.get-basic-log": "0.0.167",
|
|
46
|
-
"@teambit/config-store": "0.0.276",
|
|
47
|
-
"@teambit/component.sources": "0.0.218",
|
|
48
|
-
"@teambit/legacy.consumer-component": "0.0.167",
|
|
49
|
-
"@teambit/legacy.consumer-config": "0.0.166",
|
|
50
|
-
"@teambit/legacy.extension-data": "0.0.168",
|
|
51
|
-
"@teambit/semantics.doc-parser": "0.0.174"
|
|
50
|
+
"@teambit/semantics.doc-parser": "0.0.175",
|
|
51
|
+
"@teambit/graph": "1.0.1178"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/lodash": "4.14.165",
|
|
File without changes
|