@e280/quay 0.0.0-17 → 0.0.0-18

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 (51) hide show
  1. package/package.json +6 -4
  2. package/s/cellar/cask.ts +0 -18
  3. package/s/cellar/cellar.test.ts +19 -38
  4. package/s/cellar/cellar.ts +16 -20
  5. package/s/cellar/index.ts +0 -4
  6. package/s/cellar/mammoth.ts +50 -0
  7. package/s/cellar/mammoth.worker.bundle.ts +30 -0
  8. package/s/genius/parts/hy-tree.ts +10 -9
  9. package/s/logic/presets/media/library.ts +57 -12
  10. package/s/logic/presets/media/schema.ts +6 -0
  11. package/x/cellar/cask.d.ts +0 -2
  12. package/x/cellar/cask.js +0 -13
  13. package/x/cellar/cask.js.map +1 -1
  14. package/x/cellar/cellar.d.ts +9 -7
  15. package/x/cellar/cellar.js +17 -22
  16. package/x/cellar/cellar.js.map +1 -1
  17. package/x/cellar/cellar.test.d.ts +2 -3
  18. package/x/cellar/cellar.test.js +19 -35
  19. package/x/cellar/cellar.test.js.map +1 -1
  20. package/x/cellar/index.d.ts +0 -3
  21. package/x/cellar/index.js +0 -3
  22. package/x/cellar/index.js.map +1 -1
  23. package/x/cellar/mammoth.d.ts +23 -0
  24. package/x/cellar/mammoth.js +34 -0
  25. package/x/cellar/mammoth.js.map +1 -0
  26. package/x/cellar/mammoth.worker.bundle.d.ts +1 -0
  27. package/x/cellar/mammoth.worker.bundle.js +20 -0
  28. package/x/cellar/mammoth.worker.bundle.js.map +1 -0
  29. package/x/cellar/mammoth.worker.bundle.min.js +8 -0
  30. package/x/cellar/mammoth.worker.bundle.min.js.map +7 -0
  31. package/x/demo/main.bundle.min.js +43 -43
  32. package/x/demo/main.bundle.min.js.map +4 -4
  33. package/x/genius/parts/hy-tree.js +9 -8
  34. package/x/genius/parts/hy-tree.js.map +1 -1
  35. package/x/index.html +2 -2
  36. package/x/logic/presets/media/library.d.ts +3 -2
  37. package/x/logic/presets/media/library.js +46 -9
  38. package/x/logic/presets/media/library.js.map +1 -1
  39. package/x/logic/presets/media/schema.d.ts +6 -0
  40. package/s/cellar/forklift.ts +0 -9
  41. package/s/cellar/memory-forklift.ts +0 -30
  42. package/s/cellar/opfs-forklift.ts +0 -47
  43. package/x/cellar/forklift.d.ts +0 -7
  44. package/x/cellar/forklift.js +0 -2
  45. package/x/cellar/forklift.js.map +0 -1
  46. package/x/cellar/memory-forklift.d.ts +0 -9
  47. package/x/cellar/memory-forklift.js +0 -22
  48. package/x/cellar/memory-forklift.js.map +0 -1
  49. package/x/cellar/opfs-forklift.d.ts +0 -11
  50. package/x/cellar/opfs-forklift.js +0 -42
  51. package/x/cellar/opfs-forklift.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/quay",
3
- "version": "0.0.0-17",
3
+ "version": "0.0.0-18",
4
4
  "description": "File-browser and outliner UI for the web",
5
5
  "author": "Przemysław Gałęzki",
6
6
  "license": "MIT",
@@ -11,18 +11,20 @@
11
11
  "s"
12
12
  ],
