📄 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