@e280/mammoth 0.1.0-5 → 0.1.0-7

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.
Files changed (53) hide show
  1. package/README.md +10 -6
  2. package/package.json +2 -2
  3. package/s/browser/example.ts +2 -2
  4. package/s/browser/opfs-bucket.ts +0 -6
  5. package/s/core/{hash-stream.ts → analyze.ts} +6 -2
  6. package/s/core/consts.ts +8 -0
  7. package/s/core/index.ts +2 -2
  8. package/s/core/mammoth.ts +50 -68
  9. package/s/core/memory-bucket.ts +0 -4
  10. package/s/core/types.ts +25 -2
  11. package/s/core/utils/is-expired.ts +2 -1
  12. package/s/core/utils/manifest.ts +113 -0
  13. package/s/core/utils/{save-and-hash.ts → save.ts} +7 -2
  14. package/s/node/disk-bucket.ts +0 -5
  15. package/s/test.ts +14 -13
  16. package/x/browser/example.js +2 -2
  17. package/x/browser/example.js.map +1 -1
  18. package/x/browser/opfs-bucket.d.ts +0 -1
  19. package/x/browser/opfs-bucket.js +0 -5
  20. package/x/browser/opfs-bucket.js.map +1 -1
  21. package/x/core/analyze.d.ts +2 -0
  22. package/x/core/{hash-stream.js → analyze.js} +6 -3
  23. package/x/core/analyze.js.map +1 -0
  24. package/x/core/consts.d.ts +4 -0
  25. package/x/core/consts.js +6 -0
  26. package/x/core/consts.js.map +1 -0
  27. package/x/core/index.d.ts +2 -2
  28. package/x/core/index.js +2 -2
  29. package/x/core/index.js.map +1 -1
  30. package/x/core/mammoth.d.ts +5 -5
  31. package/x/core/mammoth.js +46 -61
  32. package/x/core/mammoth.js.map +1 -1
  33. package/x/core/memory-bucket.d.ts +0 -1
  34. package/x/core/memory-bucket.js +0 -3
  35. package/x/core/memory-bucket.js.map +1 -1
  36. package/x/core/types.d.ts +18 -2
  37. package/x/core/utils/is-expired.js +2 -1
  38. package/x/core/utils/is-expired.js.map +1 -1
  39. package/x/core/utils/manifest.d.ts +19 -0
  40. package/x/core/utils/manifest.js +91 -0
  41. package/x/core/utils/manifest.js.map +1 -0
  42. package/x/core/utils/save.d.ts +2 -0
  43. package/x/core/utils/{save-and-hash.js → save.js} +2 -2
  44. package/x/core/utils/save.js.map +1 -0
  45. package/x/node/disk-bucket.d.ts +0 -1
  46. package/x/node/disk-bucket.js +0 -4
  47. package/x/node/disk-bucket.js.map +1 -1
  48. package/x/test.js +14 -13
  49. package/x/test.js.map +1 -1
  50. package/x/core/hash-stream.d.ts +0 -1
  51. package/x/core/hash-stream.js.map +0 -1
  52. package/x/core/utils/save-and-hash.d.ts +0 -5
  53. package/x/core/utils/save-and-hash.js.map +0 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  # 🦣 mammoth
5
5
  > *big files. small api.*
6
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.
7
+ **mammoth is a file storage typescript library.** we use it in our web apps to store files like user uploads. it's content-addressed. files are streamed into a bucket, identified by their blake3 hash, and 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
8
 
9
9
  ```bash
10
10
  npm install @e280/mammoth
@@ -17,9 +17,9 @@ const mammoth = new Mammoth()
17
17
  ```
18
18
 
19
19
  ### 🦣 mammoth stores files.
20
- - **write a file,** and you get back its blake3 hash.
20
+ - **write a file,** and you get back its blake3 `hash` and `size` in bytes.
21
21
  ```ts
22
- const hash = await mammoth.write(blob.stream())
22
+ const {hash, size} = await mammoth.write(blob.stream())
23
23
  ```
24
24
  - **read a file,** identified by its hash.
