@aelionsdk/core 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/diagnostic.d.ts +33 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +32 -0
- package/dist/disposable.d.ts +12 -0
- package/dist/disposable.d.ts.map +1 -0
- package/dist/disposable.js +41 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/json.d.ts +6 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +1 -0
- package/dist/resource-tracker.d.ts +11 -0
- package/dist/resource-tracker.d.ts.map +1 -0
- package/dist/resource-tracker.js +32 -0
- package/dist/time.d.ts +19 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +138 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FoyonaCZY and AelionSDK contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @aelionsdk/core
|
|
2
|
+
|
|
3
|
+
Core time, diagnostics and resource primitives for AelionSDK
|
|
4
|
+
|
|
5
|
+
Install with `npm install @aelionsdk/core@next`.
|
|
6
|
+
|
|
7
|
+
Version 0.1.0-beta.1 is a prerelease and its API may change before the first stable release. This package is part of [AelionSDK](https://github.com/FoyonaCZY/AelionSDK); see the repository README for supported browsers, examples and deployment requirements.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { JsonValue } from './json.js';
|
|
2
|
+
export type DiagnosticSeverity = 'info' | 'warning' | 'error' | 'fatal';
|
|
3
|
+
export interface TimeRange {
|
|
4
|
+
readonly startUs: number;
|
|
5
|
+
readonly durationUs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface Diagnostic {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly severity: DiagnosticSeverity;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly path?: readonly (string | number)[];
|
|
12
|
+
readonly entityId?: string;
|
|
13
|
+
readonly rangeUs?: TimeRange;
|
|
14
|
+
readonly recoverable: boolean;
|
|
15
|
+
readonly details?: Readonly<Record<string, JsonValue>>;
|
|
16
|
+
readonly cause?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export type Result<T> = {
|
|
19
|
+
readonly ok: true;
|
|
20
|
+
readonly value: T;
|
|
21
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
25
|
+
};
|
|
26
|
+
export declare function ok<T>(value: T, diagnostics?: readonly Diagnostic[]): Result<T>;
|
|
27
|
+
export declare function err<T = never>(...diagnostics: readonly Diagnostic[]): Result<T>;
|
|
28
|
+
export declare class AelionError extends Error {
|
|
29
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
30
|
+
constructor(diagnostics: readonly Diagnostic[]);
|
|
31
|
+
}
|
|
32
|
+
export declare function throwIfAborted(signal: AbortSignal | undefined, operation: string): void;
|
|
33
|
+
//# sourceMappingURL=diagnostic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic.d.ts","sourceRoot":"","sources":["../src/diagnostic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAExE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,MAAM,CAAC,CAAC,IAChB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,GACrF;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAC;AAExE,wBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,GAAE,SAAS,UAAU,EAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAElF;AAED,wBAAgB,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAK/E;AAED,qBAAa,WAAY,SAAQ,KAAK;IACpC,SAAgB,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;gBAEhC,WAAW,EAAE,SAAS,UAAU,EAAE;CAMtD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAYvF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function ok(value, diagnostics = []) {
|
|
2
|
+
return { ok: true, value, diagnostics };
|
|
3
|
+
}
|
|
4
|
+
export function err(...diagnostics) {
|
|
5
|
+
if (diagnostics.length === 0) {
|
|
6
|
+
throw new TypeError('err() requires at least one diagnostic');
|
|
7
|
+
}
|
|
8
|
+
return { ok: false, diagnostics };
|
|
9
|
+
}
|
|
10
|
+
export class AelionError extends Error {
|
|
11
|
+
diagnostics;
|
|
12
|
+
constructor(diagnostics) {
|
|
13
|
+
const first = diagnostics[0];
|
|
14
|
+
super(first?.message ?? 'Aelion operation failed', { cause: first?.cause });
|
|
15
|
+
this.name = 'AelionError';
|
|
16
|
+
this.diagnostics = diagnostics;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function throwIfAborted(signal, operation) {
|
|
20
|
+
if (!signal?.aborted)
|
|
21
|
+
return;
|
|
22
|
+
const reason = signal.reason;
|
|
23
|
+
throw new AelionError([
|
|
24
|
+
{
|
|
25
|
+
code: 'OPERATION_ABORTED',
|
|
26
|
+
severity: 'error',
|
|
27
|
+
message: `${operation} was aborted`,
|
|
28
|
+
recoverable: true,
|
|
29
|
+
cause: reason,
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Disposable {
|
|
2
|
+
readonly disposed: boolean;
|
|
3
|
+
dispose(): void | Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export declare class DisposableStack implements Disposable {
|
|
6
|
+
#private;
|
|
7
|
+
get disposed(): boolean;
|
|
8
|
+
use<T extends Disposable>(value: T): T;
|
|
9
|
+
defer(callback: () => void | Promise<void>): void;
|
|
10
|
+
dispose(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=disposable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposable.d.ts","sourceRoot":"","sources":["../src/disposable.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,eAAgB,YAAW,UAAU;;IAKhD,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAEM,GAAG,CAAC,CAAC,SAAS,UAAU,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC;IAQtC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAOjD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAsBhC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class DisposableStack {
|
|
2
|
+
#entries = [];
|
|
3
|
+
#disposed = false;
|
|
4
|
+
#disposeTask;
|
|
5
|
+
get disposed() {
|
|
6
|
+
return this.#disposed;
|
|
7
|
+
}
|
|
8
|
+
use(value) {
|
|
9
|
+
if (this.#disposed) {
|
|
10
|
+
throw new ReferenceError('Cannot add a resource to a disposed stack');
|
|
11
|
+
}
|
|
12
|
+
this.#entries.push(() => value.dispose());
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
defer(callback) {
|
|
16
|
+
if (this.#disposed) {
|
|
17
|
+
throw new ReferenceError('Cannot add a callback to a disposed stack');
|
|
18
|
+
}
|
|
19
|
+
this.#entries.push(callback);
|
|
20
|
+
}
|
|
21
|
+
dispose() {
|
|
22
|
+
this.#disposeTask ??= this.#dispose();
|
|
23
|
+
return this.#disposeTask;
|
|
24
|
+
}
|
|
25
|
+
async #dispose() {
|
|
26
|
+
this.#disposed = true;
|
|
27
|
+
const errors = [];
|
|
28
|
+
for (const entry of this.#entries.reverse()) {
|
|
29
|
+
try {
|
|
30
|
+
await entry();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
errors.push(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.#entries.length = 0;
|
|
37
|
+
if (errors.length > 0) {
|
|
38
|
+
throw new AggregateError(errors, 'One or more resources failed to dispose');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC"}
|
package/dist/index.js
ADDED
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAE7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEnF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC"}
|
package/dist/json.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ResourceSnapshot {
|
|
2
|
+
readonly counts: Readonly<Record<string, number>>;
|
|
3
|
+
readonly total: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class ResourceTracker {
|
|
6
|
+
#private;
|
|
7
|
+
acquire(kind: string): () => void;
|
|
8
|
+
snapshot(): ResourceSnapshot;
|
|
9
|
+
assertReleased(): void;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=resource-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-tracker.d.ts","sourceRoot":"","sources":["../src/resource-tracker.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,eAAe;;IAGnB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,IAAI;IAajC,QAAQ,IAAI,gBAAgB;IAU5B,cAAc,IAAI,IAAI;CAM9B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class ResourceTracker {
|
|
2
|
+
#counts = new Map();
|
|
3
|
+
acquire(kind) {
|
|
4
|
+
if (kind.length === 0)
|
|
5
|
+
throw new TypeError('Resource kind must not be empty');
|
|
6
|
+
this.#counts.set(kind, (this.#counts.get(kind) ?? 0) + 1);
|
|
7
|
+
let released = false;
|
|
8
|
+
return () => {
|
|
9
|
+
if (released)
|
|
10
|
+
return;
|
|
11
|
+
released = true;
|
|
12
|
+
const current = this.#counts.get(kind) ?? 0;
|
|
13
|
+
if (current <= 1)
|
|
14
|
+
this.#counts.delete(kind);
|
|
15
|
+
else
|
|
16
|
+
this.#counts.set(kind, current - 1);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
snapshot() {
|
|
20
|
+
const counts = Object.fromEntries([...this.#counts.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
21
|
+
return {
|
|
22
|
+
counts,
|
|
23
|
+
total: Object.values(counts).reduce((total, count) => total + count, 0),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
assertReleased() {
|
|
27
|
+
const snapshot = this.snapshot();
|
|
28
|
+
if (snapshot.total !== 0) {
|
|
29
|
+
throw new Error(`Aelion resource leak detected: ${JSON.stringify(snapshot.counts)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/time.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Rational {
|
|
2
|
+
readonly numerator: number;
|
|
3
|
+
readonly denominator: number;
|
|
4
|
+
}
|
|
5
|
+
export type RoundingMode = 'floor' | 'ceil' | 'nearest';
|
|
6
|
+
export declare class TimeError extends RangeError {
|
|
7
|
+
readonly code: 'TIME_NOT_SAFE_INTEGER' | 'RATIONAL_INVALID' | 'INDEX_NOT_SAFE_INTEGER' | 'TIME_RESULT_OUT_OF_RANGE';
|
|
8
|
+
constructor(code: TimeError['code'], message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare function assertTimeUs(value: number, name?: string): number;
|
|
11
|
+
export declare function assertRational(value: Rational, name?: string): Rational;
|
|
12
|
+
export declare function normalizeRational(value: Rational): Rational;
|
|
13
|
+
export declare function frameStartUs(frameIndex: number, frameRate: Rational): number;
|
|
14
|
+
export declare function frameDurationUs(frameIndex: number, frameRate: Rational): number;
|
|
15
|
+
export declare function frameIndexAtTime(timeUs: number, frameRate: Rational, mode?: RoundingMode): number;
|
|
16
|
+
export declare function sampleIndexAtTime(timeUs: number, sampleRate: number, mode?: RoundingMode): number;
|
|
17
|
+
export declare function sampleBoundaryUs(sampleIndex: number, sampleRate: number): number;
|
|
18
|
+
export declare function sampleCountForRange(startUs: number, durationUs: number, sampleRate: number): number;
|
|
19
|
+
//# sourceMappingURL=time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../src/time.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAExD,qBAAa,SAAU,SAAQ,UAAU;IACvC,SAAgB,IAAI,EAChB,uBAAuB,GACvB,kBAAkB,GAClB,wBAAwB,GACxB,0BAA0B,CAAC;gBAEZ,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM;CAK5D;AAQD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAW,GAAG,MAAM,CAGnE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,SAAa,GAAG,QAAQ,CAS3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAO3D;AA4CD,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,GAAG,MAAM,CAK5E;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,GAAG,MAAM,CAK/E;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,EACnB,IAAI,GAAE,YAAsB,GAC3B,MAAM,CA2BR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,YAAsB,GAC3B,MAAM,CAQR;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAUhF;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR"}
|
package/dist/time.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const MICROSECONDS_PER_SECOND = 1000000n;
|
|
2
|
+
const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
3
|
+
const MIN_SAFE_INTEGER_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
4
|
+
export class TimeError extends RangeError {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'TimeError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function assertSafeInteger(value, name, code) {
|
|
13
|
+
if (!Number.isSafeInteger(value)) {
|
|
14
|
+
throw new TimeError(code, `${name} must be a safe integer; received ${String(value)}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function assertTimeUs(value, name = 'timeUs') {
|
|
18
|
+
assertSafeInteger(value, name, 'TIME_NOT_SAFE_INTEGER');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function assertRational(value, name = 'rational') {
|
|
22
|
+
assertSafeInteger(value.numerator, `${name}.numerator`, 'RATIONAL_INVALID');
|
|
23
|
+
assertSafeInteger(value.denominator, `${name}.denominator`, 'RATIONAL_INVALID');
|
|
24
|
+
if (value.numerator <= 0 || value.denominator <= 0) {
|
|
25
|
+
throw new TimeError('RATIONAL_INVALID', `${name} numerator and denominator must be positive`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
export function normalizeRational(value) {
|
|
30
|
+
assertRational(value);
|
|
31
|
+
const divisor = greatestCommonDivisor(BigInt(value.numerator), BigInt(value.denominator));
|
|
32
|
+
return {
|
|
33
|
+
numerator: Number(BigInt(value.numerator) / divisor),
|
|
34
|
+
denominator: Number(BigInt(value.denominator) / divisor),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function greatestCommonDivisor(left, right) {
|
|
38
|
+
let a = left < 0n ? -left : left;
|
|
39
|
+
let b = right < 0n ? -right : right;
|
|
40
|
+
while (b !== 0n) {
|
|
41
|
+
const remainder = a % b;
|
|
42
|
+
a = b;
|
|
43
|
+
b = remainder;
|
|
44
|
+
}
|
|
45
|
+
return a;
|
|
46
|
+
}
|
|
47
|
+
function divide(numerator, denominator, mode) {
|
|
48
|
+
if (denominator <= 0n) {
|
|
49
|
+
throw new TimeError('RATIONAL_INVALID', 'division denominator must be positive');
|
|
50
|
+
}
|
|
51
|
+
const quotient = numerator / denominator;
|
|
52
|
+
const remainder = numerator % denominator;
|
|
53
|
+
if (remainder === 0n)
|
|
54
|
+
return quotient;
|
|
55
|
+
if (mode === 'floor')
|
|
56
|
+
return numerator >= 0n ? quotient : quotient - 1n;
|
|
57
|
+
if (mode === 'ceil')
|
|
58
|
+
return numerator >= 0n ? quotient + 1n : quotient;
|
|
59
|
+
const absoluteRemainder = remainder < 0n ? -remainder : remainder;
|
|
60
|
+
const direction = numerator < 0n ? -1n : 1n;
|
|
61
|
+
return absoluteRemainder * 2n >= denominator ? quotient + direction : quotient;
|
|
62
|
+
}
|
|
63
|
+
function toSafeNumber(value, context) {
|
|
64
|
+
if (value > MAX_SAFE_INTEGER_BIGINT || value < MIN_SAFE_INTEGER_BIGINT) {
|
|
65
|
+
throw new TimeError('TIME_RESULT_OUT_OF_RANGE', `${context} exceeds safe integer range`);
|
|
66
|
+
}
|
|
67
|
+
return Number(value);
|
|
68
|
+
}
|
|
69
|
+
function assertIndex(value, name) {
|
|
70
|
+
assertSafeInteger(value, name, 'INDEX_NOT_SAFE_INTEGER');
|
|
71
|
+
if (value < 0) {
|
|
72
|
+
throw new TimeError('INDEX_NOT_SAFE_INTEGER', `${name} must be non-negative`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function frameStartUs(frameIndex, frameRate) {
|
|
76
|
+
assertIndex(frameIndex, 'frameIndex');
|
|
77
|
+
assertRational(frameRate, 'frameRate');
|
|
78
|
+
const numerator = BigInt(frameIndex) * MICROSECONDS_PER_SECOND * BigInt(frameRate.denominator);
|
|
79
|
+
return toSafeNumber(divide(numerator, BigInt(frameRate.numerator), 'floor'), 'frameStartUs');
|
|
80
|
+
}
|
|
81
|
+
export function frameDurationUs(frameIndex, frameRate) {
|
|
82
|
+
assertIndex(frameIndex, 'frameIndex');
|
|
83
|
+
const current = frameStartUs(frameIndex, frameRate);
|
|
84
|
+
const next = frameStartUs(frameIndex + 1, frameRate);
|
|
85
|
+
return next - current;
|
|
86
|
+
}
|
|
87
|
+
export function frameIndexAtTime(timeUs, frameRate, mode = 'floor') {
|
|
88
|
+
assertTimeUs(timeUs);
|
|
89
|
+
assertRational(frameRate, 'frameRate');
|
|
90
|
+
const denominator = MICROSECONDS_PER_SECOND * BigInt(frameRate.denominator);
|
|
91
|
+
const rateNumerator = BigInt(frameRate.numerator);
|
|
92
|
+
// frameStartUs() intentionally floors rational frame boundaries to integer
|
|
93
|
+
// microseconds. Its inverse must therefore reason about the quantized
|
|
94
|
+
// boundary, not merely divide timeUs by the rational frame duration.
|
|
95
|
+
//
|
|
96
|
+
// max n where floor(n * denominator / rateNumerator) <= timeUs
|
|
97
|
+
// = ceil((timeUs + 1) * rateNumerator / denominator) - 1
|
|
98
|
+
const containing = divide((BigInt(timeUs) + 1n) * rateNumerator, denominator, 'ceil') - 1n;
|
|
99
|
+
if (mode === 'floor') {
|
|
100
|
+
return toSafeNumber(containing, 'frameIndexAtTime');
|
|
101
|
+
}
|
|
102
|
+
const atOrAfter = divide(BigInt(timeUs) * rateNumerator, denominator, 'ceil');
|
|
103
|
+
if (mode === 'ceil') {
|
|
104
|
+
return toSafeNumber(atOrAfter, 'frameIndexAtTime');
|
|
105
|
+
}
|
|
106
|
+
const containingStart = divide(containing * denominator, rateNumerator, 'floor');
|
|
107
|
+
const atOrAfterStart = divide(atOrAfter * denominator, rateNumerator, 'floor');
|
|
108
|
+
const distanceBefore = BigInt(timeUs) - containingStart;
|
|
109
|
+
const distanceAfter = atOrAfterStart - BigInt(timeUs);
|
|
110
|
+
return toSafeNumber(distanceAfter <= distanceBefore ? atOrAfter : containing, 'frameIndexAtTime');
|
|
111
|
+
}
|
|
112
|
+
export function sampleIndexAtTime(timeUs, sampleRate, mode = 'floor') {
|
|
113
|
+
assertTimeUs(timeUs);
|
|
114
|
+
assertSafeInteger(sampleRate, 'sampleRate', 'RATIONAL_INVALID');
|
|
115
|
+
if (sampleRate <= 0) {
|
|
116
|
+
throw new TimeError('RATIONAL_INVALID', 'sampleRate must be positive');
|
|
117
|
+
}
|
|
118
|
+
const numerator = BigInt(timeUs) * BigInt(sampleRate);
|
|
119
|
+
return toSafeNumber(divide(numerator, MICROSECONDS_PER_SECOND, mode), 'sampleIndexAtTime');
|
|
120
|
+
}
|
|
121
|
+
export function sampleBoundaryUs(sampleIndex, sampleRate) {
|
|
122
|
+
assertIndex(sampleIndex, 'sampleIndex');
|
|
123
|
+
assertSafeInteger(sampleRate, 'sampleRate', 'RATIONAL_INVALID');
|
|
124
|
+
if (sampleRate <= 0) {
|
|
125
|
+
throw new TimeError('RATIONAL_INVALID', 'sampleRate must be positive');
|
|
126
|
+
}
|
|
127
|
+
return toSafeNumber(divide(BigInt(sampleIndex) * MICROSECONDS_PER_SECOND, BigInt(sampleRate), 'floor'), 'sampleBoundaryUs');
|
|
128
|
+
}
|
|
129
|
+
export function sampleCountForRange(startUs, durationUs, sampleRate) {
|
|
130
|
+
assertTimeUs(startUs, 'startUs');
|
|
131
|
+
assertTimeUs(durationUs, 'durationUs');
|
|
132
|
+
if (durationUs < 0) {
|
|
133
|
+
throw new TimeError('TIME_NOT_SAFE_INTEGER', 'durationUs must be non-negative');
|
|
134
|
+
}
|
|
135
|
+
const endUs = startUs + durationUs;
|
|
136
|
+
assertTimeUs(endUs, 'endUs');
|
|
137
|
+
return sampleIndexAtTime(endUs, sampleRate) - sampleIndexAtTime(startUs, sampleRate);
|
|
138
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aelionsdk/core",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Core time, diagnostics and resource primitives for AelionSDK",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/FoyonaCZY/AelionSDK.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"aelion",
|
|
13
|
+
"video",
|
|
14
|
+
"editing",
|
|
15
|
+
"browser"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!dist/.tsbuildinfo"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.19"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -b",
|
|
38
|
+
"typecheck": "tsc -b --pretty false"
|
|
39
|
+
}
|
|
40
|
+
}
|