Initial Commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import type { GunFactoryConfig, GunNodeLike } from "../../src/types/types.js";
|
||||
|
||||
/**
|
||||
* In-memory deterministic implementation of the `GunNodeLike` surface.
|
||||
* Used across every test in this package so the real `gun` runtime is
|
||||
* never pulled into the test environment (it expects `window` /
|
||||
* `localStorage` / WebSocket globals and has side effects) and
|
||||
* `vi.mock` is never needed.
|
||||
*
|
||||
* Behavioural model:
|
||||
*
|
||||
* - The "graph" is a `Map<string, unknown>`. `get(path)` returns a
|
||||
* node-handle scoped to that path; chained `get(child)` extends the
|
||||
* path with `/<child>`.
|
||||
* - `put(value)` writes the value at the scoped path AND fires every
|
||||
* registered `.map().on(handler)` listener whose root prefix
|
||||
* matches the path. The handler receives `(value, key)` where
|
||||
* `key` is the leaf segment past the listener's root.
|
||||
* - `map().on(handler)` subscribes the current scope's children. The
|
||||
* handler is invoked once for every existing child at subscription
|
||||
* time, then again on every subsequent `put`.
|
||||
* - `off()` detaches every subscription created on the current scope.
|
||||
*
|
||||
* This is a deliberately tiny subset of real Gun's semantics — just
|
||||
* enough to drive the orchestrator. Anything more would risk
|
||||
* coupling the test fake to Gun's actual gossip behaviour and lose
|
||||
* determinism.
|
||||
*/
|
||||
export class FakeGun implements GunNodeLike {
|
||||
public readonly trackers: readonly string[];
|
||||
public destroyed = false;
|
||||
|
||||
private readonly store: Map<string, unknown>;
|
||||
private readonly listeners: Map<
|
||||
string,
|
||||
Array<(data: unknown, key: string) => void>
|
||||
>;
|
||||
private readonly path: string;
|
||||
|
||||
public constructor(
|
||||
config: GunFactoryConfig,
|
||||
store?: Map<string, unknown>,
|
||||
listeners?: Map<string, Array<(data: unknown, key: string) => void>>,
|
||||
path = ""
|
||||
) {
|
||||
this.trackers = config.peers;
|
||||
this.store = store ?? new Map();
|
||||
this.listeners = listeners ?? new Map();
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public get(segment: string): GunNodeLike {
|
||||
const next = this.path === "" ? segment : `${this.path}/${segment}`;
|
||||
return new FakeGun(
|
||||
{ peers: this.trackers },
|
||||
this.store,
|
||||
this.listeners,
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
public put(value: unknown): GunNodeLike {
|
||||
this.store.set(this.path, value);
|
||||
// Notify every listener whose root is a prefix of this.path
|
||||
// AND whose path is exactly one segment shallower (i.e. the
|
||||
// `.map()` of the parent).
|
||||
for (const [root, handlers] of this.listeners.entries()) {
|
||||
const prefix = root === "" ? "" : `${root}/`;
|
||||
if (!this.path.startsWith(prefix)) continue;
|
||||
const rest = this.path.slice(prefix.length);
|
||||
if (rest === "" || rest.includes("/")) continue;
|
||||
for (const h of [...handlers]) {
|
||||
h(value, rest);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public map(): GunNodeLike {
|
||||
// map() in this fake is a marker — the subsequent .on(handler)
|
||||
// subscribes to the children of the current path.
|
||||
return this;
|
||||
}
|
||||
|
||||
public on(handler: (data: unknown, key: string) => void): GunNodeLike {
|
||||
const list = this.listeners.get(this.path) ?? [];
|
||||
list.push(handler);
|
||||
this.listeners.set(this.path, list);
|
||||
// Replay every existing child at the time of subscription.
|
||||
const prefix = this.path === "" ? "" : `${this.path}/`;
|
||||
for (const [k, v] of this.store.entries()) {
|
||||
if (!k.startsWith(prefix)) continue;
|
||||
const rest = k.slice(prefix.length);
|
||||
if (rest === "" || rest.includes("/")) continue;
|
||||
handler(v, rest);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public off(): GunNodeLike {
|
||||
this.listeners.delete(this.path);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory helper producing a `FakeGun` and recording every instance the
|
||||
* factory created so the test can introspect destroy / put state and
|
||||
* drive late events through a captured reference.
|
||||
*/
|
||||
export function makeFakeGunFactory(): {
|
||||
readonly instances: FakeGun[];
|
||||
readonly factory: (config: GunFactoryConfig) => FakeGun;
|
||||
} {
|
||||
const instances: FakeGun[] = [];
|
||||
return {
|
||||
instances,
|
||||
factory: (config) => {
|
||||
const inst = new FakeGun(config);
|
||||
instances.push(inst);
|
||||
return inst;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Ensure `globalThis.crypto` is available in bare-Node vitest runs.
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
Reference in New Issue
Block a user