25
25
  ```ts
@@ -37,11 +37,15 @@ const mammoth = new Mammoth()
37
37
  import {Kv} from "@e280/kv"
38
38
 
39
39
  const mammoth = new Mammoth(
40
+
41
+ // bucket for blob storage
40
42
  new MemoryBucket(),
43
+
44
+ // kv for metadata bookkeeping
41
45
  new Kv(),
42
46
  )
43
47
  ```
44
- defaults shown, so, this is equivalent:
48
+ defaults shown, this is equivalent:
45
49
  ```ts
46
50
  const mammoth = new Mammoth()
47
51
  ```
@@ -74,9 +78,9 @@ const mammoth = new Mammoth()
74
78
  ```ts
75
79
  const exists = await mammoth.has(hash)
76
80
  ```
77
- - **get file size,** in bytes.
81
+ - **get file info,** including `size` in bytes, `added` timestamp, and bucket `id`.
78
82
  ```ts
79
- const size = await mammoth.size(hash)
83
+ const {size, added, id} = await mammoth.info(hash)
80
84
  ```
81
85
  - **get stats,** for the whole datalake.
82
86
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/mammoth",
3
- "version": "0.1.0-5",
3
+ "version": "0.1.0-7",
4
4
  "description": "big file storage",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@e280/kv": "^0.1.5",
27
- "@e280/stz": "^0.3.4",
27
+ "@e280/stz": "^0.3.5",
28
28
  "@noble/hashes": "^2.2.0"
29
29
  },
30
30
  "devDependencies": {
@@ -19,7 +19,7 @@ export async function example() {
19
19
  const myFile = new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])])
20
20
 
21
21
  // write a file as an example
22
- const hash = await mammoth.write(myFile.stream())
23
- console.log(`mammoth stored file by hash "${hash}"`)
22
+ const {hash} = await mammoth.write(myFile.stream())
23
+ console.log(`mammoth stored file "${hash}"`)
24
24
  }
25
25
 
@@ -20,12 +20,6 @@ export class OpfsBucket implements Bucket {
20
20
  }
21
21
  }
22
22
 
