Skip to content
Open
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
6 changes: 6 additions & 0 deletions libs/isograph-react/src/core/IsographEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from './FragmentReference';
import { RetainedQuery } from './garbageCollection';
import { LogFunction, WrappedLogFunction } from './logging';
import { createOptimisticProxy, type OptimisticLayer } from './optimisticProxy';
import { PromiseWrapper, wrapPromise } from './PromiseWrapper';
import { WithEncounteredRecords } from './read';
import type { ReaderAst, StartUpdate } from './reader';
Expand Down Expand Up @@ -52,6 +53,8 @@ export type CacheMap<T> = { [index: string]: ParentCache<T> };

export type IsographEnvironment = {
readonly store: IsographStore;
readonly optimisticLayer: OptimisticLayer;
readonly optimisticStore: OptimisticLayer;
readonly networkFunction: IsographNetworkFunction;
readonly missingFieldHandler: MissingFieldHandler | null;
readonly componentCache: FieldCache<React.FC<any>>;
Expand Down Expand Up @@ -134,8 +137,11 @@ export function createIsographEnvironment(
logFunction?.({
kind: 'EnvironmentCreated',
});
let optimisticLayer: OptimisticLayer = {};
return {
store,
optimisticLayer,
optimisticStore: createOptimisticProxy(store, optimisticLayer),
networkFunction,
missingFieldHandler: missingFieldHandler ?? null,
componentCache: {},
Expand Down
124 changes: 124 additions & 0 deletions libs/isograph-react/src/core/optimisticProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type {
DataId,
IsographEnvironment,
IsographStore,
StoreRecord,
TypeName,
} from './IsographEnvironment';

function createLayerProxy<T>(
object: {
[key: string]: T | null;
},
optimisticObject: {
[key: string]: T | null;
},
getter: (
value: T | null | undefined,
optimisticValue: T | undefined,
p: string,
) => T | null | undefined,
): {
[key: string]: T | null;
} {
return new Proxy(optimisticObject, {
get(target, p: string) {
let optimisticValue = target[p];

if (optimisticValue === null) {
return optimisticValue;
}

const value = object[p];

if (optimisticValue === undefined && value == null) {
return value;
}

return getter(value, optimisticValue, p);
},
has(target, p) {
return Reflect.has(target, p) || Reflect.has(object, p);
},
ownKeys(target) {
const merged = {
...object,
...target,
};
return Reflect.ownKeys(merged);
},
set(target, p: string, value: any) {
return Reflect.set(target, p, value);
},
getOwnPropertyDescriptor(target, p: string) {
return (
Reflect.getOwnPropertyDescriptor(target, p) ??
Reflect.getOwnPropertyDescriptor(object, p)
);
},
});
}

export function createOptimisticProxy(
store: IsographStore,
optimisticLayer: OptimisticLayer,
): OptimisticLayer {
return createLayerProxy(
store,
optimisticLayer,
(recordsById, optimisticRecordsById, p) => {
optimisticRecordsById = optimisticLayer[p] ??= {};
return createLayerProxy(
recordsById ?? {},
optimisticRecordsById,
(storeRecord, optimisticStoreRecord, p) => {
optimisticStoreRecord = optimisticRecordsById[p] ??= {};
return createLayerProxy(
storeRecord ?? {},
optimisticStoreRecord,
(value, optimisticValue) =>
optimisticValue === undefined ? value : optimisticValue,
);
},
);
},
);
}

export type OptimisticLayer = {
[index: TypeName]: {
[index: DataId]: StoreRecord | null;
} | null;
};

export function mergeOptimisticLayer(environment: IsographEnvironment): void {
for (const [typeName, patchById] of Object.entries(
environment.optimisticLayer,
)) {
let recordById = environment.store[typeName];

if (patchById === null) {
environment.store[typeName] = null;
continue;
}
recordById = environment.store[typeName] ??= {};

for (const [recordId, patch] of Object.entries(patchById)) {
const data = recordById[recordId];

if (patch == null || data == null) {
recordById[recordId] = patch;
} else {
Object.assign(data, patch);
}
}
}

resetOptimisticLayer(environment);
}

export function resetOptimisticLayer(environment: IsographEnvironment) {
for (const key in environment.optimisticLayer) {
delete environment.optimisticLayer[key];
}
}
98 changes: 98 additions & 0 deletions libs/isograph-react/src/tests/optimisticProxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
createIsographEnvironment,
type IsographStore,
} from '../core/IsographEnvironment';
import { mergeOptimisticLayer } from '../core/optimisticProxy';

describe('optimisticProxy', () => {
let environment: ReturnType<typeof createIsographEnvironment>;

beforeEach(() => {
const store: IsographStore = {
Query: {
__ROOT: {},
},
Economist: {
0: {
__typename: 'Economist',
id: '0',
name: 'Jeremy Bentham',
},
},
};
const networkFunction = vi
.fn()
.mockRejectedValue(new Error('Fetch failed'));
environment = createIsographEnvironment(store, networkFunction);
});

test('is equal to store', () => {
expect(environment.optimisticStore.Economist?.[0]).toStrictEqual({
__typename: 'Economist',
id: '0',
name: 'Jeremy Bentham',
});
});

test('writes update proxy', () => {
environment.optimisticStore.Economist![0]!.name = 'Updated Jeremy Bentham';

expect(environment.optimisticStore.Economist![0]).toStrictEqual({
__typename: 'Economist',
id: '0',
name: 'Updated Jeremy Bentham',
});
});

test('writes update optimistic layer', () => {
environment.optimisticStore.Economist![0]!.name = 'Updated Jeremy Bentham';

expect(environment.optimisticLayer).toStrictEqual({
Economist: {
0: { name: 'Updated Jeremy Bentham' },
},
});
});

test('writes keep store intact', () => {
environment.optimisticStore.Economist![0]!.name = 'Updated Jeremy Bentham';

expect(environment.store.Economist?.[0]).toStrictEqual({
__typename: 'Economist',
id: '0',
name: 'Jeremy Bentham',
});
});

test('reads from optimistic layer if record is undefined', () => {
environment.optimisticLayer.Economist = {
1: {
__typename: 'Economist',
id: '1',
name: 'John Stuart Mill',
},
};

expect(environment.optimisticStore.Economist![1]).toStrictEqual({
__typename: 'Economist',
id: '1',
name: 'John Stuart Mill',
});
});

describe('mergeOptimisticLayer', () => {
test('merges optimistic layer with store', () => {
environment.optimisticStore.Economist![0]!.name =
'Updated Jeremy Bentham';

mergeOptimisticLayer(environment);
expect(environment.optimisticLayer).toStrictEqual({});
expect(environment.optimisticStore.Economist?.[0]).toStrictEqual({
__typename: 'Economist',
id: '0',
name: 'Updated Jeremy Bentham',
});
});
});
});
Loading