Skip to content

Commit

Permalink
Merge pull request #41 from dhilt/issue-39-first-last-visible-wanted-…
Browse files Browse the repository at this point in the history
…part-2

Adapter "wanted" container fixes. Part 2
  • Loading branch information
dhilt authored Sep 14, 2022
2 parents 2cf22aa + bcf4af7 commit 5ebbe79
Show file tree
Hide file tree
Showing 19 changed files with 285 additions and 97 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dist
node_modules
.vscode
*.tgz
*.tgz
.DS_Store
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vscroll",
"version": "1.5.2",
"version": "1.5.3",
"description": "Virtual scroll engine",
"main": "dist/bundles/vscroll.umd.js",
"module": "dist/bundles/vscroll.esm5.js",
Expand Down
104 changes: 71 additions & 33 deletions src/classes/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { Logger } from './logger';
import { Buffer } from './buffer';
import { Reactive } from './reactive';
import {
AdapterPropName, AdapterPropType, getDefaultAdapterProps, methodPreResult, reactiveConfigStorage
AdapterPropName, AdapterPropType, EMPTY_ITEM, getDefaultAdapterProps, methodPreResult, reactiveConfigStorage
} from './adapter/props';
import { wantedUtils } from './adapter/wanted';
import { Viewport } from './viewport';
import { Direction } from '../inputs/index';
import { AdapterProcess, ProcessStatus } from '../processes/index';
import {
WorkflowGetter,
Expand All @@ -28,7 +31,7 @@ import {
ProcessSubject,
} from '../interfaces/index';

type MethodResolver = (...args: any[]) => Promise<AdapterMethodResult>;
type MethodResolver = (...args: unknown[]) => Promise<AdapterMethodResult>;

const ADAPTER_PROPS_STUB = getDefaultAdapterProps();

Expand Down Expand Up @@ -60,7 +63,9 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
private source: { [key: string]: Reactive<unknown> } = {}; // for Reactive props
private box: { [key: string]: unknown } = {}; // for Scalars over Reactive props
private demand: { [key: string]: unknown } = {}; // for Scalars on demand
public wanted: { [key: string]: boolean } = {};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
setFirstOrLastVisible = (_: { first?: boolean, last?: boolean, workflow?: ScrollerWorkflow }) => { };

get workflow(): ScrollerWorkflow<Item> {
return this.getWorkflow();
Expand Down Expand Up @@ -98,7 +103,7 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
private relaxRun: Promise<AdapterMethodResult> | null;

private getPromisifiedMethod(method: MethodResolver, defaultMethod: MethodResolver) {
return (...args: any[]): Promise<AdapterMethodResult> =>
return (...args: unknown[]): Promise<AdapterMethodResult> =>
this.relax$
? new Promise(resolve => {
if (this.relax$) {
Expand All @@ -115,10 +120,15 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
this.relax$ = null;
this.relaxRun = null;
this.reloadCounter = 0;
const contextId = context?.id || -1;

// public context (if exists) should provide access Reactive props configuration by id
// public context (if exists) should provide access to Reactive props config by id
const reactivePropsStore = context && reactiveConfigStorage.get(context.id) || {};

// the Adapter initialization should not trigger "wanted" props setting;
// after the initialization is completed, "wanted" functionality must be unblocked
wantedUtils.setBlock(true, contextId);

// make array of the original values from public context if present
const adapterProps = context
? ADAPTER_PROPS_STUB.map(prop => {
Expand Down Expand Up @@ -152,37 +162,38 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
})
);

// Reactive props
// 1) store original values in "source" container, to avoid extra .get() calls on scalar twins set
// 2) "wanted" container is bound with scalars; get() updates it
// Reactive props: store original values in "source" container, to avoid extra .get() calls on scalar twins set
adapterProps
.filter(prop => prop.type === AdapterPropType.Reactive)
.forEach(({ name, value }: IAdapterProp) => {
this.source[name] = value as Reactive<unknown>;
Object.defineProperty(this, name, {
configurable: true,
get: () => {
const scalarWanted = ADAPTER_PROPS_STUB.find(
({ wanted, reactive }) => wanted && reactive === name
);
if (scalarWanted && this.externalContext) {
this.wanted[scalarWanted.name] = true;
}
return this.source[name];
}
get: () => this.source[name]
});
});

// for "wanted" props that can be explicitly requested for the first time after the Adapter initialization,
// an implicit calculation of the initial value is required;
// so this method should be called when accessing the "wanted" props through one of the following getters
const processWanted = (prop: IAdapterProp) => {
if (wantedUtils.setBox(prop, contextId)) {
if ([AdapterPropName.firstVisible, AdapterPropName.firstVisible$].some(n => n === prop.name)) {
this.setFirstOrLastVisible({ first: true });
} else if ([AdapterPropName.lastVisible, AdapterPropName.lastVisible$].some(n => n === prop.name)) {
this.setFirstOrLastVisible({ last: true });
}
}
};

// Scalar props that have Reactive twins
// 1) scalars should use "box" container
// 2) "wanted" should be updated on get
// 3) reactive props (from "source") are triggered on set
// 1) reactive props (from "source") should be triggered on set
// 2) scalars should use "box" container on get
// 3) "wanted" scalars should also run wanted-related logic on get
adapterProps
.filter(prop => prop.type === AdapterPropType.Scalar && !!prop.reactive)
.forEach(({ name, value, reactive, wanted }: IAdapterProp) => {
if (wanted) {
this.wanted[name] = false;
}
.forEach((prop: IAdapterProp) => {
const { name, value, reactive } = prop;
this.box[name] = value;
Object.defineProperty(this, name, {
configurable: true,
Expand All @@ -198,9 +209,7 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
}
},
get: () => {
if (wanted && this.externalContext) {
this.wanted[name] = true;
}
processWanted(prop);
return this.box[name];
}
});
Expand All @@ -225,7 +234,8 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {

// Adapter public context augmentation
adapterProps
.forEach(({ name, type, value: defaultValue, permanent }: IAdapterProp) => {
.forEach((prop: IAdapterProp) => {
const { name, type, value: defaultValue, permanent } = prop;
let value = (this as IAdapter)[name];
if (type === AdapterPropType.Function) {
value = (value as () => void).bind(this);
Expand All @@ -236,18 +246,26 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
} else if (name === AdapterPropName.augmented) {
value = true;
}
const nonPermanentScalar = !permanent && type === AdapterPropType.Scalar;
Object.defineProperty(context, name, {
configurable: true,
get: () => !permanent && type === AdapterPropType.Scalar
? (this as IAdapter)[name] // non-permanent Scalars should be taken in runtime
: value // Reactive props and methods (Functions/WorkflowRunners) can be defined once
get: () => {
processWanted(prop); // consider accessing "wanted" Reactive props
if (nonPermanentScalar) {
return (this as IAdapter)[name]; // non-permanent Scalars should be taken in runtime
}
return value; // other props (Reactive/Functions/WorkflowRunners) can be defined once
}
});
});

this.externalContext = context;
wantedUtils.setBlock(false, contextId);
}

initialize(buffer: Buffer<Item>, state: State, logger: Logger, adapterRun$?: Reactive<ProcessSubject>): void {
initialize(
buffer: Buffer<Item>, state: State, viewport: Viewport, logger: Logger, adapterRun$?: Reactive<ProcessSubject>
): void {
// buffer
Object.defineProperty(this.demand, AdapterPropName.itemsCount, {
get: () => buffer.getVisibleItemsCount()
Expand Down Expand Up @@ -277,6 +295,26 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
this.isLoading = state.cycle.busy.get();
state.cycle.busy.on(busy => this.isLoading = busy);

//viewport
this.setFirstOrLastVisible = ({ first, last, workflow }) => {
if ((!first && !last) || workflow?.call?.interrupted) {
return;
}
const token = first ? AdapterPropName.firstVisible : AdapterPropName.lastVisible;
if (!wantedUtils.getBox(this.externalContext?.id)?.[token]) {
return;
}
if (buffer.items.some(({ element }) => !element)) {
logger.log('skipping first/lastVisible set because not all buffered items are rendered at this moment');
return;
}
const direction = first ? Direction.backward : Direction.forward;
const { item } = viewport.getEdgeVisibleItem(buffer.items, direction);
if (!item || item.element !== this[token].element) {
this[token] = (item ? item.get() : EMPTY_ITEM) as ItemAdapter<Item>;
}
};

// logger
this.logger = logger;

Expand Down Expand Up @@ -323,7 +361,7 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
}

resetContext(): void {
const reactiveStore = reactiveConfigStorage.get(this.externalContext.id);
const reactiveStore = reactiveConfigStorage.get(this.externalContext?.id);
ADAPTER_PROPS_STUB
.forEach(({ type, permanent, name, value }) => {
// assign initial values to non-reactive non-permanent props
Expand Down
18 changes: 12 additions & 6 deletions src/classes/adapter/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AdapterPropName, AdapterPropType, getDefaultAdapterProps, reactiveConfigStorage } from './props';
import core from '../../version';
import { Reactive } from '../reactive';
import { wantedStorage, wantedUtils } from './wanted';
import { IReactivePropsStore, IAdapterConfig } from '../../interfaces/index';

let instanceCount = 0;
Expand All @@ -12,6 +13,7 @@ export class AdapterContext {
const id = ++instanceCount;
const conf = { configurable: true };
const reactivePropsStore: IReactivePropsStore = {};
wantedStorage.set(id, { box: {}, block: false });

// set up permanent props
Object.defineProperty(this, AdapterPropName.id, { get: () => id, ...conf });
Expand All @@ -22,25 +24,29 @@ export class AdapterContext {
// set up default props, they will be reassigned during the Adapter instantiation
getDefaultAdapterProps()
.filter(({ permanent }) => !permanent)
.forEach(({ name, value, type }) => {
.forEach(prop => {
let { value } = prop;

// reactive props might be reconfigured by the vscroll consumer
if (reactive && type === AdapterPropType.Reactive) {
const react = reactive[name];
if (reactive && prop.type === AdapterPropType.Reactive) {
const react = reactive[prop.name];
if (react) {
// here we have a configured reactive property that came from the outer config
// this prop must be exposed via Adapter, but at the same time we need to
// persist the original default value as it will be used by the Adapter internally
reactivePropsStore[name] = {
reactivePropsStore[prop.name] = {
...react,
default: value as Reactive<unknown> // persisting the default native Reactive prop
};
value = react.source; // exposing the configured prop instead of the default one
}
}

Object.defineProperty(this, name, {
get: () => value,
Object.defineProperty(this, prop.name, {
get: () => {
wantedUtils.setBox(prop, id);
return value;
},
...conf
});
});
Expand Down
6 changes: 4 additions & 2 deletions src/classes/adapter/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,14 @@ export const getDefaultAdapterProps = (): IAdapterProp[] => [
{
type: Type.Reactive,
name: Name.firstVisible$,
value: new Reactive<ItemAdapter>(EMPTY_ITEM, { emitOnSubscribe: true })
value: new Reactive<ItemAdapter>(EMPTY_ITEM, { emitOnSubscribe: true }),
wanted: true
},
{
type: Type.Reactive,
name: Name.lastVisible$,
value: new Reactive<ItemAdapter>(EMPTY_ITEM, { emitOnSubscribe: true })
value: new Reactive<ItemAdapter>(EMPTY_ITEM, { emitOnSubscribe: true }),
wanted: true
},
{
type: Type.Reactive,
Expand Down
38 changes: 38 additions & 0 deletions src/classes/adapter/wanted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IAdapterProp } from '../../interfaces/index';
import { AdapterPropName } from './props';

interface IWanted {
box: { [key: string]: boolean };
block: boolean;
}

const getBox = (id?: number): IWanted['box'] | undefined => {
return wantedStorage.get(id || -1)?.box;
};

const setBox = ({ name, wanted }: IAdapterProp, id?: number): boolean => {
const Wanted = wantedStorage.get(id || -1);
if (wanted && Wanted && !Wanted.box[name] && !Wanted.block) {
const { firstVisible: a, firstVisible$: a$ } = AdapterPropName;
const { lastVisible: b, lastVisible$: b$ } = AdapterPropName;
Wanted.box[a] = Wanted.box[a$] = [a, a$].some(n => n === name) || Wanted.box[a];
Wanted.box[b] = Wanted.box[b$] = [b, b$].some(n => n === name) || Wanted.box[b];
return true;
}
return false;
};

const setBlock = (value: boolean, id?: number): void => {
const Wanted = wantedStorage.get(id || -1);
if (Wanted) {
Wanted.block = value;
}
};

export const wantedUtils = {
getBox,
setBox,
setBlock
};

export const wantedStorage = new Map<number, IWanted>();
4 changes: 3 additions & 1 deletion src/classes/datasource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AdapterContext } from './adapter/context';
import { reactiveConfigStorage } from './adapter/props';
import { wantedStorage } from './adapter/wanted';
import {
IDatasource,
IDatasourceConstructed,
Expand All @@ -25,11 +26,12 @@ export class DatasourceGeneric<Data> implements IDatasourceConstructed<Data> {
this.devSettings = datasource.devSettings;
}
const adapterContext = new AdapterContext(config || { mock: false });
this.adapter = adapterContext as IAdapter<Data>;
this.adapter = adapterContext as unknown as IAdapter<Data>;
}

dispose(): void { // todo: should it be published?
reactiveConfigStorage.delete(this.adapter.id);
wantedStorage.delete(this.adapter.id);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/classes/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class Logger {
this.log(`adapter: ${methodName}(${params || ''})${add || ''}`);
};

log(...args: any[]): void {
log(...args: unknown[]): void {
if (this.debug) {
if (typeof args[0] === 'function') {
args = args[0]();
Expand All @@ -197,7 +197,7 @@ export class Logger {
}
}

// logNow(...args: any[]) {
// logNow(...args: unknown[]) {
// const immediateLog = this.immediateLog;
// const debug = this.debug;
// (this as any).debug = true;
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const onItemList = (value: unknown): ValidatedValue => {
return { value: parsedValue as unknown[], isSet: true, isValid: !errors.length, errors };
};

type Func = (...args: any[]) => void;
type Func = (...args: unknown[]) => void;

const onFunction = (value: unknown): ValidatedValue => {
const errors = [];
Expand Down
Loading

0 comments on commit 5ebbe79

Please sign in to comment.