@zodic/shared 0.0.177 → 0.0.179

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,39 @@
1
- import { DurableObjectState } from "@cloudflare/workers-types";
2
- import { BackendBindings, ConceptProgress } from "../../types";
1
+ import { DurableObjectState } from '@cloudflare/workers-types';
3
2
 
4
3
  export class ConceptNameDO {
5
4
  state: DurableObjectState;
6
5
  env: any;
7
- names: Record<string, { 'en-us': string[]; 'pt-br': string[] }>;
8
- counts: Record<string, number>;
9
- timestamps: Record<string, number[]>;
6
+ names: { 'en-us': string[]; 'pt-br': string[] };
7
+ count: number;
8
+ timestamps: number[];
10
9
 
11
- static TOTAL_NAMES = 1728; // Max names per concept
10
+ static TOTAL_NAMES = 1728; // Maximum total names to be generated
12
11
 
13
- constructor(state: DurableObjectState, env: BackendBindings) {
12
+ constructor(state: DurableObjectState, env: any) {
14
13
  this.state = state;
15
14
  this.env = env;
16
- this.names = {};
17
- this.counts = {};
18
- this.timestamps = {};
15
+ this.names = { 'en-us': [], 'pt-br': [] };
16
+ this.count = 0;
17
+ this.timestamps = [];
19
18
  }
20
19
 
21
20
  async fetch(request: Request): Promise<Response> {
22
21
  const url = new URL(request.url);
23
22
  const method = request.method;
24
- const conceptSlug = url.pathname.split('/')[2]; // Extract conceptSlug from URL
25
23
 
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)), {
24
+ if (method === 'GET' && url.pathname === '/names') {
25
+ return new Response(JSON.stringify(await this.getNames()), {
32
26
  headers: { 'Content-Type': 'application/json' },
33
27
  });
34
28
  }
35
29
 
36
- if (method === 'GET' && url.pathname.endsWith('/progress')) {
37
- return new Response(JSON.stringify(await this.calculateProgress(conceptSlug)), {
30
+ if (method === 'GET' && url.pathname === '/progress') {
31
+ return new Response(JSON.stringify(await this.getProgress()), {
38
32
  headers: { 'Content-Type': 'application/json' },
39
33
  });
40
34
  }
41
35
 
42
- if (method === 'POST' && url.pathname.endsWith('/add-name')) {
36
+ if (method === 'POST' && url.pathname === '/add-name') {
43
37
  const { language, name } = (await request.json()) as {
44
38
  language: 'en-us' | 'pt-br';
45
39
  name: string;
@@ -49,87 +43,90 @@ export class ConceptNameDO {
49
43
  return new Response('Invalid language', { status: 400 });
50
44
  }
51
45
 
52
- await this.addName(conceptSlug, language, name);
46
+ await this.addName(language, name);
53
47
  return new Response('OK', { status: 200 });
54
48
  }
55
49
 
56
50
  return new Response('Not Found', { status: 404 });
57
51
  }
58
52
 
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
- };
53
+ async getNames(): Promise<{ 'en-us': string[]; 'pt-br': string[] }> {
54
+ if (!this.names['en-us'].length && !this.names['pt-br'].length) {
55
+ this.names = (await this.state.storage.get('names')) || {
56
+ 'en-us': [],
57
+ 'pt-br': [],
58
+ };
66
59
  }
67
- return this.names[conceptSlug];
60
+ return this.names;
68
61
  }
69
62
 
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')) || {};
63
+ async getProgress(): Promise<{
64
+ count: number;
65
+ total: number;
66
+ progress: string;
67
+ ratePerMinute: number;
68
+ estimatedTimeMinutes: number | 'N/A';
69
+ }> {
70
+ if (this.count === 0) {
71
+ this.count = (await this.state.storage.get<number>('count')) || 0;
73
72
  }
74
- return this.counts[conceptSlug] || 0;
75
- }
76
-
77
- async getTimestamps(conceptSlug: string): Promise<number[]> {
78
- if (!this.timestamps[conceptSlug]) {
73
+ if (!Array.isArray(this.timestamps) || this.timestamps.length === 0) {
79
74
  this.timestamps =
80
- (await this.state.storage.get<Record<string, number[]>>('timestamps')) || {};
75
+ (await this.state.storage.get<number[]>('timestamps')) || [];
81
76
  }
82
- return this.timestamps[conceptSlug] || [];
83
- }
84
77
 
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
78
+ const now = Date.now();
79
+ const oneMinuteAgo = now - 60 * 1000;
90
80
 
91
- this.incrementCount(conceptSlug);
92
- this.recordTimestamp(conceptSlug);
93
- }
94
- }
81
+ const recentTimestamps = Array.isArray(this.timestamps)
82
+ ? this.timestamps.filter((ts) => ts >= oneMinuteAgo)
83
+ : []; // ✅ Ensure it's an array before calling filter
84
+
85
+ const ratePerMinute = recentTimestamps.length;
95
86
 
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
87
+ const remainingNames = ConceptNameDO.TOTAL_NAMES - this.count;
88
+ const estimatedTimeMinutes =
89
+ ratePerMinute > 0 ? Math.round(remainingNames / ratePerMinute) : 'N/A';
90
+
91
+ return {
92
+ count: this.count,
93
+ total: ConceptNameDO.TOTAL_NAMES,
94
+ progress:
95
+ ((this.count / ConceptNameDO.TOTAL_NAMES) * 100).toFixed(2) + '%',
96
+ ratePerMinute,
97
+ estimatedTimeMinutes,
98
+ };
100
99
  }
101
100
 
102
- async recordTimestamp(conceptSlug: string): Promise<void> {
103
- const now = Date.now();
104
- const timestamps = await this.getTimestamps(conceptSlug);
101
+ async addName(language: 'en-us' | 'pt-br', name: string): Promise<void> {
102
+ await this.getNames(); // Ensure latest state
105
103
 
106
- // Keep only last 60 minutes
107
- const filteredTimestamps = timestamps.filter((ts) => now - ts < 60 * 60 * 1000);
108
- filteredTimestamps.push(now);
104
+ if (!this.names[language].includes(name)) {
105
+ this.names[language].push(name);
106
+ this.count++;
107
+ this.recordTimestamp();
109
108
 
110
- this.timestamps[conceptSlug] = filteredTimestamps;
111
- this.state.storage.put('timestamps', this.timestamps); // ✅ No need to await
109
+ // **Batch storage writes** instead of multiple separate `put()` calls
110
+ await this.state.storage.put({
111
+ names: this.names,
112
+ count: this.count,
113
+ timestamps: this.timestamps,
114
+ });
115
+ }
112
116
  }
113
117
 
114
- async calculateProgress(conceptSlug: string): Promise<ConceptProgress> {
118
+ recordTimestamp(): void {
115
119
  const now = Date.now();
116
- const oneMinuteAgo = now - 60 * 1000;
117
-
118
- const count = await this.getCount(conceptSlug);
119
- const timestamps = await this.getTimestamps(conceptSlug);
120
- const recentTimestamps = timestamps.filter((ts) => ts >= oneMinuteAgo);
121
- const ratePerMinute = recentTimestamps.length;
122
120
 
123
- const remainingNames = ConceptNameDO.TOTAL_NAMES - count;
124
- const estimatedTimeMinutes = ratePerMinute > 0 ? remainingNames / ratePerMinute : null;
121
+ // Ensure timestamps is always an array before modifying
122
+ if (!Array.isArray(this.timestamps)) {
123
+ this.timestamps = [];
124
+ }
125
125
 
126
- return {
127
- concept: conceptSlug,
128
- count,
129
- total: ConceptNameDO.TOTAL_NAMES,
130
- progress: ((count / ConceptNameDO.TOTAL_NAMES) * 100).toFixed(2) + '%',
131
- ratePerMinute,
132
- estimatedTimeMinutes: estimatedTimeMinutes ? Math.round(estimatedTimeMinutes) : 'N/A',
133
- };
126
+ // ✅ Keep only the last 60 timestamps
127
+ if (this.timestamps.length >= 60) {
128
+ this.timestamps.splice(0, this.timestamps.length - 59);
129
+ }
130
+ this.timestamps.push(now);
134
131
  }
135
- }
132
+ }
@@ -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.179",
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
+ };