mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* chore(ui): removes dead utilities and op-log leftovers [#8260 - Tier A] * update readme and remove-unused-log-imports.ts * restore benchmark test and make runnable
This commit is contained in:
parent
5220684b7e
commit
c437dfc35a
33 changed files with 21 additions and 543 deletions
|
|
@ -2365,7 +2365,7 @@ src/app/op-log/
|
|||
│ ├── test-client.helper.ts # Test client utilities
|
||||
│ └── operation-factory.helper.ts # Test operation builders
|
||||
└── benchmarks/
|
||||
└── operation-log-stress.spec.ts # Performance stress tests
|
||||
└── operation-log-stress.benchmark.ts # Manual perf harness (not auto-run in CI)
|
||||
|
||||
src/app/features/work-context/store/
|
||||
├── work-context-meta.actions.ts # Move actions (moveTaskInTodayList, etc.)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,10 @@ captured on the next tick.
|
|||
- ⏳ **Remains: the on-device real-engine run.** sql.js validates the engine, not
|
||||
the Capacitor bridge or the native plugin's specific SQLite build/flags. The
|
||||
`operation-log-stress.benchmark.ts` harness is the lever for the on-device
|
||||
perf + behavior pass (see B1 perf note).
|
||||
perf + behavior pass (see B1 perf note). Run it explicitly with
|
||||
`npm run test:file src/app/op-log/testing/benchmarks/operation-log-stress.benchmark.ts`;
|
||||
it is compiled by `src/tsconfig.spec.json` but excluded from the auto-run
|
||||
`**/*.spec.ts` set, so it never runs in normal CI.
|
||||
|
||||
### B3. Flip the DI token on native — init fix ✅ landed; token flip device-gated
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ e2e/
|
|||
│ ├── settings.page.ts
|
||||
│ ├── dialog.page.ts
|
||||
│ ├── planner.page.ts
|
||||
│ ├── schedule.page.ts
|
||||
│ ├── side-nav.page.ts
|
||||
│ ├── sync.page.ts
|
||||
│ ├── tag.page.ts
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
// Schedule page removed - not used
|
||||
export {};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'base',
|
||||
template: '',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BaseComponent implements OnDestroy {
|
||||
protected onDestroy$ = new Subject<void>();
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy$.next();
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from './clipboard-image.service';
|
||||
export * from './resolve-clipboard-images.directive';
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { Pipe, PipeTransform, inject } from '@angular/core';
|
||||
import { ClipboardImageService } from './clipboard-image.service';
|
||||
import { Observable, from, of, defer } from 'rxjs';
|
||||
import { map, shareReplay, catchError } from 'rxjs/operators';
|
||||
import { Log } from '../log';
|
||||
|
||||
@Pipe({
|
||||
name: 'resolveClipboardUrl',
|
||||
standalone: true,
|
||||
})
|
||||
export class ResolveClipboardUrlPipe implements PipeTransform {
|
||||
private readonly _clipboardImageService = inject(ClipboardImageService);
|
||||
private readonly _cache = new Map<string, Observable<string>>();
|
||||
|
||||
transform(url: string | undefined | null): Observable<string> {
|
||||
if (!url) {
|
||||
return of('');
|
||||
}
|
||||
|
||||
// If it's not an indexeddb URL, return as-is
|
||||
if (!url.startsWith('indexeddb://clipboard-images/')) {
|
||||
return of(url);
|
||||
}
|
||||
|
||||
// Check cache first - if already resolving or resolved, return the cached observable
|
||||
const cached = this._cache.get(url);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Use defer to ensure the promise is created fresh and properly handled
|
||||
const resolved$ = defer(() =>
|
||||
from(this._clipboardImageService.resolveIndexedDbUrl(url)),
|
||||
).pipe(
|
||||
map((resolvedUrl) => {
|
||||
if (resolvedUrl) {
|
||||
return resolvedUrl;
|
||||
}
|
||||
return url;
|
||||
}),
|
||||
catchError((error) => {
|
||||
Log.err('Error resolving clipboard URL:', error);
|
||||
return of(url);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
|
||||
// Cache immediately before returning so concurrent calls will get the same observable
|
||||
this._cache.set(url, resolved$);
|
||||
return resolved$;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { createFeatureSelector, createReducer, on } from '@ngrx/store';
|
||||
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
|
||||
import { ArchiveModel } from '../archive.model';
|
||||
|
||||
export const ARCHIVE_OLD_FEATURE_NAME = 'archiveOld';
|
||||
|
||||
export const initialArchiveOldState: ArchiveModel | null = null;
|
||||
|
||||
export const archiveOldReducer = createReducer<ArchiveModel | null>(
|
||||
initialArchiveOldState,
|
||||
on(
|
||||
loadAllData,
|
||||
(_state, { appDataComplete }) =>
|
||||
(appDataComplete as { archiveOld?: ArchiveModel | null }).archiveOld ?? null,
|
||||
),
|
||||
);
|
||||
|
||||
export const selectArchiveOldFeatureState = createFeatureSelector<ArchiveModel | null>(
|
||||
ARCHIVE_OLD_FEATURE_NAME,
|
||||
);
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { createFeatureSelector, createReducer, on } from '@ngrx/store';
|
||||
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
|
||||
import { ArchiveModel } from '../archive.model';
|
||||
|
||||
export const ARCHIVE_YOUNG_FEATURE_NAME = 'archiveYoung';
|
||||
|
||||
export const initialArchiveYoungState: ArchiveModel | null = null;
|
||||
|
||||
export const archiveYoungReducer = createReducer<ArchiveModel | null>(
|
||||
initialArchiveYoungState,
|
||||
on(
|
||||
loadAllData,
|
||||
(_state, { appDataComplete }) =>
|
||||
(appDataComplete as { archiveYoung?: ArchiveModel | null }).archiveYoung ?? null,
|
||||
),
|
||||
);
|
||||
|
||||
export const selectArchiveYoungFeatureState = createFeatureSelector<ArchiveModel | null>(
|
||||
ARCHIVE_YOUNG_FEATURE_NAME,
|
||||
);
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { SearchResultItem } from '../issue/issue.model';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace AddTaskPanel {
|
||||
export interface IssueItem {
|
||||
issueProviderId: string;
|
||||
searchResultItem: SearchResultItem;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export const isValidProjectExport = (d: any): boolean => {
|
||||
return !!(
|
||||
d &&
|
||||
d.id &&
|
||||
d.title &&
|
||||
d.relatedModels &&
|
||||
d.advancedCfg &&
|
||||
d.relatedModels.task
|
||||
);
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { CapacitorHttp, HttpResponse } from '@capacitor/core';
|
||||
import {
|
||||
executeNativeRequestWithRetry as packageExecuteNativeRequestWithRetry,
|
||||
isTransientNetworkError as packageIsTransientNetworkError,
|
||||
type NativeHttpExecutor,
|
||||
type NativeHttpRequestConfig,
|
||||
type NativeHttpResponse,
|
||||
} from '@sp/sync-providers/http';
|
||||
import { OP_LOG_SYNC_LOGGER } from '../core/sync-logger.adapter';
|
||||
|
||||
export type NativeRequestConfig = NativeHttpRequestConfig;
|
||||
|
||||
/**
|
||||
* App-side adapter: wires CapacitorHttp + OP_LOG_SYNC_LOGGER into the
|
||||
* package-owned retry helper. Existing callers keep their positional API.
|
||||
*
|
||||
* The package version (with `{ executor, logger, label, delay }` options)
|
||||
* is the canonical interface — provider files that move into
|
||||
* `@sp/sync-providers` should call it directly.
|
||||
*/
|
||||
export const executeNativeRequestWithRetry = async (
|
||||
config: NativeRequestConfig,
|
||||
label: string = 'NativeHttp',
|
||||
requestFn: (opts: NativeRequestConfig) => Promise<HttpResponse> = (opts) =>
|
||||
CapacitorHttp.request(opts),
|
||||
delayFn?: (ms: number) => Promise<void>,
|
||||
): Promise<HttpResponse> => {
|
||||
const executor: NativeHttpExecutor = (cfg) =>
|
||||
requestFn(cfg) as unknown as Promise<NativeHttpResponse>;
|
||||
|
||||
const response = await packageExecuteNativeRequestWithRetry(config, {
|
||||
executor,
|
||||
logger: OP_LOG_SYNC_LOGGER,
|
||||
label,
|
||||
delay: delayFn,
|
||||
});
|
||||
return response as unknown as HttpResponse;
|
||||
};
|
||||
|
||||
export const isTransientNetworkError = packageIsTransientNetworkError;
|
||||
|
|
@ -6,28 +6,35 @@
|
|||
*
|
||||
* To run these benchmarks:
|
||||
* 1. Run: npm run test:file src/app/op-log/testing/benchmarks/operation-log-stress.benchmark.ts
|
||||
* 3. Watch the console output for timing information
|
||||
* 2. Watch the console output for timing information
|
||||
*
|
||||
* Note: Results vary significantly based on:
|
||||
* - Browser/test runner
|
||||
* - System I/O performance
|
||||
* - Concurrent processes
|
||||
*
|
||||
* These tests are excluded from regular test runs to avoid flaky CI failures
|
||||
* due to timing dependencies on system load.
|
||||
* These tests are excluded from regular test runs (the Karma builder only
|
||||
* auto-runs `**\/*.spec.ts`) to avoid flaky CI failures from timing
|
||||
* dependencies on system load. They are still part of the spec TS program
|
||||
* (`src/tsconfig.spec.json` includes `**\/*.benchmark.ts`) so they compile and
|
||||
* only run when named explicitly via `test:file`.
|
||||
*/
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { OperationLogStoreService } from '../../persistence/operation-log-store.service';
|
||||
import { ActionType, Operation, OpType, EntityType } from '../../core/operation.types';
|
||||
import { uuidv7 } from '../../../util/uuid-v7';
|
||||
|
||||
// Increase timeout for stress tests
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
|
||||
// Stress-run timeout for the large-payload / 10k-op benchmarks. Must be set
|
||||
// in `beforeEach` (not at module scope): `src/test.ts` has a global `beforeAll`
|
||||
// that resets `DEFAULT_TIMEOUT_INTERVAL` to 2000ms, which would otherwise clobber
|
||||
// a module-level assignment and time these benchmarks out.
|
||||
const STRESS_TIMEOUT_MS = 60000;
|
||||
|
||||
describe('OperationLog Performance Benchmarks', () => {
|
||||
let service: OperationLogStoreService;
|
||||
|
||||
beforeEach(async () => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = STRESS_TIMEOUT_MS;
|
||||
TestBed.configureTestingModule({
|
||||
providers: [OperationLogStoreService],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
export const fixNumberField = (value: unknown, defaultValue?: number): number => {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsedValue = parseFloat(value);
|
||||
if (!isNaN(parsedValue)) {
|
||||
return parsedValue;
|
||||
}
|
||||
}
|
||||
return typeof defaultValue === 'number' ? defaultValue : 0;
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// import { GlobalConfigState } from '../../features/config/global-config.model';
|
||||
// import { fixNumberField } from './fix-number-field';
|
||||
// import { DEFAULT_GLOBAL_CONFIG } from '../../features/config/default-global-config.const';
|
||||
//
|
||||
// export const repairGlobalConfig = (
|
||||
// globalConfig: GlobalConfigState,
|
||||
// ): GlobalConfigState => {
|
||||
// // Helper function to fix a number field at a specific path
|
||||
// const fixNumberFieldInConfig = (obj: any, path: string[], defaultValue: number) => {
|
||||
// const lastKey = path[path.length - 1];
|
||||
// const parentObj =
|
||||
// path.length > 1 ? path.slice(0, -1).reduce((o, k) => o[k], obj) : obj;
|
||||
//
|
||||
// if (typeof parentObj[lastKey] !== 'number') {
|
||||
// parentObj[lastKey] = fixNumberField(parentObj[lastKey], defaultValue);
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// // Fields to check with their paths and default values
|
||||
// const fieldsToFix = [
|
||||
// {
|
||||
// path: ['misc', 'firstDayOfWeek'],
|
||||
// defaultValue: DEFAULT_GLOBAL_CONFIG.misc.firstDayOfWeek,
|
||||
// },
|
||||
// {
|
||||
// path: ['misc', 'startOfNextDay'],
|
||||
// defaultValue: DEFAULT_GLOBAL_CONFIG.misc.startOfNextDay,
|
||||
// },
|
||||
// {
|
||||
// path: ['pomodoro', 'breakDuration'],
|
||||
// defaultValue: DEFAULT_GLOBAL_CONFIG.pomodoro.breakDuration,
|
||||
// },
|
||||
// {
|
||||
// path: ['pomodoro', 'cyclesBeforeLongerBreak'],
|
||||
// defaultValue: DEFAULT_GLOBAL_CONFIG.pomodoro.cyclesBeforeLongerBreak,
|
||||
// },
|
||||
// ];
|
||||
//
|
||||
// // Apply fixes to all fields
|
||||
// fieldsToFix.forEach(({ path, defaultValue }) => {
|
||||
// fixNumberFieldInConfig(globalConfig, path, defaultValue);
|
||||
// });
|
||||
//
|
||||
// return globalConfig as GlobalConfigState;
|
||||
// };
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { ActionReducerMap } from '@ngrx/store';
|
||||
import { RootState } from './root-state';
|
||||
|
||||
export const reducers: Partial<ActionReducerMap<RootState>> = {};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { ANI_STANDARD_TIMING } from './animation.const';
|
||||
|
||||
export const dynamicHeightAnimation = [
|
||||
trigger('dynamicHeight', [
|
||||
transition('void <=> *', []),
|
||||
transition(
|
||||
'* <=> *',
|
||||
[style({ height: '{{startHeight}}px', opacity: 0 }), animate(ANI_STANDARD_TIMING)],
|
||||
{ params: { startHeight: 0 } },
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { transition, trigger } from '@angular/animations';
|
||||
|
||||
// use on parent to skip initial animation
|
||||
export const noopAnimation = trigger('noop', [transition(':enter', [])]);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { ANI_ENTER_TIMING } from './animation.const';
|
||||
|
||||
export const slideInFromLeftAni = [
|
||||
trigger('slideInFromLeft', [
|
||||
transition(':enter', [
|
||||
style({ transform: 'translateX(-100%)' }),
|
||||
animate(ANI_ENTER_TIMING, style({ transform: 'translateX(0)' })),
|
||||
]),
|
||||
|
||||
// transition(':leave', [
|
||||
// style({ transform: 'translateX(0)' }),
|
||||
// animate(ANI_ENTER_TIMING, style({ transform: 'translateX(-100%)' })),
|
||||
// ]),
|
||||
]),
|
||||
];
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { ANI_ENTER_TIMING, ANI_LEAVE_TIMING } from './animation.const';
|
||||
|
||||
export const slideAnimation = [
|
||||
trigger('slide', [
|
||||
transition(':enter', [
|
||||
style({ marginTop: '-{{elHeight}}px', opacity: 0 }),
|
||||
animate(ANI_ENTER_TIMING, style({ marginTop: '*', opacity: 1 })),
|
||||
]), // void => *
|
||||
transition(':leave', [
|
||||
style({ marginTop: 0, opacity: 1 }),
|
||||
animate(ANI_LEAVE_TIMING, style({ marginTop: '-{{elHeight}}px', opacity: 0 })),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
|
||||
// export const slideAnimation = [
|
||||
// trigger('slide', [
|
||||
// transition(':enter', [
|
||||
// style({height: 0, overflow: 'hidden', transform: 'translateY(-120%)'}),
|
||||
// animate(ANI_ENTER_TIMING, style({height: '*', transform: 'translateY(0)'}))
|
||||
// ]), // void => *
|
||||
// transition(':leave', [
|
||||
// style({overflow: 'hidden'}),
|
||||
// animate(ANI_LEAVE_TIMING, style({height: 0, transform: 'translateY(-120%)'}))
|
||||
// ])
|
||||
// ])
|
||||
// ];
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { filter, OperatorFunction } from 'rxjs';
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
/**
|
||||
* RxJS operator that filters out remote actions from sync.
|
||||
*
|
||||
* Use this in effects that should only run for local user actions,
|
||||
* not for actions replayed from remote sync.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* myEffect$ = createEffect(() =>
|
||||
* this._actions$.pipe(
|
||||
* ofType(TaskSharedActions.addTask),
|
||||
* filterLocalAction(),
|
||||
* // ... rest of effect
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const filterLocalAction = <T extends Action>(): OperatorFunction<T, T> =>
|
||||
filter((action: T) => !(action as any).meta?.isRemote);
|
||||
|
||||
/**
|
||||
* RxJS operator that filters to only remote actions from sync.
|
||||
*
|
||||
* Use this in effects that should only run for remote sync actions,
|
||||
* not for local user actions.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* myEffect$ = createEffect(() =>
|
||||
* this._actions$.pipe(
|
||||
* ofType(TaskSharedActions.moveToArchive),
|
||||
* filterRemoteAction(),
|
||||
* // ... rest of effect
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const filterRemoteAction = <T extends Action>(): OperatorFunction<T, T> =>
|
||||
filter((action: T) => !!(action as any).meta?.isRemote);
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
export const formatISODateWithOffset = (date: Date): string => {
|
||||
const pad = (num, length = 2): string => String(num).padStart(length, '0');
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1); // Months are 0-indexed
|
||||
const day = pad(date.getDate());
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
const seconds = pad(date.getSeconds());
|
||||
const milliseconds = pad(date.getMilliseconds(), 3); // Standard 3 digits
|
||||
|
||||
// --- Timezone Offset Calculation ---
|
||||
const offsetMinutes = date.getTimezoneOffset();
|
||||
const offsetHours = Math.abs(Math.floor(offsetMinutes / 60));
|
||||
const offsetMinPart = Math.abs(offsetMinutes % 60);
|
||||
// getTimezoneOffset returns POSITIVE for zones WEST of UTC, and NEGATIVE for EAST.
|
||||
// The standard ISO format requires the opposite sign (+ for EAST, - for WEST).
|
||||
const offsetSign = offsetMinutes <= 0 ? '+' : '-';
|
||||
const offsetFormatted = `${offsetSign}${pad(offsetHours)}:${pad(offsetMinPart)}`;
|
||||
// Special case for UTC (offset is 0) - some prefer 'Z'
|
||||
// const offsetFormatted = offsetMinutes === 0 ? 'Z' : `${offsetSign}${pad(offsetHours)}:${pad(offsetMinPart)}`;
|
||||
|
||||
// --- Assemble the String ---
|
||||
// Using standard 3-digit milliseconds (.SSS)
|
||||
const formattedString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}${offsetFormatted}`;
|
||||
|
||||
/*
|
||||
// If you strictly need 2-digit milliseconds (.SS), uncomment this:
|
||||
const millisecondsTwoDigits = milliseconds.substring(0, 2);
|
||||
formattedString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${millisecondsTwoDigits}${offsetFormatted}`;
|
||||
*/
|
||||
|
||||
return formattedString;
|
||||
};
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// borrowed
|
||||
export const getTextFromArrayBuffer = (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
encoding: string = 'UTF-8',
|
||||
): Promise<string | ArrayBuffer> => {
|
||||
return new Promise((resolve /*, reject*/) => {
|
||||
if (typeof Blob === 'undefined') {
|
||||
const buffer = new Buffer(new Uint8Array(arrayBuffer));
|
||||
resolve(buffer.toString(encoding));
|
||||
} else {
|
||||
let blob;
|
||||
const gc = window as any;
|
||||
// TODO fix as BlobBuilder is not available in all browsers
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/BlobBuilder
|
||||
gc.BlobBuilder = gc.BlobBuilder || gc.WebKitBlobBuilder;
|
||||
if (typeof gc.BlobBuilder !== 'undefined') {
|
||||
const bb = new gc.BlobBuilder();
|
||||
bb.append(arrayBuffer);
|
||||
blob = bb.getBlob();
|
||||
} else {
|
||||
blob = new Blob([arrayBuffer]);
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
if (typeof fileReader.addEventListener === 'function') {
|
||||
fileReader.addEventListener('loadend', (evt) => {
|
||||
resolve(evt.target.result);
|
||||
});
|
||||
} else {
|
||||
fileReader.onloadend = (evt) => {
|
||||
resolve(evt.target.result);
|
||||
};
|
||||
}
|
||||
fileReader.readAsText(blob, encoding);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export const getYesterdaysDate = (): Date => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
return yesterday;
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// TAKEN from https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
|
||||
// NOTE: NEVER CHANGE THIS AS IT IS USED FOR ID GENERATION
|
||||
const SEED = 454;
|
||||
const cyrb53a = (str: string): number => {
|
||||
let h1 = 0xdeadbeef ^ SEED,
|
||||
h2 = 0x41c6ce57 ^ SEED;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 0x85ebca77);
|
||||
h2 = Math.imul(h2 ^ ch, 0xc2b2ae3d);
|
||||
}
|
||||
h1 ^= Math.imul(h1 ^ (h2 >>> 15), 0x735a2d97);
|
||||
h2 ^= Math.imul(h2 ^ (h1 >>> 15), 0xcaf649a9);
|
||||
h1 ^= h2 >>> 16;
|
||||
h2 ^= h1 >>> 16;
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
return 2097152 * (h2 >>> 0) + (h1 >>> 11);
|
||||
};
|
||||
|
||||
// NOTE: NEVER CHANGE THIS FN AS IT IS USED FOR ID GENERATION
|
||||
export const hash = (str: string): string => cyrb53a(str).toString();
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const IS_CHROME = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
export const isEmail = (str: string): boolean =>
|
||||
typeof str === 'string' && !!str.match(emailRegex);
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { defer, Observable, Subscriber } from 'rxjs';
|
||||
import { finalize, tap } from 'rxjs/operators';
|
||||
import { Log } from '../core/log';
|
||||
|
||||
export const obsLogAll$ = <T>(
|
||||
sourceOrName: Observable<T> | string,
|
||||
source?: Observable<T>,
|
||||
): Observable<T> => {
|
||||
const name = typeof sourceOrName === 'string' ? sourceOrName : 'obs$';
|
||||
const usedSource = typeof sourceOrName === 'string' ? source : sourceOrName;
|
||||
return subscriberCount$(obsLog$(usedSource, name), name);
|
||||
};
|
||||
|
||||
export const obsLog$ = <T>(source: Observable<T>, name: string = 'obs$'): Observable<T> =>
|
||||
defer(() => {
|
||||
Log.log(`_o_ ${name}: subscribed ++`);
|
||||
return source.pipe(
|
||||
tap({
|
||||
next: (value) => Log.log(`_o_ ${name}: ${value}`),
|
||||
complete: () => Log.log(`_o_ ${name}: $$$ complete $$$`),
|
||||
}),
|
||||
finalize(() => Log.log(`_o_ ${name}: unsubscribed --`)),
|
||||
);
|
||||
});
|
||||
|
||||
export const subscriberCount$ = <T>(
|
||||
sourceObservable: Observable<T>,
|
||||
name: string = 'obs$',
|
||||
): Observable<T> => {
|
||||
let counter = 0;
|
||||
return new Observable((subscriber: Subscriber<T>) => {
|
||||
const subscription = sourceObservable.subscribe(subscriber);
|
||||
counter++;
|
||||
Log.log(`_o_ ${name} subs: ${counter}`);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
counter--;
|
||||
Log.log(`_o_ ${name} subs: ${counter}`);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
export const observeObjectProperty = <T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
propertyKey: K,
|
||||
): Observable<T[K]> => {
|
||||
// Create a subject with the initial value
|
||||
const subject = new BehaviorSubject<T[K]>(obj[propertyKey]);
|
||||
|
||||
// Store the original value
|
||||
let value = obj[propertyKey];
|
||||
|
||||
// Define a getter/setter for the property
|
||||
Object.defineProperty(obj, propertyKey, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => value,
|
||||
set: (newValue) => {
|
||||
value = newValue;
|
||||
subject.next(newValue);
|
||||
},
|
||||
});
|
||||
|
||||
// Return the observable part of the subject
|
||||
return subject.asObservable();
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { Observable, Subscriber } from 'rxjs';
|
||||
import { Log } from '../core/log';
|
||||
|
||||
export const observeWidth = (target: HTMLElement): Observable<number> => {
|
||||
return new Observable((observer: Subscriber<number>) => {
|
||||
if ((window as any).ResizeObserver) {
|
||||
const resizeObserver = new (window as any).ResizeObserver(
|
||||
(entries: ResizeObserverEntry[]) => {
|
||||
observer.next(entries[0].contentRect.width);
|
||||
},
|
||||
);
|
||||
resizeObserver.observe(target);
|
||||
return () => {
|
||||
resizeObserver.unobserve(target);
|
||||
};
|
||||
} else {
|
||||
Log.err('ResizeObserver not supported in this browser');
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export const timestampToDatetimeInputString = (timestamp: number): string => {
|
||||
const date = new Date(timestamp + _getTimeZoneOffsetInMs());
|
||||
return date.toISOString().slice(0, 19);
|
||||
};
|
||||
|
||||
const _getTimeZoneOffsetInMs = (): number => {
|
||||
return new Date().getTimezoneOffset() * -60 * 1000;
|
||||
};
|
||||
|
|
@ -29,5 +29,8 @@
|
|||
}
|
||||
},
|
||||
"files": ["test.ts", "polyfills.ts"],
|
||||
"include": ["**/*.spec.ts", "**/*.d.ts"]
|
||||
// `**/*.benchmark.ts` files are part of the spec TS program so they compile
|
||||
// (and are type-checked in CI), but they are NOT auto-run: the Karma builder's
|
||||
// default `include` is `**/*.spec.ts`, so benchmarks need to be run manually.
|
||||
"include": ["**/*.spec.ts", "**/*.benchmark.ts", "**/*.d.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue