@hozon/blob 0.1.0 → 0.1.1

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/lib/fresh.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { BlobEntry, BlobStoreAPI } from '@hozon/store-blob';
2
+ export declare function getFreshEntry(store: BlobStoreAPI, blobID: string): Promise<BlobEntry | null>;
package/lib/fresh.js ADDED
@@ -0,0 +1,6 @@
1
+ // A scope remembers getEntry results; reads made under the blob lock must see the current row.
2
+ export async function getFreshEntry(store, blobID) {
3
+ return (await store.getEntries([
4
+ blobID
5
+ ])).get(blobID) ?? null;
6
+ }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { BlobLockTimeoutError } from '@hozon/blob-backend';
2
2
  export { InvalidBlobIDError } from '@hozon/blob-id';
3
+ export { InvalidCursorError } from '@hozon/store-blob';
3
4
  export { BlobIDMismatchError, type BlobIDMismatchErrorParams, BlobNotFoundError, BlobTooLargeError, BlobWriteAbortedError, ChunkDigestMismatchError, type ChunkDigestMismatchErrorParams, ChunkLengthError, type ChunkLengthErrorParams, ContentTypeMismatchError, type ContentTypeMismatchErrorParams, InvalidManifestError, InvalidRangeError, TransferIncompleteError, type TransferIncompleteErrorParams, } from './errors.js';
4
5
  export { type BlobLimits, DEFAULT_CHUNK_SIZE, DEFAULT_LIMITS } from './limits.js';
5
6
  export { type BlobService, type BlobServiceParams, createBlobService } from './service.js';
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { BlobLockTimeoutError } from '@hozon/blob-backend';
2
2
  export { InvalidBlobIDError } from '@hozon/blob-id';
3
+ export { InvalidCursorError } from '@hozon/store-blob';
3
4
  export { BlobIDMismatchError, BlobNotFoundError, BlobTooLargeError, BlobWriteAbortedError, ChunkDigestMismatchError, ChunkLengthError, ContentTypeMismatchError, InvalidManifestError, InvalidRangeError, TransferIncompleteError } from './errors.js';
4
5
  export { DEFAULT_CHUNK_SIZE, DEFAULT_LIMITS } from './limits.js';
5
6
  export { createBlobService } from './service.js';
package/lib/manage.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { getBlobStore } from '@hozon/store-blob';
2
2
  import { BlobNotFoundError } from './errors.js';
3
+ import { getFreshEntry } from './fresh.js';
3
4
  export async function setBlobPinned(ctx, id, pinned) {
4
5
  const blobID = ctx.codec.canonicalize(id);
5
6
  await ctx.lock.withLock(blobID, async ()=>{
6
7
  const store = await getBlobStore(ctx.db);
7
- if (await store.getEntry(blobID) === null) throw new BlobNotFoundError(blobID);
8
+ if (await getFreshEntry(store, blobID) === null) throw new BlobNotFoundError(blobID);
8
9
  await store.setPinned(blobID, pinned);
9
10
  });
10
11
  }
