@e280/mammoth 0.1.0-3 → 0.1.0-5
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 +89 -5
- package/package.json +3 -2
- package/s/browser/example.ts +9 -10
- package/s/browser/index.ts +1 -1
- package/s/browser/{iceberg-opfs.ts → opfs-bucket.ts} +2 -2
- package/s/core/index.ts +1 -1
- package/s/core/mammoth.ts +66 -38
- package/s/core/{iceberg-memory.ts → memory-bucket.ts} +2 -2
- package/s/core/types.ts +13 -8
- package/s/core/utils/is-expired.ts +9 -0
- package/s/core/utils/not-found.ts +7 -0
- package/s/core/utils/save-and-hash.ts +25 -0
- package/s/node/disk-bucket.ts +60 -0
- package/s/node/index.ts +4 -0
- package/s/node/utils/is-not-found.ts +9 -0
- package/s/test.ts +33 -9
- package/x/browser/example.js +9 -10
- 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 -2
- package/x/browser/{iceberg-opfs.js → opfs-bucket.js} +2 -2
- package/x/browser/opfs-bucket.js.map +1 -0
- package/x/core/index.d.ts +1 -1
- package/x/core/index.js +1 -1
- package/x/core/index.js.map +1 -1
- package/x/core/mammoth.d.ts +6 -4
- package/x/core/mammoth.js +62 -35
- package/x/core/mammoth.js.map +1 -1
- package/x/core/{iceberg-memory.d.ts → memory-bucket.d.ts} +2 -2
- package/x/core/{iceberg-memory.js → memory-bucket.js} +2 -2
- package/x/core/memory-bucket.js.map +1 -0
- package/x/core/types.d.ts +10 -8
- package/x/core/utils/is-expired.d.ts +2 -0
- package/x/core/utils/is-expired.js +6 -0
- package/x/core/utils/is-expired.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-and-hash.d.ts +5 -0
- package/x/core/utils/save-and-hash.js +19 -0
- package/x/core/utils/save-and-hash.js.map +1 -0
- package/x/node/disk-bucket.d.ts +10 -0
- package/x/node/disk-bucket.js +47 -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 +31 -9
- package/x/test.js.map +1 -1
- package/x/browser/iceberg-opfs.js.map +0 -1
- package/x/core/iceberg-memory.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,97 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+

