Skip to content

Commit

Permalink
add devtools
Browse files Browse the repository at this point in the history
  • Loading branch information
kilbot committed Dec 30, 2023
1 parent f000b27 commit 980b676
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 29 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"@orama/orama": "^1.2.11",
"@shelf/fast-natural-order-by": "^2.0.0",
"@wcpos/components": "*",
"lodash": "^4.17.21",
"observable-hooks": "^4.2.3",
"rxjs": "^7.8.1",
Expand Down
51 changes: 51 additions & 0 deletions src/cache-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BehaviorSubject, Observable } from 'rxjs';

/**
*
*/
export class CacheBase<K, V> {
private map: Map<K, V>;
private registrySubject: BehaviorSubject<Map<K, V>>;

constructor() {
this.map = new Map<K, V>();
this.registrySubject = new BehaviorSubject<Map<K, V>>(this.map);
}

get $(): Observable<Map<K, V>> {
return this.registrySubject.asObservable();
}

set(key: K, value: V): void {
this.map.set(key, value);
this.notifyChange();
}

has(key: K): boolean {
return this.map.has(key);
}

forEach(callback: (value: V, key: K, map: Map<K, V>) => void): void {
this.map.forEach(callback);
}

get(key: K): V | undefined {
return this.map.get(key);
}

getAll() {
return new Map(this.map);
}

delete(key: K): boolean {
const result = this.map.delete(key);
if (result) {
this.notifyChange();
}
return result;
}

private notifyChange(): void {
this.registrySubject.next(new Map(this.map));
}
}
54 changes: 34 additions & 20 deletions src/collection-replication-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class CollectionReplicationState<T extends RxCollection> {
public readonly subs: Subscription[] = [];
public readonly subjects = {
error: new Subject<Error>(),
active: new BehaviorSubject<boolean>(false), // true when something is running, false when not
remoteIDs: new BehaviorSubject<number[]>([]), // emits all remote ids that are known to the replication
localIDs: new BehaviorSubject<number[]>([]), // emits all local ids that are known to the replication
lastModified: new BehaviorSubject<string>(null), // emits the date of the last modified document
Expand All @@ -53,6 +54,7 @@ export class CollectionReplicationState<T extends RxCollection> {
*
*/
readonly error$: Observable<Error> = this.subjects.error.asObservable();
readonly active$: Observable<boolean> = this.subjects.active.asObservable();
readonly remoteIDs$: Observable<number[]> = this.subjects.remoteIDs.asObservable();
readonly localIDs$: Observable<number[]> = this.subjects.localIDs.asObservable();
readonly lastModified$: Observable<string> = this.subjects.lastModified.asObservable();
Expand Down Expand Up @@ -152,29 +154,33 @@ export class CollectionReplicationState<T extends RxCollection> {
return;
}

let remoteIDs;
if (this.lastFetchRemoteIDsTime < new Date().getTime() - this.pollingTime) {
remoteIDs = await this.fetchRemoteIDs();
if (isArrayOfIntegers(remoteIDs)) {
await this.collection.upsertLocal('audit', { remoteIDs });
this.subjects.remoteIDs.next(remoteIDs);
try {
let remoteIDs;
if (this.lastFetchRemoteIDsTime < new Date().getTime() - this.pollingTime) {
remoteIDs = await this.fetchRemoteIDs();
if (isArrayOfIntegers(remoteIDs)) {
await this.collection.upsertLocal('audit', { remoteIDs });
this.subjects.remoteIDs.next(remoteIDs);
}
} else {
remoteIDs = this.subjects.remoteIDs.getValue();
}
} else {
remoteIDs = this.subjects.remoteIDs.getValue();
}

if (!Array.isArray(remoteIDs) || remoteIDs.length === 0) {
return;
}
if (!Array.isArray(remoteIDs) || remoteIDs.length === 0) {
return;
}

/**
* @TODO - variations can be orphaned at the moment, we need a relationship table with parent
*/
const remove = this.subjects.localIDs.getValue().filter((id) => !remoteIDs.includes(id));
if (remove.length > 0 && this.collection.name !== 'variations') {
// deletion should be rare, only when an item is deleted from the server
console.warn('removing', remove, 'from', this.collection.name);
await this.collection.find({ selector: { id: { $in: remove } } }).remove();
/**
* @TODO - variations can be orphaned at the moment, we need a relationship table with parent
*/
const remove = this.subjects.localIDs.getValue().filter((id) => !remoteIDs.includes(id));
if (remove.length > 0 && this.collection.name !== 'variations') {
// deletion should be rare, only when an item is deleted from the server
console.warn('removing', remove, 'from', this.collection.name);
await this.collection.find({ selector: { id: { $in: remove } } }).remove();
}
} catch (error) {
this.subjects.error.next(error);
}
}

Expand All @@ -193,6 +199,12 @@ export class CollectionReplicationState<T extends RxCollection> {
return this.hooks?.fetchRemoteIDs(this.endpoint, this.collection);
}

if (this.subjects.active.getValue()) {
return;
}

this.subjects.active.next(true);

try {
const response = await this.httpClient.get(this.endpoint, {
params: { fields: ['id'], posts_per_page: -1 },
Expand All @@ -205,6 +217,8 @@ export class CollectionReplicationState<T extends RxCollection> {
return response.data.map((doc) => doc.id);
} catch (error) {
this.subjects.error.next(error);
} finally {
this.subjects.active.next(false);
}
}

Expand Down
105 changes: 105 additions & 0 deletions src/devtools/devtools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useObservableState } from 'observable-hooks';

import Box from '@wcpos/components/src/box';
import Button from '@wcpos/components/src/button';
import Table from '@wcpos/components/src/simple-table';
import Text from '@wcpos/components/src/text';
import Tree from '@wcpos/components/src/tree';

import { useQueryManager } from '../provider';

const QueryKeyCell = ({ item: query, column }) => {
return (
<Button
size="small"
onPress={() => {
console.log(query);
}}
>
{query.id}
</Button>
);
};

const QueryParamsCell = ({ item: query, column }) => {
const params = useObservableState(query.params$, query.getParams());
return <Tree rootName="params" data={params} />;
};

const ReplicationStateButton = ({ replication }) => {
const active = useObservableState(replication.active$, false);
return (
<Box horizontal>
<Button
type={active ? 'success' : 'secondary'}
size="small"
key={replication.endpoint}
onPress={() => {
console.log(replication);
}}
>
{replication.endpoint}
</Button>
</Box>
);
};

const ReplicationsCell = ({ item: query, column }) => {
const manager = useQueryManager();
const replicationsMap = manager.getReplicationStatesByQueryID(query.id);
const replicationsArray = Array.from(replicationsMap.values());
return (
<Box space="small">
{replicationsArray.map((replication) => {
return <ReplicationStateButton replication={replication} />;
})}
</Box>
);
};

export const Devtools = () => {
const manager = useQueryManager();
const queriesMap = useObservableState(manager.queries.$, manager.queries.getAll());
const queriesArray = Array.from(queriesMap.values());

return Table({
data: queriesArray,
columns: [
{
key: 'queryKeys',
label: 'Query Keys',
width: 200,
cellRenderer: QueryKeyCell,
},
{
key: 'queryParams',
label: 'Query Params',
width: 200,
cellRenderer: QueryParamsCell,
},
{
key: 'replications',
label: 'Replication States',
cellRenderer: ReplicationsCell,
},
],
});

return (
<Box>
{queriesArray.map((query) => {
const replicationsMap = manager.getReplicationStatesByQueryID(query.id);
console.log(manager);
const replicationsArray = Array.from(replicationsMap.values());
return (
<>
<Text key={query.id}>{query.id}</Text>
{replicationsArray.map((replication) => {
return <Text key={replication.endpoint}>{replication.endpoint}</Text>;
})}
</>
);
})}
</Box>
);
};
8 changes: 8 additions & 0 deletions src/devtools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as devtools from './devtools';

export const QueryDevtools: (typeof devtools)['Devtools'] =
process.env.NODE_ENV !== 'development'
? function () {
return null;
}
: devtools.Devtools;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { Query } from './query-state';
export { QueryProvider, useQueryManager } from './provider';
export { useQuery } from './use-query';
export { useReplicationState } from './use-replication-state';
export { QueryDevtools } from './devtools';
export type { Query };
18 changes: 12 additions & 6 deletions src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Observable, Subject, Subscription } from 'rxjs';

import { CollectionReplicationState } from './collection-replication-state';
import allHooks from './hooks';
import { QueryCache } from './query-cache';
import { QueryReplicationState } from './query-replication-state';
import { Query } from './query-state';
import { ReplicationCache } from './replication-cache';
import { Search } from './search-state';
import { buildUrlWithParams } from './utils';
import { buildEndpointWithParams } from './utils';

import type { QueryParams } from './query-state';
import type { RxDatabase, RxCollection } from 'rxdb';
Expand All @@ -30,7 +32,7 @@ export class Manager<TDatabase extends RxDatabase> {
/**
* Registry of all RxDB queries, indexed by queryKeys
*/
public queries: Map<string, Query<RxCollection>> = new Map();
public queries: QueryCache<string, Query<RxCollection>> = new QueryCache();

/**
* Registry of all replication states, indexed by endpoint & query params
Expand All @@ -43,10 +45,10 @@ export class Manager<TDatabase extends RxDatabase> {
*
* NOTE: replication states can be shared between queryKeys
*/
public replicationStates: Map<
public replicationStates: ReplicationCache<
string,
CollectionReplicationState<RxCollection> | QueryReplicationState<RxCollection>
> = new Map();
> = new ReplicationCache();

/**
* Each queryKey should have one collection replication and at least one query replication
Expand Down Expand Up @@ -83,6 +85,10 @@ export class Manager<TDatabase extends RxDatabase> {
const endpoints = this.queryKeyToReplicationsMap.get(key);
endpoints.forEach((endpoint) => {
const replication = this.replicationStates.get(endpoint);
if (!replication) {
console.log('endpoint', endpoint);
console.log('replicationStates', this.replicationStates);
}
replication.cancel();
this.replicationStates.delete(endpoint);
});
Expand Down Expand Up @@ -143,7 +149,7 @@ export class Manager<TDatabase extends RxDatabase> {
if (hooks?.filterApiQueryParams) {
apiQueryParams = hooks.filterApiQueryParams(apiQueryParams, params);
}
const queryEndpoint = buildUrlWithParams(endpoint, apiQueryParams);
const queryEndpoint = buildEndpointWithParams(endpoint, apiQueryParams);

if (!this.replicationStates.has(queryEndpoint)) {
const queryReplication = this.registerQueryReplication({
Expand Down Expand Up @@ -282,7 +288,7 @@ export class Manager<TDatabase extends RxDatabase> {
})
);

this.replicationStates.set(endpoint, collectionReplication);
this.replicationStates.set(endpoint, queryReplication);
return queryReplication;
}

Expand Down
6 changes: 6 additions & 0 deletions src/query-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CacheBase } from './cache-base';

/**
*
*/
export class QueryCache<K, V> extends CacheBase<K, V> {}
6 changes: 6 additions & 0 deletions src/replication-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CacheBase } from './cache-base';

/**
*
*/
export class ReplicationCache<K, V> extends CacheBase<K, V> {}
6 changes: 3 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ export function isArrayOfIntegers(value) {
}

/**
*
* eg: customers?orderby=last_name&order=asc&per_page=10&role=all
*/
export function buildUrlWithParams(endpoint: string, params: Record<string, any>) {
export function buildEndpointWithParams(endpoint: string, params: Record<string, any>) {
const url = new URL(endpoint, 'http://dummybase'); // Dummy base, actual base URL is in httpClient
Object.keys(params).forEach((key) => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, params[key]);
}
});
return url.pathname + url.search;
return url.pathname.slice(1) + url.search;
}

/**
Expand Down

0 comments on commit 980b676

Please sign in to comment.