Compare commits

...

9 Commits

Author SHA1 Message Date
Scott Wei 1b53cf5ff8 feat: remove Store.list() from interface
Removes the list() method from the Store type and all implementations.
Callers now use listByType() or has() instead.

The CLI 'list' subcommand is removed. 'schema list' now uses
listByType(metaHash) to enumerate schemas.

Closes #11
2026-05-19 01:24:12 +08:00
xiaoju 17ed619900 chore: release @uncaged/* 0.3.0 2026-05-18 15:00:40 +00:00
xiaomo bad62a82a9 Merge pull request 'feat: disallow self-referencing nodes except via bootstrap()' (#13) from feat/12-no-null-type into main 2026-05-18 14:59:17 +00:00
xiaoju 4989cd31ba Remove null from Store.put typeHash parameter
Self-referencing nodes are created only through bootstrap() via an internal BOOTSTRAP_STORE symbol on memory and fs store implementations. put() always requires a Hash typeHash and uses computeHash.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 14:54:40 +00:00
xiaoju 38aad696fc chore: release @uncaged/* 0.2.0 2026-05-18 14:15:33 +00:00
xiaomo fb81f5a429 Merge pull request 'feat: add listByType(typeHash) to Store interface' (#10) from feat/9-list-by-type into main 2026-05-18 14:13:16 +00:00
xiaoju 5fc475704b feat: add listByType(typeHash) to Store interface
Implement in-memory type index and fs append-only _index files. Rebuild index from existing .bin nodes on first load when _index is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 14:11:53 +00:00
xiaoju 6e77e4a110 fix: replace workspace:^ deps with real versions + merge cli global store path
- All packages bumped to 0.1.3 via changesets (fixed group)
- Replace workspace:^ with ^0.1.x in json-cas-fs, json-cas-workflow, cli-json-cas
- Merge PR #8: cli default store path → ~/.uncaged/json-cas/

Co-authored-by: 星月 🌙 (SORA Team)
小橘 🍊(NEKO Team)
2026-05-18 10:47:12 +00:00
xiaoju a3a21b153c fix: replace workspace:^ with ^0.1.1 in json-cas-fs deps 2026-05-18 10:43:22 +00:00
18 changed files with 449 additions and 68 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/changelog-github",
"changelog": ["@changesets/changelog-github", { "repo": "uncaged/json-cas" }],
"commit": false,
"fixed": [["@uncaged/*"]],
"linked": [],
+27
View File
@@ -0,0 +1,27 @@
# @uncaged/cli-json-cas
## 0.3.0
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.3.0
- @uncaged/json-cas-fs@0.3.0
## 0.2.0
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.2.0
- @uncaged/json-cas-fs@0.2.0
## 0.1.3
### Patch Changes
- fix: replace workspace:^ with actual version numbers in published dependencies
- Updated dependencies []:
- @uncaged/json-cas@0.1.3
- @uncaged/json-cas-fs@0.1.3
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/cli-json-cas",
"version": "0.1.1",
"version": "0.3.0",
"type": "module",
"bin": {
"json-cas": "./src/index.ts"
@@ -9,7 +9,7 @@
"test": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "workspace:^",
"@uncaged/json-cas-fs": "workspace:^"
"@uncaged/json-cas": "^0.3.0",
"@uncaged/json-cas-fs": "^0.3.0"
}
}
+3 -13
View File
@@ -116,10 +116,10 @@ async function cmdSchemaGet(args: string[]): Promise<void> {
async function cmdSchemaList(): Promise<void> {
const store = openStore();
const metaHash = await bootstrap(store);
for (const hash of store.list()) {
for (const hash of store.listByType(metaHash)) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null && node.type === metaHash) {
if (node !== null) {
const schema = node.payload as JSONSchema;
const name =
(schema.title as string | undefined) ??
@@ -176,12 +176,7 @@ async function cmdVerify(args: string[]): Promise<void> {
console.log(ok ? "ok" : "corrupted");
}
async function cmdList(): Promise<void> {
const store = openStore();
for (const hash of store.list()) {
console.log(hash);
}
}
async function cmdRefs(args: string[]): Promise<void> {
const hash = args[0];
@@ -271,7 +266,6 @@ Commands:
get <hash> Print node as JSON
has <hash> Print true/false
verify <hash> Verify integrity, print ok/corrupted
list List all hashes
refs <hash> List direct cas_ref edges
walk <hash> [--format tree] Recursive traversal
hash <type-hash> <file.json> Compute hash without storing (dry run)
@@ -337,10 +331,6 @@ switch (cmd) {
await cmdVerify(rest);
break;
case "list":
await cmdList();
break;
case "refs":
await cmdRefs(rest);
break;
+30
View File
@@ -0,0 +1,30 @@
# @uncaged/json-cas-fs
## 0.3.0
### Minor Changes
- Disallow self-referencing nodes in put(). typeHash is now required (no null). Self-ref only via bootstrap().
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.3.0
## 0.2.0
### Minor Changes
- Add listByType(typeHash) to Store interface for O(1) type-based queries, with append-only fs index
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.2.0
## 0.1.3
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.1.3
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/json-cas-fs",
"version": "0.1.1",
"version": "0.3.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
@@ -10,7 +10,7 @@
"test": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "workspace:^",
"@uncaged/json-cas": "^0.3.0",
"cborg": "^4.2.3"
}
}
+92 -11
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { CasNode } from "@uncaged/json-cas";
@@ -30,15 +30,15 @@ describe("createFsStore – init and bootstrap", () => {
test("store opens against an existing empty dir", () => {
const store = createFsStore(dir);
expect(store.list()).toEqual([]);
expect(store.listByType("0000000000000")).toEqual([]);
});
test("store creates the directory on first put", async () => {
const nested = join(dir, "sub", "store");
const store = createFsStore(nested);
const typeHash = await computeSelfHash({ name: "t" });
await store.put(typeHash, { x: 1 });
expect(store.list()).toHaveLength(1);
const hash = await store.put(typeHash, { x: 1 });
expect(store.has(hash)).toBe(true);
});
test("bootstrap returns a valid 13-char self-referencing hash", async () => {
@@ -58,7 +58,7 @@ describe("createFsStore – init and bootstrap", () => {
const h2 = await bootstrap(store);
expect(h1).toBe(h2);
expect(store.list()).toHaveLength(1);
expect(store.listByType(h1)).toHaveLength(1);
});
});
@@ -84,7 +84,7 @@ describe("createFsStore – persistence round-trip", () => {
const store2 = createFsStore(dir);
expect(store2.has(h1)).toBe(true);
expect(store2.has(h2)).toBe(true);
expect(store2.list()).toHaveLength(2);
expect(store2.listByType(typeHash)).toHaveLength(2);
});
test("round-trip preserves type, payload, and timestamp", async () => {
@@ -125,7 +125,7 @@ describe("createFsStore – persistence round-trip", () => {
const ts2 = store2.get(hash)?.timestamp;
expect(ts1).toBe(ts2);
expect(store2.list()).toHaveLength(1);
expect(store2.listByType(typeHash)).toHaveLength(1);
});
});
@@ -151,7 +151,7 @@ describe("createFsStore – has and list", () => {
expect(store.has(hash)).toBe(true);
});
test("list returns all stored hashes", async () => {
test("listByType returns all stored hashes for a type", async () => {
const store = createFsStore(dir);
const typeHash = await computeSelfHash({ name: "t" });
@@ -159,16 +159,16 @@ describe("createFsStore – has and list", () => {
const h2 = await store.put(typeHash, { a: 2 });
const h3 = await store.put(typeHash, { a: 3 });
const all = store.list();
const all = store.listByType(typeHash);
expect(all).toHaveLength(3);
expect(all).toContain(h1);
expect(all).toContain(h2);
expect(all).toContain(h3);
});
test("list returns empty array on fresh store", () => {
test("listByType returns empty array on fresh store", () => {
const store = createFsStore(dir);
expect(store.list()).toEqual([]);
expect(store.listByType("0000000000000")).toEqual([]);
});
test("get returns null for unknown hash", () => {
@@ -177,6 +177,87 @@ describe("createFsStore – has and list", () => {
});
});
// ──────────────────────────────────────────────────────────────────────────────
// listByType and index migration
// ──────────────────────────────────────────────────────────────────────────────
describe("createFsStore – listByType", () => {
let dir: string;
beforeEach(() => {
dir = makeTmpDir();
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
test("returns empty array for unknown type", () => {
const store = createFsStore(dir);
expect(store.listByType("0000000000000")).toEqual([]);
});
test("returns all hashes for the given type", async () => {
const store = createFsStore(dir);
const typeHash = await computeSelfHash({ name: "t" });
const otherType = await computeSelfHash({ name: "other" });
const h1 = await store.put(typeHash, { a: 1 });
const h2 = await store.put(typeHash, { a: 2 });
await store.put(otherType, { b: 1 });
const byType = store.listByType(typeHash);
expect(byType).toHaveLength(2);
expect(byType).toContain(h1);
expect(byType).toContain(h2);
});
test("listByType survives round-trip across store instances", async () => {
const typeHash = await computeSelfHash({ name: "persist-by-type" });
const store1 = createFsStore(dir);
const h1 = await store1.put(typeHash, { x: 1 });
const h2 = await store1.put(typeHash, { x: 2 });
const store2 = createFsStore(dir);
const byType = store2.listByType(typeHash);
expect(byType).toHaveLength(2);
expect(byType).toContain(h1);
expect(byType).toContain(h2);
});
test("idempotent put does not duplicate in listByType", async () => {
const typeHash = await computeSelfHash({ name: "idempotent-index" });
const store1 = createFsStore(dir);
const hash = await store1.put(typeHash, { n: 7 });
await store1.put(typeHash, { n: 7 });
const store2 = createFsStore(dir);
expect(store2.listByType(typeHash)).toEqual([hash]);
});
test("rebuilds _index from .bin files when index is missing", async () => {
const typeHash = await computeSelfHash({ name: "migrate" });
const store1 = createFsStore(dir);
const h1 = await store1.put(typeHash, { a: 1 });
const h2 = await store1.put(typeHash, { a: 2 });
rmSync(join(dir, "_index"), { recursive: true, force: true });
const store2 = createFsStore(dir);
expect(store2.listByType(typeHash)).toEqual([h1, h2]);
expect(existsSync(join(dir, "_index", typeHash))).toBe(true);
expect(readdirSync(join(dir, "_index"))).toContain(typeHash);
});
test("bootstrap node is listed under its self type after reload", async () => {
const store1 = createFsStore(dir);
const hash = await bootstrap(store1);
const store2 = createFsStore(dir);
expect(store2.listByType(hash)).toEqual([hash]);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// verify on disk-loaded nodes
// ──────────────────────────────────────────────────────────────────────────────
+115 -12
View File
@@ -1,4 +1,6 @@
import {
appendFileSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
@@ -8,9 +10,16 @@ import {
import { join } from "node:path";
import type { CasNode, Hash, Store } from "@uncaged/json-cas";
import { cborEncode, computeHash, computeSelfHash } from "@uncaged/json-cas";
import {
BOOTSTRAP_STORE,
cborEncode,
computeHash,
computeSelfHash,
} from "@uncaged/json-cas";
import { decode } from "cborg";
const INDEX_DIR = "_index";
function loadDir(dir: string, data: Map<Hash, CasNode>): void {
let entries: string[];
try {
@@ -31,20 +40,108 @@ function loadDir(dir: string, data: Map<Hash, CasNode>): void {
}
}
function parseIndexFile(content: string): Hash[] {
if (content.length === 0) return [];
return content.split("\n").filter((line) => line.length > 0) as Hash[];
}
function loadTypeIndex(indexDir: string): Map<Hash, Hash[]> {
const typeIndex = new Map<Hash, Hash[]>();
let entries: string[];
try {
entries = readdirSync(indexDir);
} catch {
return typeIndex;
}
for (const typeHash of entries) {
try {
const content = readFileSync(join(indexDir, typeHash), "utf8");
typeIndex.set(typeHash as Hash, parseIndexFile(content));
} catch {
// skip unreadable index files
}
}
return typeIndex;
}
function buildTypeIndexFromNodes(data: Map<Hash, CasNode>): Map<Hash, Hash[]> {
const typeIndex = new Map<Hash, Hash[]>();
for (const [hash, node] of data) {
const list = typeIndex.get(node.type) ?? [];
list.push(hash);
typeIndex.set(node.type, list);
}
return typeIndex;
}
function writeTypeIndex(indexDir: string, typeIndex: Map<Hash, Hash[]>): void {
mkdirSync(indexDir, { recursive: true });
for (const [typeHash, hashes] of typeIndex) {
const body = hashes.length > 0 ? `${hashes.join("\n")}\n` : "";
writeFileSync(join(indexDir, typeHash), body, "utf8");
}
}
function loadOrMigrateTypeIndex(
dir: string,
data: Map<Hash, CasNode>,
): Map<Hash, Hash[]> {
const indexDir = join(dir, INDEX_DIR);
if (!existsSync(indexDir)) {
const typeIndex = buildTypeIndexFromNodes(data);
if (typeIndex.size > 0) {
writeTypeIndex(indexDir, typeIndex);
}
return typeIndex;
}
return loadTypeIndex(indexDir);
}
function appendToTypeIndex(
indexDir: string,
typeIndex: Map<Hash, Hash[]>,
type: Hash,
hash: Hash,
): void {
mkdirSync(indexDir, { recursive: true });
appendFileSync(join(indexDir, type), `${hash}\n`, "utf8");
const list = typeIndex.get(type) ?? [];
list.push(hash);
typeIndex.set(type, list);
}
export function createFsStore(dir: string): Store {
const data = new Map<Hash, CasNode>();
loadDir(dir, data);
const indexDir = join(dir, INDEX_DIR);
const typeIndex = loadOrMigrateTypeIndex(dir, data);
return {
async put(typeHash: Hash | null, payload: unknown): Promise<Hash> {
const hash =
typeHash === null
? await computeSelfHash(payload)
: await computeHash(typeHash, payload);
async function putSelfReferencing(payload: unknown): Promise<Hash> {
const hash = await computeSelfHash(payload);
if (!data.has(hash)) {
const node: CasNode = { type: hash, payload, timestamp: Date.now() };
data.set(hash, node);
mkdirSync(dir, { recursive: true });
const tmp = join(dir, `${hash}.tmp`);
const dest = join(dir, `${hash}.bin`);
writeFileSync(
tmp,
cborEncode({ type: hash, payload, timestamp: node.timestamp }),
);
renameSync(tmp, dest);
appendToTypeIndex(indexDir, typeIndex, hash, hash);
}
return hash;
}
const store: Store = {
async put(typeHash: Hash, payload: unknown): Promise<Hash> {
const hash = await computeHash(typeHash, payload);
if (!data.has(hash)) {
const type = typeHash === null ? hash : typeHash;
const node: CasNode = { type, payload, timestamp: Date.now() };
const node: CasNode = { type: typeHash, payload, timestamp: Date.now() };
data.set(hash, node);
mkdirSync(dir, { recursive: true });
@@ -52,9 +149,11 @@ export function createFsStore(dir: string): Store {
const dest = join(dir, `${hash}.bin`);
writeFileSync(
tmp,
cborEncode({ type, payload, timestamp: node.timestamp }),
cborEncode({ type: typeHash, payload, timestamp: node.timestamp }),
);
renameSync(tmp, dest);
appendToTypeIndex(indexDir, typeIndex, typeHash, hash);
}
return hash;
@@ -68,8 +167,12 @@ export function createFsStore(dir: string): Store {
return data.has(hash);
},
list(): Hash[] {
return [...data.keys()];
listByType(typeHash: Hash): Hash[] {
return typeIndex.get(typeHash) ?? [];
},
[BOOTSTRAP_STORE]: putSelfReferencing,
};
return store;
}
+24
View File
@@ -0,0 +1,24 @@
# @uncaged/json-cas-workflow
## 0.3.0
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.3.0
## 0.2.0
### Patch Changes
- Updated dependencies []:
- @uncaged/json-cas@0.2.0
## 0.1.3
### Patch Changes
- fix: replace workspace:^ with actual version numbers in published dependencies
- Updated dependencies []:
- @uncaged/json-cas@0.1.3
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/json-cas-workflow",
"version": "0.1.1",
"version": "0.3.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
@@ -10,6 +10,6 @@
"test": "bun test"
},
"dependencies": {
"@uncaged/json-cas": "workspace:^"
"@uncaged/json-cas": "^0.3.0"
}
}
+15
View File
@@ -0,0 +1,15 @@
# @uncaged/json-cas
## 0.3.0
### Minor Changes
- Disallow self-referencing nodes in put(). typeHash is now required (no null). Self-ref only via bootstrap().
## 0.2.0
### Minor Changes
- Add listByType(typeHash) to Store interface for O(1) type-based queries, with append-only fs index
## 0.1.3
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/json-cas",
"version": "0.1.1",
"version": "0.3.0",
"type": "module",
"main": "./src/index.ts",
"exports": {
@@ -0,0 +1,16 @@
import type { Hash, Store } from "./types.js";
/** @internal Store implementations attach this for bootstrap() only. */
export const BOOTSTRAP_STORE = Symbol.for("@uncaged/json-cas/bootstrap-store");
export type BootstrapCapableStore = Store & {
[BOOTSTRAP_STORE](payload: unknown): Promise<Hash>;
};
export function isBootstrapCapableStore(
store: Store,
): store is BootstrapCapableStore {
return (
typeof (store as BootstrapCapableStore)[BOOTSTRAP_STORE] === "function"
);
}
+8 -1
View File
@@ -1,3 +1,7 @@
import {
BOOTSTRAP_STORE,
isBootstrapCapableStore,
} from "./bootstrap-capable.js";
import type { Hash, Store } from "./types.js";
/**
@@ -23,5 +27,8 @@ const BOOTSTRAP_PAYLOAD = {
* Idempotent: calling bootstrap multiple times returns the same hash.
*/
export async function bootstrap(store: Store): Promise<Hash> {
return store.put(null, BOOTSTRAP_PAYLOAD);
if (!isBootstrapCapableStore(store)) {
throw new Error("Store does not support bootstrap");
}
return store[BOOTSTRAP_STORE](BOOTSTRAP_PAYLOAD);
}
+74 -9
View File
@@ -4,7 +4,7 @@ import { bootstrap } from "./bootstrap.js";
import { cborEncode } from "./cbor.js";
import { computeHash, computeSelfHash } from "./hash.js";
import { createMemoryStore } from "./store.js";
import type { CasNode } from "./types.js";
import type { CasNode, Store } from "./types.js";
import { verify } from "./verify.js";
// ──────────────────────────────────────────────────────────────────────────────
@@ -97,7 +97,18 @@ describe("createMemoryStore – put and get", () => {
const h1 = await store.put(typeHash, { n: 42 });
const h2 = await store.put(typeHash, { n: 42 });
expect(h1).toBe(h2);
expect(store.list()).toHaveLength(1);
expect(store.listByType(typeHash)).toHaveLength(1);
});
test("put does not create self-referencing nodes", async () => {
const store = createMemoryStore();
const payload = { name: "type-descriptor" };
const typeHash = await computeSelfHash(payload);
const hash = await store.put(typeHash, payload);
const node = store.get(hash);
expect(node?.type).toBe(typeHash);
expect(node?.type).not.toBe(hash);
});
test("timestamp is preserved on second put (idempotency)", async () => {
@@ -116,9 +127,9 @@ describe("createMemoryStore – put and get", () => {
});
// ──────────────────────────────────────────────────────────────────────────────
// Step 4: store.has() and store.list()
// Step 4: store.has()
// ──────────────────────────────────────────────────────────────────────────────
describe("createMemoryStore – has and list", () => {
describe("createMemoryStore – has", () => {
test("has returns false before put, true after", async () => {
const store = createMemoryStore();
const typeHash = await computeSelfHash({ name: "t" });
@@ -129,7 +140,7 @@ describe("createMemoryStore – has and list", () => {
expect(store.has(hash)).toBe(true);
});
test("list returns all stored hashes", async () => {
test("listByType returns all stored hashes for a type", async () => {
const store = createMemoryStore();
const typeHash = await computeSelfHash({ name: "t" });
@@ -137,16 +148,58 @@ describe("createMemoryStore – has and list", () => {
const h2 = await store.put(typeHash, { a: 2 });
const h3 = await store.put(typeHash, { a: 3 });
const all = store.list();
const all = store.listByType(typeHash);
expect(all).toHaveLength(3);
expect(all).toContain(h1);
expect(all).toContain(h2);
expect(all).toContain(h3);
});
test("list returns empty array on fresh store", () => {
test("listByType returns empty array on fresh store", () => {
const store = createMemoryStore();
expect(store.list()).toEqual([]);
expect(store.listByType("0000000000000")).toEqual([]);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// Step 4b: store.listByType()
// ──────────────────────────────────────────────────────────────────────────────
describe("createMemoryStore – listByType", () => {
test("returns empty array for unknown type", () => {
const store = createMemoryStore();
expect(store.listByType("0000000000000")).toEqual([]);
});
test("returns all hashes for the given type", async () => {
const store = createMemoryStore();
const typeHash = await computeSelfHash({ name: "t" });
const otherType = await computeSelfHash({ name: "other" });
const h1 = await store.put(typeHash, { a: 1 });
const h2 = await store.put(typeHash, { a: 2 });
await store.put(otherType, { b: 1 });
const byType = store.listByType(typeHash);
expect(byType).toHaveLength(2);
expect(byType).toContain(h1);
expect(byType).toContain(h2);
});
test("idempotent put does not duplicate in listByType", async () => {
const store = createMemoryStore();
const typeHash = await computeSelfHash({ name: "t" });
const h1 = await store.put(typeHash, { n: 1 });
await store.put(typeHash, { n: 1 });
expect(store.listByType(typeHash)).toEqual([h1]);
});
test("bootstrap node is listed under its self type", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
expect(store.listByType(hash)).toEqual([hash]);
});
});
@@ -191,6 +244,18 @@ describe("verify", () => {
// Step 6: bootstrap()
// ──────────────────────────────────────────────────────────────────────────────
describe("bootstrap", () => {
test("throws when store lacks internal bootstrap path", async () => {
const store: Store = {
put: async () => "0000000000000",
get: () => null,
has: () => false,
listByType: () => [],
};
await expect(bootstrap(store)).rejects.toThrow(
"Store does not support bootstrap",
);
});
test("returns a valid 13-char hash", async () => {
const store = createMemoryStore();
const hash = await bootstrap(store);
@@ -229,6 +294,6 @@ describe("bootstrap", () => {
const h2 = await bootstrap(store);
expect(h1).toBe(h2);
expect(store.list()).toHaveLength(1);
expect(store.listByType(h1)).toHaveLength(1);
});
});
+1
View File
@@ -1,4 +1,5 @@
export { bootstrap } from "./bootstrap.js";
export { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
export { cborEncode } from "./cbor.js";
export { computeHash, computeSelfHash } from "./hash.js";
export type { JSONSchema } from "./schema.js";
+32 -10
View File
@@ -1,19 +1,36 @@
import { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
import { computeHash, computeSelfHash } from "./hash.js";
import type { CasNode, Hash, Store } from "./types.js";
export function createMemoryStore(): Store {
const data = new Map<Hash, CasNode>();
const byType = new Map<Hash, Set<Hash>>();
return {
async put(typeHash: Hash | null, payload: unknown): Promise<Hash> {
const hash =
typeHash === null
? await computeSelfHash(payload)
: await computeHash(typeHash, payload);
function indexHash(type: Hash, hash: Hash): void {
let set = byType.get(type);
if (!set) {
set = new Set();
byType.set(type, set);
}
set.add(hash);
}
async function putSelfReferencing(payload: unknown): Promise<Hash> {
const hash = await computeSelfHash(payload);
if (!data.has(hash)) {
data.set(hash, { type: hash, payload, timestamp: Date.now() });
indexHash(hash, hash);
}
return hash;
}
const store: Store = {
async put(typeHash: Hash, payload: unknown): Promise<Hash> {
const hash = await computeHash(typeHash, payload);
if (!data.has(hash)) {
const type = typeHash === null ? hash : typeHash;
data.set(hash, { type, payload, timestamp: Date.now() });
data.set(hash, { type: typeHash, payload, timestamp: Date.now() });
indexHash(typeHash, hash);
}
return hash;
@@ -27,8 +44,13 @@ export function createMemoryStore(): Store {
return data.has(hash);
},
list(): Hash[] {
return [...data.keys()];
listByType(typeHash: Hash): Hash[] {
const set = byType.get(typeHash);
return set ? [...set] : [];
},
[BOOTSTRAP_STORE]: putSelfReferencing,
};
return store;
}
+3 -3
View File
@@ -17,11 +17,11 @@ export type CasNode<T = unknown> = {
/**
* Content-addressable store interface.
* put(null, payload) creates a self-referencing (bootstrap) node.
* Self-referencing nodes are created only via bootstrap().
*/
export type Store = {
put(typeHash: Hash | null, payload: unknown): Promise<Hash>;
put(typeHash: Hash, payload: unknown): Promise<Hash>;
get(hash: Hash): CasNode | null;
has(hash: Hash): boolean;
list(): Hash[];
listByType(typeHash: Hash): Hash[];
};