@zodic/shared 0.0.181 → 0.0.183

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,4 +1,5 @@
1
1
  import { DurableObjectState } from '@cloudflare/workers-types';
2
+ import { ConceptProgress } from '../../types';
2
3
 
3
4
  export class ConceptNameDO {
4
5
  state: DurableObjectState;
@@ -6,6 +7,7 @@ export class ConceptNameDO {
6
7
  names: { 'en-us': string[]; 'pt-br': string[] };
7
8
  count: number;
8
9
  timestamps: number[];
10
+ lastIndex: number; // ✅ Stores last processed index for batch processing
9
11
 
10
12
  static TOTAL_NAMES = 1728; // Maximum total names to be generated
11
13
 
@@ -15,6 +17,7 @@ export class ConceptNameDO {
15
17
  this.names = { 'en-us': [], 'pt-br': [] };
16
18
  this.count = 0;
17
19
  this.timestamps = [];
20
+ this.lastIndex = 0; // ✅ Initialize lastIndex
18
21
  }
19
22
 
20
23
  async fetch(request: Request): Promise<Response> {
@@ -33,6 +36,12 @@ export class ConceptNameDO {
33
36
  });
34
37
  }
35
38
 
39
+ if (method === 'GET' && url.pathname === '/last-index') {
40
+ return new Response(JSON.stringify(await this.getLastIndex()), {
41
+ headers: { 'Content-Type': 'application/json' },
42
+ });
43
+ }
44
+
36
45
  if (method === 'POST' && url.pathname === '/add-name') {
37
46
  const { language, name } = (await request.json()) as {
38
47
  language: 'en-us' | 'pt-br';
@@ -47,6 +56,12 @@ export class ConceptNameDO {
47
56
  return new Response('OK', { status: 200 });
48
57
  }
49
58
 
59
+ if (method === 'POST' && url.pathname === '/set-progress') {
60
+ const { lastIndex } = (await request.json()) as { lastIndex: number };
61
+ await this.setLastIndex(lastIndex);
62
+ return new Response('OK', { status: 200 });
63
+ }
64
+
50
65
  return new Response('Not Found', { status: 404 });
51
66
  }
52
67
 
@@ -60,28 +75,14 @@ export class ConceptNameDO {
60
75
  return this.names;
61
76
  }
62
77
 
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;
72
- }
73
- if (!Array.isArray(this.timestamps) || this.timestamps.length === 0) {
74
- this.timestamps =
75
- (await this.state.storage.get<number[]>('timestamps')) || [];
76
- }
78
+ async getProgress(): Promise<ConceptProgress> {
79
+ await this.loadCount();
80
+ await this.loadTimestamps();
77
81
 
78
82
  const now = Date.now();
79
83
  const oneMinuteAgo = now - 60 * 1000;
80
84
 
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 recentTimestamps = this.timestamps.filter((ts) => ts >= oneMinuteAgo);
85
86
  const ratePerMinute = recentTimestamps.length;
86
87
 
87
88
  const remainingNames = ConceptNameDO.TOTAL_NAMES - this.count;
@@ -91,6 +92,7 @@ export class ConceptNameDO {
91
92
  return {
92
93
  count: this.count,
93
94
  total: ConceptNameDO.TOTAL_NAMES,
95
+ lastIndex: this.lastIndex,
94
96
  progress:
95
97
  ((this.count / ConceptNameDO.TOTAL_NAMES) * 100).toFixed(2) + '%',
96
98
  ratePerMinute,
@@ -98,20 +100,25 @@ export class ConceptNameDO {
98
100
  };
99
101
  }
100
102
 
103
+ async getLastIndex(): Promise<number> {
104
+ if (this.lastIndex === 0) {
105
+ this.lastIndex = (await this.state.storage.get<number>('lastIndex')) || 0;
106
+ }
107
+ return this.lastIndex;
108
+ }
109
+
101
110
  async addName(language: 'en-us' | 'pt-br', name: string): Promise<void> {
102
- await this.getNames(); // Ensure latest name list is loaded
103
- await this.loadCount(); // ✅ Load latest count before modifying
111
+ await this.getNames();
112
+ await this.loadCount();
104
113
 
105
114
  if (!this.names[language].includes(name)) {
106
115
  this.names[language].push(name);
107
- this.count += 1; // ✅ Increment count correctly
108
-
116
+ this.count += 1;
109
117
  this.recordTimestamp();
110
118
 
111
- // ✅ **Store all updated values in a single put() call**
112
119
  await this.state.storage.put({
113
120
  names: this.names,
114
- count: this.count, // ✅ Store updated count properly
121
+ count: this.count,
115
122
  timestamps: this.timestamps,
116
123
  });
117
124
 
@@ -121,27 +128,23 @@ export class ConceptNameDO {
121
128
  }
122
129
  }
123
130
 
131
+ async setLastIndex(lastIndex: number): Promise<void> {
132
+ this.lastIndex = lastIndex;
133
+ await this.state.storage.put('lastIndex', this.lastIndex);
134
+ }
135
+
124
136
  async loadCount(): Promise<void> {
125
- const storedCount = await this.state.storage.get<number>('count');
126
- if (typeof storedCount === 'number') {
127
- this.count = storedCount; // ✅ Ensure we always load the latest count
128
- } else {
129
- this.count = 0; // ✅ Ensure count is initialized properly
130
- }
137
+ this.count = (await this.state.storage.get<number>('count')) || 0;
138
+ }
139
+
140
+ async loadTimestamps(): Promise<void> {
141
+ this.timestamps =
142
+ (await this.state.storage.get<number[]>('timestamps')) || [];
131
143
  }
132
144
 
133
145
  recordTimestamp(): void {
134
146
  const now = Date.now();
135
-
136
- // ✅ Ensure timestamps is always an array before modifying
137
- if (!Array.isArray(this.timestamps)) {
138
- this.timestamps = [];
139
- }
140
-
141
- // ✅ Keep only the last 60 timestamps
142
- if (this.timestamps.length >= 60) {
143
- this.timestamps.splice(0, this.timestamps.length - 59);
144
- }
147
+ this.timestamps = this.timestamps.filter((ts) => now - ts < 60 * 60 * 1000);
145
148
  this.timestamps.push(now);
146
149
  }
147
150
  }
@@ -28,51 +28,51 @@ export class ConceptService {
28
28
  console.log(
29
29
  `🚀 Generating basic info for concept: ${conceptSlug}, combination: ${combinationString}, override: ${override}`
30
30
  );
31
-
31
+
32
32
  const kvStore = this.context.kvConceptsStore();
33
33
  const kvFailuresStore = this.context.kvConceptFailuresStore();
34
34
  const kvKeyEN = buildConceptKVKey('en-us', conceptSlug, combinationString);
35
35
  const kvKeyPT = buildConceptKVKey('pt-br', conceptSlug, combinationString);
36
36
  const failureKey = `failures:basic-info:${conceptSlug}:${combinationString}`;
37
-
37
+
38
38
  // ✅ Use Durable Object stub
39
39
  const id = this.context.env.CONCEPT_NAMES_DO.idFromName(conceptSlug);
40
40
  const stub = this.context.env.CONCEPT_NAMES_DO.get(id);
41
-
41
+
42
42
  let attempts = 0;
43
43
  const maxAttempts = 3;
44
-
44
+
45
45
  while (attempts < maxAttempts) {
46
46
  let phase = 'generation';
47
47
  try {
48
48
  attempts++;
49
- console.log(`🔄 Attempt ${attempts} to generate basic info...`);
50
-
49
+ console.log(`🔄 Attempt ${attempts} to generate basic info for ${conceptSlug}...`);
50
+
51
51
  // ✅ Fetch the latest name list from Durable Object
52
+ console.log(`📡 Fetching names from Durable Object for ${conceptSlug}...`);
52
53
  let allNamesEN: string[] = [];
53
54
  let allNamesPT: string[] = [];
54
- const response = await stub.fetch(
55
- `https://internal/names`
56
- );
55
+ const response = await stub.fetch(`https://internal/names`);
56
+
57
57
  if (response.ok) {
58
- const data = (await response.json()) as {
59
- 'en-us': string[];
60
- 'pt-br': string[];
61
- };
58
+ const data = (await response.json()) as { 'en-us': string[]; 'pt-br': string[] };
62
59
  allNamesEN = data['en-us'] || [];
63
60
  allNamesPT = data['pt-br'] || [];
61
+ console.log(`✅ Retrieved ${allNamesEN.length} EN names and ${allNamesPT.length} PT names.`);
64
62
  } else {
65
- console.log(`!-- Error fetching names from DO for ${conceptSlug}`);
66
- console.log('!-- Response:', await response.text());
63
+ console.log(`❌ Error fetching names from DO for ${conceptSlug}: ${await response.text()}`);
67
64
  }
68
-
65
+
69
66
  // ✅ Fetch existing KV data to backfill Durable Object if necessary
67
+ console.log(`📡 Fetching existing KV data for ${conceptSlug}...`);
70
68
  const existingEN = await this.getKVConcept(kvKeyEN);
71
69
  const existingPT = await this.getKVConcept(kvKeyPT);
72
-
73
- // 🔥 If names already exist in KV but not in DO, add them
70
+
71
+ console.log(`📝 Existing EN name: ${existingEN.name || 'None'}, PT name: ${existingPT.name || 'None'}`);
72
+
73
+ // 🔥 If names exist in KV but not in DO, add them
74
74
  if (existingEN.name && !allNamesEN.includes(existingEN.name)) {
75
- console.log(`⚡ Backfilling existing name to DO: ${existingEN.name}`);
75
+ console.log(`⚡ Backfilling existing EN name to DO: ${existingEN.name}`);
76
76
  await stub.fetch(`https://internal/add-name`, {
77
77
  method: 'POST',
78
78
  body: JSON.stringify({ language: 'en-us', name: existingEN.name }),
@@ -80,23 +80,22 @@ export class ConceptService {
80
80
  });
81
81
  }
82
82
  if (existingPT.name && !allNamesPT.includes(existingPT.name)) {
83
- console.log(`⚡ Backfilling existing name to DO: ${existingPT.name}`);
83
+ console.log(`⚡ Backfilling existing PT name to DO: ${existingPT.name}`);
84
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' },
88
88
  });
89
89
  }
90
-
90
+
91
91
  // ✅ Skip generation if basic info already exists and override is false
92
92
  if (!override && existingEN.name && existingPT.name) {
93
- console.log(
94
- `⚡ Basic info already exists for ${conceptSlug}, skipping.`
95
- );
93
+ console.log(`⚡ Basic info already exists for ${conceptSlug}, skipping.`);
96
94
  return;
97
95
  }
98
-
99
- const recentNamesEN = allNamesEN.slice(-144);
96
+
97
+ console.log(`✏️ Generating new name...`);
98
+ const recentNamesEN = allNamesEN.slice(-60);
100
99
  const messages = this.context
101
100
  .buildLLMMessages()
102
101
  .generateConceptBasicInfo({
@@ -104,43 +103,44 @@ export class ConceptService {
104
103
  conceptSlug,
105
104
  existingNames: recentNamesEN,
106
105
  });
107
-
108
- let aiResponse = await this.context
109
- .api()
110
- .callTogether.single(messages, {});
106
+
107
+ let aiResponse = await this.context.api().callTogether.single(messages, {});
111
108
  if (!aiResponse) throw new Error(`❌ AI returned an empty response`);
112
-
109
+
113
110
  phase = 'cleaning';
111
+ console.log(`🧼 Cleaning AI response...`);
114
112
  aiResponse = this.cleanAIResponse(aiResponse);
115
-
113
+
116
114
  phase = 'parsing';
115
+ console.log(`📜 Parsing AI response...`);
117
116
  let { nameEN, descriptionEN, poemEN, namePT, descriptionPT, poemPT } =
118
117
  this.parseBasicInfoResponse(aiResponse, conceptSlug);
119
-
118
+
119
+ console.log(`🎭 Generated names: EN - "${nameEN}", PT - "${namePT}"`);
120
+
120
121
  // ✅ Check uniqueness before storing
121
122
  if (allNamesEN.includes(nameEN) || allNamesPT.includes(namePT)) {
122
- console.warn(
123
- `⚠️ Duplicate Name Detected: "${nameEN}" or "${namePT}"`
124
- );
123
+ console.warn(`⚠️ Duplicate Name Detected: "${nameEN}" or "${namePT}"`);
125
124
  if (attempts >= maxAttempts)
126
125
  throw new Error(`🚨 Could not generate a unique name`);
127
126
  console.log('🔁 Retrying due to duplicate name...');
128
127
  continue;
129
128
  }
130
-
129
+
130
+ console.log(`📝 Storing names in Durable Object...`);
131
131
  // ✅ **Immediately update Durable Object with the new name**
132
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
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' },
142
142
  });
143
-
143
+
144
144
  // ✅ Store the generated basic info in KV
145
145
  Object.assign(existingEN, {
146
146
  name: nameEN,
@@ -149,7 +149,7 @@ export class ConceptService {
149
149
  status: 'idle',
150
150
  });
151
151
  await kvStore.put(kvKeyEN, JSON.stringify(existingEN));
152
-
152
+
153
153
  Object.assign(existingPT, {
154
154
  name: namePT,
155
155
  description: descriptionPT,
@@ -157,17 +157,12 @@ export class ConceptService {
157
157
  status: 'idle',
158
158
  });
159
159
  await kvStore.put(kvKeyPT, JSON.stringify(existingPT));
160
-
161
- console.log(
162
- `✅ Stored basic info for ${conceptSlug}, combination: ${combinationString}.`
163
- );
160
+
161
+ console.log(`✅ Stored basic info for ${conceptSlug}, combination: ${combinationString}.`);
164
162
  return;
165
163
  } catch (error) {
166
- console.error(
167
- `❌ Attempt ${attempts} failed at phase: ${phase}`,
168
- (error as Error).message
169
- );
170
-
164
+ console.error(`❌ Attempt ${attempts} failed at phase: ${phase}`, (error as Error).message);
165
+
171
166
  // ✅ Store failure details in KV for debugging
172
167
  await kvFailuresStore.put(
173
168
  failureKey,
@@ -180,12 +175,10 @@ export class ConceptService {
180
175
  timestamp: new Date().toISOString(),
181
176
  })
182
177
  );
183
-
178
+
184
179
  if (attempts >= maxAttempts) {
185
180
  console.error(`🚨 All ${maxAttempts} attempts failed.`);
186
- throw new Error(
187
- `Failed to generate basic info after ${maxAttempts} attempts.`
188
- );
181
+ throw new Error(`Failed to generate basic info after ${maxAttempts} attempts.`);
189
182
  }
190
183
  console.log('🔁 Retrying...');
191
184
  await new Promise((resolve) => setTimeout(resolve, 500));
@@ -1,12 +1,16 @@
1
1
  import { inject, injectable } from 'inversify';
2
2
  import pMap from 'p-map';
3
3
  import { zodiacSignCombinations } from '../../data/zodiacSignCombinations';
4
- import { Concept, ConceptPhase } from '../../types';
4
+ import { Concept, ConceptPhase, ConceptProgress } from '../../types';
5
+ import { AppContext } from '../base';
5
6
  import { ConceptService } from '../services/ConceptService';
6
7
 
7
8
  @injectable()
8
9
  export class ConceptWorkflow {
9
- constructor(@inject(ConceptService) private conceptService: ConceptService) {}
10
+ constructor(
11
+ @inject(AppContext) private context: AppContext,
12
+ @inject(ConceptService) private conceptService: ConceptService
13
+ ) {}
10
14
 
11
15
  async processSingle(
12
16
  conceptSlug: Concept,
@@ -66,34 +70,79 @@ export class ConceptWorkflow {
66
70
  conceptSlug: Concept,
67
71
  combinations: string[],
68
72
  phase: ConceptPhase,
69
- override?: boolean
73
+ override: boolean = false
70
74
  ) {
71
75
  console.log(
72
76
  `🚀 Processing batch for concept: ${conceptSlug}, Phase: ${phase}, Total Combinations: ${combinations.length}`
73
77
  );
74
78
 
75
- const concurrency = 20; // Adjust based on response speed
79
+ const concurrency = 20;
76
80
  const failedCombinations: string[] = [];
77
81
 
82
+ // ✅ Get Durable Object stub for progress tracking
83
+ const id = this.context.env.CONCEPT_NAMES_DO.idFromName(conceptSlug);
84
+ const stub = this.context.env.CONCEPT_NAMES_DO.get(id);
85
+
86
+ // ✅ Try to load last checkpoint from DO
87
+ let lastIndex = 0;
88
+ try {
89
+ const progressRes = await stub.fetch(`https://internal/progress`);
90
+ if (progressRes.ok) {
91
+ const progressData = (await progressRes.json()) as ConceptProgress;
92
+ lastIndex = progressData.lastIndex || 0;
93
+ console.log(`🔄 Resuming from index ${lastIndex}`);
94
+ } else {
95
+ console.log(`📌 No previous checkpoint found, starting from scratch.`);
96
+ }
97
+ } catch (error) {
98
+ console.error(`❌ Error fetching progress from DO:`, error);
99
+ }
100
+
101
+ // ✅ Filter unprocessed combinations
102
+ const remainingCombinations = combinations.slice(lastIndex);
103
+ console.log(
104
+ `⏳ Processing ${remainingCombinations.length} remaining combinations...`
105
+ );
106
+
78
107
  await pMap(
79
- combinations,
80
- async (combination) => {
108
+ remainingCombinations,
109
+ async (combination, index) => {
110
+ const currentIndex = lastIndex + index;
81
111
  try {
112
+ console.log(
113
+ `🛠️ Processing combination ${currentIndex}/${combinations.length}: ${combination}`
114
+ );
82
115
  await this.processSingle(conceptSlug, combination, phase, override);
116
+
117
+ // ✅ Save progress every 10 iterations in DO
118
+ if (currentIndex % 10 === 0) {
119
+ await stub.fetch(`https://internal/set-progress`, {
120
+ method: 'POST',
121
+ body: JSON.stringify({ lastIndex: currentIndex }),
122
+ headers: { 'Content-Type': 'application/json' },
123
+ });
124
+ console.log(`✅ Checkpoint saved at index ${currentIndex}`);
125
+ }
83
126
  } catch (error) {
84
127
  console.error(`❌ Error processing ${combination}:`, error);
85
- failedCombinations.push(combination); // Store failed ones
128
+ failedCombinations.push(combination);
86
129
  }
87
130
  },
88
131
  { concurrency }
89
132
  );
90
133
 
134
+ console.log(
135
+ `✅ Batch processing completed for ${conceptSlug}, Phase: ${phase}`
136
+ );
137
+
138
+ // ✅ Retry failed combinations after all batches
91
139
  if (failedCombinations.length > 0) {
92
140
  console.warn(`⚠️ Retrying failed combinations:`, failedCombinations);
93
- await this.processBatch(conceptSlug, failedCombinations, phase, override); // Retry
141
+ await this.processBatch(conceptSlug, failedCombinations, phase, override);
94
142
  }
143
+
95
144
  console.log(
96
- `✅ Batch processing completed for concept: ${conceptSlug}, Phase: ${phase}`
145
+ `🎉 All combinations successfully processed for ${conceptSlug}!`
97
146
  );
98
147
  }
99
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zodic/shared",
3
- "version": "0.0.181",
3
+ "version": "0.0.183",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -402,6 +402,7 @@ export type AstroKVData = {
402
402
  export type ConceptProgress = {
403
403
  count: number;
404
404
  total: number;
405
+ lastIndex: number;
405
406
  progress: string; // Percentage as string (e.g., "75.32%")
406
407
  ratePerMinute: number;
407
408
  estimatedTimeMinutes: number | 'N/A';