feat(client): generic shared-wallet shim surface (docs/storeRegistry/isolation/accounts)
Port the shared-wallet shim mechanics from the Festipod app into the
generic @ng-eventually/client library — zero app-specific knowledge, the
consumer injects the domain (entity->scope mapping, connections, storage).
New namespaces exposed from src/index.ts:
- docs docCreate/sparqlUpdate/sparqlQuery via the REAL injected `ng`
(getConfig().ng), never the public makeNg proxy — the JS-over-
iframe double proxy breaks doc_create postMessage marshaling
(DataCloneError). Validated hard constraint.
- storeRegistry generic (account,scope)->NURI resolver, createEntityDoc/
listEntityDocs + per-scope index, sharedWalletShim in the
private_store, cache. Consumer wiring injected via
configureStoreRegistry({ getSession, normalizeUser }).
- isolation pure applyIsolation (public=all / protected=owner+connections
/ private=owner); accessors + connection graph injected.
- accounts AccountStore (localStorage-backed faux login, storage injected)
+ normalizeUsername. React wrapper intentionally NOT ported.
polyfill.ts gains configureStoreRegistry/getStoreRegistryDeps + resetConfig.
36/36 bun test, tsc --noEmit rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import {
|
||||
AccountStore,
|
||||
browserAccountStore,
|
||||
normalizeUsername,
|
||||
ACCOUNT_STORAGE_KEY,
|
||||
type AccountStorage,
|
||||
} from "../src/accounts";
|
||||
|
||||
// In-memory fake of the Storage subset — keeps this framework/DOM-agnostic.
|
||||
function fakeStorage(): AccountStorage & { map: Map<string, string> } {
|
||||
const map = new Map<string, string>();
|
||||
return {
|
||||
map,
|
||||
getItem: (k) => (map.has(k) ? (map.get(k) as string) : null),
|
||||
setItem: (k, v) => void map.set(k, v),
|
||||
removeItem: (k) => void map.delete(k),
|
||||
};
|
||||
}
|
||||
|
||||
test("normalizeUsername: strips leading @, trims, lowercases", () => {
|
||||
expect(normalizeUsername("marie")).toBe("marie");
|
||||
expect(normalizeUsername("@Marie")).toBe("marie");
|
||||
expect(normalizeUsername(" @@MARIE ")).toBe("marie");
|
||||
expect(normalizeUsername(null)).toBe("");
|
||||
expect(normalizeUsername(undefined)).toBe("");
|
||||
});
|
||||
|
||||
test("AccountStore: login persists a trimmed username, get reads it back", () => {
|
||||
const s = fakeStorage();
|
||||
const store = new AccountStore(s);
|
||||
expect(store.get()).toBeNull();
|
||||
|
||||
expect(store.login(" Marie ")).toBe("Marie"); // trimmed, NOT normalized (display form)
|
||||
expect(store.get()).toBe("Marie");
|
||||
expect(s.map.get(ACCOUNT_STORAGE_KEY)).toBe("Marie");
|
||||
});
|
||||
|
||||
test("AccountStore: blank login is ignored, keeps the previous value", () => {
|
||||
const store = new AccountStore(fakeStorage());
|
||||
store.login("bob");
|
||||
expect(store.login(" ")).toBe("bob");
|
||||
expect(store.get()).toBe("bob");
|
||||
});
|
||||
|
||||
test("AccountStore: logout clears the username (no throw)", () => {
|
||||
const store = new AccountStore(fakeStorage());
|
||||
store.login("bob");
|
||||
store.logout();
|
||||
expect(store.get()).toBeNull();
|
||||
});
|
||||
|
||||
test("AccountStore: null storage degrades to non-persisting (SSR-safe)", () => {
|
||||
const store = new AccountStore(null);
|
||||
expect(store.get()).toBeNull();
|
||||
expect(store.login("bob")).toBe("bob"); // returns the value, just doesn't persist
|
||||
expect(store.get()).toBeNull();
|
||||
store.logout(); // no throw
|
||||
});
|
||||
|
||||
test("AccountStore: swallows storage errors on read and write", () => {
|
||||
const throwing: AccountStorage = {
|
||||
getItem: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
removeItem: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
};
|
||||
const store = new AccountStore(throwing);
|
||||
expect(store.get()).toBeNull(); // read error swallowed → null
|
||||
expect(() => store.login("bob")).not.toThrow();
|
||||
expect(() => store.logout()).not.toThrow();
|
||||
});
|
||||
|
||||
test("browserAccountStore returns a working store (uses global localStorage if present)", () => {
|
||||
const store = browserAccountStore("ng-eventually.test.account");
|
||||
expect(store).toBeInstanceOf(AccountStore);
|
||||
// Behaves regardless of environment: login returns the value.
|
||||
expect(store.login("zoe")).toBe("zoe");
|
||||
});
|
||||
Reference in New Issue
Block a user