13
13
  "dependencies": {
14
- "@awasm/noble": "^0.1.4",
15
- "@e280/kv": "^0.0.0-7",
14
+ "@e280/comrade": "^0.1.0",
15
+ "@e280/kv": "^0.2.0-3",
16
+ "@e280/mammoth": "^0.1.0-19",
16
17
  "@e280/scute": "^0.4.0-next.4",
17
18
  "@e280/sly": "^0.4.0-next.5",
18
19
  "@e280/strata": "^0.4.0-next.8",
19
- "@e280/stz": "^0.0.0-22",
20
+ "@e280/stz": "^0.2.8",
20
21
  "lit": "^3.3.1"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@e280/hottie": "^0.1.1",
24
25
  "@e280/science": "^0.0.5",
25
26
  "@shoelace-style/shoelace": "^2.20.1",
27
+ "@types/wicg-file-system-access": "^2023.10.7",
26
28
  "npm-run-all": "^4.1.5",
27
29
  "typescript": "^5.8.3"
28
30
  },
package/s/cellar/cask.ts CHANGED
@@ -1,24 +1,6 @@
1
1
 
2
- import {Hex} from "@e280/stz"
3
- import {blake3} from "@awasm/noble"
4
-
5
2
  /** File with a hash label */
6
3
  export class Cask {
7
- static async hash(file: Blob) {
8
- const hasher = blake3.create()
9
-
10
- for await (const chunk of file.stream())
11
- hasher.update(chunk)
12
-
13
- const digest = hasher.digest()
14
- return Hex.fromBytes(digest)
15
- }
16
-
17
- static async make(file: Blob) {
18
- const hash = await this.hash(file)
19
- return new this(hash, file)
20
- }
21
-
22
4
  constructor(public hash: string, public file: Blob) {}
23
5
  }
24
6
 
@@ -2,59 +2,42 @@
2
2
  import {Txt} from "@e280/stz"
3
3
  import {expect, Science, test} from "@e280/science"
4
4
 
5
- import {Cask} from "./cask.js"
6
5
  import {Cellar} from "./cellar.js"
7
- import {MemoryForklift} from "./memory-forklift.js"
8
6
 
9
7
  const blob = (text: string) => new Blob([Txt.toBytes(text)])
10
8
 
