@e280/mammoth 0.1.0-1 → 0.1.0-10
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 +114 -5
- package/package.json +7 -4
- package/s/browser/example.ts +10 -11
- package/s/browser/index.ts +1 -1
- package/s/browser/{iceberg-opfs.ts → opfs-bucket.ts} +2 -8
- package/s/core/{hash-stream.ts → analyze.ts} +6 -2
- package/s/core/consts.ts +8 -0
- package/s/core/index.ts +3 -2
- package/s/core/mammoth.ts +55 -45
- package/s/core/{iceberg-memory.ts → memory-bucket.ts} +2 -6
- package/s/core/streamify.ts +18 -0
- package/s/core/types.ts +37 -9
- package/s/core/utils/is-expired.ts +10 -0
- package/s/core/utils/manifest.ts +113 -0
- package/s/core/utils/not-found.ts +7 -0
- package/s/core/utils/save.ts +30 -0
- package/s/node/disk-bucket.ts +55 -0
- package/s/node/index.ts +4 -0
- package/s/node/utils/is-not-found.ts +9 -0
- package/s/test.ts +37 -12
- package/x/browser/example.js +10 -11
- package/x/browser/example.js.map +1 -1
- package/x/browser/index.d.ts +1 -1
- package/x/browser/index.js +1 -1
- package/x/browser/index.js.map +1 -1
- package/x/browser/{iceberg-opfs.d.ts → opfs-bucket.d.ts} +2 -3
- package/x/browser/{iceberg-opfs.js → opfs-bucket.js} +2 -7
- package/x/browser/opfs-bucket.js.map +1 -0
- package/x/core/analyze.d.ts +2 -0
- package/x/core/{hash-stream.js → analyze.js} +6 -3
- package/x/core/analyze.js.map +1 -0
- package/x/core/consts.d.ts +4 -0
- package/x/core/consts.js +6 -0
- package/x/core/consts.js.map +1 -0
- package/x/core/index.d.ts +3 -2
- package/x/core/index.js +3 -2
- package/x/core/index.js.map +1 -1
- package/x/core/mammoth.d.ts +7 -5
- package/x/core/mammoth.js +52 -40
- package/x/core/mammoth.js.map +1 -1
- package/x/core/{iceberg-memory.d.ts → memory-bucket.d.ts} +2 -3
- package/x/core/{iceberg-memory.js → memory-bucket.js} +2 -5
- package/x/core/memory-bucket.js.map +1 -0
- package/x/core/streamify.d.ts +1 -0
- package/x/core/streamify.js +15 -0
- package/x/core/streamify.js.map +1 -0
- package/x/core/types.d.ts +27 -9
- package/x/core/utils/is-expired.d.ts +2 -0
- package/x/core/utils/is-expired.js +7 -0
- package/x/core/utils/is-expired.js.map +1 -0
- package/x/core/utils/manifest.d.ts +19 -0
- package/x/core/utils/manifest.js +91 -0
- package/x/core/utils/manifest.js.map +1 -0
- package/x/core/utils/not-found.d.ts +2 -0
- package/x/core/utils/not-found.js +4 -0
- package/x/core/utils/not-found.js.map +1 -0
- package/x/core/utils/save.d.ts +2 -0
- package/x/core/utils/save.js +19 -0
- package/x/core/utils/save.js.map +1 -0
- package/x/node/disk-bucket.d.ts +9 -0
- package/x/node/disk-bucket.js +43 -0
- package/x/node/disk-bucket.js.map +1 -0
- package/x/node/index.d.ts +2 -0
- package/x/node/index.js +3 -0
- package/x/node/index.js.map +1 -0
- package/x/node/utils/is-not-found.d.ts +1 -0
- package/x/node/utils/is-not-found.js +6 -0
- package/x/node/utils/is-not-found.js.map +1 -0
- package/x/test.js +35 -12
- package/x/test.js.map +1 -1
- package/x/browser/iceberg-opfs.js.map +0 -1
- package/x/core/hash-stream.d.ts +0 -1
- package/x/core/hash-stream.js.map +0 -1
- package/x/core/iceberg-memory.js.map +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import {join} from "node:path"
|
|
3
|
+
import {Readable} from "node:stream"
|
|
4
|
+
import {pipeline} from "node:stream/promises"
|
|
5
|
+
import {mkdir, stat, unlink} from "node:fs/promises"
|
|
6
|
+
import {createWriteStream, openAsBlob} from "node:fs"
|
|
7
|
+
|
|
8
|
+
import {Bucket, Id} from "../core/types.js"
|
|
9
|
+
import {isNotFound} from "./utils/is-not-found.js"
|
|
10
|
+
|
|
11
|
+
export class DiskBucket implements Bucket {
|
|
12
|
+
#directory
|
|
13
|
+
|
|
14
|
+
constructor(directory: string) {
|
|
15
|
+
this.#directory = directory
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async has(id: Id) {
|
|
19
|
+
try {
|
|
20
|
+
await stat(this.#path(id))
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (isNotFound(error)) return false
|
|
25
|
+
throw error
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async delete(id: Id) {
|
|
30
|
+
try {
|
|
31
|
+
await unlink(this.#path(id))
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!isNotFound(error))
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async read(id: Id) {
|
|
40
|
+
return openAsBlob(this.#path(id))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async write(id: Id, readable: ReadableStream<Uint8Array>) {
|
|
44
|
+
await mkdir(this.#directory, {recursive: true})
|
|
45
|
+
await pipeline(
|
|
46
|
+
Readable.fromWeb(readable as any),
|
|
47
|
+
createWriteStream(this.#path(id)),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#path(id: Id) {
|
|
52
|
+
return join(this.#directory, id)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
package/s/node/index.ts
ADDED
package/s/test.ts
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
|
|
2
2
|
import {Kv} from "@e280/kv"
|
|
3
|
+
import {rm} from "fs/promises"
|
|
3
4
|
import {bytes, collect} from "@e280/stz"
|
|
4
5
|
import {science, test, expect} from "@e280/science"
|
|
5
6
|
|
|
6
7
|
import {Mammoth} from "./core/mammoth.js"
|
|
8
|
+
import {DiskBucket} from "./node/disk-bucket.js"
|
|
7
9
|
import {randomId} from "./core/utils/random-id.js"
|
|
8
|
-
import {
|
|
10
|
+
import {MemoryBucket} from "./core/memory-bucket.js"
|
|
9
11
|
|
|
10
12
|
const blob = () => new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])])
|
|
11
13
|
const quickstream = (b: number[]) => new Blob([new Uint8Array(b)]).stream()
|
|
12
14
|
|
|
13
15
|
function setup() {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const mammoth = new Mammoth(
|
|
16
|
+
const kv = new Kv()
|
|
17
|
+
const bucket = new MemoryBucket()
|
|
18
|
+
const mammoth = new Mammoth(bucket, kv)
|
|
17
19
|
return {mammoth}
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
await science.run({
|
|
21
|
-
"mammoth": {
|
|
23
|
+
"mammoth node": test(async() => {
|
|
24
|
+
const dataDir = "data"
|
|
25
|
+
await rm(dataDir, {recursive: true, force: true})
|
|
26
|
+
const mammoth = new Mammoth(new DiskBucket(dataDir), new Kv())
|
|
27
|
+
const hash = await mammoth.write(blob().stream())
|
|
28
|
+
const second = await mammoth.read(hash)
|
|
29
|
+
expect(bytes.eq(await second.bytes(), await blob().bytes()))
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
"mammoth memory": {
|
|
22
33
|
".write and .read": test(async() => {
|
|
23
34
|
const {mammoth} = setup()
|
|
24
35
|
const hash = await mammoth.write(blob().stream())
|
|
@@ -30,7 +41,7 @@ await science.run({
|
|
|
30
41
|
const {mammoth} = setup()
|
|
31
42
|
const hashA = await mammoth.write(blob().stream())
|
|
32
43
|
const hashB = await mammoth.write(blob().stream())
|
|
33
|
-
const keys = await collect(mammoth.
|
|
44
|
+
const keys = await collect(mammoth.hashes())
|
|
34
45
|
expect(hashA).is(hashB)
|
|
35
46
|
expect(keys.length).is(1)
|
|
36
47
|
expect(keys[0]).is(hashA)
|
|
@@ -39,7 +50,7 @@ await science.run({
|
|
|
39
50
|
".write empty file": test(async() => {
|
|
40
51
|
const {mammoth} = setup()
|
|
41
52
|
const hash = await mammoth.write(new Blob().stream())
|
|
42
|
-
expect(await mammoth.
|
|
53
|
+
expect((await mammoth.info(hash)).size).is(0)
|
|
43
54
|
expect((await mammoth.read(hash)).size).is(0)
|
|
44
55
|
}),
|
|
45
56
|
|
|
@@ -48,7 +59,20 @@ await science.run({
|
|
|
48
59
|
const hashA = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]))
|
|
49
60
|
const hashB = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]))
|
|
50
61
|
expect(hashA).not.is(hashB)
|
|
51
|
-
expect((await collect(mammoth.
|
|
62
|
+
expect((await collect(mammoth.hashes())).length).is(2)
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
".stats": test(async() => {
|
|
66
|
+
const {mammoth} = setup()
|
|
67
|
+
expect(await mammoth.stats()).deep({count: 0, size: 0})
|
|
68
|
+
const hashA = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]))
|
|
69
|
+
expect(await mammoth.stats()).deep({count: 1, size: 3})
|
|
70
|
+
const hashB = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]))
|
|
71
|
+
expect(await mammoth.stats()).deep({count: 2, size: 7})
|
|
72
|
+
await mammoth.delete(hashA)
|
|
73
|
+
expect(await mammoth.stats()).deep({count: 1, size: 4})
|
|
74
|
+
await mammoth.delete(hashB)
|
|
75
|
+
expect(await mammoth.stats()).deep({count: 0, size: 0})
|
|
52
76
|
}),
|
|
53
77
|
|
|
54
78
|
".has": test(async() => {
|
|
@@ -60,8 +84,9 @@ await science.run({
|
|
|
60
84
|
|
|
61
85
|
".size": test(async() => {
|
|
62
86
|
const {mammoth} = setup()
|
|
63
|
-
const
|
|
64
|
-
|
|
87
|
+
const b = blob()
|
|
88
|
+
const hash = await mammoth.write(b.stream())
|
|
89
|
+
expect((await mammoth.info(hash)).size).is(b.size)
|
|
65
90
|
}),
|
|
66
91
|
|
|
67
92
|
".delete": test(async() => {
|
|
@@ -69,7 +94,7 @@ await science.run({
|
|
|
69
94
|
const hash = await mammoth.write(blob().stream())
|
|
70
95
|
await mammoth.delete(hash)
|
|
71
96
|
expect(await mammoth.has(hash)).is(false)
|
|
72
|
-
expect(await collect(mammoth.
|
|
97
|
+
expect(await collect(mammoth.hashes())).deep([])
|
|
73
98
|
}),
|
|
74
99
|
|
|
75
100
|
".delete idempotent": test(async() => {
|
|
@@ -80,7 +105,7 @@ await science.run({
|
|
|
80
105
|
".keys": test(async() => {
|
|
81
106
|
const {mammoth} = setup()
|
|
82
107
|
const hash = await mammoth.write(blob().stream())
|
|
83
|
-
const keys = await collect(mammoth.
|
|
108
|
+
const keys = await collect(mammoth.hashes())
|
|
84
109
|
expect(keys.length).is(1)
|
|
85
110
|
expect(keys[0]).is(hash)
|
|
86
111
|
}),
|
package/x/browser/example.js
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { Kv, StorageDriver } from "@e280/kv";
|
|
2
2
|
import { Mammoth } from "../core/mammoth.js";
|
|
3
|
-
import {
|
|
3
|
+
import { OpfsBucket } from "./opfs-bucket.js";
|
|
4
4
|
export async function example() {
|
|
5
|
-
// setup the localstorage kv
|
|
5
|
+
// setup the localstorage kv
|
|
6
6
|
const driver = new StorageDriver(window.localStorage);
|
|
7
|
-
const kv = new Kv(driver);
|
|
8
|
-
|
|
9
|
-
// setup the opfs iceberg
|
|
7
|
+
const kv = new Kv(driver).scope("mammoth");
|
|
8
|
+
// setup the opfs file bucket
|
|
10
9
|
const root = await navigator.storage.getDirectory();
|
|
11
|
-
const directory = await root.getDirectoryHandle("
|
|
12
|
-
const
|
|
10
|
+
const directory = await root.getDirectoryHandle("mammoth", { create: true });
|
|
11
|
+
const bucket = new OpfsBucket(directory);
|
|
13
12
|
// setup the mammoth
|
|
14
|
-
const mammoth = new Mammoth(
|
|
15
|
-
const
|
|
13
|
+
const mammoth = new Mammoth(bucket, kv);
|
|
14
|
+
const myFile = new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])]);
|
|
16
15
|
// write a file as an example
|
|
17
|
-
const hash = await mammoth.write(
|
|
18
|
-
console.log(`mammoth stored file
|
|
16
|
+
const hash = await mammoth.write(myFile.stream());
|
|
17
|
+
console.log(`mammoth stored file "${hash}"`);
|
|
19
18
|
}
|
|
20
19
|
//# sourceMappingURL=example.js.map
|
package/x/browser/example.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"example.js","sourceRoot":"","sources":["../../s/browser/example.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAE,aAAa,EAAC,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAC,
|
|
1
|
+
{"version":3,"file":"example.js","sourceRoot":"","sources":["../../s/browser/example.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAE,aAAa,EAAC,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAE3C,MAAM,CAAC,KAAK,UAAU,OAAO;IAE5B,4BAA4B;IAC5B,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACrD,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAE1C,6BAA6B;IAC7B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;IACnD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,EAAE,EAAC,MAAM,EAAE,IAAI,EAAC,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAA;IAExC,oBAAoB;IACpB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACvC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAEnE,6BAA6B;IAC7B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IACjD,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,GAAG,CAAC,CAAA;AAC7C,CAAC"}
|
package/x/browser/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from "../core/index.js";
|
|
2
|
-
export * from "./
|
|
2
|
+
export * from "./opfs-bucket.js";
|
package/x/browser/index.js
CHANGED
package/x/browser/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/browser/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAA;AAChC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/browser/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA"}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare class
|
|
1
|
+
import { Bucket, Id } from "../core/types.js";
|
|
2
|
+
export declare class OpfsBucket implements Bucket {
|
|
3
3
|
#private;
|
|
4
4
|
constructor(directory: FileSystemDirectoryHandle);
|
|
5
5
|
has(id: Id): Promise<boolean>;
|
|
6
|
-
size(id: Id): Promise<number>;
|
|
7
6
|
delete(id: Id): Promise<void>;
|
|
8
7
|
read(id: Id): Promise<File>;
|
|
9
8
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isNotFound } from "./utils/is-not-found.js";
|
|
2
|
-
export class
|
|
2
|
+
export class OpfsBucket {
|
|
3
3
|
#directory;
|
|
4
4
|
constructor(directory) {
|
|
5
5
|
this.#directory = directory;
|
|
@@ -15,11 +15,6 @@ export class IcebergOpfs {
|
|
|
15
15
|
throw error;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
async size(id) {
|
|
19
|
-
const handle = await this.#directory.getFileHandle(id);
|
|
20
|
-
const file = await handle.getFile();
|
|
21
|
-
return file.size;
|
|
22
|
-
}
|
|
23
18
|
async delete(id) {
|
|
24
19
|
try {
|
|
25
20
|
await this.#directory.removeEntry(id);
|
|
@@ -39,4 +34,4 @@ export class IcebergOpfs {
|
|
|
39
34
|
await readable.pipeTo(writable);
|
|
40
35
|
}
|
|
41
36
|
}
|
|
42
|
-
//# sourceMappingURL=
|
|
37
|
+
//# sourceMappingURL=opfs-bucket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opfs-bucket.js","sourceRoot":"","sources":["../../s/browser/opfs-bucket.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAA;AAElD,MAAM,OAAO,UAAU;IACtB,UAAU,CAAA;IAEV,YAAY,SAAoC;QAC/C,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAM;QACf,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;YACvC,OAAO,IAAI,CAAA;QACZ,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACd,IAAI,UAAU,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAA;YACnC,MAAM,KAAK,CAAA;QACZ,CAAC;IACF,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAM;QAClB,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACtC,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACd,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;gBACrB,MAAM,KAAK,CAAA;QACb,CAAC;IACF,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAM;QAChB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QACtD,OAAO,MAAM,CAAC,OAAO,EAAE,CAAA;IACxB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAM,EAAE,QAAoC;QACvD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,EAAE,EAAC,MAAM,EAAE,IAAI,EAAC,CAAC,CAAA;QACtE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,cAAc,EAAE,CAAA;QAC9C,MAAM,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAChC,CAAC;CACD"}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { hex, nap } from "@e280/stz";
|
|
2
2
|
import { blake3 } from "@noble/hashes/blake3.js";
|
|
3
3
|
import { relaxer } from "./utils/relaxer.js";
|
|
4
|
-
export async function
|
|
4
|
+
export async function analyze(readable) {
|
|
5
|
+
let size = 0;
|
|
5
6
|
const hasher = blake3.create();
|
|
6
7
|
const relax = relaxer();
|
|
7
8
|
for await (const chunk of readable) {
|
|
8
9
|
hasher.update(chunk);
|
|
10
|
+
size += chunk.byteLength;
|
|
9
11
|
if (relax())
|
|
10
12
|
await nap();
|
|
11
13
|
}
|
|
12
|
-
|
|
14
|
+
const hash = hex.fromBytes(hasher.digest());
|
|
15
|
+
return { hash, size };
|
|
13
16
|
}
|
|
14
|
-
//# sourceMappingURL=
|
|
17
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../s/core/analyze.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAE,GAAG,EAAC,MAAM,WAAW,CAAA;AAClC,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAA;AAE9C,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAE1C,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAoC;IACjE,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;IAEvB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACpB,IAAI,IAAI,KAAK,CAAC,UAAU,CAAA;QAExB,IAAI,KAAK,EAAE;YACV,MAAM,GAAG,EAAE,CAAA;IACb,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IAC3C,OAAO,EAAC,IAAI,EAAE,IAAI,EAAC,CAAA;AACpB,CAAC"}
|
package/x/core/consts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../../s/core/consts.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAE9B,MAAM,CAAC,MAAM,MAAM,GAAG;IACrB,QAAQ,EAAE,EAAE;IACZ,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;CACzB,CAAA"}
|
package/x/core/index.d.ts
CHANGED
package/x/core/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./iceberg-memory.js";
|
|
1
|
+
export * from "./analyze.js";
|
|
3
2
|
export * from "./mammoth.js";
|
|
3
|
+
export * from "./memory-bucket.js";
|
|
4
|
+
export * from "./streamify.js";
|
|
4
5
|
export * from "./types.js";
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/x/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/core/index.ts"],"names":[],"mappings":"AACA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/core/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|
package/x/core/mammoth.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Kv } from "@e280/kv";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { Hash, Bucket } from "./types.js";
|
|
3
|
+
/** file storage datalake, content-addressed with blake3 hashes. */
|
|
4
|
+
export declare class Mammoth {
|
|
4
5
|
#private;
|
|
5
|
-
constructor(
|
|
6
|
+
constructor(bucket?: Bucket, kv?: Kv<any>);
|
|
7
|
+
hashes(): AsyncGenerator<string, void, unknown>;
|
|
6
8
|
has(hash: Hash): Promise<boolean>;
|
|
7
|
-
|
|
9
|
+
info(hash: Hash): Promise<import("./types.js").Info>;
|
|
10
|
+
stats(): Promise<import("./types.js").Stats>;
|
|
8
11
|
read(hash: Hash): Promise<Blob>;
|
|
9
12
|
delete(hash: Hash): Promise<void>;
|
|
10
13
|
write(readable: ReadableStream<Uint8Array>): Promise<Hash>;
|
|
11
|
-
keys(): AsyncGenerator<string, void, unknown>;
|
|
12
14
|
}
|
package/x/core/mammoth.js
CHANGED
|
@@ -1,56 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Kv } from "@e280/kv";
|
|
2
|
+
import { lane, queue } from "@e280/stz";
|
|
3
|
+
import { consts } from "./consts.js";
|
|
4
|
+
import { save } from "./utils/save.js";
|
|
5
|
+
import { Manifest } from "./utils/manifest.js";
|
|
3
6
|
import { randomId } from "./utils/random-id.js";
|
|
7
|
+
import { MemoryBucket } from "./memory-bucket.js";
|
|
8
|
+
/** file storage datalake, content-addressed with blake3 hashes. */
|
|
4
9
|
export class Mammoth {
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this.#
|
|
10
|
+
#bucket;
|
|
11
|
+
#manifest;
|
|
12
|
+
#wholesome = lane(consts.max_jobs);
|
|
13
|
+
constructor(bucket = new MemoryBucket(), kv = new Kv()) {
|
|
14
|
+
this.#bucket = bucket;
|
|
15
|
+
this.#manifest = new Manifest(kv);
|
|
16
|
+
}
|
|
17
|
+
async *hashes() {
|
|
18
|
+
yield* this.#manifest.hashes();
|
|
10
19
|
}
|
|
11
20
|
async has(hash) {
|
|
12
|
-
return this.#
|
|
21
|
+
return this.#manifest.hasHash(hash);
|
|
22
|
+
}
|
|
23
|
+
async info(hash) {
|
|
24
|
+
return this.#manifest.needInfo(hash);
|
|
13
25
|
}
|
|
14
|
-
async
|
|
15
|
-
|
|
16
|
-
return this.#iceberg.size(id);
|
|
26
|
+
async stats() {
|
|
27
|
+
return this.#manifest.getStats();
|
|
17
28
|
}
|
|
18
29
|
async read(hash) {
|
|
19
|
-
const id = await this.#
|
|
20
|
-
return this.#
|
|
30
|
+
const { id } = await this.#manifest.needInfo(hash);
|
|
31
|
+
return this.#bucket.read(id);
|
|
21
32
|
}
|
|
22
33
|
async delete(hash) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
34
|
+
await this.#wholesome(async () => {
|
|
35
|
+
const info = await this.#manifest.getInfo(hash);
|
|
36
|
+
if (info)
|
|
37
|
+
await this.#manifest.scheduleDeletion(hash, info);
|
|
38
|
+
});
|
|
39
|
+
await this.#cleanup();
|
|
28
40
|
}
|
|
29
41
|
async write(readable) {
|
|
30
42
|
const id = randomId();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await
|
|
37
|
-
|
|
43
|
+
await this.#manifest.addWip(id);
|
|
44
|
+
try {
|
|
45
|
+
const analysis = await save(this.#bucket, id, readable);
|
|
46
|
+
const { hash, size } = analysis;
|
|
47
|
+
const info = { id, size, added: Date.now() };
|
|
48
|
+
await this.#wholesome(() => this.#manifest.commit(hash, info));
|
|
49
|
+
return analysis.hash;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
await this.#manifest.moveWipToTrash(id);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
void this.#cleanup().catch(e => console.error("cleanup failed", e));
|
|
38
57
|
}
|
|
39
|
-
await writer.close();
|
|
40
|
-
await done;
|
|
41
|
-
const hash = hex.fromBytes(hasher.digest());
|
|
42
|
-
await this.delete(hash);
|
|
43
|
-
await this.#index.set(hash, id);
|
|
44
|
-
return hash;
|
|
45
|
-
}
|
|
46
|
-
async #getFileId(hash) {
|
|
47
|
-
return this.#index.get(hash);
|
|
48
|
-
}
|
|
49
|
-
async #needFileId(hash) {
|
|
50
|
-
return got(await this.#getFileId(hash), `file not found by hash "${hash}"`);
|
|
51
|
-
}
|
|
52
|
-
async *keys() {
|
|
53
|
-
yield* this.#index.keys();
|
|
54
58
|
}
|
|
59
|
+
#cleanup = queue(async () => {
|
|
60
|
+
for await (const id of this.#manifest.getExpiredWipIds())
|
|
61
|
+
await this.#manifest.moveWipToTrash(id);
|
|
62
|
+
for await (const id of this.#manifest.listTrashIds()) {
|
|
63
|
+
await this.#bucket.delete(id);
|
|
64
|
+
await this.#manifest.dropTrashRecord(id);
|
|
65
|
+
}
|
|
66
|
+
}, consts.max_jobs);
|
|
55
67
|
}
|
|
56
68
|
//# sourceMappingURL=mammoth.js.map
|
package/x/core/mammoth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mammoth.js","sourceRoot":"","sources":["../../s/core/mammoth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mammoth.js","sourceRoot":"","sources":["../../s/core/mammoth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare class
|
|
1
|
+
import { Bucket, Id } from "./types.js";
|
|
2
|
+
export declare class MemoryBucket implements Bucket {
|
|
3
3
|
#private;
|
|
4
4
|
has(id: Id): Promise<boolean>;
|
|
5
|
-
size(id: Id): Promise<number>;
|
|
6
5
|
delete(id: Id): Promise<void>;
|
|
7
6
|
read(id: Id): Promise<Blob>;
|
|
8
7
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { bytes, got, nap } from "@e280/stz";
|
|
2
2
|
import { relaxer } from "./utils/relaxer.js";
|
|
3
|
-
export class
|
|
3
|
+
export class MemoryBucket {
|
|
4
4
|
#map = new Map();
|
|
5
5
|
async has(id) {
|
|
6
6
|
return this.#map.has(id);
|
|
7
7
|
}
|
|
8
|
-
async size(id) {
|
|
9
|
-
return got(this.#map.get(id)).byteLength;
|
|
10
|
-
}
|
|
11
8
|
async delete(id) {
|
|
12
9
|
this.#map.delete(id);
|
|
13
10
|
}
|
|
@@ -26,4 +23,4 @@ export class IcebergMemory {
|
|
|
26
23
|
this.#map.set(id, payload);
|
|
27
24
|
}
|
|
28
25
|
}
|
|
29
|
-
//# sourceMappingURL=
|
|
26
|
+
//# sourceMappingURL=memory-bucket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-bucket.js","sourceRoot":"","sources":["../../s/core/memory-bucket.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAC,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAE1C,MAAM,OAAO,YAAY;IACxB,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAA;IAEpC,KAAK,CAAC,GAAG,CAAC,EAAM;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAM;QAClB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACrB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAM;QAChB,OAAO,IAAI,IAAI,CACd,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAA4B,CAAC,EACnD,EAAC,IAAI,EAAE,0BAA0B,EAAC,CAClC,CAAA;IACF,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAM,EAAE,QAAoC;QACvD,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;QACvB,MAAM,KAAK,GAAiB,EAAE,CAAA;QAE9B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YACpC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACjB,IAAI,KAAK,EAAE;gBAAE,MAAM,GAAG,EAAE,CAAA;QACzB,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAC3B,CAAC;CACD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function streamify(bytes: Uint8Array, chunkSize?: number): ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function streamify(bytes, chunkSize = 64 * 1024) {
|
|
2
|
+
let offset = 0;
|
|
3
|
+
return new ReadableStream({
|
|
4
|
+
pull(controller) {
|
|
5
|
+
if (offset >= bytes.length) {
|
|
6
|
+
controller.close();
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const end = Math.min(offset + chunkSize, bytes.length);
|
|
10
|
+
controller.enqueue(bytes.subarray(offset, end));
|
|
11
|
+
offset = end;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=streamify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamify.js","sourceRoot":"","sources":["../../s/core/streamify.ts"],"names":[],"mappings":"AACA,MAAM,UAAU,SAAS,CAAC,KAAiB,EAAE,SAAS,GAAG,EAAE,GAAG,IAAI;IACjE,IAAI,MAAM,GAAG,CAAC,CAAA;IAEd,OAAO,IAAI,cAAc,CAAa;QACrC,IAAI,CAAC,UAAU;YACd,IAAI,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC5B,UAAU,CAAC,KAAK,EAAE,CAAA;gBAClB,OAAM;YACP,CAAC;YAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;YACtD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;YAC/C,MAAM,GAAG,GAAG,CAAA;QACb,CAAC;KACD,CAAC,CAAA;AACH,CAAC"}
|
package/x/core/types.d.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
export type Hash = string;
|
|
2
2
|
export type Id = string;
|
|
3
|
-
|
|
3
|
+
/** file blob store. */
|
|
4
|
+
export type Bucket = {
|
|
4
5
|
has(id: Id): Promise<boolean>;
|
|
5
|
-
size(id: Id): Promise<number>;
|
|
6
6
|
delete(id: Id): Promise<void>;
|
|
7
7
|
read(id: Id): Promise<Blob>;
|
|
8
8
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>;
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/** report about a file */
|
|
11
|
+
export type Analysis = {
|
|
12
|
+
/** blake3 hash of the file's contents */
|
|
13
|
+
hash: Hash;
|
|
14
|
+
/** filesize in bytes */
|
|
15
|
+
size: number;
|
|
16
|
+
};
|
|
17
|
+
/** metadata for a single file. */
|
|
18
|
+
export type Info = {
|
|
19
|
+
/** bucket id for this file's data. */
|
|
20
|
+
id: Id;
|
|
21
|
+
/** filesize in bytes. */
|
|
22
|
+
size: number;
|
|
23
|
+
/** when this file was added to the datalake. */
|
|
24
|
+
added: number;
|
|
25
|
+
};
|
|
26
|
+
/** statistics for the whole datalake. */
|
|
27
|
+
export type Stats = {
|
|
28
|
+
/** total number of bytes in the whole datalake. */
|
|
29
|
+
size: number;
|
|
30
|
+
/** total number of files in the datalake. */
|
|
31
|
+
count: number;
|
|
32
|
+
};
|
|
33
|
+
export type Wip = {
|
|
34
|
+
created: number;
|
|
17
35
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-expired.js","sourceRoot":"","sources":["../../../s/core/utils/is-expired.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAE9B,OAAO,EAAC,MAAM,EAAC,MAAM,cAAc,CAAA;AAEnC,MAAM,UAAU,SAAS,CAAC,EAAC,OAAO,EAAM;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAA;IAClC,OAAO,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;AAC7C,CAAC"}
|