@aura3d/core 1.0.6
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/README.md +51 -0
- package/dist/Diagnostics.d.ts +25 -0
- package/dist/Diagnostics.d.ts.map +1 -0
- package/dist/Diagnostics.js +60 -0
- package/dist/Diagnostics.js.map +1 -0
- package/dist/Disposable.d.ts +12 -0
- package/dist/Disposable.d.ts.map +1 -0
- package/dist/Disposable.js +37 -0
- package/dist/Disposable.js.map +1 -0
- package/dist/Engine.d.ts +46 -0
- package/dist/Engine.d.ts.map +1 -0
- package/dist/Engine.js +130 -0
- package/dist/Engine.js.map +1 -0
- package/dist/EngineConfig.d.ts +18 -0
- package/dist/EngineConfig.d.ts.map +1 -0
- package/dist/EngineConfig.js +23 -0
- package/dist/EngineConfig.js.map +1 -0
- package/dist/EngineLoop.d.ts +33 -0
- package/dist/EngineLoop.d.ts.map +1 -0
- package/dist/EngineLoop.js +63 -0
- package/dist/EngineLoop.js.map +1 -0
- package/dist/Errors.d.ts +14 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +24 -0
- package/dist/Errors.js.map +1 -0
- package/dist/EventBus.d.ts +18 -0
- package/dist/EventBus.d.ts.map +1 -0
- package/dist/EventBus.js +45 -0
- package/dist/EventBus.js.map +1 -0
- package/dist/FixedStepAccumulator.d.ts +15 -0
- package/dist/FixedStepAccumulator.d.ts.map +1 -0
- package/dist/FixedStepAccumulator.js +34 -0
- package/dist/FixedStepAccumulator.js.map +1 -0
- package/dist/Logger.d.ts +23 -0
- package/dist/Logger.d.ts.map +1 -0
- package/dist/Logger.js +53 -0
- package/dist/Logger.js.map +1 -0
- package/dist/ResourceScope.d.ts +18 -0
- package/dist/ResourceScope.d.ts.map +1 -0
- package/dist/ResourceScope.js +53 -0
- package/dist/ResourceScope.js.map +1 -0
- package/dist/Scheduler.d.ts +31 -0
- package/dist/Scheduler.d.ts.map +1 -0
- package/dist/Scheduler.js +77 -0
- package/dist/Scheduler.js.map +1 -0
- package/dist/TaskQueue.d.ts +13 -0
- package/dist/TaskQueue.d.ts.map +1 -0
- package/dist/TaskQueue.js +42 -0
- package/dist/TaskQueue.js.map +1 -0
- package/dist/Time.d.ts +24 -0
- package/dist/Time.d.ts.map +1 -0
- package/dist/Time.js +45 -0
- package/dist/Time.js.map +1 -0
- package/dist/VersionedSerialization.d.ts +96 -0
- package/dist/VersionedSerialization.d.ts.map +1 -0
- package/dist/VersionedSerialization.js +278 -0
- package/dist/VersionedSerialization.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/package.json +16 -0
- package/src/Diagnostics.ts +80 -0
- package/src/Disposable.ts +41 -0
- package/src/Engine.ts +140 -0
- package/src/EngineConfig.ts +38 -0
- package/src/EngineLoop.ts +83 -0
- package/src/Errors.ts +28 -0
- package/src/EventBus.ts +52 -0
- package/src/FixedStepAccumulator.ts +39 -0
- package/src/Logger.ts +67 -0
- package/src/ResourceScope.ts +55 -0
- package/src/Scheduler.ts +94 -0
- package/src/TaskQueue.ts +50 -0
- package/src/Time.ts +54 -0
- package/src/VersionedSerialization.ts +351 -0
- package/src/index.ts +14 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { ValidationError } from "./Errors.js";
|
|
2
|
+
|
|
3
|
+
export type VersionedFieldKind =
|
|
4
|
+
| "boolean"
|
|
5
|
+
| "int32"
|
|
6
|
+
| "uint32"
|
|
7
|
+
| "float32"
|
|
8
|
+
| "float64"
|
|
9
|
+
| "string"
|
|
10
|
+
| "array"
|
|
11
|
+
| "object";
|
|
12
|
+
|
|
13
|
+
export interface VersionedSchemaField {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly kind: VersionedFieldKind;
|
|
16
|
+
readonly required?: boolean;
|
|
17
|
+
readonly defaultValue?: unknown;
|
|
18
|
+
readonly elementKind?: VersionedFieldKind;
|
|
19
|
+
readonly schema?: VersionedSchema;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VersionedSchemaValidation {
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
readonly issues: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class VersionedSchema {
|
|
28
|
+
private readonly fields = new Map<string, VersionedSchemaField>();
|
|
29
|
+
|
|
30
|
+
constructor(readonly name: string, readonly version: number) {
|
|
31
|
+
if (!name.trim()) throw new ValidationError("SCHEMA_NAME", "VersionedSchema requires a non-empty name.");
|
|
32
|
+
if (!Number.isSafeInteger(version) || version < 0) throw new ValidationError("SCHEMA_VERSION", "VersionedSchema version must be a non-negative integer.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
addField(field: VersionedSchemaField): this {
|
|
36
|
+
if (!field.name.trim()) throw new ValidationError("SCHEMA_FIELD", "Schema field requires a non-empty name.");
|
|
37
|
+
if (this.fields.has(field.name)) throw new ValidationError("SCHEMA_FIELD_DUPLICATE", `Duplicate schema field: ${field.name}`);
|
|
38
|
+
if (field.kind === "array" && !field.elementKind) throw new ValidationError("SCHEMA_ARRAY_ELEMENT", `Array field ${field.name} requires elementKind.`);
|
|
39
|
+
this.fields.set(field.name, field);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getField(name: string): VersionedSchemaField | undefined {
|
|
44
|
+
return this.fields.get(name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getFields(): readonly VersionedSchemaField[] {
|
|
48
|
+
return [...this.fields.values()];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
validate(data: unknown): VersionedSchemaValidation {
|
|
52
|
+
const issues: string[] = [];
|
|
53
|
+
if (!isRecord(data)) {
|
|
54
|
+
return { ok: false, issues: [`${this.name} must be an object.`] };
|
|
55
|
+
}
|
|
56
|
+
for (const field of this.fields.values()) {
|
|
57
|
+
const value = data[field.name];
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
if (field.required && field.defaultValue === undefined) issues.push(`${field.name} is required.`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
collectTypeIssues(value, field, field.name, issues);
|
|
63
|
+
}
|
|
64
|
+
return { ok: issues.length === 0, issues };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
serialize(data: unknown): Record<string, unknown> {
|
|
68
|
+
const validation = this.validate(data);
|
|
69
|
+
if (!validation.ok) throw new ValidationError("SCHEMA_VALIDATE", validation.issues.join(" "));
|
|
70
|
+
const record = data as Record<string, unknown>;
|
|
71
|
+
const result: Record<string, unknown> = {};
|
|
72
|
+
for (const field of this.fields.values()) {
|
|
73
|
+
const value = record[field.name];
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
result[field.name] = value;
|
|
76
|
+
} else if (field.defaultValue !== undefined) {
|
|
77
|
+
result[field.name] = cloneValue(field.defaultValue);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
deserialize(data: unknown): Record<string, unknown> {
|
|
84
|
+
return this.serialize(data);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static builder(name: string, version: number): VersionedSchemaBuilder {
|
|
88
|
+
return new VersionedSchemaBuilder(name, version);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class VersionedSchemaBuilder {
|
|
93
|
+
private readonly schema: VersionedSchema;
|
|
94
|
+
|
|
95
|
+
constructor(name: string, version: number) {
|
|
96
|
+
this.schema = new VersionedSchema(name, version);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
boolean(name: string, options: FieldOptions<boolean> = {}): this {
|
|
100
|
+
return this.field({ name, kind: "boolean", ...options });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
int32(name: string, options: FieldOptions<number> = {}): this {
|
|
104
|
+
return this.field({ name, kind: "int32", ...options });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
uint32(name: string, options: FieldOptions<number> = {}): this {
|
|
108
|
+
return this.field({ name, kind: "uint32", ...options });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
float32(name: string, options: FieldOptions<number> = {}): this {
|
|
112
|
+
return this.field({ name, kind: "float32", ...options });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
string(name: string, options: FieldOptions<string> = {}): this {
|
|
116
|
+
return this.field({ name, kind: "string", ...options });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
array(name: string, elementKind: VersionedFieldKind, options: FieldOptions<readonly unknown[]> = {}): this {
|
|
120
|
+
return this.field({ name, kind: "array", elementKind, ...options });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
object(name: string, schema?: VersionedSchema, options: FieldOptions<Record<string, unknown>> = {}): this {
|
|
124
|
+
return this.field({ name, kind: "object", schema, ...options });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
field(field: VersionedSchemaField): this {
|
|
128
|
+
this.schema.addField(field);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
build(): VersionedSchema {
|
|
133
|
+
return this.schema;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface FieldOptions<T> {
|
|
138
|
+
readonly required?: boolean;
|
|
139
|
+
readonly defaultValue?: T;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface MigrationDefinition<T = unknown> {
|
|
143
|
+
readonly from: number;
|
|
144
|
+
readonly to: number;
|
|
145
|
+
readonly description?: string;
|
|
146
|
+
migrate(data: T): T;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class MigrationRegistry<T = unknown> {
|
|
150
|
+
private readonly migrations = new Map<number, MigrationDefinition<T>[]>();
|
|
151
|
+
|
|
152
|
+
register(migration: MigrationDefinition<T>): this {
|
|
153
|
+
if (!Number.isSafeInteger(migration.from) || !Number.isSafeInteger(migration.to) || migration.to <= migration.from) {
|
|
154
|
+
throw new ValidationError("MIGRATION_VERSION", "Migration must move from a lower integer version to a higher integer version.");
|
|
155
|
+
}
|
|
156
|
+
const list = this.migrations.get(migration.from) ?? [];
|
|
157
|
+
if (list.some((entry) => entry.to === migration.to)) {
|
|
158
|
+
throw new ValidationError("MIGRATION_DUPLICATE", `Duplicate migration path: ${migration.from} -> ${migration.to}`);
|
|
159
|
+
}
|
|
160
|
+
list.push(migration);
|
|
161
|
+
list.sort((left, right) => left.to - right.to);
|
|
162
|
+
this.migrations.set(migration.from, list);
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
canMigrate(from: number, to: number): boolean {
|
|
167
|
+
try {
|
|
168
|
+
this.plan(from, to);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
migrate(data: T, from: number, to: number): T {
|
|
176
|
+
let current = data;
|
|
177
|
+
for (const migration of this.plan(from, to)) {
|
|
178
|
+
current = migration.migrate(current);
|
|
179
|
+
}
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
list(): readonly MigrationDefinition<T>[] {
|
|
184
|
+
return [...this.migrations.values()].flat().sort((left, right) => left.from - right.from || left.to - right.to);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private plan(from: number, to: number): readonly MigrationDefinition<T>[] {
|
|
188
|
+
if (from === to) return [];
|
|
189
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from > to) {
|
|
190
|
+
throw new ValidationError("MIGRATION_TARGET", "Migration target must be an integer version greater than or equal to the source.");
|
|
191
|
+
}
|
|
192
|
+
const plan: MigrationDefinition<T>[] = [];
|
|
193
|
+
let current = from;
|
|
194
|
+
while (current < to) {
|
|
195
|
+
const next = (this.migrations.get(current) ?? []).find((migration) => migration.to <= to);
|
|
196
|
+
if (!next) throw new ValidationError("MIGRATION_PATH", `No migration path from version ${current} to ${to}.`);
|
|
197
|
+
plan.push(next);
|
|
198
|
+
current = next.to;
|
|
199
|
+
}
|
|
200
|
+
return plan;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type SaveSlotFormat = "json" | "binary";
|
|
205
|
+
|
|
206
|
+
export interface SaveSlotMetadata {
|
|
207
|
+
readonly name?: string;
|
|
208
|
+
readonly description?: string;
|
|
209
|
+
readonly playTime?: number;
|
|
210
|
+
readonly version?: string;
|
|
211
|
+
readonly autoSave?: boolean;
|
|
212
|
+
readonly [key: string]: unknown;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface SerializedSaveSlot {
|
|
216
|
+
readonly id: string;
|
|
217
|
+
readonly format: SaveSlotFormat;
|
|
218
|
+
readonly timestamp: number;
|
|
219
|
+
readonly metadata: SaveSlotMetadata;
|
|
220
|
+
readonly compressed: boolean;
|
|
221
|
+
readonly data: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export class SaveSlot {
|
|
225
|
+
private data: string | Uint8Array | null = null;
|
|
226
|
+
private format: SaveSlotFormat = "json";
|
|
227
|
+
private timestamp: number;
|
|
228
|
+
private metadata: SaveSlotMetadata = {};
|
|
229
|
+
private compressed = false;
|
|
230
|
+
|
|
231
|
+
constructor(readonly id: string, timestamp = Date.now()) {
|
|
232
|
+
if (!id.trim()) throw new ValidationError("SAVE_SLOT_ID", "SaveSlot requires a non-empty id.");
|
|
233
|
+
this.timestamp = timestamp;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setData(data: string | Uint8Array, format: SaveSlotFormat, options: { readonly compressed?: boolean } = {}): void {
|
|
237
|
+
this.data = typeof data === "string" ? data : new Uint8Array(data);
|
|
238
|
+
this.format = format;
|
|
239
|
+
this.compressed = options.compressed ?? false;
|
|
240
|
+
this.timestamp = Date.now();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getData(): string | Uint8Array | null {
|
|
244
|
+
return typeof this.data === "string" || this.data === null ? this.data : new Uint8Array(this.data);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getFormat(): SaveSlotFormat {
|
|
248
|
+
return this.format;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getTimestamp(): number {
|
|
252
|
+
return this.timestamp;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getMetadata(): SaveSlotMetadata {
|
|
256
|
+
return { ...this.metadata };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setMetadata(metadata: SaveSlotMetadata): void {
|
|
260
|
+
this.metadata = { ...this.metadata, ...metadata };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
isCompressed(): boolean {
|
|
264
|
+
return this.compressed;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
getSize(): number {
|
|
268
|
+
if (this.data === null) return 0;
|
|
269
|
+
return typeof this.data === "string" ? new TextEncoder().encode(this.data).byteLength : this.data.byteLength;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
toJSON(): SerializedSaveSlot {
|
|
273
|
+
return {
|
|
274
|
+
id: this.id,
|
|
275
|
+
format: this.format,
|
|
276
|
+
timestamp: this.timestamp,
|
|
277
|
+
metadata: { ...this.metadata },
|
|
278
|
+
compressed: this.compressed,
|
|
279
|
+
data: this.data instanceof Uint8Array ? bytesToBase64(this.data) : this.data ?? ""
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static fromJSON(serialized: SerializedSaveSlot): SaveSlot {
|
|
284
|
+
const slot = new SaveSlot(serialized.id, serialized.timestamp);
|
|
285
|
+
slot.format = serialized.format;
|
|
286
|
+
slot.metadata = { ...serialized.metadata };
|
|
287
|
+
slot.compressed = serialized.compressed;
|
|
288
|
+
slot.data = serialized.format === "binary" ? base64ToBytes(serialized.data) : serialized.data;
|
|
289
|
+
return slot;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function collectTypeIssues(value: unknown, field: VersionedSchemaField, path: string, issues: string[]): void {
|
|
294
|
+
if (!matchesKind(value, field.kind)) {
|
|
295
|
+
issues.push(`${path} expected ${field.kind}.`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (field.kind === "array" && field.elementKind && Array.isArray(value)) {
|
|
299
|
+
value.forEach((entry, index) => {
|
|
300
|
+
if (!matchesKind(entry, field.elementKind as VersionedFieldKind)) issues.push(`${path}[${index}] expected ${field.elementKind}.`);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (field.kind === "object" && field.schema) {
|
|
304
|
+
const nested = field.schema.validate(value);
|
|
305
|
+
for (const issue of nested.issues) issues.push(`${path}.${issue}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function matchesKind(value: unknown, kind: VersionedFieldKind): boolean {
|
|
310
|
+
switch (kind) {
|
|
311
|
+
case "boolean":
|
|
312
|
+
return typeof value === "boolean";
|
|
313
|
+
case "int32":
|
|
314
|
+
return Number.isInteger(value) && Number(value) >= -2147483648 && Number(value) <= 2147483647;
|
|
315
|
+
case "uint32":
|
|
316
|
+
return Number.isInteger(value) && Number(value) >= 0 && Number(value) <= 4294967295;
|
|
317
|
+
case "float32":
|
|
318
|
+
case "float64":
|
|
319
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
320
|
+
case "string":
|
|
321
|
+
return typeof value === "string";
|
|
322
|
+
case "array":
|
|
323
|
+
return Array.isArray(value);
|
|
324
|
+
case "object":
|
|
325
|
+
return isRecord(value);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function cloneValue(value: unknown): unknown {
|
|
330
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
331
|
+
return JSON.parse(JSON.stringify(value)) as unknown;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
335
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
339
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
340
|
+
let binary = "";
|
|
341
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
342
|
+
return btoa(binary);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function base64ToBytes(base64: string): Uint8Array {
|
|
346
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(base64, "base64"));
|
|
347
|
+
const binary = atob(base64);
|
|
348
|
+
const bytes = new Uint8Array(binary.length);
|
|
349
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
350
|
+
return bytes;
|
|
351
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./Engine.js";
|
|
2
|
+
export * from "./EngineConfig.js";
|
|
3
|
+
export * from "./EngineLoop.js";
|
|
4
|
+
export * from "./Time.js";
|
|
5
|
+
export * from "./FixedStepAccumulator.js";
|
|
6
|
+
export * from "./EventBus.js";
|
|
7
|
+
export * from "./Scheduler.js";
|
|
8
|
+
export * from "./TaskQueue.js";
|
|
9
|
+
export * from "./Logger.js";
|
|
10
|
+
export * from "./Diagnostics.js";
|
|
11
|
+
export * from "./Errors.js";
|
|
12
|
+
export * from "./Disposable.js";
|
|
13
|
+
export * from "./ResourceScope.js";
|
|
14
|
+
export * from "./VersionedSerialization.js";
|