11
9
  export default Science.suite({
12
- "save + load roundtrip": test(async() => {
10
+ "write + load roundtrip": test(async() => {
13
11
  const cellar = new Cellar()
14
12
  const text = "hello world"
15
13
  const file = blob(text)
16
- await cellar.save(file)
14
+ const digest = await cellar.write(file.stream())
17
15
 
18
- const hash = await Cask.hash(file)
19
- expect(await cellar.has(hash)).is(true)
16
+ expect(await cellar.has(digest)).is(true)
20
17
 
21
- const loaded = await cellar.load(hash)
22
- expect(loaded.hash).is(hash)
18
+ const loaded = await cellar.load(digest)
19
+ expect(loaded.hash).is(digest)
23
20
  expect(await loaded.file.text()).is(text)
24
21
  }),
25
22
 
26
- "save is idempotent": test(async() => {
23
+ "write is idempotent": test(async() => {
27
24
  const cellar = new Cellar()
28
25
  const file = blob("foo bar")
29
- await cellar.save(file)
30
- await cellar.save(file) // again
26
+ const digest = await cellar.write(file.stream())
27
+ expect(await cellar.write(file.stream())).is(digest)
31
28
 
32
- const hash = await Cask.hash(file)
33
- expect(await cellar.has(hash)).is(true)
34
- }),
35
-
36
- "throws on corruption": test(async() => {
37
- const forklift = new MemoryForklift()
38
- const cellar = new Cellar(forklift)
39
- const good = blob("valid data")
40
- const bad = blob("corrupt data")
41
-
42
- const hash = await Cask.hash(good)
43
- await forklift.save(hash, bad)
44
-
45
- await expect(async() => cellar.load(hash)).throwsAsync()
29
+ expect(await cellar.has(digest)).is(true)
46
30
  }),
47
31
 
48
32
  "delete removes file": test(async() => {
49
33
  const cellar = new Cellar()
50
34
  const file = blob("temporary data")
51
- await cellar.save(file)
35
+ const digest = await cellar.write(file.stream())
52
36
 
53
- const hash = await Cask.hash(file)
54
- expect(await cellar.has(hash)).is(true)
37
+ expect(await cellar.has(digest)).is(true)
55
38
 
56
- await cellar.delete(hash)
57
- expect(await cellar.has(hash)).is(false)
39
+ await cellar.delete(digest)
40
+ expect(await cellar.has(digest)).is(false)
58
41
  }),
59
42
 
60
43
  "load missing throws": test(async() => {
@@ -69,24 +52,22 @@ export default Science.suite({
69
52
  const texts = ["a", "b", "c"]
70
53
 
71
54
  for (const t of texts)
72
- await cellar.save(blob(t))
55
+ await cellar.write(blob(t).stream())
73
56
 
74
57
  const hashes = []
75
58
  for await (const hash of cellar.list())
76
59
  hashes.push(hash)
77
60
 
78
61
  expect(hashes.length).is(3)
79
- for (const t of texts) {
80
- const h = await Cask.hash(blob(t))
81
- expect(hashes.includes(h)).is(true)
82
- }
62
+ for (const text of texts)
63
+ expect(hashes.includes(await cellar.write(blob(text).stream()))).is(true)
83
64
  }),
84
65
 
85
66
  "clear removes all entries": test(async() => {
86
67
  const cellar = new Cellar()
87
- await cellar.save(blob("one"))
88
- await cellar.save(blob("two"))
89
- await cellar.save(blob("three"))
68
+ await cellar.write(blob("one").stream())
69
+ await cellar.write(blob("two").stream())
70
+ await cellar.write(blob("three").stream())
90
71
 
91
72
  let count = 0
92
73
  for await (const _ of cellar.list()) count++
@@ -1,49 +1,45 @@
1
1
 
2
+ import {Mammoth} from "@e280/mammoth"
3
+
2
4
  import {Cask} from "./cask.js"
3
- import {Forklift} from "./forklift.js"
4
- import {OpfsForklift} from "./opfs-forklift.js"
5
- import {MemoryForklift} from "./memory-forklift.js"
5
+ import {MammothOpfs} from "./mammoth.js"
6
6
 
7
7
  /**
8
8
  * Cellar is an immutable content-addressable file store
9
9
  * - files are identified by their hashes (duplicates are impossible)
10
10
  * - files cannot be modified (delete it and save another)
11
11
  * - no filenames or metadata (store those somewhere else)
12
- * - pass in a forklift, which is the backend that actually stores the data
13
- * - MemoryForklift is the default
14
- * - OpfsForklift is probably what you really want in the browser
12
+ * - new Cellar uses Mammoth's in-memory backend by default
13
+ * - Cellar.opfs creates MammothOpfs for persistent browser storage
14
+ * - pass in a Mammoth to use a different backend
15
+ * - most likely you want to use Cellar.opfs for storing files in the browser
15
16
  */
16
17
  export class Cellar {
17
18
  static async opfs(dirname = "cellar") {
18
- return new this(await OpfsForklift.setup(dirname))
19
+ return new this(await MammothOpfs.setup(dirname))
19
20
  }
20
21
 
21
- constructor(private forklift: Forklift = new MemoryForklift()) {}
22
+ constructor(private mammoth: Mammoth | MammothOpfs = new Mammoth()) {}
22
23
 
23
24
  async *list(): AsyncIterable<string> {
24
- yield* this.forklift.list()
25
+ for await (const [hash] of this.mammoth.entries())
26
+ yield hash
25
27
  }
26
28
 
27
29
  async has(hash: string): Promise<boolean> {
28
- return this.forklift.has(hash)
30
+ return this.mammoth.has(hash)
29
31
  }
30
32
 
31
- async save(file: Blob) {
32
- const cask = await Cask.make(file)
33
- if (!await this.forklift.has(cask.hash))
34
- await this.forklift.save(cask.hash, cask.file)
35
- return cask
33
+ async write(readable: ReadableStream<Uint8Array>) {
34
+ return this.mammoth.write(readable)
36
35
  }
37
36
 
38
37
  async load(hash: string): Promise<Cask> {
39
- const bytes = await this.forklift.load(hash)
40
- const cask = await Cask.make(bytes)
41
- if (cask.hash !== hash) throw new Error(`corruption error, hash mismatch (requested ${hash}) (got ${cask.hash})`)
42
- return cask
38
+ return new Cask(hash, await this.mammoth.read(hash))
43
39
  }
44
40
 
45
41
  async delete(hash: string) {
46
- await this.forklift.delete(hash)
42
+ await this.mammoth.delete(hash)
47
43
  }
48
44
 
49
45
  async clear() {
package/s/cellar/index.ts CHANGED
@@ -1,7 +1,3 @@
1
1
 
2
2
  export * from "./cask.js"
3
3
  export * from "./cellar.js"
4
- export * from "./forklift.js"
5
- export * from "./memory-forklift.js"
6
- export * from "./opfs-forklift.js"
7
-
@@ -0,0 +1,50 @@
1
+
2
+ import {Comrade, Thread, tune, AsSchematic} from "@e280/comrade"
3
+
4
+ export type MammothSchematic = AsSchematic<{
5
+ work: {
6
+ setup(dirname: string): Promise<void>
7
+ write(readable: ReadableStream<Uint8Array>): Promise<string>
8
+ has(hash: string): Promise<boolean>
9
+ read(hash: string): Promise<Blob>
10
+ delete(hash: string): Promise<void>
11
+ hashes(): Promise<string[]>
12
+ }
13
+ host: {}
14
+ }>
15
+
16
+ /** Worker proxy for Mammoth using an OPFS bucket and IndexedDB manifest. */
17
+ export class MammothOpfs {
18
+ static async setup(dirname: string) {
19
+ const thread = await Comrade.thread<MammothSchematic>({
20
+ workerUrl: "/node_modules/@e280/quay/x/cellar/mammoth.worker.bundle.min.js",
21
+ setupHost: () => ({}),
22
+ })
23
+ const mammoth = new this(thread)
24
+ await mammoth.thread.work.setup(dirname)
25
+ return mammoth
26
+ }
27
+
28
+ constructor(private thread: Thread<MammothSchematic>) {}
29
+
30
+ async write(readable: ReadableStream<Uint8Array>) {
31
+ return this.thread.work.write[tune]({transfer: [readable]})(readable)
32
+ }
33
+
34
+ async has(hash: string) {
35
+ return this.thread.work.has(hash)
36
+ }
37
+
38
+ async read(hash: string) {
39
+ return this.thread.work.read(hash)
40
+ }
41
+
42
+ async delete(hash: string) {
43
+ return this.thread.work.delete(hash)
44
+ }
45
+
46
+ async *entries(): AsyncIterable<[string, unknown]> {
47
+ for (const hash of await this.thread.work.hashes())
48
+ yield [hash, undefined]
49
+ }
50
+ }
@@ -0,0 +1,30 @@
1
+
2
+ import {collect} from "@e280/stz"
3
+ import {Comrade} from "@e280/comrade"
4
+ import {Mammoth, OpfsBucket} from "@e280/mammoth"
5
+ import {idbOpen, IdbMagazine, Kv} from "@e280/kv"
6
+
7
+ import type {MammothSchematic} from "./mammoth.js"
8
+
9
+ let mammoth: Mammoth
10
+
11
+ await Comrade.worker<MammothSchematic>(() => ({
12
+ async setup(dirname) {
13
+ const root = await navigator.storage.getDirectory()
14
+ const directory = await root.getDirectoryHandle(dirname, {create: true})
15
+ mammoth = new Mammoth(
16
+ new OpfsBucket(directory),
17
+ new Kv(new IdbMagazine(await idbOpen(`quay.mammoth:${dirname}`))),
18
+ )
19
+ },
20
+
21
+ write: readable => mammoth.write(readable),
22
+ has: hash => mammoth.has(hash),
23
+ read: hash => mammoth.read(hash),
24
+ delete: hash => mammoth.delete(hash),
25
+
26
+ async hashes() {
27
+ return (await collect(mammoth.entries())).map(([hash]) => hash)
28
+ },
29
+ }))
30
+
@@ -1,5 +1,6 @@
1
1
 
2
- import {Kv, collect} from "@e280/kv"
2
+ import {collect} from "@e280/stz"
3
+ import {Kv} from "@e280/kv"
3
4
 
4
5
  export type HyId = string
5
6
  export type HyItem = {parent: HyId | null, children: HyId[]}
@@ -27,7 +28,7 @@ export class HyTree {
27
28
  }
28
29
 
29
30
  async gets(...ids: HyId[]): Promise<(HyItem | undefined)[]> {
30
- const records = await this.records.gets(...ids)
31
+ const records = await this.records.getMany(ids)
31
32
  return records.map(r => {
32
33
  if (!r) return undefined
33
34
  const [parent, children] = r
@@ -36,13 +37,13 @@ export class HyTree {
36
37
  }
37
38
 
38
39
  async require(id: HyId): Promise<HyItem> {
39
- const record = await this.records.require(id)
40
+ const record = await this.records.need(id)
40
41
  const [parent, children] = record
41
42
  return {parent, children}
42
43
  }
43
44
 
44
45
  async requires(...ids: HyId[]): Promise<HyItem[]> {
45
- const records = await this.records.requires(...ids)
46
+ const records = await this.records.needMany(ids)
46
47
  return records.map(([parent, children]) => ({parent, children}))
47
48
  }
48
49
 
@@ -73,10 +74,10 @@ export class HyTree {
73
74
  }
74
75
 
75
76
  // save all new children
76
- await this.records.sets(...newcomers)
77
-
78
- // save updated parent item
79
- await this.records.set(parent, [parentItem.parent, [...siblings]])
77
+ await this.records.commit([
78
+ ...newcomers.map(([id, record]) => this.records.op.set(id, record)),
79
+ this.records.op.set(parent, [parentItem.parent, [...siblings]]),
80
+ ])
80
81
  }
81
82
 
82
83
  /** detach this id from its parent in the hierarchy */
@@ -102,7 +103,7 @@ export class HyTree {
102
103
  await this.detach(id)
103
104
  const tree = await collect(this.crawl(id))
104
105
  const ids = tree.map(({id}) => id)
105
- await this.records.del(...ids)
106
+ await this.records.commit(ids.map(id => this.records.op.delete(id)))
106
107
  }
107
108
 
108
109
  /** delete absolutely everything */
@@ -1,9 +1,10 @@
1
1
 
2
- import {Kv, StorageDriver} from "@e280/kv"
2
+ import {sub} from "@e280/stz"
3
+ import {Kv, StorageMagazine} from "@e280/kv"
3
4
 
4
5
  import {MediaGroup} from "./group.js"
5
6
  import {Cellar} from "../../../cellar/cellar.js"
6
- import {MediaFormat, MediaSchema} from "./schema.js"
7
+ import {MediaFormat, MediaProgress, MediaSchema} from "./schema.js"
7
8
  import {CodexItem} from "../../aspects/codex/parts/codex-item.js"
8
9
 
9
10
  export type MediaRecord = {
@@ -28,6 +29,7 @@ export class MediaLibrary extends MediaGroup {
28
29
  #objectUrls = new Map<string, string>()
29
30
  #index: Kv<MediaRecord>
30
31
 
32
+ progress = sub<[MediaProgress]>()
31
33
  cellar = new Cellar()
32
34
 
33
35
  constructor() {
@@ -60,11 +62,42 @@ export class MediaLibrary extends MediaGroup {
60
62
  }
61
63
 
62
64
  async #storeFile(file: File, parent: CodexItem<MediaSchema>) {
63
- const cask = await this.cellar.save(file)
64
- const existing = await this.#index.get(cask.hash)
65
+ const item = this.#attachPending(file, parent)
66
+
67
+ try {
68
+ const hash = await this.cellar.write(trackProgress(file.stream(), loaded =>
69
+ this.progress.pub({item, loaded, total: file.size})
70
+ ))
71
+ const record = await this.#saveRecord(file, hash)
72
+ item.destroy()
73
+ this.#attachRecord(record, parent)
74
+ return record
75
+ }
76
+ catch (error) {
77
+ item.destroy()
78
+ this.on.refresh.pub({})
79
+ throw error
80
+ }
81
+ }
82
+
83
+ #attachPending(file: File, parent: CodexItem<MediaSchema>) {
84
+ const item = this.config.codex.create("file", {
85
+ label: file.name,
86
+ format: mediaFormat(file.type),
87
+ mime: file.type,
88
+ size: file.size,
89
+ previewUrl: null,
90
+ })
91
+ parent.attach(item)
92
+ this.on.refresh.pub({})
93
+ return item
94
+ }
95
+
96
+ async #saveRecord(file: File, hash: string) {
97
+ const existing = await this.#index.get(hash)
65
98
  const now = Date.now()
66
99
  const record: MediaRecord = {
67
- hash: cask.hash,
100
+ hash,
68
101
  label: existing?.label ?? file.name,
69
102
  format: mediaFormat(file.type),
70
103
  mime: file.type,
@@ -72,10 +105,9 @@ export class MediaLibrary extends MediaGroup {
72
105
  createdAt: existing?.createdAt ?? now,
73
106
  updatedAt: now,
74
107
  }
75
- await this.#index.set(record.hash, record)
108
+ await this.#index.set(hash, record)
76
109
  if (record.format === "image")
77
- this.#setPreview(record.hash, URL.createObjectURL(file))
78
- this.#attachRecord(record, parent)
110
+ this.#setPreview(hash, URL.createObjectURL(file))
79
111
  return record
80
112
  }
81
113
 
@@ -87,7 +119,7 @@ export class MediaLibrary extends MediaGroup {
87
119
 
88
120
  async #delete(hashes: string[]) {
89
121
  for (const hash of hashes) {
90
- await this.#index.del(hash)
122
+ await this.#index.delete(hash)
91
123
  await this.cellar.delete(hash)
92
124
  this.#revokePreview(hash)
93
125
  }
@@ -155,9 +187,9 @@ function mediaFormat(mime: string): MediaFormat {
155
187
  function mediaIndex(scope: string) {
156
188
  const storage = globalThis.localStorage
157
189
  if (storage)
158
- return new Kv<MediaRecord>(new StorageDriver(storage))
159
- .scope("quay.media")
160
- .scope(scope)
190
+ return new Kv<MediaRecord>(new StorageMagazine(storage))
191
+ .scope<MediaRecord>("quay.media")
192
+ .scope<MediaRecord>(scope)
161
193
 
162
194
  const existing = memoryIndexes.get(scope)
163
195
  if (existing)
@@ -168,3 +200,16 @@ function mediaIndex(scope: string) {
168
200
  return index
169
201
  }
170
202
 
203
+ function trackProgress(
204
+ readable: ReadableStream<Uint8Array>,
205
+ onProgress: (loaded: number) => void,
206
+ ) {
207
+ let loaded = 0
208
+ return readable.pipeThrough(new TransformStream({
209
+ transform(chunk, controller) {
210
+ loaded += chunk.byteLength
211
+ onProgress(loaded)
212
+ controller.enqueue(chunk)
213
+ },
214
+ }))
215
+ }
@@ -1,6 +1,7 @@
1
1
 
2
2
  import {Content} from "@e280/sly"
3
3
  import {AsSchema} from "../../aspects/codex/parts/types.js"
4
+ import {CodexItem} from "../../aspects/codex/parts/codex-item.js"
4
5
 
5
6
  export type MediaFormat = "video" | "image" | "audio" | "other"
6
7
 
@@ -23,3 +24,8 @@ export type MediaSchema = AsSchema<{
23
24
  }
24
25
  }>
25
26
 
27
+ export type MediaProgress = {
28
+ item: CodexItem<MediaSchema>
29
+ loaded: number
30
+ total: number
31
+ }
@@ -2,7 +2,5 @@
2
2
  export declare class Cask {
3
3
  hash: string;
4
4
  file: Blob;
5
- static hash(file: Blob): Promise<string>;
6
- static make(file: Blob): Promise<Cask>;
7
5
  constructor(hash: string, file: Blob);
8
6
  }
package/x/cellar/cask.js CHANGED
@@ -1,20 +1,7 @@
1
- import { Hex } from "@e280/stz";
2
- import { blake3 } from "@awasm/noble";
3
1
  /** File with a hash label */
4
2
  export class Cask {
5
3
  hash;
6
4
  file;
7
- static async hash(file) {
8
- const hasher = blake3.create();
9
- for await (const chunk of file.stream())
10
- hasher.update(chunk);
11
- const digest = hasher.digest();
12
- return Hex.fromBytes(digest);
13
- }
14
- static async make(file) {
15
- const hash = await this.hash(file);
16
- return new this(hash, file);
17
- }
18
5
  constructor(hash, file) {
19
6
  this.hash = hash;
20
7
  this.file = file;
@@ -1 +1 @@
1
- {"version":3,"file":"cask.js","sourceRoot":"","sources":["../../s/cellar/cask.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAC,MAAM,WAAW,CAAA;AAC7B,OAAO,EAAC,MAAM,EAAC,MAAM,cAAc,CAAA;AAEnC,6BAA6B;AAC7B,MAAM,OAAO,IAAI;IAgBG;IAAqB;IAfxC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAU;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;QAE9B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE;YACtC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAErB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;QAC9B,OAAO,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAU;QAC3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC5B,CAAC;IAED,YAAmB,IAAY,EAAS,IAAU;QAA/B,SAAI,GAAJ,IAAI,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAM;IAAG,CAAC;CACtD"}
1
+ {"version":3,"file":"cask.js","sourceRoot":"","sources":["../../s/cellar/cask.ts"],"names":[],"mappings":"AACA,6BAA6B;AAC7B,MAAM,OAAO,IAAI;IACG;IAAqB;IAAxC,YAAmB,IAAY,EAAS,IAAU;QAA/B,SAAI,GAAJ,IAAI,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAM;IAAG,CAAC;CACtD"}
@@ -1,21 +1,23 @@
1
+ import { Mammoth } from "@e280/mammoth";
1
2
  import { Cask } from "./cask.js";
2
- import { Forklift } from "./forklift.js";
3
+ import { MammothOpfs } from "./mammoth.js";
3
4
  /**
4
5
  * Cellar is an immutable content-addressable file store
5
6
  * - files are identified by their hashes (duplicates are impossible)
6
7
  * - files cannot be modified (delete it and save another)
7
8
  * - no filenames or metadata (store those somewhere else)
8
- * - pass in a forklift, which is the backend that actually stores the data
9
- * - MemoryForklift is the default
10
- * - OpfsForklift is probably what you really want in the browser
9
+ * - new Cellar uses Mammoth's in-memory backend by default
10
+ * - Cellar.opfs creates MammothOpfs for persistent browser storage
11
+ * - pass in a Mammoth to use a different backend
12
+ * - most likely you want to use Cellar.opfs for storing files in the browser
11
13
  */
12
14
  export declare class Cellar {
13
- private forklift;
15
+ private mammoth;
14
16
  static opfs(dirname?: string): Promise<Cellar>;
15
- constructor(forklift?: Forklift);
17
+ constructor(mammoth?: Mammoth | MammothOpfs);
16
18
  list(): AsyncIterable<string>;
17
19
  has(hash: string): Promise<boolean>;
18
- save(file: Blob): Promise<Cask>;
20
+ write(readable: ReadableStream<Uint8Array>): Promise<string>;
19
21
  load(hash: string): Promise<Cask>;
20
22
  delete(hash: string): Promise<void>;
21
23
  clear(): Promise<void>;
@@ -1,44 +1,39 @@
1
+ import { Mammoth } from "@e280/mammoth";
1
2
  import { Cask } from "./cask.js";
2
- import { OpfsForklift } from "./opfs-forklift.js";
3
- import { MemoryForklift } from "./memory-forklift.js";
3
+ import { MammothOpfs } from "./mammoth.js";
4
4
  /**
5
5
  * Cellar is an immutable content-addressable file store
6
6
  * - files are identified by their hashes (duplicates are impossible)
7
7
  * - files cannot be modified (delete it and save another)
8
8
  * - no filenames or metadata (store those somewhere else)
9
- * - pass in a forklift, which is the backend that actually stores the data
10
- * - MemoryForklift is the default
11
- * - OpfsForklift is probably what you really want in the browser
9
+ * - new Cellar uses Mammoth's in-memory backend by default
10
+ * - Cellar.opfs creates MammothOpfs for persistent browser storage
11
+ * - pass in a Mammoth to use a different backend
12
+ * - most likely you want to use Cellar.opfs for storing files in the browser
12
13
  */
13
14
  export class Cellar {
14
- forklift;
15
+ mammoth;
15
16
  static async opfs(dirname = "cellar") {
16
- return new this(await OpfsForklift.setup(dirname));
17
+ return new this(await MammothOpfs.setup(dirname));
17
18
  }
18
- constructor(forklift = new MemoryForklift()) {
19
- this.forklift = forklift;
19
+ constructor(mammoth = new Mammoth()) {
20
+ this.mammoth = mammoth;
20
21
  }
21
22
  async *list() {
22
- yield* this.forklift.list();
23
+ for await (const [hash] of this.mammoth.entries())
24
+ yield hash;
23
25
  }
24
26
  async has(hash) {
25
- return this.forklift.has(hash);
27
+ return this.mammoth.has(hash);
26
28
  }
27
- async save(file) {
28
- const cask = await Cask.make(file);
29
- if (!await this.forklift.has(cask.hash))
30
- await this.forklift.save(cask.hash, cask.file);
31
- return cask;
29
+ async write(readable) {
30
+ return this.mammoth.write(readable);
32
31
  }
33
32
  async load(hash) {
34
- const bytes = await this.forklift.load(hash);
35
- const cask = await Cask.make(bytes);
36
- if (cask.hash !== hash)
37
- throw new Error(`corruption error, hash mismatch (requested ${hash}) (got ${cask.hash})`);
38
- return cask;
33
+ return new Cask(hash, await this.mammoth.read(hash));
39
34
  }
40
35
  async delete(hash) {
41
- await this.forklift.delete(hash);
36
+ await this.mammoth.delete(hash);
42
37
  }
43
38
  async clear() {
44
39
  for await (const hash of this.list())
@@ -1 +1 @@
1
- {"version":3,"file":"cellar.js","sourceRoot":"","sources":["../../s/cellar/cellar.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAE9B,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAA;AAEnD;;;;;;;;GAQG;AACH,MAAM,OAAO,MAAM;IAKE;IAJpB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,QAAQ;QACnC,OAAO,IAAI,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;IACnD,CAAC;IAED,YAAoB,WAAqB,IAAI,cAAc,EAAE;QAAzC,aAAQ,GAAR,QAAQ,CAAiC;IAAG,CAAC;IAEjE,KAAK,CAAC,CAAC,IAAI;QACV,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAU;QACpB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YACtC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/C,OAAO,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACtB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,IAAI,UAAU,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;QACjH,OAAO,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACxB,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,CAAC,KAAK;QACV,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;YACnC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;CACD"}
1
+ {"version":3,"file":"cellar.js","sourceRoot":"","sources":["../../s/cellar/cellar.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AAErC,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAC9B,OAAO,EAAC,WAAW,EAAC,MAAM,cAAc,CAAA;AAExC;;;;;;;;;GASG;AACH,MAAM,OAAO,MAAM;IAKE;IAJpB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,QAAQ;QACnC,OAAO,IAAI,IAAI,CAAC,MAAM,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;IAClD,CAAC;IAED,YAAoB,UAAiC,IAAI,OAAO,EAAE;QAA9C,YAAO,GAAP,OAAO,CAAuC;IAAG,CAAC;IAEtE,KAAK,CAAC,CAAC,IAAI;QACV,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YAChD,MAAM,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,QAAoC;QAC/C,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACpC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACtB,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACrD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACxB,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAChC,CAAC;IAED,KAAK,CAAC,KAAK;QACV,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;YACnC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;CACD"}
@@ -1,8 +1,7 @@
1
1
  import { Science } from "@e280/science";
2
2
  declare const _default: {
3
- "save + load roundtrip": Science.Test;
4
- "save is idempotent": Science.Test;
5
- "throws on corruption": Science.Test;
3
+ "write + load roundtrip": Science.Test;
4
+ "write is idempotent": Science.Test;
6
5
  "delete removes file": Science.Test;
7
6
  "load missing throws": Science.Test;
8
7
  "list returns saved hashes": Science.Test;