📄 src/models/test/hydration_integration.test.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import 'fake-indexeddb/auto';
16import { describe, it, expect, beforeEach } from 'vitest';
17import { Book, type BookSlimCols, type BookExtendedCols, type BookProvCols } from '../generated-src/Book.ts';
18import { PatchOperation, type AnyPatch, type EvmPayload } from '../binary.ts';
19import { clear, configureDb, hasRecord, getRecord } from '../db.ts';
20import { setDefaultLanguage } from '../orm.ts';
21import { PatchInstance, serializePatches, deserializePatches } from '../patcher.ts';
22import { WorkerCommands } from '../enums.ts';
23import type { IThread, WorkerPayload, QueueItem } from '../interfaces.ts';
24import { sidecarStore, getRecordCols } from '../worker/libs/initialLoad.ts';
25import { bulkAddPatches, getAllPatchesByUsername, clearAllPatches } from '../indexdb/patch_storage.ts';
26import type { ParsedSidecar, ColumnData } from '../enriched-index.ts';
27
28class MockWorkerThread implements IThread {
29    received: WorkerPayload[] = [];
30    constructor(public response: any) {}
31    async sendRequest(payload: WorkerPayload): Promise<any> {
32        this.received.push(payload);
33        return this.response;
34    }
35}
36
37describe('hydration_integration — LoadAsync, getRecordCols, serialize/deserialize Patches', () => {
38    beforeEach(async () => {
39        clear();
40        setDefaultLanguage('eng');
41        configureDb({ cacheLimit: 2 });
42        sidecarStore.clear();
43        await clearAllPatches();
44        PatchInstance.clearQueue();
45    });
46
47    it('LoadAsync hydrates the record on cache miss', async () => {
48        const mockResponse = {
49            lang: 'eng',
50            slim: {
51                ids: new Uint16Array([100]),
52                titles: ['Hydrated Title'],
53                originalLangs: ['eng'],
54                previewTxIds: ['tx-123'],
55                ageGradeBands: new Uint8Array([3]),
56                safes: new Uint8Array([1]),
57                deleteds: new Uint8Array([0]),
58                defaultDownloadTxIds: [null],
59                defaultMethods: new Uint8Array([0]),
60                defaultNbPages: new Uint16Array([0]),
61                defaultSizeBytes: new Uint32Array([0]),
62            } as any as BookSlimCols,
63            extended: {
64                ids: new Uint16Array([100]),
65                descriptions: ['Hydrated Description'],
66                prerequisites: [null],
67                deweyDecimals: ['123.45'],
68                locCallNumbers: [null],
69                copyrightStatus: new Uint8Array([0]),
70                textQuality: new Uint8Array([0]),
71                illustrations: new Uint8Array([0]),
72                educationQuality: new Uint8Array([0]),
73                practicality: new Uint8Array([0]),
74                accessibilityLevel: new Uint8Array([0]),
75                teacherMaterials: new Uint8Array([0]),
76                pedagogicalScaffolding: new Uint8Array([0]),
77                clarityOfExplanation: new Uint8Array([0]),
78                workedExamples: new Uint8Array([0]),
79                exercisesAndAssessment: new Uint8Array([0]),
80                selfStudyReadiness: new Uint8Array([0]),
81                referenceApparatus: new Uint8Array([0]),
82                safetyGuidance: new Uint8Array([0]),
83                contentCurrency: new Uint8Array([0]),
84                ecoRegeneration: new Uint8Array([0]),
85                communityLiving: new Uint8Array([0]),
86                promotesPeace: new Uint8Array([0]),
87                businessDevelopment: new Uint8Array([0]),
88                authorIds: [[5]],
89                instances: [[
90                    { downloadTxId: 'tx-inst', method: 1, isAiTranslation: false, isDefault: true }
91                ]]
92            } as any,
93            prov: {
94                ids: new Uint16Array([100]),
95                fields: {
96                    textQuality: new Uint8Array([2]),
97                }
98            } as BookProvCols
99        };
100
101        const thread = new MockWorkerThread([mockResponse]);
102        PatchInstance.initThread(thread, 0);
103
104        expect(Book.Has(100)).toBe(false);
105
106        const book = await Book.LoadAsync(100);
107
108        expect(Book.Has(100)).toBe(true);
109        expect(book.id).toBe(100);
110        expect(book.title).toBe('Hydrated Title');
111        expect(book.description).toBe('Hydrated Description');
112        expect(book.authorsIds).toEqual([5]);
113        expect(book.getProvenance(12, 'eng')).toBe(2); // textQuality propId 12
114
115        expect(thread.received.length).toBe(1);
116        expect(thread.received[0]).toEqual({
117            cmd: WorkerCommands.GetRecord,
118            payload: { objectType: Book.type, id: 100 }
119        });
120    });
121
122    it('getRecordCols slices single record columns correctly in the worker', () => {
123        const mockSlim: ParsedSidecar = {
124            header: { formatVersion: 1, recordCount: 1, columnCount: 2, sidecarType: 0 },
125            columns: new Map<string, ColumnData>([
126                ['ids', new Uint16Array([50])],
127                ['titles', ['Sidecar Book Title']],
128            ]),
129        };
130        const mockExtended: ParsedSidecar = {
131            header: { formatVersion: 1, recordCount: 1, columnCount: 2, sidecarType: 1 },
132            columns: new Map<string, ColumnData>([
133                ['ids', new Uint16Array([50])],
134                ['descriptions', ['Sidecar Book Description']],
135            ]),
136            authorIds: [[3]],
137            instances: [[
138                { downloadTxId: 'tx-a', method: 1, isDefault: true }
139            ]] as any,
140        };
141
142        sidecarStore.set(0, new Map([['eng', {
143            slim: mockSlim,
144            extended: mockExtended,
145            idIndex: new Map([[50, 0]]),
146        } as any]]));
147
148        const res = getRecordCols(0, 50);
149
150        expect(res).not.toBeNull();
151        expect(res!.length).toBe(1);
152        expect(res![0].lang).toBe('eng');
153        expect(Array.from(res![0].slim!.ids)).toEqual([50]);
154        expect(res![0].slim!.titles).toEqual(['Sidecar Book Title']);
155        expect(res![0].extended!.descriptions).toEqual(['Sidecar Book Description']);
156        expect(res![0].extended!.authorIds).toEqual([[3]]);
157        expect(res![0].extended!.instances).toEqual([[
158            { downloadTxId: 'tx-a', method: 1, isDefault: true }
159        ]]);
160    });
161
162    it('serializePatches and deserializePatches round-trips correctly', async () => {
163        const edit1: AnyPatch = {
164            operation: PatchOperation.EditPropertyString,
165            objectType: 0, objectId: 10, langId: 1, propertyId: 1, value: 'Hello String'
166        };
167        const edit2: AnyPatch = {
168            operation: PatchOperation.EditPropertyNumber,
169            objectType: 0, objectId: 10, langId: 1, propertyId: 12, value: 5
170        };
171
172        await bulkAddPatches('mine', [edit1, edit2]);
173
174        const payload = await serializePatches(1, 3);
175
176        expect(payload.patches.length).toBe(2);
177        expect(payload.textData).toEqual(['Hello String']);
178        expect(payload.textIndex).toEqual([0]);
179
180        await deserializePatches('bob', payload);
181
182        const bobPatches = await getAllPatchesByUsername('bob');
183        expect(bobPatches.length).toBe(2);
184
185        const op1 = bobPatches.find(p => (p as AnyPatch).operation === PatchOperation.EditPropertyString);
186        const op2 = bobPatches.find(p => (p as AnyPatch).operation === PatchOperation.EditPropertyNumber);
187
188        expect(op1).toBeDefined();
189        expect((op1 as any).value).toBe('Hello String');
190        expect((op1 as any).objectId).toBe(10);
191
192        expect(op2).toBeDefined();
193        expect((op2 as any).value).toBe(5);
194        expect((op2 as any).objectId).toBe(10);
195    });
196});
197