23
- async size(id: Id) {
24
- const handle = await this.#directory.getFileHandle(id)
25
- const file = await handle.getFile()
26
- return file.size
27
- }
28
-
29
23
  async delete(id: Id) {
30
24
  try {
31
25
  await this.#directory.removeEntry(id)
@@ -1,19 +1,23 @@
1
1
 
2
2
  import {hex, nap} from "@e280/stz"
3
3
  import {blake3} from "@noble/hashes/blake3.js"
4
+ import {Analysis} from "./types.js"
4
5
  import {relaxer} from "./utils/relaxer.js"
5
6
 
6
- export async function hashStream(readable: ReadableStream<Uint8Array>) {
7
+ export async function analyze(readable: ReadableStream<Uint8Array>): Promise<Analysis> {
8
+ let size = 0
7
9
  const hasher = blake3.create()
8
10
  const relax = relaxer()
9
11
 
10
12
  for await (const chunk of readable) {
11
13
  hasher.update(chunk)
14
+ size += chunk.byteLength
12
15
 
13
16
  if (relax())
14
17
  await nap()
15
18
  }
16
19
 
17
- return hex.fromBytes(hasher.digest())
20
+ const hash = hex.fromBytes(hasher.digest())
21
+ return {hash, size}
18
22
  }
19
23
 
@@ -0,0 +1,8 @@
1
+
2
+ import {time} from "@e280/stz"
3
+
4
+ export const consts = {
5
+ max_jobs: 64,
6
+ expiry_time: time.days(7),
7
+ }
8
+
package/s/core/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
- export * from "./hash-stream.js"
3
- export * from "./memory-bucket.js"
2
+ export * from "./analyze.js"
4
3
  export * from "./mammoth.js"
4
+ export * from "./memory-bucket.js"
5
5
  export * from "./types.js"
6
6
 
package/s/core/mammoth.ts CHANGED
@@ -1,100 +1,82 @@
1
1
 
2
2
  import {Kv} from "@e280/kv"
3
- import {collect, got, queue} from "@e280/stz"
3
+ import {lane, queue} from "@e280/stz"
4
4
 
5
+ import {consts} from "./consts.js"
6
+ import {save} from "./utils/save.js"
7
+ import {Manifest} from "./utils/manifest.js"
5
8
  import {randomId} from "./utils/random-id.js"
6
- import {notFound} from "./utils/not-found.js"
7
9
  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"
10
+ import {Hash, Bucket, Analysis} from "./types.js"
11
11
 
12
12
  /** file storage datalake, content-addressed with blake3 hashes. */
13
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
14
+ #bucket
15
+ #manifest
16
+ #wholesome = lane(consts.max_jobs)
18
17
 
19
18
  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
19
  this.#bucket = bucket
20
+ this.#manifest = new Manifest(kv)
21
+ }
22
+
23
+ async* hashes() {
24
+ yield* this.#manifest.hashes()
24
25
  }
25
26
 
26
27
  async has(hash: Hash) {
27
- return this.#ids.has(hash)
28
+ return this.#manifest.hasHash(hash)
28
29
  }
29
30
 
30
- async size(hash: Hash) {
31
- const id = got(await this.#ids.get(hash), notFound(hash))
32
- return this.#bucket.size(id)
31
+ async info(hash: Hash) {
32
+ return this.#manifest.needInfo(hash)
33
+ }
34
+
35
+ async stats() {
36
+ return this.#manifest.getStats()
33
37
  }
34
38
 
35
39
  async read(hash: Hash) {
36
- const id = got(await this.#ids.get(hash), notFound(hash))
40
+ const {id} = await this.#manifest.needInfo(hash)
37
41
  return this.#bucket.read(id)
38
42
  }
39
43
 
40
44
  async delete(hash: Hash) {
41
- const id = await this.#ids.get(hash)
42
- if (id) {
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
- })
50
- }
45
+ await this.#wholesome(async() => {
46
+ const info = await this.#manifest.getInfo(hash)
47
+ if (info)
48
+ await this.#manifest.scheduleDeletion(hash, info)
49
+ })
50
+ await this.#cleanup()
51
51
  }
52
52
 
53
- async write(readable: ReadableStream<Uint8Array>): Promise<Hash> {
53
+ async write(readable: ReadableStream<Uint8Array>): Promise<Analysis> {
54
54
  const id = randomId()
55
-
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(() => {})
62
- return hash
63
- }
64
-
65
- async* hashes() {
66
- yield* this.#ids.keys()
67
- }
68
-
69
- async stats() {
70
- return structuredClone((await this.#stats.get()) ?? {count: 0, size: 0})
55
+ await this.#manifest.addWip(id)
56
+ try {
57
+ const analysis = await save(this.#bucket, id, readable)
58
+ const {hash, size} = analysis
59
+ const info = {id, size, added: Date.now()}
60
+ await this.#wholesome(() => this.#manifest.commit(hash, info))
61
+ return analysis
62
+ }
63
+ catch (error) {
64
+ await this.#manifest.moveWipToTrash(id)
65
+ throw error
66
+ }
67
+ finally {
68
+ void this.#cleanup().catch(e => console.error("cleanup failed", e))
69
+ }
71
70
  }
72
71
 
73
- #updateStats = queue(async(fn: (stats: Stats) => void) => {
74
- const stats = await this.stats()
75
- fn(stats)
76
- await this.#stats.set(stats)
77
- })
72
+ #cleanup = queue(async() => {
73
+ for await (const id of this.#manifest.getExpiredWipIds())
74
+ await this.#manifest.moveWipToTrash(id)
78
75
 
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
- })
76
+ for await (const id of this.#manifest.listTrashIds()) {
77
+ await this.#bucket.delete(id)
78
+ await this.#manifest.dropTrashRecord(id)
89
79
  }
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
- })
80
+ }, consts.max_jobs)
99
81
  }
100
82
 
@@ -10,10 +10,6 @@ export class MemoryBucket implements Bucket {
10
10
  return this.#map.has(id)
11
11
  }
12
12
 
