@empty-sekai/sekai-custom-profile-sdk 0.3.0
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 +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/NOTICE +11 -0
- package/README.en.md +456 -0
- package/README.md +458 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +34 -0
- package/dist/cache/glyphPersistentCache.d.ts +145 -0
- package/dist/cache/glyphPersistentCache.js +346 -0
- package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
- package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
- package/dist/cache/sessionImageResourceCache.d.ts +39 -0
- package/dist/cache/sessionImageResourceCache.js +156 -0
- package/dist/emscripten.d.ts +11 -0
- package/dist/emscripten.js +1 -0
- package/dist/fontProvider.d.ts +36 -0
- package/dist/fontProvider.js +84 -0
- package/dist/fontSdfAtlas.d.ts +92 -0
- package/dist/fontSdfAtlas.js +296 -0
- package/dist/gpu/browserSemanticResources.d.ts +64 -0
- package/dist/gpu/browserSemanticResources.js +232 -0
- package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
- package/dist/gpu/generalTextRenderPlacement.js +131 -0
- package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
- package/dist/gpu/previewTransformTextureLayout.js +12 -0
- package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
- package/dist/gpu/semanticCommandGeometry.js +226 -0
- package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
- package/dist/gpu/semanticCommandPlanner.js +203 -0
- package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
- package/dist/gpu/semanticTextGlyphBridge.js +22 -0
- package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
- package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
- package/dist/gpu/slotRanges.d.ts +5 -0
- package/dist/gpu/slotRanges.js +14 -0
- package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
- package/dist/gpu/webglSdfAtlasTexture.js +72 -0
- package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
- package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
- package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
- package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/interaction/numericTextRegions.d.ts +46 -0
- package/dist/interaction/numericTextRegions.js +44 -0
- package/dist/localizationProvider.d.ts +33 -0
- package/dist/localizationProvider.js +74 -0
- package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
- package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
- package/dist/prebuiltSdfAtlas.d.ts +36 -0
- package/dist/prebuiltSdfAtlas.js +261 -0
- package/dist/protocol.d.ts +456 -0
- package/dist/protocol.js +1 -0
- package/dist/renderer.d.ts +218 -0
- package/dist/renderer.js +641 -0
- package/dist/resourceProvider.d.ts +24 -0
- package/dist/resourceProvider.js +41 -0
- package/dist/telemetry/rendererTelemetry.d.ts +158 -0
- package/dist/telemetry/rendererTelemetry.js +232 -0
- package/dist/third-party/freetype/FTL.txt +169 -0
- package/dist/types/atlas.d.ts +90 -0
- package/dist/types/atlas.js +1 -0
- package/dist/types/authoring.d.ts +81 -0
- package/dist/types/authoring.js +1 -0
- package/dist/types/core.d.ts +128 -0
- package/dist/types/core.js +1 -0
- package/dist/types/freeType.d.ts +54 -0
- package/dist/types/freeType.js +13 -0
- package/dist/types/glyph.d.ts +33 -0
- package/dist/types/glyph.js +1 -0
- package/dist/types/layout.d.ts +25 -0
- package/dist/types/layout.js +1 -0
- package/dist/types/text.d.ts +28 -0
- package/dist/types/text.js +1 -0
- package/dist/worker/glyphWorkScheduler.d.ts +29 -0
- package/dist/worker/glyphWorkScheduler.js +107 -0
- package/dist/worker-client.d.ts +114 -0
- package/dist/worker-client.js +413 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +708 -0
- package/package.json +46 -0
- package/src/atlas.rs +652 -0
- package/src/authoring.ts +42 -0
- package/src/authoring_runtime.rs +303 -0
- package/src/cache/glyphPersistentCache.ts +463 -0
- package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
- package/src/cache/sessionImageResourceCache.ts +186 -0
- package/src/edt.rs +97 -0
- package/src/emscripten.ts +17 -0
- package/src/fontProvider.ts +127 -0
- package/src/fontSdfAtlas.ts +431 -0
- package/src/geometry.rs +149 -0
- package/src/glyph_plan.rs +226 -0
- package/src/gpu/browserSemanticResources.ts +294 -0
- package/src/gpu/generalTextRenderPlacement.ts +175 -0
- package/src/gpu/previewTransformTextureLayout.ts +12 -0
- package/src/gpu/semanticCommandGeometry.ts +260 -0
- package/src/gpu/semanticCommandPlanner.ts +280 -0
- package/src/gpu/semanticTextGlyphBridge.ts +35 -0
- package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
- package/src/gpu/slotRanges.ts +15 -0
- package/src/gpu/webglSdfAtlasTexture.ts +69 -0
- package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
- package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
- package/src/index.ts +84 -0
- package/src/interaction/numericTextRegions.ts +80 -0
- package/src/layout.rs +1882 -0
- package/src/lib.rs +1443 -0
- package/src/localizationProvider.ts +113 -0
- package/src/main.rs +55 -0
- package/src/masterdata_runtime.rs +496 -0
- package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
- package/src/prebuiltSdfAtlas.ts +306 -0
- package/src/protocol.ts +150 -0
- package/src/renderer.ts +792 -0
- package/src/resourceProvider.ts +69 -0
- package/src/scene.rs +665 -0
- package/src/telemetry/rendererTelemetry.ts +358 -0
- package/src/types/atlas.ts +80 -0
- package/src/types/authoring.ts +85 -0
- package/src/types/core.ts +110 -0
- package/src/types/freeType.ts +63 -0
- package/src/types/glyph.ts +33 -0
- package/src/types/layout.ts +28 -0
- package/src/types/text.ts +28 -0
- package/src/worker/glyphWorkScheduler.ts +130 -0
- package/src/worker-client.ts +484 -0
- package/src/worker.ts +895 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { RendererAuthoringDocument, type RendererWorkerClientOptions } from "./worker-client.js";
|
|
2
|
+
import type { AuthoringCheckpoint } from "./types/authoring.js";
|
|
3
|
+
/** Worker-backed authoring runtime for game-compatible custom-profile documents. */
|
|
4
|
+
export declare class BrowserAuthoringClient {
|
|
5
|
+
private readonly worker;
|
|
6
|
+
private destroyed;
|
|
7
|
+
private constructor();
|
|
8
|
+
static create(options?: RendererWorkerClientOptions): Promise<BrowserAuthoringClient>;
|
|
9
|
+
createBlank(): Promise<RendererAuthoringDocument>;
|
|
10
|
+
importProfile(profile: unknown): Promise<RendererAuthoringDocument>;
|
|
11
|
+
restoreCheckpoint(checkpoint: AuthoringCheckpoint): Promise<RendererAuthoringDocument>;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
private assertAlive;
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RendererWorkerClient, } from "./worker-client.js";
|
|
2
|
+
/** Worker-backed authoring runtime for game-compatible custom-profile documents. */
|
|
3
|
+
export class BrowserAuthoringClient {
|
|
4
|
+
worker;
|
|
5
|
+
destroyed = false;
|
|
6
|
+
constructor(worker) {
|
|
7
|
+
this.worker = worker;
|
|
8
|
+
}
|
|
9
|
+
static async create(options = {}) {
|
|
10
|
+
return new BrowserAuthoringClient(await RendererWorkerClient.create(options));
|
|
11
|
+
}
|
|
12
|
+
async createBlank() {
|
|
13
|
+
this.assertAlive();
|
|
14
|
+
return this.worker.createAuthoringDocument();
|
|
15
|
+
}
|
|
16
|
+
async importProfile(profile) {
|
|
17
|
+
this.assertAlive();
|
|
18
|
+
return this.worker.createAuthoringDocument(profile);
|
|
19
|
+
}
|
|
20
|
+
async restoreCheckpoint(checkpoint) {
|
|
21
|
+
this.assertAlive();
|
|
22
|
+
return this.worker.restoreAuthoringDocument(checkpoint);
|
|
23
|
+
}
|
|
24
|
+
destroy() {
|
|
25
|
+
if (this.destroyed)
|
|
26
|
+
return;
|
|
27
|
+
this.destroyed = true;
|
|
28
|
+
this.worker.terminate();
|
|
29
|
+
}
|
|
30
|
+
assertAlive() {
|
|
31
|
+
if (this.destroyed)
|
|
32
|
+
throw new Error("Browser authoring client is destroyed");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export declare const GLYPH_CACHE_SCHEMA = "allium.glyph-raster-cache.v1";
|
|
2
|
+
export type PersistentCacheMode = "memory-only" | "origin";
|
|
3
|
+
export type GlyphRasterKeyInput = {
|
|
4
|
+
region: string;
|
|
5
|
+
fontSha256: string;
|
|
6
|
+
faceIndex: number;
|
|
7
|
+
variationAxes: ReadonlyArray<readonly [string, number]>;
|
|
8
|
+
glyphId: number;
|
|
9
|
+
pointSize26d6: number;
|
|
10
|
+
dpiX: number;
|
|
11
|
+
dpiY: number;
|
|
12
|
+
loadFlags: string;
|
|
13
|
+
renderMode: string;
|
|
14
|
+
spread26d6: number;
|
|
15
|
+
sdfAlgorithm: string;
|
|
16
|
+
supersample: number;
|
|
17
|
+
threshold: number;
|
|
18
|
+
downsampleVersion: string;
|
|
19
|
+
fontEngineFingerprint: string;
|
|
20
|
+
rasterContractId: string;
|
|
21
|
+
};
|
|
22
|
+
export type GlyphRasterIdentity = {
|
|
23
|
+
opaqueKey: string;
|
|
24
|
+
schemaNamespace: string;
|
|
25
|
+
fontEngineFingerprint: string;
|
|
26
|
+
rasterContractId: string;
|
|
27
|
+
};
|
|
28
|
+
export type PersistentGlyphMetrics = {
|
|
29
|
+
advance: number;
|
|
30
|
+
xOffset: number;
|
|
31
|
+
yOffset: number;
|
|
32
|
+
planeBearingX: number;
|
|
33
|
+
planeBearingY: number;
|
|
34
|
+
planeWidth: number;
|
|
35
|
+
planeHeight: number;
|
|
36
|
+
drawable: boolean;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
pixels: Uint8Array;
|
|
40
|
+
};
|
|
41
|
+
export type PersistentGlyphRecord = {
|
|
42
|
+
opaqueKey: string;
|
|
43
|
+
schemaNamespace: string;
|
|
44
|
+
fontEngineFingerprint: string;
|
|
45
|
+
rasterContractId: string;
|
|
46
|
+
identity: GlyphRasterIdentity;
|
|
47
|
+
format: "r8";
|
|
48
|
+
advance: number;
|
|
49
|
+
xOffset: number;
|
|
50
|
+
yOffset: number;
|
|
51
|
+
planeBearingX: number;
|
|
52
|
+
planeBearingY: number;
|
|
53
|
+
planeWidth: number;
|
|
54
|
+
planeHeight: number;
|
|
55
|
+
drawable: boolean;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
pixels: ArrayBuffer;
|
|
59
|
+
payloadLength: number;
|
|
60
|
+
payloadDigest: string;
|
|
61
|
+
byteSize: number;
|
|
62
|
+
lastAccessDay: number;
|
|
63
|
+
};
|
|
64
|
+
export type PersistentStoreStats = {
|
|
65
|
+
entries: number;
|
|
66
|
+
bytes: number;
|
|
67
|
+
};
|
|
68
|
+
export type PersistentSweepResult = {
|
|
69
|
+
scanned: number;
|
|
70
|
+
deleted: number;
|
|
71
|
+
};
|
|
72
|
+
export type PersistentWriteToken = {
|
|
73
|
+
epoch: number;
|
|
74
|
+
startedAtMs: number;
|
|
75
|
+
};
|
|
76
|
+
export interface GlyphRecordStore {
|
|
77
|
+
getMany(keys: readonly string[]): Promise<PersistentGlyphRecord[]>;
|
|
78
|
+
putMany(records: readonly PersistentGlyphRecord[], token?: PersistentWriteToken): Promise<boolean>;
|
|
79
|
+
deleteMany(keys: readonly string[]): Promise<void>;
|
|
80
|
+
stats(): Promise<PersistentStoreStats>;
|
|
81
|
+
trimLruTo(maxBytes: number, protectedKeys?: ReadonlySet<string>): Promise<number>;
|
|
82
|
+
sweepBefore(lastAccessDayExclusive: number, maxRecords: number): Promise<PersistentSweepResult>;
|
|
83
|
+
clear(clearedAtMs?: number): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export type GlyphPersistentCacheStats = {
|
|
86
|
+
lookups: number;
|
|
87
|
+
hits: number;
|
|
88
|
+
misses: number;
|
|
89
|
+
inserts: number;
|
|
90
|
+
evictions: number;
|
|
91
|
+
corruptions: number;
|
|
92
|
+
skippedWrites: number;
|
|
93
|
+
cancelledWrites: number;
|
|
94
|
+
readErrors: number;
|
|
95
|
+
writeErrors: number;
|
|
96
|
+
degraded: boolean;
|
|
97
|
+
};
|
|
98
|
+
export type GlyphPersistentCacheOptions = {
|
|
99
|
+
mode?: PersistentCacheMode;
|
|
100
|
+
store?: GlyphRecordStore;
|
|
101
|
+
softBytes?: number;
|
|
102
|
+
hardBytes?: number;
|
|
103
|
+
ttlDays?: number;
|
|
104
|
+
now?: () => number;
|
|
105
|
+
};
|
|
106
|
+
export declare function createGlyphRasterIdentity(input: GlyphRasterKeyInput): Promise<GlyphRasterIdentity>;
|
|
107
|
+
export declare function createPersistentGlyphRecord(identity: GlyphRasterIdentity, glyph: PersistentGlyphMetrics, nowMs?: number): Promise<PersistentGlyphRecord>;
|
|
108
|
+
export declare function persistentRecordPublicShape(record: PersistentGlyphRecord): Omit<PersistentGlyphRecord, "pixels">;
|
|
109
|
+
export declare class GlyphPersistentCache {
|
|
110
|
+
private readonly mode;
|
|
111
|
+
private readonly store?;
|
|
112
|
+
private readonly softBytes;
|
|
113
|
+
private readonly hardBytes;
|
|
114
|
+
private readonly ttlDays;
|
|
115
|
+
private readonly now;
|
|
116
|
+
private readonly counters;
|
|
117
|
+
private writeEpoch;
|
|
118
|
+
constructor(options?: GlyphPersistentCacheOptions);
|
|
119
|
+
getMany(identities: readonly GlyphRasterIdentity[]): Promise<Map<string, PersistentGlyphRecord>>;
|
|
120
|
+
beginWrite(): PersistentWriteToken;
|
|
121
|
+
putMany(records: readonly PersistentGlyphRecord[], writeToken?: PersistentWriteToken): Promise<void>;
|
|
122
|
+
sweep(options?: {
|
|
123
|
+
maxRecords?: number;
|
|
124
|
+
}): Promise<PersistentSweepResult>;
|
|
125
|
+
trimPersistentCache(targetBytes?: number): Promise<number>;
|
|
126
|
+
clearPersistentCache(): Promise<void>;
|
|
127
|
+
getPersistentCacheStats(): Promise<PersistentStoreStats & GlyphPersistentCacheStats>;
|
|
128
|
+
stats(): GlyphPersistentCacheStats;
|
|
129
|
+
private available;
|
|
130
|
+
}
|
|
131
|
+
export declare class MemoryGlyphRecordStore implements GlyphRecordStore {
|
|
132
|
+
private readonly records;
|
|
133
|
+
private readonly failWrites;
|
|
134
|
+
constructor(options?: {
|
|
135
|
+
failWrites?: boolean;
|
|
136
|
+
});
|
|
137
|
+
getMany(keys: readonly string[]): Promise<PersistentGlyphRecord[]>;
|
|
138
|
+
private lastClearMs;
|
|
139
|
+
putMany(records: readonly PersistentGlyphRecord[], token?: PersistentWriteToken): Promise<boolean>;
|
|
140
|
+
deleteMany(keys: readonly string[]): Promise<void>;
|
|
141
|
+
stats(): Promise<PersistentStoreStats>;
|
|
142
|
+
trimLruTo(maxBytes: number, protectedKeys?: ReadonlySet<string>): Promise<number>;
|
|
143
|
+
sweepBefore(lastAccessDayExclusive: number, maxRecords: number): Promise<PersistentSweepResult>;
|
|
144
|
+
clear(clearedAtMs?: number): Promise<void>;
|
|
145
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
export const GLYPH_CACHE_SCHEMA = "allium.glyph-raster-cache.v1";
|
|
2
|
+
const DAY_MS = 86_400_000;
|
|
3
|
+
const DEFAULT_SOFT_BYTES = 64 * 1024 * 1024;
|
|
4
|
+
const DEFAULT_HARD_BYTES = 96 * 1024 * 1024;
|
|
5
|
+
export async function createGlyphRasterIdentity(input) {
|
|
6
|
+
validateKeyInput(input);
|
|
7
|
+
const canonical = [
|
|
8
|
+
["schema", GLYPH_CACHE_SCHEMA],
|
|
9
|
+
["region", input.region],
|
|
10
|
+
["font_sha256", input.fontSha256.toLowerCase()],
|
|
11
|
+
["face_index", input.faceIndex],
|
|
12
|
+
["variation_axes", [...input.variationAxes].sort(([a], [b]) => a.localeCompare(b))],
|
|
13
|
+
["glyph_id", input.glyphId],
|
|
14
|
+
["point_size_26d6", input.pointSize26d6],
|
|
15
|
+
["dpi_x", input.dpiX],
|
|
16
|
+
["dpi_y", input.dpiY],
|
|
17
|
+
["load_flags", input.loadFlags],
|
|
18
|
+
["render_mode", input.renderMode],
|
|
19
|
+
["spread_26d6", input.spread26d6],
|
|
20
|
+
["sdf_algorithm", input.sdfAlgorithm],
|
|
21
|
+
["supersample", input.supersample],
|
|
22
|
+
["threshold", input.threshold],
|
|
23
|
+
["downsample_version", input.downsampleVersion],
|
|
24
|
+
["font_engine", input.fontEngineFingerprint],
|
|
25
|
+
["raster_contract", input.rasterContractId],
|
|
26
|
+
];
|
|
27
|
+
const opaqueKey = await sha256Hex(new TextEncoder().encode(JSON.stringify(canonical)));
|
|
28
|
+
return {
|
|
29
|
+
opaqueKey,
|
|
30
|
+
schemaNamespace: GLYPH_CACHE_SCHEMA,
|
|
31
|
+
fontEngineFingerprint: input.fontEngineFingerprint,
|
|
32
|
+
rasterContractId: input.rasterContractId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function createPersistentGlyphRecord(identity, glyph, nowMs = Date.now()) {
|
|
36
|
+
const pixels = glyph.pixels.slice();
|
|
37
|
+
const payloadDigest = await sha256Hex(pixels);
|
|
38
|
+
const byteSize = estimateRecordBytes(pixels.byteLength);
|
|
39
|
+
return {
|
|
40
|
+
opaqueKey: identity.opaqueKey,
|
|
41
|
+
schemaNamespace: identity.schemaNamespace,
|
|
42
|
+
fontEngineFingerprint: identity.fontEngineFingerprint,
|
|
43
|
+
rasterContractId: identity.rasterContractId,
|
|
44
|
+
identity: { ...identity },
|
|
45
|
+
format: "r8",
|
|
46
|
+
advance: glyph.advance,
|
|
47
|
+
xOffset: glyph.xOffset,
|
|
48
|
+
yOffset: glyph.yOffset,
|
|
49
|
+
planeBearingX: glyph.planeBearingX,
|
|
50
|
+
planeBearingY: glyph.planeBearingY,
|
|
51
|
+
planeWidth: glyph.planeWidth,
|
|
52
|
+
planeHeight: glyph.planeHeight,
|
|
53
|
+
drawable: glyph.drawable,
|
|
54
|
+
width: glyph.width,
|
|
55
|
+
height: glyph.height,
|
|
56
|
+
pixels: pixels.buffer.slice(pixels.byteOffset, pixels.byteOffset + pixels.byteLength),
|
|
57
|
+
payloadLength: pixels.byteLength,
|
|
58
|
+
payloadDigest,
|
|
59
|
+
byteSize,
|
|
60
|
+
lastAccessDay: dayBucket(nowMs),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function persistentRecordPublicShape(record) {
|
|
64
|
+
const { pixels: _, ...publicShape } = record;
|
|
65
|
+
return publicShape;
|
|
66
|
+
}
|
|
67
|
+
export class GlyphPersistentCache {
|
|
68
|
+
mode;
|
|
69
|
+
store;
|
|
70
|
+
softBytes;
|
|
71
|
+
hardBytes;
|
|
72
|
+
ttlDays;
|
|
73
|
+
now;
|
|
74
|
+
counters = {
|
|
75
|
+
lookups: 0,
|
|
76
|
+
hits: 0,
|
|
77
|
+
misses: 0,
|
|
78
|
+
inserts: 0,
|
|
79
|
+
evictions: 0,
|
|
80
|
+
corruptions: 0,
|
|
81
|
+
skippedWrites: 0,
|
|
82
|
+
cancelledWrites: 0,
|
|
83
|
+
readErrors: 0,
|
|
84
|
+
writeErrors: 0,
|
|
85
|
+
degraded: false,
|
|
86
|
+
};
|
|
87
|
+
writeEpoch = 0;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
this.mode = options.mode ?? "memory-only";
|
|
90
|
+
this.store = options.store;
|
|
91
|
+
this.softBytes = options.softBytes ?? DEFAULT_SOFT_BYTES;
|
|
92
|
+
this.hardBytes = options.hardBytes ?? DEFAULT_HARD_BYTES;
|
|
93
|
+
this.ttlDays = options.ttlDays ?? 30;
|
|
94
|
+
this.now = options.now ?? Date.now;
|
|
95
|
+
if (this.softBytes <= 0 || this.hardBytes < this.softBytes) {
|
|
96
|
+
throw new Error("persistent glyph cache requires 0 < softBytes <= hardBytes");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async getMany(identities) {
|
|
100
|
+
this.counters.lookups += identities.length;
|
|
101
|
+
if (!this.available()) {
|
|
102
|
+
this.counters.misses += identities.length;
|
|
103
|
+
return new Map();
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const expected = new Map(identities.map((identity) => [identity.opaqueKey, identity]));
|
|
107
|
+
const records = await this.store.getMany([...expected.keys()]);
|
|
108
|
+
const valid = new Map();
|
|
109
|
+
const corruptKeys = [];
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
const identity = expected.get(record.opaqueKey);
|
|
112
|
+
if (!identity || !(await validateRecord(record, identity))) {
|
|
113
|
+
corruptKeys.push(record.opaqueKey);
|
|
114
|
+
this.counters.corruptions += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
record.lastAccessDay = dayBucket(this.now());
|
|
118
|
+
valid.set(record.opaqueKey, record);
|
|
119
|
+
}
|
|
120
|
+
if (corruptKeys.length > 0)
|
|
121
|
+
await this.store.deleteMany(corruptKeys);
|
|
122
|
+
if (valid.size > 0)
|
|
123
|
+
await this.store.putMany([...valid.values()]);
|
|
124
|
+
this.counters.hits += valid.size;
|
|
125
|
+
this.counters.misses += identities.length - valid.size;
|
|
126
|
+
return valid;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
this.counters.readErrors += 1;
|
|
130
|
+
this.counters.degraded = true;
|
|
131
|
+
this.counters.misses += identities.length;
|
|
132
|
+
return new Map();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
beginWrite() {
|
|
136
|
+
return { epoch: this.writeEpoch, startedAtMs: this.now() };
|
|
137
|
+
}
|
|
138
|
+
async putMany(records, writeToken = this.beginWrite()) {
|
|
139
|
+
if (writeToken.epoch !== this.writeEpoch) {
|
|
140
|
+
this.counters.cancelledWrites += records.length;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!this.available() || records.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
const accepted = [];
|
|
147
|
+
let pendingBytes = 0;
|
|
148
|
+
for (const record of records) {
|
|
149
|
+
if (record.byteSize > this.hardBytes) {
|
|
150
|
+
this.counters.skippedWrites += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const before = await this.store.stats();
|
|
154
|
+
const pendingWithRecord = pendingBytes + record.byteSize;
|
|
155
|
+
if (before.bytes + pendingWithRecord > this.softBytes) {
|
|
156
|
+
const target = Math.max(0, this.softBytes - pendingWithRecord);
|
|
157
|
+
this.counters.evictions += await this.store.trimLruTo(target, new Set([record.opaqueKey]));
|
|
158
|
+
}
|
|
159
|
+
const afterTrim = await this.store.stats();
|
|
160
|
+
if (afterTrim.bytes + pendingWithRecord > this.hardBytes) {
|
|
161
|
+
this.counters.skippedWrites += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
accepted.push(record);
|
|
165
|
+
pendingBytes = pendingWithRecord;
|
|
166
|
+
}
|
|
167
|
+
if (accepted.length > 0) {
|
|
168
|
+
const acceptedByStore = await this.store.putMany(accepted, writeToken);
|
|
169
|
+
if (acceptedByStore) {
|
|
170
|
+
this.counters.inserts += accepted.length;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.counters.cancelledWrites += accepted.length;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.counters.writeErrors += 1;
|
|
179
|
+
this.counters.degraded = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async sweep(options = {}) {
|
|
183
|
+
if (!this.available())
|
|
184
|
+
return { scanned: 0, deleted: 0 };
|
|
185
|
+
try {
|
|
186
|
+
const cutoff = dayBucket(this.now()) - this.ttlDays;
|
|
187
|
+
return await this.store.sweepBefore(cutoff, options.maxRecords ?? 128);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
this.counters.writeErrors += 1;
|
|
191
|
+
this.counters.degraded = true;
|
|
192
|
+
return { scanned: 0, deleted: 0 };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async trimPersistentCache(targetBytes = this.softBytes) {
|
|
196
|
+
if (!this.available())
|
|
197
|
+
return 0;
|
|
198
|
+
try {
|
|
199
|
+
const evicted = await this.store.trimLruTo(Math.max(0, targetBytes));
|
|
200
|
+
this.counters.evictions += evicted;
|
|
201
|
+
return evicted;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
this.counters.writeErrors += 1;
|
|
205
|
+
this.counters.degraded = true;
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async clearPersistentCache() {
|
|
210
|
+
this.writeEpoch += 1;
|
|
211
|
+
if (!this.store)
|
|
212
|
+
return;
|
|
213
|
+
try {
|
|
214
|
+
await this.store.clear(this.now());
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
this.counters.writeErrors += 1;
|
|
218
|
+
this.counters.degraded = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async getPersistentCacheStats() {
|
|
222
|
+
const stored = this.available() ? await this.store.stats().catch(() => ({ entries: 0, bytes: 0 })) : { entries: 0, bytes: 0 };
|
|
223
|
+
return { ...stored, ...this.stats() };
|
|
224
|
+
}
|
|
225
|
+
stats() {
|
|
226
|
+
return { ...this.counters };
|
|
227
|
+
}
|
|
228
|
+
available() {
|
|
229
|
+
return this.mode === "origin" && this.store != null && !this.counters.degraded;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export class MemoryGlyphRecordStore {
|
|
233
|
+
records = new Map();
|
|
234
|
+
failWrites;
|
|
235
|
+
constructor(options = {}) {
|
|
236
|
+
this.failWrites = options.failWrites ?? false;
|
|
237
|
+
}
|
|
238
|
+
async getMany(keys) {
|
|
239
|
+
return keys.flatMap((key) => {
|
|
240
|
+
const record = this.records.get(key);
|
|
241
|
+
return record ? [cloneRecord(record)] : [];
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
lastClearMs = Number.NEGATIVE_INFINITY;
|
|
245
|
+
async putMany(records, token) {
|
|
246
|
+
if (this.failWrites)
|
|
247
|
+
throw new Error("simulated quota/private-mode failure");
|
|
248
|
+
if (token && token.startedAtMs <= this.lastClearMs)
|
|
249
|
+
return false;
|
|
250
|
+
for (const record of records)
|
|
251
|
+
this.records.set(record.opaqueKey, cloneRecord(record));
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
async deleteMany(keys) {
|
|
255
|
+
if (this.failWrites)
|
|
256
|
+
throw new Error("simulated write failure");
|
|
257
|
+
for (const key of keys)
|
|
258
|
+
this.records.delete(key);
|
|
259
|
+
}
|
|
260
|
+
async stats() {
|
|
261
|
+
return {
|
|
262
|
+
entries: this.records.size,
|
|
263
|
+
bytes: [...this.records.values()].reduce((sum, record) => sum + record.byteSize, 0),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async trimLruTo(maxBytes, protectedKeys = new Set()) {
|
|
267
|
+
if (this.failWrites)
|
|
268
|
+
throw new Error("simulated write failure");
|
|
269
|
+
let bytes = (await this.stats()).bytes;
|
|
270
|
+
let evicted = 0;
|
|
271
|
+
const candidates = [...this.records.values()]
|
|
272
|
+
.filter((record) => !protectedKeys.has(record.opaqueKey))
|
|
273
|
+
.sort((a, b) => a.lastAccessDay - b.lastAccessDay || a.opaqueKey.localeCompare(b.opaqueKey));
|
|
274
|
+
for (const record of candidates) {
|
|
275
|
+
if (bytes <= maxBytes)
|
|
276
|
+
break;
|
|
277
|
+
this.records.delete(record.opaqueKey);
|
|
278
|
+
bytes -= record.byteSize;
|
|
279
|
+
evicted += 1;
|
|
280
|
+
}
|
|
281
|
+
return evicted;
|
|
282
|
+
}
|
|
283
|
+
async sweepBefore(lastAccessDayExclusive, maxRecords) {
|
|
284
|
+
if (this.failWrites)
|
|
285
|
+
throw new Error("simulated write failure");
|
|
286
|
+
let scanned = 0;
|
|
287
|
+
let deleted = 0;
|
|
288
|
+
for (const record of [...this.records.values()].sort((a, b) => a.lastAccessDay - b.lastAccessDay)) {
|
|
289
|
+
if (scanned >= maxRecords)
|
|
290
|
+
break;
|
|
291
|
+
scanned += 1;
|
|
292
|
+
if (record.lastAccessDay < lastAccessDayExclusive) {
|
|
293
|
+
this.records.delete(record.opaqueKey);
|
|
294
|
+
deleted += 1;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { scanned, deleted };
|
|
298
|
+
}
|
|
299
|
+
async clear(clearedAtMs = Date.now()) {
|
|
300
|
+
if (this.failWrites)
|
|
301
|
+
throw new Error("simulated write failure");
|
|
302
|
+
this.lastClearMs = Math.max(this.lastClearMs, clearedAtMs);
|
|
303
|
+
this.records.clear();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function validateKeyInput(input) {
|
|
307
|
+
if (!/^[0-9a-f]{64}$/i.test(input.fontSha256))
|
|
308
|
+
throw new Error("fontSha256 must be a full SHA-256 hex digest");
|
|
309
|
+
if (!input.region || !input.fontEngineFingerprint || !input.rasterContractId)
|
|
310
|
+
throw new Error("glyph raster key identity fields must be non-empty");
|
|
311
|
+
for (const value of [input.faceIndex, input.glyphId, input.pointSize26d6, input.dpiX, input.dpiY, input.spread26d6, input.supersample, input.threshold]) {
|
|
312
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
313
|
+
throw new Error("glyph raster numeric fields must be non-negative safe integers");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function validateRecord(record, identity) {
|
|
317
|
+
if (record.opaqueKey !== identity.opaqueKey ||
|
|
318
|
+
record.schemaNamespace !== identity.schemaNamespace ||
|
|
319
|
+
record.fontEngineFingerprint !== identity.fontEngineFingerprint ||
|
|
320
|
+
record.rasterContractId !== identity.rasterContractId ||
|
|
321
|
+
record.format !== "r8" ||
|
|
322
|
+
record.payloadLength !== record.pixels.byteLength ||
|
|
323
|
+
record.byteSize !== estimateRecordBytes(record.payloadLength))
|
|
324
|
+
return false;
|
|
325
|
+
return (await sha256Hex(new Uint8Array(record.pixels))) === record.payloadDigest;
|
|
326
|
+
}
|
|
327
|
+
function estimateRecordBytes(payloadBytes) {
|
|
328
|
+
return payloadBytes + 256;
|
|
329
|
+
}
|
|
330
|
+
function dayBucket(nowMs) {
|
|
331
|
+
return Math.floor(nowMs / DAY_MS);
|
|
332
|
+
}
|
|
333
|
+
async function sha256Hex(bytes) {
|
|
334
|
+
const subtle = globalThis.crypto?.subtle;
|
|
335
|
+
if (!subtle)
|
|
336
|
+
throw new Error("Web Crypto SHA-256 is required for persistent glyph identity");
|
|
337
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
338
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
339
|
+
}
|
|
340
|
+
function cloneRecord(record) {
|
|
341
|
+
return {
|
|
342
|
+
...record,
|
|
343
|
+
identity: { ...record.identity },
|
|
344
|
+
pixels: record.pixels.slice(0),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { GlyphPersistentCache, type GlyphRecordStore, type PersistentGlyphRecord, type PersistentStoreStats, type PersistentSweepResult, type PersistentWriteToken } from "./glyphPersistentCache.js";
|
|
2
|
+
export declare class IndexedDbGlyphRecordStore implements GlyphRecordStore {
|
|
3
|
+
private readonly databaseName;
|
|
4
|
+
private databasePromise;
|
|
5
|
+
constructor(databaseName?: string);
|
|
6
|
+
getMany(keys: readonly string[]): Promise<PersistentGlyphRecord[]>;
|
|
7
|
+
putMany(records: readonly PersistentGlyphRecord[], token?: PersistentWriteToken): Promise<boolean>;
|
|
8
|
+
deleteMany(keys: readonly string[]): Promise<void>;
|
|
9
|
+
stats(): Promise<PersistentStoreStats>;
|
|
10
|
+
trimLruTo(maxBytes: number, protectedKeys?: ReadonlySet<string>): Promise<number>;
|
|
11
|
+
sweepBefore(lastAccessDayExclusive: number, maxRecords: number): Promise<PersistentSweepResult>;
|
|
12
|
+
clear(clearedAtMs?: number): Promise<void>;
|
|
13
|
+
close(): void;
|
|
14
|
+
private database;
|
|
15
|
+
}
|
|
16
|
+
export declare function createOriginGlyphPersistentCache(options?: {
|
|
17
|
+
databaseName?: string;
|
|
18
|
+
softBytes?: number;
|
|
19
|
+
hardBytes?: number;
|
|
20
|
+
ttlDays?: number;
|
|
21
|
+
}): GlyphPersistentCache;
|
|
22
|
+
export declare const GLYPH_PERSISTENT_CACHE_INFO: Readonly<{
|
|
23
|
+
databaseName: "sekai-custom-profile-sdk-glyph-cache-v1";
|
|
24
|
+
databaseVersion: 1;
|
|
25
|
+
schemaNamespace: "allium.glyph-raster-cache.v1";
|
|
26
|
+
}>;
|