📄 src/models/stats.ts
D-OPEN SOVEREIGN

Documentation & Insights

Stats are local-only analytics (ActionTypes ops). They are persisted in
IndexedDB through the patcher but are never serialized to the EVM.
The store is reconstructed on each page load.
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 { DateUtils } from "./dates.ts";
16import { PatchInstance } from "./patcher.ts";
17import type {
18    BaseLocalOp,
19    LocalOp,
20    PartOp,
21    RateOp,
22    RatePartOp,
23    ShareOp,
24    SharePartOp,
25    StatSummary,
26    uid
27} from "./interfaces.ts";
28import { ActionTypes, Rating } from "./enums.ts";
29
30/**
31 * Stats are local-only analytics (ActionTypes ops). They are persisted in
32 * IndexedDB through the patcher but are never serialized to the EVM.
33 * The store is reconstructed on each page load.
34 */
35// ObjectTypeId => ObjectId => Summary
36const StatsStore = new Map<number, Map<uid, StatSummary>>()
37
38const check = (objectType: number, objectId: number) => {
39    if (!StatsStore.has(objectType)) {
40        StatsStore.set(objectType, new Map())
41    }
42    if (!StatsStore.get(objectType)!.has(objectId)) {
43        StatsStore.get(objectType)!.set(objectId, {
44            visits: [],
45            downloads: [],
46            rating: Rating.None,
47            socialMediaId: [],
48            parts: {},
49        })
50    }
51}
52
53export const PopulateStatsStore = (operations: Array<LocalOp>) => {
54    for (const op of operations) {
55        check(op.objectType, op.objectId)
56
57        const map = StatsStore.get(op.objectType) as Map<uid, StatSummary>
58        const stats = map.get(op.objectId) as StatSummary
59
60        switch (op.action) {
61            case ActionTypes.Visit:
62                stats.visits.push(DateUtils.now());
63                break
64            case ActionTypes.Download:
65                stats.downloads.push(DateUtils.now());
66                break;
67            case ActionTypes.VisitPart:
68                if (stats.parts[(op as PartOp).part]) {
69                    stats.parts[(op as PartOp).part].visits.push(DateUtils.now());
70                } else {
71                    stats.parts[(op as PartOp).part] = {
72                        rating: Rating.None,
73                        visits: [DateUtils.now()],
74                        socialMediaId: []
75                    }
76                }
77                break;
78            case ActionTypes.RatePart:
79                if (stats.parts[(op as RatePartOp).part]) {
80                    stats.parts[(op as RatePartOp).part].rating = (op as RatePartOp).rate;
81                } else {
82                    stats.parts[(op as RatePartOp).part] = {
83                        rating: (op as RatePartOp).rate,
84                        visits: [],
85                        socialMediaId: []
86                    }
87                }
88                break;
89            case ActionTypes.Rate:
90                stats.rating = (op as RateOp).rate
91                break;
92            case ActionTypes.SharePart:
93                if (stats.parts[(op as SharePartOp).part]) {
94                    stats.parts[(op as SharePartOp).part].socialMediaId.push((op as SharePartOp).socialMediaId);
95                } else {
96                    stats.parts[(op as SharePartOp).part] = {
97                        rating: Rating.None,
98                        visits: [],
99                        socialMediaId: [(op as SharePartOp).socialMediaId],
100                    }
101                }
102                break;
103            case ActionTypes.Share:
104                stats.socialMediaId.push((op as ShareOp).socialMediaId);
105                break;
106            default: break;
107        }
108    }
109}
110
111export const DownloadsByType = (objectType: number): Array<number> => {
112    if (!StatsStore.has(objectType)) {
113        return [];
114    }
115    const map = StatsStore.get(objectType) as Map<uid, StatSummary>
116    return Array.from(map.entries())
117        .filter(([, value]) => value.downloads.length > 0)
118        .map(([k,]) => k);
119}
120
121export const VisitsByType = (objectType: number): Array<number> => {
122    if (!StatsStore.has(objectType)) {
123        return [];
124    }
125    const map = StatsStore.get(objectType) as Map<uid, StatSummary>
126    return Array.from(map.entries())
127        .filter(([, value]) => value.visits.length > 0)
128        .map(([k,]) => k);
129}
130
131export const FavoritesByTypes = (objectType: number): Array<{ id: uid, nbVisits: number, rating: Rating }> => {
132    if (!StatsStore.has(objectType)) {
133        return []
134    }
135    const map = StatsStore.get(objectType) as Map<uid, StatSummary>
136    const result = Array.from(map.keys())
137        .filter(id => {
138            const summary = map.get(id) as StatSummary
139            return summary.visits.length > 0
140                || summary.downloads.length > 0
141                || [Rating.Keep, Rating.New, Rating.Shared].includes(summary.rating)
142        })
143        .map(id => {
144            const summary = map.get(id) as StatSummary
145            return { id, nbVisits: summary.visits.length, rating: summary.rating }
146        })
147    result.sort((a, b) => b.nbVisits - a.nbVisits)
148    return result
149}
150
151export class Stats {
152
153    constructor(private objectId: number, protected objectType: number) { }
154
155    favoriteParts(): Array<{ partId: number, nbVisits: number, rating: Rating }> {
156        if (!StatsStore.has(this.objectType)) {
157            return []
158        }
159        const map = StatsStore.get(this.objectType) as Map<uid, StatSummary>
160        if (!map.has(this.objectId)) {
161            return []
162        }
163        const summary = map.get(this.objectId) as StatSummary
164        const result = Object.keys(summary.parts).map(partId => {
165            return {
166                partId: parseInt(partId),
167                nbVisits: summary.parts[partId].visits.length,
168                rating: summary.parts[partId].rating
169            }
170        })
171
172        result.sort((a, b) => b.nbVisits - a.nbVisits)
173        return result
174    }
175
176    check() {
177        check(this.objectType, this.objectId)
178    }
179
180    visit() {
181        const op: BaseLocalOp = {
182            action: ActionTypes.Visit,
183            ts: DateUtils.now(),
184            objectId: this.objectId,
185            objectType: this.objectType
186        }
187        PopulateStatsStore([op])
188        PatchInstance.addPatch(op)
189    }
190
191    download() {
192        const op: BaseLocalOp = {
193            objectType: this.objectType,
194            objectId: this.objectId,
195            ts: DateUtils.now(),
196            action: ActionTypes.Download
197        }
198        PopulateStatsStore([op])
199        PatchInstance.addPatch(op)
200    }
201
202    get rating(): Rating {
203        this.check()
204        return StatsStore.get(this.objectType)?.get(this.objectId)?.rating as Rating;
205    }
206
207    set rating(rating: Rating) {
208        const op: RateOp = {
209            objectType: this.objectType,
210            objectId: this.objectId,
211            ts: DateUtils.now(),
212            action: ActionTypes.Rate,
213            rate: rating
214        }
215        PopulateStatsStore([op])
216        PatchInstance.addPatch(op)
217    }
218
219    share(socialMediaId: number) {
220        const op: ShareOp = {
221            objectType: this.objectType,
222            objectId: this.objectId,
223            ts: DateUtils.now(),
224            action: ActionTypes.Share,
225            socialMediaId
226        }
227        PopulateStatsStore([op])
228        PatchInstance.addPatch(op)
229    }
230
231    ratePart(partId: number, rating: Rating) {
232        const op: RatePartOp = {
233            objectType: this.objectType,
234            objectId: this.objectId,
235            ts: DateUtils.now(),
236            action: ActionTypes.RatePart,
237            part: partId,
238            rate: rating
239        }
240        PopulateStatsStore([op])
241        PatchInstance.addPatch(op)
242    }
243
244    visitPart(partId: number) {
245        const op: PartOp = {
246            objectType: this.objectType,
247            objectId: this.objectId,
248            ts: DateUtils.now(),
249            action: ActionTypes.VisitPart,
250            part: partId,
251        }
252        PopulateStatsStore([op])
253        PatchInstance.addPatch(op)
254    }
255}
256