13
- async size(id: Id) {
14
- return got(this.#map.get(id)).byteLength
15
- }
16
-
17
13
  async delete(id: Id) {
18
14
  this.#map.delete(id)
19
15
  }
package/s/core/types.ts CHANGED
@@ -5,18 +5,41 @@ export type Id = string
5
5
  /** file blob store. */
6
6
  export type Bucket = {
7
7
  has(id: Id): Promise<boolean>
8
- size(id: Id): Promise<number>
9
8
  delete(id: Id): Promise<void>
10
9
  read(id: Id): Promise<Blob>
11
10
  write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>
12
11
  }
13
12
 
13
+ /** report about a file */
14
+ export type Analysis = {
15
+
16
+ /** blake3 hash of the file's contents */
17
+ hash: Hash
18
+
19
+ /** filesize in bytes */
20
+ size: number
21
+ }
22
+
23
+ /** metadata for a single file. */
24
+ export type Info = {
25
+
26
+ /** bucket id for this file's data. */
27
+ id: Id
28
+
29
+ /** filesize in bytes. */
30
+ size: number
31
+
32
+ /** when this file was added to the datalake. */
33
+ added: number
34
+ }
35
+
36
+ /** statistics for the whole datalake. */
14
37
  export type Stats = {
15
38
 
16
39
  /** total number of bytes in the whole datalake. */
17
40
  size: number
18
41
 
19
- /** total number of known files in the datalake. */
42
+ /** total number of files in the datalake. */
20
43
  count: number
21
44
  }
22
45
 
@@ -1,9 +1,10 @@
1
1
 
2
2
  import {time} from "@e280/stz"
3
3
  import {Wip} from "../types.js"
4
+ import {consts} from "../consts.js"
4
5
 
5
6
  export function isExpired({created}: Wip) {
6
7
  const since = Date.now() - created
7
- return since > time.days(7)
8
+ return since > time.days(consts.expiry_time)
8
9
  }
9
10
 
@@ -0,0 +1,113 @@
1
+
2
+ import {Kv} from "@e280/kv"
3
+ import {got} from "@e280/stz"
4
+ import {notFound} from "./not-found.js"
5
+ import {isExpired} from "./is-expired.js"
6
+ import {Hash, Id, Info, Stats, Wip} from "../types.js"
7
+
8
+ export class Manifest {
9
+ #kv
10
+
11
+ /** statistics about the whole datalake */
12
+ #stats
13
+
14
+ /** associates file hashes with bucket ids */
15
+ #info
16
+
17
+ /** temporary record of writes in-progress */
18
+ #wip
19
+
20
+ /** statistics about the whole datalake */
21
+ #trash
22
+
23
+ constructor(kv = new Kv()) {
24
+ this.#kv = kv
25
+ this.#stats = kv.store<Stats>("stats")
26
+ this.#info = kv.scope<Info>("info")
27
+ this.#wip = kv.scope<Wip>("wip")
28
+ this.#trash = kv.scope<true>("trash")
29
+ }
30
+
31
+ async getStats() {
32
+ return structuredClone((await this.#stats.get()) ?? {count: 0, size: 0})
33
+ }
34
+
35
+ async* hashes() {
36
+ yield* this.#info.keys()
37
+ }
38
+
39
+ async hasHash(hash: Hash) {
40
+ return this.#info.has(hash)
41
+ }
42
+
43
+ async getInfo(hash: Hash) {
44
+ return await this.#info.get(hash)
45
+ }
46
+
47
+ async needInfo(hash: Hash) {
48
+ return got(await this.getInfo(hash), notFound(hash))
49
+ }
50
+
51
+ async addWip(id: Id) {
52
+ await this.#wip.set(id, {created: Date.now()})
53
+ }
54
+
55
+ async deleteWip(...ids: Id[]) {
56
+ await this.#wip.del(...ids)
57
+ }
58
+
59
+ async* getExpiredWipIds() {
60
+ for await (const [id, wip] of this.#wip.entries()) {
61
+ if (isExpired(wip))
62
+ yield id
63
+ }
64
+ }
65
+
66
+ async moveWipToTrash(id: Id) {
67
+ await this.#kv.transaction(() => [
68
+ this.#wip.write.del(id),
69
+ this.#trash.write.set(id, true),
70
+ ])
71
+ }
72
+
73
+ async dropTrashRecord(id: Id) {
74
+ await this.#trash.del(id)
75
+ }
76
+
77
+ async* listTrashIds() {
78
+ yield* this.#trash.keys()
79
+ }
80
+
81
+ async scheduleDeletion(hash: Hash, info: Info) {
82
+ const stats = await this.getStats()
83
+ stats.count -= 1
84
+ stats.size -= info.size
85
+ await this.#kv.transaction(() => [
86
+ this.#info.write.del(hash),
87
+ this.#trash.write.set(info.id, true),
88
+ this.#stats.write.set(stats),
89
+ ])
90
+ }
91
+
92
+ async commit(hash: Hash, info: Info) {
93
+ const isNewFile = !await this.hasHash(hash)
94
+
95
+ if (isNewFile) {
96
+ const stats = await this.getStats()
97
+ stats.count += 1
98
+ stats.size += info.size
99
+ await this.#kv.transaction(() => [
100
+ this.#wip.write.del(info.id),
101
+ this.#info.write.set(hash, info),
102
+ this.#stats.write.set(stats)
103
+ ])
104
+ }
105
+ else {
106
+ await this.#kv.transaction(() => [
107
+ this.#wip.write.del(info.id),
108
+ this.#trash.write.set(info.id, true),
109
+ ])
110
+ }
111
+ }
112
+ }
113
+
@@ -1,9 +1,14 @@
1
1
 
