📄 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