@zodic/shared 0.0.177 → 0.0.178

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.
@@ -1,45 +1,40 @@
1
- import { DurableObjectState } from "@cloudflare/workers-types";
2
- import { BackendBindings, ConceptProgress } from "../../types";
1
+ import { DurableObjectState } from '@cloudflare/workers-types';
2
+ import { BackendBindings, ConceptProgress } from '../../types';
3
3
 
4
4
  export class ConceptNameDO {
5
5
  state: DurableObjectState;
6
- env: any;
7
- names: Record<string, { 'en-us': string[]; 'pt-br': string[] }>;
8
- counts: Record<string, number>;
9
- timestamps: Record<string, number[]>;
6
+ env: BackendBindings;
7
+ names: { 'en-us': string[]; 'pt-br': string[] };
8
+ count: number;
9
+ timestamps: number[];
10
10
 
11
- static TOTAL_NAMES = 1728; // Max names per concept
11
+ static TOTAL_NAMES = 1728; // Maximum total names to be generated
12
12
 
13
13
  constructor(state: DurableObjectState, env: BackendBindings) {
14
14
  this.state = state;
15
15
  this.env = env;
16
- this.names = {};
17
- this.counts = {};
18
- this.timestamps = {};
16
+ this.names = { 'en-us': [], 'pt-br': [] };
17
+ this.count = 0;
18
+ this.timestamps = [];
19
19
  }
20
20
 
21
21
  async fetch(request: Request): Promise<Response> {
22
22
  const url = new URL(request.url);
23
23
  const method = request.method;
24
- const conceptSlug = url.pathname.split('/')[2]; // Extract conceptSlug from URL
25
24
 
26
- if (!conceptSlug) {
27
- return new Response('Concept slug required', { status: 400 });
28
- }
29
-
30
- if (method === 'GET' && url.pathname.endsWith('/names')) {
31
- return new Response(JSON.stringify(await this.getNames(conceptSlug)), {
25
+ if (method === 'GET' && url.pathname === '/names') {
26
+ return new Response(JSON.stringify(await this.getNames()), {
32
27
  headers: { 'Content-Type': 'application/json' },
33
28
  });
34
29
  }
35
30
 
36
- if (method === 'GET' && url.pathname.endsWith('/progress')) {
37
- return new Response(JSON.stringify(await this.calculateProgress(conceptSlug)), {
31
+ if (method === 'GET' && url.pathname === '/progress') {
32
+ return new Response(JSON.stringify(await this.getProgress()), {
38
33
  headers: { 'Content-Type': 'application/json' },
39
34
  });
40
35
  }
41
36
 
42
- if (method === 'POST' && url.pathname.endsWith('/add-name')) {
37
+ if (method === 'POST' && url.pathname === '/add-name') {
43
38
  const { language, name } = (await request.json()) as {
44
39
  language: 'en-us' | 'pt-br';
45
40
  name: string;
@@ -49,87 +44,76 @@ export class ConceptNameDO {
49
44
  return new Response('Invalid language', { status: 400 });
50
45
  }
51
46
 
52
- await this.addName(conceptSlug, language, name);
47
+ await this.addName(language, name);
53
48
  return new Response('OK', { status: 200 });
54
49
  }
55
50
 
56
51
  return new Response('Not Found', { status: 404 });
57
52
  }
58
53
 
59
- async getNames(conceptSlug: string): Promise<{ 'en-us': string[]; 'pt-br': string[] }> {
60
- if (!this.names[conceptSlug]) {
61
- this.names[conceptSlug] =
62
- (await this.state.storage.get<{ 'en-us': string[]; 'pt-br': string[] }>(`names:${conceptSlug}`)) || {
63
- 'en-us': [],
64
- 'pt-br': [],
65
- };
54
+ async getNames(): Promise<{ 'en-us': string[]; 'pt-br': string[] }> {
55
+ if (!this.names['en-us'].length && !this.names['pt-br'].length) {
56
+ this.names = (await this.state.storage.get('names')) || {
57
+ 'en-us': [],
58
+ 'pt-br': [],
59
+ };
66
60
  }
67
- return this.names[conceptSlug];
61
+ return this.names;
68
62
  }
69
63
 
70
- async getCount(conceptSlug: string): Promise<number> {
71
- if (this.counts[conceptSlug] === undefined) {
72
- this.counts = (await this.state.storage.get<Record<string, number>>('counts')) || {};
64
+ async getProgress(): Promise<ConceptProgress> {
65
+ if (this.count === 0) {
66
+ this.count = (await this.state.storage.get<number>('count')) || 0;
73
67
  }
74
- return this.counts[conceptSlug] || 0;
75
- }
76
-
77
- async getTimestamps(conceptSlug: string): Promise<number[]> {
78
- if (!this.timestamps[conceptSlug]) {
68
+ if (this.timestamps.length === 0) {
79
69
  this.timestamps =
80
- (await this.state.storage.get<Record<string, number[]>>('timestamps')) || {};
81
- }
82
- return this.timestamps[conceptSlug] || [];
83
- }
84
-
85
- async addName(conceptSlug: string, language: 'en-us' | 'pt-br', name: string): Promise<void> {
86
- const names = await this.getNames(conceptSlug);
87
- if (!names[language].includes(name)) {
88
- names[language].push(name);
89
- this.state.storage.put(`names:${conceptSlug}`, names); // ✅ No need to await
90
-
91
- this.incrementCount(conceptSlug);
92
- this.recordTimestamp(conceptSlug);
70
+ (await this.state.storage.get<number[]>('timestamps')) || [];
93
71
  }
94
- }
95
-
96
- async incrementCount(conceptSlug: string): Promise<void> {
97
- const count = (await this.getCount(conceptSlug)) + 1;
98
- this.counts[conceptSlug] = count;
99
- this.state.storage.put('counts', this.counts); // ✅ No need to await
100
- }
101
-
102
- async recordTimestamp(conceptSlug: string): Promise<void> {
103
- const now = Date.now();
104
- const timestamps = await this.getTimestamps(conceptSlug);
105
72
 
106
- // Keep only last 60 minutes
107
- const filteredTimestamps = timestamps.filter((ts) => now - ts < 60 * 60 * 1000);
108
- filteredTimestamps.push(now);
109
-
110
- this.timestamps[conceptSlug] = filteredTimestamps;
111
- this.state.storage.put('timestamps', this.timestamps); // ✅ No need to await
112
- }
113
-
114
- async calculateProgress(conceptSlug: string): Promise<ConceptProgress> {
115
73
  const now = Date.now();
116
74
  const oneMinuteAgo = now - 60 * 1000;
117
75
 
118
- const count = await this.getCount(conceptSlug);
119
- const timestamps = await this.getTimestamps(conceptSlug);
120
- const recentTimestamps = timestamps.filter((ts) => ts >= oneMinuteAgo);
76
+ const recentTimestamps = this.timestamps.filter((ts) => ts >= oneMinuteAgo);
121
77
  const ratePerMinute = recentTimestamps.length;
122
78
 
123
- const remainingNames = ConceptNameDO.TOTAL_NAMES - count;
124
- const estimatedTimeMinutes = ratePerMinute > 0 ? remainingNames / ratePerMinute : null;
79
+ const remainingNames = ConceptNameDO.TOTAL_NAMES - this.count;
80
+ const estimatedTimeMinutes =
81
+ ratePerMinute > 0 ? Math.round(remainingNames / ratePerMinute) : 'N/A';
125
82
 
126
83
  return {
127
- concept: conceptSlug,
128
- count,
84
+ count: this.count,
129
85
  total: ConceptNameDO.TOTAL_NAMES,
130
- progress: ((count / ConceptNameDO.TOTAL_NAMES) * 100).toFixed(2) + '%',
86
+ progress:
87
+ ((this.count / ConceptNameDO.TOTAL_NAMES) * 100).toFixed(2) + '%',
131
88
  ratePerMinute,
132
- estimatedTimeMinutes: estimatedTimeMinutes ? Math.round(estimatedTimeMinutes) : 'N/A',
89
+ estimatedTimeMinutes,
133
90
  };
134
91
  }
135
- }
92
+
93
+ async addName(language: 'en-us' | 'pt-br', name: string): Promise<void> {
94
+ await this.getNames(); // Ensure latest state
95
+
96
+ if (!this.names[language].includes(name)) {
97
+ this.names[language].push(name);
98
+ this.count++;
99
+ this.recordTimestamp();
100
+
101
+ // ✅ **Batch storage writes** instead of multiple separate `put()` calls
102
+ await this.state.storage.put({
103
+ names: this.names,
104
+ count: this.count,
105
+ timestamps: this.timestamps,
106
+ });
107
+ }
108
+ }
109
+
110
+ recordTimestamp(): void {
111
+ const now = Date.now();
112
+
113
+ // Keep only the last 60 timestamps
114
+ if (this.timestamps.length >= 60) {
115
+ this.timestamps.splice(0, this.timestamps.length - 59);
116
+ }
117
+ this.timestamps.push(now);
118
+ }
119
+ }
@@ -52,7 +52,7 @@ export class ConceptService {
52
52
  let allNamesEN: string[] = [];
53
53
  let allNamesPT: string[] = [];
54
54
  const response = await stub.fetch(
55
- `https://internal/${conceptSlug}/names`
55
+ `https://internal/names`
56
56
  );
57
57
  if (response.ok) {
58
58
  const data = (await response.json()) as {
@@ -73,7 +73,7 @@ export class ConceptService {
73
73
  // 🔥 If names already exist in KV but not in DO, add them
74
74
  if (existingEN.name && !allNamesEN.includes(existingEN.name)) {
75
75
  console.log(`⚡ Backfilling existing name to DO: ${existingEN.name}`);
76
- await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
76
+ await stub.fetch(`https://internal/add-name`, {
77
77
  method: 'POST',
78
78
  body: JSON.stringify({ language: 'en-us', name: existingEN.name }),
79
79
  headers: { 'Content-Type': 'application/json' },
@@ -81,7 +81,7 @@ export class ConceptService {
81
81
  }
82
82
  if (existingPT.name && !allNamesPT.includes(existingPT.name)) {
83
83
  console.log(`⚡ Backfilling existing name to DO: ${existingPT.name}`);
84
- await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
84
+ await stub.fetch(`https://internal/add-name`, {
85
85
  method: 'POST',
86
86
  body: JSON.stringify({ language: 'pt-br', name: existingPT.name }),
87
87
  headers: { 'Content-Type': 'application/json' },
@@ -129,13 +129,13 @@ export class ConceptService {
129
129
  }
130
130
 
131
131
  // ✅ **Immediately update Durable Object with the new name**
132
- await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
132
+ await stub.fetch(`https://internal/add-name`, {
133
133
  method: 'POST',
134
134
  body: JSON.stringify({ language: 'en-us', name: nameEN }),
135
135
  headers: { 'Content-Type': 'application/json' },
136
136
  });
137
137
 
138
- await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
138
+ await stub.fetch(`https://internal/add-name`, {
139
139
  method: 'POST',
140
140
  body: JSON.stringify({ language: 'pt-br', name: namePT }),
141
141
  headers: { 'Content-Type': 'application/json' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zodic/shared",
3
- "version": "0.0.177",
3
+ "version": "0.0.178",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -400,10 +400,9 @@ export type AstroKVData = {
400
400
  };
401
401
 
402
402
  export type ConceptProgress = {
403
- concept: string;
404
403
  count: number;
405
404
  total: number;
406
405
  progress: string; // Percentage as string (e.g., "75.32%")
407
406
  ratePerMinute: number;
408
407
  estimatedTimeMinutes: number | 'N/A';
409
- };
408
+ };