@zodic/shared 0.0.175 → 0.0.177

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,69 +1,135 @@
1
- import { DurableObjectState } from '@cloudflare/workers-types';
1
+ import { DurableObjectState } from "@cloudflare/workers-types";
2
+ import { BackendBindings, ConceptProgress } from "../../types";
2
3
 
3
- export class ConceptNameDO extends DurableObject {
4
+ export class ConceptNameDO {
4
5
  state: DurableObjectState;
5
6
  env: any;
6
- names: { 'en-us': string[]; 'pt-br': string[] };
7
+ names: Record<string, { 'en-us': string[]; 'pt-br': string[] }>;
8
+ counts: Record<string, number>;
9
+ timestamps: Record<string, number[]>;
7
10
 
8
- constructor(state: DurableObjectState, env: any) {
9
- super(state, env); // ✅ Call super() for DurableObject
11
+ static TOTAL_NAMES = 1728; // Max names per concept
12
+
13
+ constructor(state: DurableObjectState, env: BackendBindings) {
10
14
  this.state = state;
11
15
  this.env = env;
12
- this.names = { 'en-us': [], 'pt-br': [] };
16
+ this.names = {};
17
+ this.counts = {};
18
+ this.timestamps = {};
13
19
  }
14
20
 
15
21
  async fetch(request: Request): Promise<Response> {
16
22
  const url = new URL(request.url);
17
23
  const method = request.method;
24
+ const conceptSlug = url.pathname.split('/')[2]; // Extract conceptSlug from URL
25
+
26
+ if (!conceptSlug) {
27
+ return new Response('Concept slug required', { status: 400 });
28
+ }
18
29
 
19
- if (method === 'GET' && url.pathname === '/names') {
20
- await this.loadNames();
21
- return new Response(JSON.stringify(this.names), {
30
+ if (method === 'GET' && url.pathname.endsWith('/names')) {
31
+ return new Response(JSON.stringify(await this.getNames(conceptSlug)), {
22
32
  headers: { 'Content-Type': 'application/json' },
23
33
  });
24
34
  }
25
35
 
26
- if (method === 'POST' && url.pathname === '/add-name') {
27
- try {
28
- const { language, name } = (await request.json()) as {
29
- language: 'en-us' | 'pt-br';
30
- name: string;
31
- };
36
+ if (method === 'GET' && url.pathname.endsWith('/progress')) {
37
+ return new Response(JSON.stringify(await this.calculateProgress(conceptSlug)), {
38
+ headers: { 'Content-Type': 'application/json' },
39
+ });
40
+ }
32
41
 
33
- if (!['en-us', 'pt-br'].includes(language)) {
34
- return new Response('Invalid language', { status: 400 });
35
- }
42
+ if (method === 'POST' && url.pathname.endsWith('/add-name')) {
43
+ const { language, name } = (await request.json()) as {
44
+ language: 'en-us' | 'pt-br';
45
+ name: string;
46
+ };
36
47
 
37
- await this.addName(language, name);
38
- return new Response('OK', { status: 200 });
39
- } catch (error) {
40
- console.error('❌ Error processing request:', error);
41
- return new Response('Bad Request', { status: 400 });
48
+ if (!['en-us', 'pt-br'].includes(language)) {
49
+ return new Response('Invalid language', { status: 400 });
42
50
  }
51
+
52
+ await this.addName(conceptSlug, language, name);
53
+ return new Response('OK', { status: 200 });
43
54
  }
44
55
 
45
56
  return new Response('Not Found', { status: 404 });
46
57
  }
47
58
 
48
- async loadNames(): Promise<void> {
49
- const stored = await this.state.storage.get<{
50
- 'en-us': string[];
51
- 'pt-br': string[];
52
- }>('names');
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
+ };
66
+ }
67
+ return this.names[conceptSlug];
68
+ }
53
69
 
54
- if (stored) {
55
- this.names = stored;
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')) || {};
56
73
  }
74
+ return this.counts[conceptSlug] || 0;
57
75
  }
58
76
 
59
- async addName(language: 'en-us' | 'pt-br', name: string): Promise<void> {
60
- await this.loadNames(); // Ensure we have the latest names
77
+ async getTimestamps(conceptSlug: string): Promise<number[]> {
78
+ if (!this.timestamps[conceptSlug]) {
79
+ this.timestamps =
80
+ (await this.state.storage.get<Record<string, number[]>>('timestamps')) || {};
81
+ }
82
+ return this.timestamps[conceptSlug] || [];
83
+ }
61
84
 
62
- if (!this.names[language].includes(name)) {
63
- this.names[language].push(name);
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
64
90
 
65
- // ✅ Ensure concurrent writes do not overwrite existing stored data
66
- await this.state.storage.put('names', this.names);
91
+ this.incrementCount(conceptSlug);
92
+ this.recordTimestamp(conceptSlug);
67
93
  }
68
94
  }
69
- }
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
+
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
+ 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
+
123
+ const remainingNames = ConceptNameDO.TOTAL_NAMES - count;
124
+ const estimatedTimeMinutes = ratePerMinute > 0 ? remainingNames / ratePerMinute : null;
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
+ };
134
+ }
135
+ }
@@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm';
2
2
  import { inject, injectable } from 'inversify';
3
3
  import 'reflect-metadata';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
- import { ConceptNameDO, schema } from '../..';
5
+ import { schema } from '../..';
6
6
  import { Concept, ControlNetConfig, Languages } from '../../types';
7
7
  import {
8
8
  KVConcept,
@@ -51,7 +51,9 @@ export class ConceptService {
51
51
  // ✅ Fetch the latest name list from Durable Object
52
52
  let allNamesEN: string[] = [];
53
53
  let allNamesPT: string[] = [];
54
- const response = await stub.fetch('https://internal/names');
54
+ const response = await stub.fetch(
55
+ `https://internal/${conceptSlug}/names`
56
+ );
55
57
  if (response.ok) {
56
58
  const data = (await response.json()) as {
57
59
  'en-us': string[];
@@ -60,7 +62,7 @@ export class ConceptService {
60
62
  allNamesEN = data['en-us'] || [];
61
63
  allNamesPT = data['pt-br'] || [];
62
64
  } else {
63
- console.log('!-- Error fetching names from Durable Object');
65
+ console.log(`!-- Error fetching names from DO for ${conceptSlug}`);
64
66
  console.log('!-- Response:', await response.text());
65
67
  }
66
68
 
@@ -71,7 +73,7 @@ export class ConceptService {
71
73
  // 🔥 If names already exist in KV but not in DO, add them
72
74
  if (existingEN.name && !allNamesEN.includes(existingEN.name)) {
73
75
  console.log(`⚡ Backfilling existing name to DO: ${existingEN.name}`);
74
- await stub.fetch('https://internal/add-name', {
76
+ await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
75
77
  method: 'POST',
76
78
  body: JSON.stringify({ language: 'en-us', name: existingEN.name }),
77
79
  headers: { 'Content-Type': 'application/json' },
@@ -79,7 +81,7 @@ export class ConceptService {
79
81
  }
80
82
  if (existingPT.name && !allNamesPT.includes(existingPT.name)) {
81
83
  console.log(`⚡ Backfilling existing name to DO: ${existingPT.name}`);
82
- await stub.fetch('https://internal/add-name', {
84
+ await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
83
85
  method: 'POST',
84
86
  body: JSON.stringify({ language: 'pt-br', name: existingPT.name }),
85
87
  headers: { 'Content-Type': 'application/json' },
@@ -127,13 +129,13 @@ export class ConceptService {
127
129
  }
128
130
 
129
131
  // ✅ **Immediately update Durable Object with the new name**
130
- await stub.fetch('https://internal/add-name', {
132
+ await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
131
133
  method: 'POST',
132
134
  body: JSON.stringify({ language: 'en-us', name: nameEN }),
133
135
  headers: { 'Content-Type': 'application/json' },
134
136
  });
135
137
 
136
- await stub.fetch('https://internal/add-name', {
138
+ await stub.fetch(`https://internal/${conceptSlug}/add-name`, {
137
139
  method: 'POST',
138
140
  body: JSON.stringify({ language: 'pt-br', name: namePT }),
139
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.175",
3
+ "version": "0.0.177",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -26,7 +26,7 @@ export type CentralBindings = {
26
26
  KV_CONCEPT_FAILURES: KVNamespace;
27
27
  KV_CONCEPT_NAMES: KVNamespace;
28
28
  CONCEPT_GENERATION_QUEUE: Queue;
29
- CONCEPT_NAMES_DO: DurableObjectNamespace<ConceptNameDO>;
29
+ CONCEPT_NAMES_DO: DurableObjectNamespace;
30
30
 
31
31
  PROMPT_IMAGE_DESCRIBER: string;
32
32
  PROMPT_GENERATE_ARCHETYPE_BASIC_INFO: string;
@@ -398,3 +398,12 @@ export type AstroKVData = {
398
398
  diff: number;
399
399
  }[];
400
400
  };
401
+
402
+ export type ConceptProgress = {
403
+ concept: string;
404
+ count: number;
405
+ total: number;
406
+ progress: string; // Percentage as string (e.g., "75.32%")
407
+ ratePerMinute: number;
408
+ estimatedTimeMinutes: number | 'N/A';
409
+ };