2
2
  import {hex} from "@e280/stz"
3
3
  import {blake3} from "@noble/hashes/blake3.js"
4
- import {Bucket, Id} from "../types.js"
4
+ import {Analysis, Bucket, Id} from "../types.js"
5
+
6
+ export async function save(
7
+ bucket: Bucket,
8
+ id: Id,
9
+ readable: ReadableStream<Uint8Array>,
10
+ ): Promise<Analysis> {
5
11
 
6
- export async function saveAndHash(bucket: Bucket, id: Id, readable: ReadableStream<Uint8Array>) {
7
12
  let size = 0
8
13
  const pipe = new TransformStream()
9
14
  const done = bucket.write(id, pipe.readable)
@@ -26,11 +26,6 @@ export class DiskBucket implements Bucket {
26
26
  }
27
27
  }
28
28
 
29
- async size(id: Id) {
30
- const stats = await stat(this.#path(id))
31
- return stats.size
32
- }
33
-
34
29
  async delete(id: Id) {
35
30
  try {
36
31
  await unlink(this.#path(id))
package/s/test.ts CHANGED
@@ -24,7 +24,7 @@ await science.run({
24
24
  const dataDir = "data"
25
25
  await rm(dataDir, {recursive: true, force: true})
26
26
  const mammoth = new Mammoth(new DiskBucket(dataDir), new Kv())
27
- const hash = await mammoth.write(blob().stream())
27
+ const {hash} = await mammoth.write(blob().stream())
28
28
  const second = await mammoth.read(hash)
29
29
  expect(bytes.eq(await second.bytes(), await blob().bytes()))
30
30
  }),
@@ -32,15 +32,15 @@ await science.run({
32
32
  "mammoth memory": {
33
33
  ".write and .read": test(async() => {
34
34
  const {mammoth} = setup()
35
- const hash = await mammoth.write(blob().stream())
35
+ const {hash} = await mammoth.write(blob().stream())
36
36
  const second = await mammoth.read(hash)
37
37
  expect(bytes.eq(await second.bytes(), await blob().bytes()))
38
38
  }),
39
39
 
40
40
  ".write deduplication": test(async() => {
41
41
  const {mammoth} = setup()
42
- const hashA = await mammoth.write(blob().stream())
43
- const hashB = await mammoth.write(blob().stream())
42
+ const {hash: hashA} = await mammoth.write(blob().stream())
43
+ const {hash: hashB} = await mammoth.write(blob().stream())
44
44
  const keys = await collect(mammoth.hashes())
45
45
  expect(hashA).is(hashB)
46
46
  expect(keys.length).is(1)
@@ -49,8 +49,8 @@ await science.run({
49
49
 
50
50
  ".write empty file": test(async() => {
51
51
  const {mammoth} = setup()
52
- const hash = await mammoth.write(new Blob().stream())
53
- expect(await mammoth.size(hash)).is(0)
52
+ const {hash} = await mammoth.write(new Blob().stream())
53
+ expect((await mammoth.info(hash)).size).is(0)
54
54
  expect((await mammoth.read(hash)).size).is(0)
55
55
  }),
56
56
 
@@ -65,9 +65,9 @@ await science.run({
65
65
  ".stats": test(async() => {
66
66
  const {mammoth} = setup()
67
67
  expect(await mammoth.stats()).deep({count: 0, size: 0})
68
- const hashA = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]))
68
+ const {hash: hashA} = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]))
69
69
  expect(await mammoth.stats()).deep({count: 1, size: 3})
70
- const hashB = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]))
70
+ const {hash: hashB} = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]))
71
71
  expect(await mammoth.stats()).deep({count: 2, size: 7})
72
72
  await mammoth.delete(hashA)
73
73
  expect(await mammoth.stats()).deep({count: 1, size: 4})
@@ -77,20 +77,21 @@ await science.run({
77
77
 
78
78
  ".has": test(async() => {
79
79
  const {mammoth} = setup()
80
- const hash = await mammoth.write(blob().stream())
80
+ const {hash} = await mammoth.write(blob().stream())
81
81
  expect(await mammoth.has(hash)).is(true)
82
82
  expect(await mammoth.has(randomId())).is(false)
83
83
  }),
84
84
 
85
85
  ".size": test(async() => {
86
86
  const {mammoth} = setup()
87
- const hash = await mammoth.write(blob().stream())
88
- expect(await mammoth.size(hash)).is(4)
87
+ const b = blob()
88
+ const {hash} = await mammoth.write(b.stream())
89
+ expect((await mammoth.info(hash)).size).is(b.size)
89
90
  }),
90
91
 
91
92
  ".delete": test(async() => {
92
93
  const {mammoth} = setup()
93
- const hash = await mammoth.write(blob().stream())
94
+ const {hash} = await mammoth.write(blob().stream())
94
95
  await mammoth.delete(hash)
95
96
  expect(await mammoth.has(hash)).is(false)
96
97
  expect(await collect(mammoth.hashes())).deep([])
@@ -103,7 +104,7 @@ await science.run({
103
104
 
104
105
  ".keys": test(async() => {
105
106
  const {mammoth} = setup()
106
- const hash = await mammoth.write(blob().stream())
107
+ const {hash} = await mammoth.write(blob().stream())
107
108
  const keys = await collect(mammoth.hashes())
108
109
  expect(keys.length).is(1)
109
110
  expect(keys[0]).is(hash)
@@ -13,7 +13,7 @@ export async function example() {
13
13
  const mammoth = new Mammoth(bucket, kv);
14
14
  const myFile = new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])]);
15
15
  // write a file as an example
16
- const hash = await mammoth.write(myFile.stream());
17
- console.log(`mammoth stored file by hash "${hash}"`);
16
+ const { hash } = await mammoth.write(myFile.stream());
17
+ console.log(`mammoth stored file "${hash}"`);
18
18
  }
19
19
  //# sourceMappingURL=example.js.map
@@ -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,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"}
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,EAAC,IAAI,EAAC,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IACnD,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,GAAG,CAAC,CAAA;AAC7C,CAAC"}
@@ -3,7 +3,6 @@ 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>;
@@ -15,11 +15,6 @@ export class OpfsBucket {
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);
@@ -1 +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"}
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"}
@@ -0,0 +1,2 @@
1
+ import { Analysis } from "./types.js";
2
+ export declare function analyze(readable: ReadableStream<Uint8Array>): Promise<Analysis>;
@@ -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 hashStream(readable) {
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
- return hex.fromBytes(hasher.digest());
14
+ const hash = hex.fromBytes(hasher.digest());
15
+ return { hash, size };
13
16
  }
14
- //# sourceMappingURL=hash-stream.js.map
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"}