|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# 🦣 mammoth
|
|
5
|
+
> *big files. small api.*
|
|
6
|
+
|
|
7
|
+
**mammoth is a content-addressed file store.** files are streamed into a bucket, identified by their blake3 hash, and automatically deduplicated. mammoth works the same whether it's backed by a bucket in memory, on disk, in the cloud, or in the browser's opfs.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @e280/mammoth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {Mammoth} from "@e280/mammoth"
|
|
15
|
+
|
|
16
|
+
const mammoth = new Mammoth()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 🦣 mammoth stores files.
|
|
20
|
+
- **write a file,** and you get back its blake3 hash.
|
|
21
|
+
```ts
|
|
22
|
+
const hash = await mammoth.write(blob.stream())
|
|
23
|
+
```
|
|
24
|
+
- **read a file,** identified by its hash.
|
|
25
|
+
```ts
|
|
26
|
+
const blob = await mammoth.read(hash)
|
|
27
|
+
```
|
|
28
|
+
- **delete a file.**
|
|
29
|
+
```ts
|
|
30
|
+
await mammoth.delete(hash)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 🦣 mammoth is bucket-agnostic.
|
|
34
|
+
- **memory bucket,** for testing.
|
|
35
|
+
```ts
|
|
36
|
+
import {Mammoth, MemoryBucket} from "@e280/mammoth"
|
|
37
|
+
import {Kv} from "@e280/kv"
|
|
38
|
+
|
|
39
|
+
const mammoth = new Mammoth(
|
|
40
|
+
new MemoryBucket(),
|
|
41
|
+
new Kv(),
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
defaults shown, so, this is equivalent:
|
|
45
|
+
```ts
|
|
46
|
+
const mammoth = new Mammoth()
|
|
47
|
+
```
|
|
48
|
+
- **disk bucket,** for nodejs servers. *(note the import paths)*
|
|
49
|
+
```ts
|
|
50
|
+
import {Mammoth} from "@e280/mammoth"
|
|
51
|
+
import {DiskBucket} from "@e280/mammoth/node"
|
|
52
|
+
import {Kv} from "@e280/kv"
|
|
53
|
+
import {LevelDriver} from "@e280/kv/level"
|
|
54
|
+
|
|
55
|
+
const mammoth = new Mammoth(
|
|
56
|
+
new DiskBucket("./data/bucket"),
|
|
57
|
+
new Kv(new LevelDriver("./data/kv")),
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
- **opfs bucket,** for local storage in the browser. *(note the import paths)*
|
|
61
|
+
```ts
|
|
62
|
+
import {Mammoth} from "@e280/mammoth"
|
|
63
|
+
import {OpfsBucket} from "@e280/mammoth/browser"
|
|
64
|
+
import {Kv, StorageDriver} from "@e280/kv"
|
|
65
|
+
|
|
66
|
+
const mammoth = new Mammoth(
|
|
67
|
+
new OpfsBucket(await navigator.storage.getDirectory()),
|
|
68
|
+
new Kv(new StorageDriver(localStorage)),
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 🦣 more mammoth methods.
|
|
73
|
+
- **check if a file exists,** get back a boolean.
|
|
74
|
+
```ts
|
|
75
|
+
const exists = await mammoth.has(hash)
|
|
76
|
+
```
|
|
77
|
+
- **get file size,** in bytes.
|
|
78
|
+
```ts
|
|
79
|
+
const size = await mammoth.size(hash)
|
|
80
|
+
```
|
|
81
|
+
- **get stats,** for the whole datalake.
|
|
82
|
+
```ts
|
|
83
|
+
await mammoth.stats()
|
|
84
|
+
// {count: 123, size: 123456789}
|
|
85
|
+
```
|
|
86
|
+
- **loop over all hashes,** for all stored files.
|
|
87
|
+
```ts
|
|
88
|
+
for await (const hash of mammoth.hashes())
|
|
89
|
+
console.log(hash)
|
|
90
|
+
```
|
|
5
91
|
|
|
6
92
|
|
|
7
93
|
|
|
8
94
|
<br/><br/>
|
|
9
95
|
|
|
10
|
-
|
|
11
|
-
reward us with github stars
|
|
12
|
-
build with us at https://e280.org/ but only if you're cool
|
|
96
|
+
*https://e280.org/*
|
|
13
97
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e280/mammoth",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-5",
|
|
4
4
|
"description": "big file storage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chase Moskal <chasemoskal@gmail.com>",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"sideEffects": false,
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./x/core/index.js",
|
|
12
|
-
"./browser": "./x/browser/index.js"
|
|
12
|
+
"./browser": "./x/browser/index.js",
|
|
13
|
+
"./node": "./x/node/index.js"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"x",
|
package/s/browser/example.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
|
|
2
2
|
import {Kv, StorageDriver} from "@e280/kv"
|
|
3
3
|
import {Mammoth} from "../core/mammoth.js"
|
|
4
|
-
import {
|
|
4
|
+
import {OpfsBucket} from "./opfs-bucket.js"
|
|
5
5
|
|
|
6
6
|
export async function example() {
|
|
7
7
|
|
|
8
|
-
// setup the localstorage kv
|
|
8
|
+
// setup the localstorage kv
|
|
9
9
|
const driver = new StorageDriver(window.localStorage)
|
|
10
|
-
const kv = new Kv(driver)
|
|
11
|
-
const index = kv.scope("glacier_index")
|
|
10
|
+
const kv = new Kv(driver).scope("mammoth")
|
|
12
11
|
|
|
13
|
-
// setup the opfs
|
|
12
|
+
// setup the opfs file bucket
|
|
14
13
|
const root = await navigator.storage.getDirectory()
|
|
15
|
-
const directory = await root.getDirectoryHandle("
|
|
16
|
-
const
|
|
14
|
+
const directory = await root.getDirectoryHandle("mammoth", {create: true})
|
|
15
|
+
const bucket = new OpfsBucket(directory)
|
|
17
16
|
|
|
18
17
|
// setup the mammoth
|
|
19
|
-
const mammoth = new Mammoth(
|
|
20
|
-
const
|
|
18
|
+
const mammoth = new Mammoth(bucket, kv)
|
|
19
|
+
const myFile = new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])])
|
|
21
20
|
|
|
22
21
|
// write a file as an example
|
|
23
|
-
const hash = await mammoth.write(
|
|
22
|
+
const hash = await mammoth.write(myFile.stream())
|
|
24
23
|
console.log(`mammoth stored file by hash "${hash}"`)
|
|
25
24
|
}
|
|
26
25
|
|
package/s/browser/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
2
|
+
import {Bucket, Id} from "../core/types.js"
|
|
3
3
|
import {isNotFound} from "./utils/is-not-found.js"
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class OpfsBucket implements Bucket {
|
|
6
6
|
#directory
|
|
7
7
|
|
|
8
8
|
constructor(directory: FileSystemDirectoryHandle) {
|
package/s/core/index.ts
CHANGED
package/s/core/mammoth.ts
CHANGED
|
@@ -1,72 +1,100 @@
|
|
|
1
1
|
|
|
2
2
|
import {Kv} from "@e280/kv"
|
|
3
|
-
import {got,
|
|
4
|
-
import {blake3} from "@noble/hashes/blake3.js"
|
|
3
|
+
import {collect, got, queue} from "@e280/stz"
|
|
5
4
|
|
|
6
5
|
import {randomId} from "./utils/random-id.js"
|
|
7
|
-
import {
|
|
6
|
+
import {notFound} from "./utils/not-found.js"
|
|
7
|
+
import {MemoryBucket} from "./memory-bucket.js"
|
|
8
|
+
import {isExpired} from "./utils/is-expired.js"
|
|
9
|
+
import {saveAndHash} from "./utils/save-and-hash.js"
|
|
10
|
+
import {Hash, Bucket, Id, Stats, Wip} from "./types.js"
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
12
|
+
/** file storage datalake, content-addressed with blake3 hashes. */
|
|
13
|
+
export class Mammoth {
|
|
14
|
+
#ids // associates file hashes with bucket ids
|
|
15
|
+
#wip // temporary record of writes in-progress
|
|
16
|
+
#stats // statistics about the whole datalake
|
|
17
|
+
#bucket // file blobstore
|
|
12
18
|
|
|
13
|
-
constructor(
|
|
14
|
-
this.#
|
|
15
|
-
this.#
|
|
19
|
+
constructor(bucket: Bucket = new MemoryBucket(), kv = new Kv()) {
|
|
20
|
+
this.#ids = kv.scope<string>("ids")
|
|
21
|
+
this.#wip = kv.scope<Wip>("wip")
|
|
22
|
+
this.#stats = kv.store<Stats>("stats")
|
|
23
|
+
this.#bucket = bucket
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
async has(hash: Hash) {
|
|
19
|
-
return this.#
|
|
27
|
+
return this.#ids.has(hash)
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
async size(hash: Hash) {
|
|
23
|
-
const id = await this.#
|
|
24
|
-
return this.#
|
|
31
|
+
const id = got(await this.#ids.get(hash), notFound(hash))
|
|
32
|
+
return this.#bucket.size(id)
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
async read(hash: Hash) {
|
|
28
|
-
const id = await this.#
|
|
29
|
-
return this.#
|
|
36
|
+
const id = got(await this.#ids.get(hash), notFound(hash))
|
|
37
|
+
return this.#bucket.read(id)
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
async delete(hash: Hash) {
|
|
33
|
-
const id = await this.#
|
|
41
|
+
const id = await this.#ids.get(hash)
|
|
34
42
|
if (id) {
|
|
35
|
-
await this.#
|
|
36
|
-
await this.#
|
|
43
|
+
const size = await this.#bucket.size(id)
|
|
44
|
+
await this.#ids.del(hash)
|
|
45
|
+
await this.#bucket.delete(id)
|
|
46
|
+
await this.#updateStats(stats => {
|
|
47
|
+
stats.count--
|
|
48
|
+
stats.size -= size
|
|
49
|
+
})
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
async write(readable: ReadableStream<Uint8Array>): Promise<Hash> {
|
|
41
54
|
const id = randomId()
|
|
42
|
-
const pipe = new TransformStream()
|
|
43
|
-
const done = this.#iceberg.write(id, pipe.readable)
|
|
44
|
-
const writer = pipe.writable.getWriter()
|
|
45
|
-
const hasher = blake3.create()
|
|
46
|
-
|
|
47
|
-
for await (const chunk of readable) {
|
|
48
|
-
await writer.write(chunk as Uint8Array<ArrayBuffer>)
|
|
49
|
-
hasher.update(chunk)
|
|
50
|
-
}
|
|
51
55
|
|
|
52
|
-
await
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
await this.#
|
|
56
|
+
await this.#wip.set(id, {created: Date.now()})
|
|
57
|
+
const {hash, size} = await saveAndHash(this.#bucket, id, readable)
|
|
58
|
+
await this.#wip.del(id)
|
|
59
|
+
|
|
60
|
+
await this.#finalizeWrite(hash, id, size)
|
|
61
|
+
void this.#selfClean().catch(() => {})
|
|
57
62
|
return hash
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
async
|
|
61
|
-
|
|
65
|
+
async* hashes() {
|
|
66
|
+
yield* this.#ids.keys()
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
async
|
|
65
|
-
return
|
|
69
|
+
async stats() {
|
|
70
|
+
return structuredClone((await this.#stats.get()) ?? {count: 0, size: 0})
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
async
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
#updateStats = queue(async(fn: (stats: Stats) => void) => {
|
|
74
|
+
const stats = await this.stats()
|
|
75
|
+
fn(stats)
|
|
76
|
+
await this.#stats.set(stats)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
#finalizeWrite = queue(async(hash: Hash, id: Id, size: number) => {
|
|
80
|
+
if (await this.#ids.has(hash)) {
|
|
81
|
+
await this.#bucket.delete(id) // forget this new file (we already have it)
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await this.#ids.set(hash, id)
|
|
85
|
+
await this.#updateStats(stats => {
|
|
86
|
+
stats.count++
|
|
87
|
+
stats.size += size
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
#selfClean = queue(async() => {
|
|
93
|
+
const expiredIds = (await collect(this.#wip.entries({limit: 64})))
|
|
94
|
+
.filter(([,wip]) => isExpired(wip))
|
|
95
|
+
.map(([id]) => id)
|
|
96
|
+
await Promise.all(expiredIds.map(id => this.#bucket.delete(id)))
|
|
97
|
+
await this.#wip.del(...expiredIds)
|
|
98
|
+
})
|
|
71
99
|
}
|
|
72
100
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import {bytes, got, nap} from "@e280/stz"
|
|
3
|
-
import {
|
|
3
|
+
import {Bucket, Id} from "./types.js"
|
|
4
4
|
import {relaxer} from "./utils/relaxer.js"
|
|
5
5
|
|
|
6
|
-
export class
|
|
6
|
+
export class MemoryBucket implements Bucket {
|
|
7
7
|
#map = new Map<string, Uint8Array>()
|
|
8
8
|
|
|
9
9
|
async has(id: Id) {
|
package/s/core/types.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
export type Hash = string
|
|
3
3
|
export type Id = string
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** file blob store. */
|
|
6
|
+
export type Bucket = {
|
|
6
7
|
has(id: Id): Promise<boolean>
|
|
7
8
|
size(id: Id): Promise<number>
|
|
8
9
|
delete(id: Id): Promise<void>
|
|
@@ -10,12 +11,16 @@ export type Iceberg = {
|
|
|
10
11
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
export type Stats = {
|
|
15
|
+
|
|
16
|
+
/** total number of bytes in the whole datalake. */
|
|
17
|
+
size: number
|
|
18
|
+
|
|
19
|
+
/** total number of known files in the datalake. */
|
|
20
|
+
count: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Wip = {
|
|
24
|
+
created: number
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import {hex} from "@e280/stz"
|
|
3
|
+
import {blake3} from "@noble/hashes/blake3.js"
|
|
4
|
+
import {Bucket, Id} from "../types.js"
|
|
5
|
+
|
|
6
|
+
export async function saveAndHash(bucket: Bucket, id: Id, readable: ReadableStream<Uint8Array>) {
|
|
7
|
+
let size = 0
|
|
8
|
+
const pipe = new TransformStream()
|
|
9
|
+
const done = bucket.write(id, pipe.readable)
|
|
10
|
+
const writer = pipe.writable.getWriter()
|
|
11
|
+
const hasher = blake3.create()
|
|
12
|
+
|
|
13
|
+
for await (const chunk of readable) {
|
|
14
|
+
await writer.write(chunk as Uint8Array<ArrayBuffer>)
|
|
15
|
+
hasher.update(chunk)
|
|
16
|
+
size += chunk.byteLength
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await writer.close()
|
|
20
|
+
await done
|
|
21
|
+
|
|
22
|
+
const hash = hex.fromBytes(hasher.digest())
|
|
23
|
+
return {hash, size}
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
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 size(id: Id) {
|
|
30
|
+
const stats = await stat(this.#path(id))
|
|
31
|
+
return stats.size
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async delete(id: Id) {
|
|
35
|
+
try {
|
|
36
|
+
await unlink(this.#path(id))
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!isNotFound(error))
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async read(id: Id) {
|
|
45
|
+
return openAsBlob(this.#path(id))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async write(id: Id, readable: ReadableStream<Uint8Array>) {
|
|
49
|
+
await mkdir(this.#directory, {recursive: true})
|
|
50
|
+
await pipeline(
|
|
51
|
+
Readable.fromWeb(readable as any),
|
|
52
|
+
createWriteStream(this.#path(id)),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#path(id: Id) {
|
|
57
|
+
return join(this.#directory, id)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
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)
|
|
@@ -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() => {
|
|
@@ -69,7 +93,7 @@ await science.run({
|
|
|
69
93
|
const hash = await mammoth.write(blob().stream())
|
|
70
94
|
await mammoth.delete(hash)
|
|
71
95
|
expect(await mammoth.has(hash)).is(false)
|
|
72
|
-
expect(await collect(mammoth.
|
|
96
|
+
expect(await collect(mammoth.hashes())).deep([])
|
|
73
97
|
}),
|
|
74
98
|
|
|
75
99
|
".delete idempotent": test(async() => {
|
|
@@ -80,7 +104,7 @@ await science.run({
|
|
|
80
104
|
".keys": test(async() => {
|
|
81
105
|
const {mammoth} = setup()
|
|
82
106
|
const hash = await mammoth.write(blob().stream())
|
|
83
|
-
const keys = await collect(mammoth.
|
|
107
|
+
const keys = await collect(mammoth.hashes())
|
|
84
108
|
expect(keys.length).is(1)
|
|
85
109
|
expect(keys[0]).is(hash)
|
|
86
110
|
}),
|
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(
|
|
16
|
+
const hash = await mammoth.write(myFile.stream());
|
|
18
17
|
console.log(`mammoth stored file by hash "${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,gCAAgC,IAAI,GAAG,CAAC,CAAA;AACrD,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,5 +1,5 @@
|
|
|
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>;
|
|
@@ -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;
|
|
@@ -39,4 +39,4 @@ export class IcebergOpfs {
|
|
|
39
39
|
await readable.pipeTo(writable);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
//# sourceMappingURL=
|
|
42
|
+
//# 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,IAAI,CAAC,EAAM;QAChB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QACtD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;QACnC,OAAO,IAAI,CAAC,IAAI,CAAA;IACjB,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"}
|
package/x/core/index.d.ts
CHANGED
package/x/core/index.js
CHANGED
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,kBAAkB,CAAA;AAChC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/core/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,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, Stats } 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>);
|
|
6
7
|
has(hash: Hash): Promise<boolean>;
|
|
7
8
|
size(hash: Hash): Promise<number>;
|
|
8
9
|
read(hash: Hash): Promise<Blob>;
|
|
9
10
|
delete(hash: Hash): Promise<void>;
|
|
10
11
|
write(readable: ReadableStream<Uint8Array>): Promise<Hash>;
|
|
11
|
-
|
|
12
|
+
hashes(): AsyncGenerator<string, void, unknown>;
|
|
13
|
+
stats(): Promise<Stats>;
|
|
12
14
|
}
|
package/x/core/mammoth.js
CHANGED
|
@@ -1,56 +1,83 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Kv } from "@e280/kv";
|
|
2
|
+
import { collect, got, queue } from "@e280/stz";
|
|
3
3
|
import { randomId } from "./utils/random-id.js";
|
|
4
|
+
import { notFound } from "./utils/not-found.js";
|
|
5
|
+
import { MemoryBucket } from "./memory-bucket.js";
|
|
6
|
+
import { isExpired } from "./utils/is-expired.js";
|
|
7
|
+
import { saveAndHash } from "./utils/save-and-hash.js";
|
|
8
|
+
/** file storage datalake, content-addressed with blake3 hashes. */
|
|
4
9
|
export class Mammoth {
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
#ids; // associates file hashes with bucket ids
|
|
11
|
+
#wip; // temporary record of writes in-progress
|
|
12
|
+
#stats; // statistics about the whole datalake
|
|
13
|
+
#bucket; // file blobstore
|
|
14
|
+
constructor(bucket = new MemoryBucket(), kv = new Kv()) {
|
|
15
|
+
this.#ids = kv.scope("ids");
|
|
16
|
+
this.#wip = kv.scope("wip");
|
|
17
|
+
this.#stats = kv.store("stats");
|
|
18
|
+
this.#bucket = bucket;
|
|
10
19
|
}
|
|
11
20
|
async has(hash) {
|
|
12
|
-
return this.#
|
|
21
|
+
return this.#ids.has(hash);
|
|
13
22
|
}
|
|
14
23
|
async size(hash) {
|
|
15
|
-
const id = await this.#
|
|
16
|
-
return this.#
|
|
24
|
+
const id = got(await this.#ids.get(hash), notFound(hash));
|
|
25
|
+
return this.#bucket.size(id);
|
|
17
26
|
}
|
|
18
27
|
async read(hash) {
|
|
19
|
-
const id = await this.#
|
|
20
|
-
return this.#
|
|
28
|
+
const id = got(await this.#ids.get(hash), notFound(hash));
|
|
29
|
+
return this.#bucket.read(id);
|
|
21
30
|
}
|
|
22
31
|
async delete(hash) {
|
|
23
|
-
const id = await this.#
|
|
32
|
+
const id = await this.#ids.get(hash);
|
|
24
33
|
if (id) {
|
|
25
|
-
await this.#
|
|
26
|
-
await this.#
|
|
34
|
+
const size = await this.#bucket.size(id);
|
|
35
|
+
await this.#ids.del(hash);
|
|
36
|
+
await this.#bucket.delete(id);
|
|
37
|
+
await this.#updateStats(stats => {
|
|
38
|
+
stats.count--;
|
|
39
|
+
stats.size -= size;
|
|
40
|
+
});
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
async write(readable) {
|
|
30
44
|
const id = randomId();
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await writer.write(chunk);
|
|
37
|
-
hasher.update(chunk);
|
|
38
|
-
}
|
|
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);
|
|
45
|
+
await this.#wip.set(id, { created: Date.now() });
|
|
46
|
+
const { hash, size } = await saveAndHash(this.#bucket, id, readable);
|
|
47
|
+
await this.#wip.del(id);
|
|
48
|
+
await this.#finalizeWrite(hash, id, size);
|
|
49
|
+
void this.#selfClean().catch(() => { });
|
|
44
50
|
return hash;
|
|
45
51
|
}
|
|
46
|
-
async
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
async #needFileId(hash) {
|
|
50
|
-
return got(await this.#getFileId(hash), `file not found by hash "${hash}"`);
|
|
52
|
+
async *hashes() {
|
|
53
|
+
yield* this.#ids.keys();
|
|
51
54
|
}
|
|
52
|
-
async
|
|
53
|
-
|
|
55
|
+
async stats() {
|
|
56
|
+
return structuredClone((await this.#stats.get()) ?? { count: 0, size: 0 });
|
|
54
57
|
}
|
|
58
|
+
#updateStats = queue(async (fn) => {
|
|
59
|
+
const stats = await this.stats();
|
|
60
|
+
fn(stats);
|
|
61
|
+
await this.#stats.set(stats);
|
|
62
|
+
});
|
|
63
|
+
#finalizeWrite = queue(async (hash, id, size) => {
|
|
64
|
+
if (await this.#ids.has(hash)) {
|
|
65
|
+
await this.#bucket.delete(id); // forget this new file (we already have it)
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
await this.#ids.set(hash, id);
|
|
69
|
+
await this.#updateStats(stats => {
|
|
70
|
+
stats.count++;
|
|
71
|
+
stats.size += size;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
#selfClean = queue(async () => {
|
|
76
|
+
const expiredIds = (await collect(this.#wip.entries({ limit: 64 })))
|
|
77
|
+
.filter(([, wip]) => isExpired(wip))
|
|
78
|
+
.map(([id]) => id);
|
|
79
|
+
await Promise.all(expiredIds.map(id => this.#bucket.delete(id)));
|
|
80
|
+
await this.#wip.del(...expiredIds);
|
|
81
|
+
});
|
|
55
82
|
}
|
|
56
83
|
//# 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,5 +1,5 @@
|
|
|
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
5
|
size(id: Id): Promise<number>;
|
|
@@ -1,6 +1,6 @@
|
|
|
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);
|
|
@@ -26,4 +26,4 @@ export class IcebergMemory {
|
|
|
26
26
|
this.#map.set(id, payload);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
//# sourceMappingURL=
|
|
29
|
+
//# 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,IAAI,CAAC,EAAM;QAChB,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAA;IACzC,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"}
|
package/x/core/types.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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
6
|
size(id: Id): Promise<number>;
|
|
6
7
|
delete(id: Id): Promise<void>;
|
|
7
8
|
read(id: Id): Promise<Blob>;
|
|
8
9
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>;
|
|
9
10
|
};
|
|
10
|
-
export type
|
|
11
|
-
|
|
12
|
-
size
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
export type Stats = {
|
|
12
|
+
/** total number of bytes in the whole datalake. */
|
|
13
|
+
size: number;
|
|
14
|
+
/** total number of known files in the datalake. */
|
|
15
|
+
count: number;
|
|
16
|
+
};
|
|
17
|
+
export type Wip = {
|
|
18
|
+
created: number;
|
|
17
19
|
};
|
|
@@ -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;AAG9B,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,CAAC,CAAC,CAAA;AAC5B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"not-found.js","sourceRoot":"","sources":["../../../s/core/utils/not-found.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,QAAQ,CAAC,IAAU;IAClC,OAAO,2BAA2B,IAAI,GAAG,CAAA;AAC1C,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { hex } from "@e280/stz";
|
|
2
|
+
import { blake3 } from "@noble/hashes/blake3.js";
|
|
3
|
+
export async function saveAndHash(bucket, id, readable) {
|
|
4
|
+
let size = 0;
|
|
5
|
+
const pipe = new TransformStream();
|
|
6
|
+
const done = bucket.write(id, pipe.readable);
|
|
7
|
+
const writer = pipe.writable.getWriter();
|
|
8
|
+
const hasher = blake3.create();
|
|
9
|
+
for await (const chunk of readable) {
|
|
10
|
+
await writer.write(chunk);
|
|
11
|
+
hasher.update(chunk);
|
|
12
|
+
size += chunk.byteLength;
|
|
13
|
+
}
|
|
14
|
+
await writer.close();
|
|
15
|
+
await done;
|
|
16
|
+
const hash = hex.fromBytes(hasher.digest());
|
|
17
|
+
return { hash, size };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=save-and-hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-and-hash.js","sourceRoot":"","sources":["../../../s/core/utils/save-and-hash.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAC,MAAM,WAAW,CAAA;AAC7B,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAA;AAG9C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,EAAM,EAAE,QAAoC;IAC7F,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAA;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;IAE9B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QACpC,MAAM,MAAM,CAAC,KAAK,CAAC,KAAgC,CAAC,CAAA;QACpD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACpB,IAAI,IAAI,KAAK,CAAC,UAAU,CAAA;IACzB,CAAC;IAED,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;IACpB,MAAM,IAAI,CAAA;IAEV,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IAC3C,OAAO,EAAC,IAAI,EAAE,IAAI,EAAC,CAAA;AACpB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Bucket, Id } from "../core/types.js";
|
|
2
|
+
export declare class DiskBucket implements Bucket {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(directory: string);
|
|
5
|
+
has(id: Id): Promise<boolean>;
|
|
6
|
+
size(id: Id): Promise<number>;
|
|
7
|
+
delete(id: Id): Promise<void>;
|
|
8
|
+
read(id: Id): Promise<Blob>;
|
|
9
|
+
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { mkdir, stat, unlink } from "node:fs/promises";
|
|
5
|
+
import { createWriteStream, openAsBlob } from "node:fs";
|
|
6
|
+
import { isNotFound } from "./utils/is-not-found.js";
|
|
7
|
+
export class DiskBucket {
|
|
8
|
+
#directory;
|
|
9
|
+
constructor(directory) {
|
|
10
|
+
this.#directory = directory;
|
|
11
|
+
}
|
|
12
|
+
async has(id) {
|
|
13
|
+
try {
|
|
14
|
+
await stat(this.#path(id));
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (isNotFound(error))
|
|
19
|
+
return false;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async size(id) {
|
|
24
|
+
const stats = await stat(this.#path(id));
|
|
25
|
+
return stats.size;
|
|
26
|
+
}
|
|
27
|
+
async delete(id) {
|
|
28
|
+
try {
|
|
29
|
+
await unlink(this.#path(id));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!isNotFound(error))
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async read(id) {
|
|
37
|
+
return openAsBlob(this.#path(id));
|
|
38
|
+
}
|
|
39
|
+
async write(id, readable) {
|
|
40
|
+
await mkdir(this.#directory, { recursive: true });
|
|
41
|
+
await pipeline(Readable.fromWeb(readable), createWriteStream(this.#path(id)));
|
|
42
|
+
}
|
|
43
|
+
#path(id) {
|
|
44
|
+
return join(this.#directory, id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=disk-bucket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disk-bucket.js","sourceRoot":"","sources":["../../s/node/disk-bucket.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAC9B,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAA;AACpC,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAC,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAC,iBAAiB,EAAE,UAAU,EAAC,MAAM,SAAS,CAAA;AAGrD,OAAO,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAA;AAElD,MAAM,OAAO,UAAU;IACtB,UAAU,CAAA;IAEV,YAAY,SAAiB;QAC5B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAM;QACf,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;YAC1B,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,IAAI,CAAC,EAAM;QAChB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;QACxC,OAAO,KAAK,CAAC,IAAI,CAAA;IAClB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAM;QAClB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7B,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,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAM,EAAE,QAAoC;QACvD,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,EAAC,SAAS,EAAE,IAAI,EAAC,CAAC,CAAA;QAC/C,MAAM,QAAQ,CACb,QAAQ,CAAC,OAAO,CAAC,QAAe,CAAC,EACjC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CACjC,CAAA;IACF,CAAC;IAED,KAAK,CAAC,EAAM;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IACjC,CAAC;CACD"}
|
package/x/node/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/node/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isNotFound(error: unknown): error is NodeJS.ErrnoException;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-not-found.js","sourceRoot":"","sources":["../../../s/node/utils/is-not-found.ts"],"names":[],"mappings":"AACA,MAAM,UAAU,UAAU,CAAC,KAAc;IACxC,OAAO,CACN,KAAK,YAAY,KAAK;WACnB,MAAM,IAAI,KAAK;WACf,KAAK,CAAC,IAAI,KAAK,QAAQ,CAC1B,CAAA;AACF,CAAC"}
|
package/x/test.js
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import { Kv } from "@e280/kv";
|
|
2
|
+
import { rm } from "fs/promises";
|
|
2
3
|
import { bytes, collect } from "@e280/stz";
|
|
3
4
|
import { science, test, expect } from "@e280/science";
|
|
4
5
|
import { Mammoth } from "./core/mammoth.js";
|
|
6
|
+
import { DiskBucket } from "./node/disk-bucket.js";
|
|
5
7
|
import { randomId } from "./core/utils/random-id.js";
|
|
6
|
-
import {
|
|
8
|
+
import { MemoryBucket } from "./core/memory-bucket.js";
|
|
7
9
|
const blob = () => new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])]);
|
|
8
10
|
const quickstream = (b) => new Blob([new Uint8Array(b)]).stream();
|
|
9
11
|
function setup() {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const mammoth = new Mammoth(
|
|
12
|
+
const kv = new Kv();
|
|
13
|
+
const bucket = new MemoryBucket();
|
|
14
|
+
const mammoth = new Mammoth(bucket, kv);
|
|
13
15
|
return { mammoth };
|
|
14
16
|
}
|
|
15
17
|
await science.run({
|
|
16
|
-
"mammoth": {
|
|
18
|
+
"mammoth node": test(async () => {
|
|
19
|
+
const dataDir = "data";
|
|
20
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
21
|
+
const mammoth = new Mammoth(new DiskBucket(dataDir), new Kv());
|
|
22
|
+
const hash = await mammoth.write(blob().stream());
|
|
23
|
+
const second = await mammoth.read(hash);
|
|
24
|
+
expect(bytes.eq(await second.bytes(), await blob().bytes()));
|
|
25
|
+
}),
|
|
26
|
+
"mammoth memory": {
|
|
17
27
|
".write and .read": test(async () => {
|
|
18
28
|
const { mammoth } = setup();
|
|
19
29
|
const hash = await mammoth.write(blob().stream());
|
|
@@ -24,7 +34,7 @@ await science.run({
|
|
|
24
34
|
const { mammoth } = setup();
|
|
25
35
|
const hashA = await mammoth.write(blob().stream());
|
|
26
36
|
const hashB = await mammoth.write(blob().stream());
|
|
27
|
-
const keys = await collect(mammoth.
|
|
37
|
+
const keys = await collect(mammoth.hashes());
|
|
28
38
|
expect(hashA).is(hashB);
|
|
29
39
|
expect(keys.length).is(1);
|
|
30
40
|
expect(keys[0]).is(hashA);
|
|
@@ -40,7 +50,19 @@ await science.run({
|
|
|
40
50
|
const hashA = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]));
|
|
41
51
|
const hashB = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]));
|
|
42
52
|
expect(hashA).not.is(hashB);
|
|
43
|
-
expect((await collect(mammoth.
|
|
53
|
+
expect((await collect(mammoth.hashes())).length).is(2);
|
|
54
|
+
}),
|
|
55
|
+
".stats": test(async () => {
|
|
56
|
+
const { mammoth } = setup();
|
|
57
|
+
expect(await mammoth.stats()).deep({ count: 0, size: 0 });
|
|
58
|
+
const hashA = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]));
|
|
59
|
+
expect(await mammoth.stats()).deep({ count: 1, size: 3 });
|
|
60
|
+
const hashB = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]));
|
|
61
|
+
expect(await mammoth.stats()).deep({ count: 2, size: 7 });
|
|
62
|
+
await mammoth.delete(hashA);
|
|
63
|
+
expect(await mammoth.stats()).deep({ count: 1, size: 4 });
|
|
64
|
+
await mammoth.delete(hashB);
|
|
65
|
+
expect(await mammoth.stats()).deep({ count: 0, size: 0 });
|
|
44
66
|
}),
|
|
45
67
|
".has": test(async () => {
|
|
46
68
|
const { mammoth } = setup();
|
|
@@ -58,7 +80,7 @@ await science.run({
|
|
|
58
80
|
const hash = await mammoth.write(blob().stream());
|
|
59
81
|
await mammoth.delete(hash);
|
|
60
82
|
expect(await mammoth.has(hash)).is(false);
|
|
61
|
-
expect(await collect(mammoth.
|
|
83
|
+
expect(await collect(mammoth.hashes())).deep([]);
|
|
62
84
|
}),
|
|
63
85
|
".delete idempotent": test(async () => {
|
|
64
86
|
const { mammoth } = setup();
|
|
@@ -67,7 +89,7 @@ await science.run({
|
|
|
67
89
|
".keys": test(async () => {
|
|
68
90
|
const { mammoth } = setup();
|
|
69
91
|
const hash = await mammoth.write(blob().stream());
|
|
70
|
-
const keys = await collect(mammoth.
|
|
92
|
+
const keys = await collect(mammoth.hashes());
|
|
71
93
|
expect(keys.length).is(1);
|
|
72
94
|
expect(keys[0]).is(hash);
|
|
73
95
|
}),
|
package/x/test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.js","sourceRoot":"","sources":["../s/test.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAC,MAAM,UAAU,CAAA;AAC3B,OAAO,EAAC,KAAK,EAAE,OAAO,EAAC,MAAM,WAAW,CAAA;AACxC,OAAO,EAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAC,MAAM,eAAe,CAAA;AAEnD,OAAO,EAAC,OAAO,EAAC,MAAM,mBAAmB,CAAA;AACzC,OAAO,EAAC,QAAQ,EAAC,MAAM,2BAA2B,CAAA;AAClD,OAAO,EAAC,
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../s/test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"iceberg-opfs.js","sourceRoot":"","sources":["../../s/browser/iceberg-opfs.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAA;AAElD,MAAM,OAAO,WAAW;IACvB,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,IAAI,CAAC,EAAM;QAChB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QACtD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;QACnC,OAAO,IAAI,CAAC,IAAI,CAAA;IACjB,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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"iceberg-memory.js","sourceRoot":"","sources":["../../s/core/iceberg-memory.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,aAAa;IACzB,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,IAAI,CAAC,EAAM;QAChB,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAA;IACzC,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"}
|