@@ -12,7 +13,7 @@ export async function deleteBlob(ctx, id) {
12
13
  const blobID = ctx.codec.canonicalize(id);
13
14
  return await ctx.lock.withLock(blobID, async ()=>{
14
15
  const store = await getBlobStore(ctx.db);
15
- const entry = await store.getEntry(blobID);
16
+ const entry = await getFreshEntry(store, blobID);
16
17
  if (entry === null) return false;
17
18
  const transfer = entry.state === 'partial' ? await store.getTransfer(blobID) : null;
18
19
  // The row goes first: a crash then leaves orphaned bytes, never a row without bytes.
package/lib/transfer.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getBlobStore } from '@hozon/store-blob';
2
2
  import { BlobIDMismatchError, BlobNotFoundError, ChunkDigestMismatchError, ChunkLengthError, InvalidManifestError, TransferIncompleteError } from './errors.js';
3
+ import { getFreshEntry } from './fresh.js';
3
4
  import { createStagingID } from './write.js';
4
5
  function bytesEqual(a, b) {
5
6
  if (a.length !== b.length) return false;
@@ -98,7 +99,7 @@ async function stagingExists(backend, transfer, chunkSize) {
98
99
  // Lock held: the transfer of a `partial` entry, or BlobNotFoundError.
99
100
  async function requireTransfer(ctx, blobID) {
100
101
  const store = await getBlobStore(ctx.db);
101
- const entry = await store.getEntry(blobID);
102
+ const entry = await getFreshEntry(store, blobID);
102
103
  const transfer = entry?.state === 'partial' ? await store.getTransfer(blobID) : null;
103
104
  if (entry === null || transfer === null) {
104
105
  throw new BlobNotFoundError(blobID, {
@@ -119,7 +120,7 @@ export async function beginFetch(ctx, id, manifest) {
119
120
  const chunkSize = manifest.contentLength === 0 ? ctx.chunkSize : manifest.chunkSize;
120
121
  await ctx.lock.withLock(blobID, async ()=>{
121
122
  const store = await getBlobStore(ctx.db);
122
- const entry = await store.getEntry(blobID);
123
+ const entry = await getFreshEntry(store, blobID);
123
124
  if (entry?.state === 'local') return;
124
125
  // A stub with another length could never complete against this manifest.
125
126
  if (entry !== null && entry.contentLength !== manifest.contentLength) {
@@ -166,7 +167,7 @@ export async function getPresentChunks(ctx, id) {
166
167
  const blobID = ctx.codec.canonicalize(id);
167
168
  return await ctx.lock.withLock(blobID, async ()=>{
168
169
  const store = await getBlobStore(ctx.db);
169
- const entry = await store.getEntry(blobID);
170
+ const entry = await getFreshEntry(store, blobID);
170
171
  if (entry?.state !== 'partial') return [];
171
172
  const transfer = await store.getTransfer(blobID);
172
173
  if (transfer === null) return [];
@@ -289,7 +290,7 @@ export async function completeFetch(ctx, id) {
289
290
  index,
290
291
  digest
291
292
  })));
292
- const promoted = await store.getEntry(blobID);
293
+ const promoted = await getFreshEntry(store, blobID);
293
294
  if (promoted === null) throw new Error(`Blob entry ${blobID} missing after promotion`);
294
295
  return promoted;
295
296
  });
package/lib/write.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { hashStream } from '@hozon/blob-id';
2
2
  import { getBlobStore } from '@hozon/store-blob';
3
3
  import { BlobIDMismatchError, BlobTooLargeError, BlobWriteAbortedError, ContentTypeMismatchError } from './errors.js';
4
+ import { getFreshEntry } from './fresh.js';
4
5
  // Random staging ID: `prefix` (`w` for writes, `t` for transfers), a dash, 32 hex chars.
5
6
  export function createStagingID(runtime, prefix) {
6
7
  const bytes = runtime.getRandomValues(new Uint8Array(16));
@@ -132,14 +133,14 @@ export async function writeBlob(ctx, params) {
132
133
  return await ctx.lock.withLock(blobID, async ()=>{
133
134
  throwIfAborted(signal);
134
135
  const store = await getBlobStore(ctx.db);
135
- const existing = await store.getEntry(blobID);
136
+ const existing = await getFreshEntry(store, blobID);
136
137
  if (existing?.state === 'local') {
137
138
  await discardStaging();
138
139
  // Existing metadata wins; only a null content type is filled in.
139
140
  const fill = async (target)=>{
140
141
  if (existing.contentType !== null || contentType === undefined) return existing;
141
142
  await target.fillContentType(blobID, contentType);
142
- return await target.getEntry(blobID) ?? existing;
143
+ return await getFreshEntry(target, blobID) ?? existing;
143
144
  };
144
145
  const { entry, result } = await record(ctx, fn, fill);
145
146
  return {
@@ -168,7 +169,7 @@ export async function writeBlob(ctx, params) {
168
169
  }));
169
170
  const promote = async (target)=>{
170
171
  await target.promoteEntry(input, chunks);
171
- const entry = await target.getEntry(blobID);
172
+ const entry = await getFreshEntry(target, blobID);
172
173
  if (entry === null) throw new Error(`Blob entry ${blobID} missing after promotion`);
173
174
  return entry;
174
175
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hozon/blob",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Content-addressed blob service with verified writes for Hozon",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,17 +18,18 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "@hozon/blob-backend": "^0.2.0",
21
- "@hozon/blob-id": "^0.1.0",
22
- "@hozon/db": "^0.2.2",
23
- "@hozon/store-blob": "^0.2.0",
21
+ "@hozon/blob-id": "^0.1.1",
22
+ "@hozon/db": "^0.3.0",
23
+ "@hozon/store-blob": "^0.2.2",
24
24
  "@sozai/runtime": "^0.1.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@hozon/blob-node-fs": "^0.2.0",
28
28
  "@hozon/node-sqlite": "^0.1.0",
29
- "@types/node": "^26.6.4",
30
- "del-cli": "^7.0.0",
31
- "vitest": "5.0.3"
29
+ "@sozai/codec": "^0.4.3",
30
+ "@types/node": "^26.6.5",
31
+ "del-cli": "^8.0.1",
32
+ "vitest": "5.0.5"
32
33
  },
33
34
  "publishConfig": {
34
35
  "access": "public"