Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,63 @@ describe("RepositoryRecord", () => {
});
});

describe("shouldInclude filtering", () => {
// `excluded:`-prefixed values stand in for records the SDK cannot represent.
function createFilteringMapper(): SdkRecordMapper<ClientType, SdkType> {
return {
...createMapper(),
shouldInclude: (value: ClientType) => !value.startsWith("excluded:"),
};
}

beforeEach(() => {
repo = new RepositoryRecord(userId, stateProvider, createFilteringMapper());
});

it("get returns the mapped value for an included element", async () => {
await setState({ "id-1": "value-1" });

const result = await repo.get("id-1");

expect(result).toEqual({ value: "value-1" });
});

it("get returns null for a stored but excluded element", async () => {
await setState({ "id-1": "excluded:value-1" });

const result = await repo.get("id-1");

expect(result).toBeNull();
});

it("list omits excluded elements and keeps the rest", async () => {
await setState({ "id-1": "value-1", "id-2": "excluded:value-2", "id-3": "value-3" });

const result = await repo.list();

expect(result).toEqual([{ value: "value-1" }, { value: "value-3" }]);
});

it("list returns everything when the mapper has no shouldInclude", async () => {
repo = new RepositoryRecord(userId, stateProvider, createMapper());
await setState({ "id-1": "value-1", "id-2": "excluded:value-2" });

const result = await repo.list();

expect(result).toEqual([{ value: "value-1" }, { value: "excluded:value-2" }]);
});

it("set still writes an excluded element to state", async () => {
await setState({});

await repo.set("id-1", { value: "excluded:value-1" });

// Excluding a record hides it from the SDK; it must still round-trip through client state.
expect(await getState()).toEqual({ "id-1": "excluded:value-1" });
expect(await repo.get("id-1")).toBeNull();
});
});

describe("set", () => {
it("adds new item to empty state", async () => {
await setState({});
Expand Down
21 changes: 19 additions & 2 deletions libs/common/src/platform/services/sdk/client-managed-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export interface SdkRecordMapper<ClientType, SdkType> {
userKeyDefinition(): UserKeyDefinition<Record<string, ClientType>>;
toSdk(value: ClientType): SdkType;
fromSdk(value: SdkType): ClientType;
/**
* Optional predicate to exclude entries from the SDK-visible view of state.
* Returning `false` makes {@link RepositoryRecord.get} return null and excludes
* the entry from {@link RepositoryRecord.list}. Use for records the SDK cannot
* represent, so it never has to parse them. Writes are unaffected β€” the entry
* still lives in client state and can be read through the client's own services.
* Defaults to including everything.
*/
shouldInclude?(value: ClientType): boolean;
}

/**
Expand Down Expand Up @@ -67,12 +76,20 @@ export class RepositoryRecord<ClientType, SdkType> implements Repository<SdkType
async get(id: string): Promise<SdkType | null> {
const record = await this.getRecord();
const element = record[id];
return element ? this.mapper.toSdk(element) : null;
if (!element || this.mapper.shouldInclude?.(element) === false) {
return null;
}
return this.mapper.toSdk(element);
}

async list(): Promise<SdkType[]> {
const record = await this.getRecord();
return Object.values(record).map((element) => this.mapper.toSdk(element));
const elements = Object.values(record);
const included =
this.mapper.shouldInclude != null
? elements.filter((element) => this.mapper.shouldInclude!(element))
: elements;
return included.map((element) => this.mapper.toSdk(element));
}

async set(id: string, value: SdkType): Promise<void> {
Expand Down
Loading