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
|
│ ├── test-client.helper.ts # Test client utilities
|
||||||
│ └── operation-factory.helper.ts # Test operation builders
|
│ └── operation-factory.helper.ts # Test operation builders
|
||||||
└── benchmarks/
|
└── 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/
|
src/app/features/work-context/store/
|
||||||
├── work-context-meta.actions.ts # Move actions (moveTaskInTodayList, etc.)
|
├── 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
|
- ⏳ **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
|
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
|
`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
|
### B3. Flip the DI token on native — init fix ✅ landed; token flip device-gated
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ e2e/
|
||||||
│ ├── settings.page.ts
|
│ ├── settings.page.ts
|
||||||
│ ├── dialog.page.ts
|
│ ├── dialog.page.ts
|
||||||
│ ├── planner.page.ts
|
│ ├── planner.page.ts
|
||||||
│ ├── schedule.page.ts
|
|
||||||
│ ├── side-nav.page.ts
|
│ ├── side-nav.page.ts
|
||||||
│ ├── sync.page.ts
|
│ ├── sync.page.ts
|
||||||
│ ├── tag.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:
|
* To run these benchmarks:
|
||||||
* 1. Run: npm run test:file src/app/op-log/testing/benchmarks/operation-log-stress.benchmark.ts
|
* 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:
|
* Note: Results vary significantly based on:
|
||||||
* - Browser/test runner
|
* - Browser/test runner
|
||||||
* - System I/O performance
|
* - System I/O performance
|
||||||
* - Concurrent processes
|
* - Concurrent processes
|
||||||
*
|
*
|
||||||
* These tests are excluded from regular test runs to avoid flaky CI failures
|
* These tests are excluded from regular test runs (the Karma builder only
|
||||||
* due to timing dependencies on system load.
|
* 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 { TestBed } from '@angular/core/testing';
|
||||||
import { OperationLogStoreService } from '../../persistence/operation-log-store.service';
|
import { OperationLogStoreService } from '../../persistence/operation-log-store.service';
|
||||||
import { ActionType, Operation, OpType, EntityType } from '../../core/operation.types';
|
import { ActionType, Operation, OpType, EntityType } from '../../core/operation.types';
|
||||||
import { uuidv7 } from '../../../util/uuid-v7';
|
import { uuidv7 } from '../../../util/uuid-v7';
|
||||||
|
|
||||||
// Increase timeout for stress tests
|
// Stress-run timeout for the large-payload / 10k-op benchmarks. Must be set
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
|
// 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', () => {
|
describe('OperationLog Performance Benchmarks', () => {
|
||||||
let service: OperationLogStoreService;
|
let service: OperationLogStoreService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = STRESS_TIMEOUT_MS;
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [OperationLogStoreService],
|
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"],
|
"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