@cocreate/usage 1.3.0 → 1.4.0

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +1 -1
  3. package/src/index.js +251 -207
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # [1.4.0](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.3.0...v1.4.0) (2026-07-18)
2
+
3
+
4
+ ### Features
5
+
6
+ * enhance event listener handling and improve logging for organization deletion ([cca2e40](https://github.com/CoCreate-app/CoCreate-usage/commit/cca2e408ba2c6599aab2b56430b1d74b923fbb57))
7
+ * refactor usage tracking with new bandwidth management and event listeners ([76f8ddb](https://github.com/CoCreate-app/CoCreate-usage/commit/76f8ddb539ad6a3384ed5d1557b1e36b9d64222a))
8
+
1
9
  # [1.3.0](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.2.2...v1.3.0) (2026-07-17)
2
10
 
3
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/usage",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CoCreate-usage",
5
5
  "keywords": [
6
6
  "cocreate-usage",
package/src/index.js CHANGED
@@ -13,232 +13,276 @@
13
13
  *
14
14
  * You should have received a copy of the GNU Affero General Public License
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
- *
17
16
  ********************************************************************************/
18
17
 
19
- // Commercial Licensing Information:
20
- // For commercial use of this software without the copyleft provisions of the AGPLv3,
21
- // you must obtain a commercial license from CoCreate LLC.
22
- // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
-
24
- import server from '@cocreate/server';
25
-
26
- // ==========================================
27
- // Module-Level Sandbox State (ESM Singleton Container)
28
- // ==========================================
29
- const organizations = new Map();
30
-
31
- /**
32
- * Normalizes input data types and calculates payload sizing in bytes.
33
- */
34
- function getBytes(data) {
35
- if (typeof data === 'number')
36
- return data;
37
- else if (data instanceof Buffer)
38
- return data.byteLength || 0;
39
- else if (data instanceof String || typeof data === 'string')
40
- return Buffer.byteLength(data, 'utf8') || 0;
41
- else if (typeof data === 'object' && data !== null)
42
- return Buffer.byteLength(JSON.stringify(data), 'utf8') || 0;
43
- else return 0;
44
- }
18
+ import crud from '@cocreate/crud-server';
45
19
 
46
- /**
47
- * Determines pricing tier rates based on aggregate historical usage indicators.
48
- */
49
- function getRate(totalUsage = 0) {
50
- const tiers = [
51
- { limit: 1, rate: 10 },
52
- { limit: 10, rate: 5 },
53
- { limit: 100, rate: 2.5 },
54
- { limit: 1000, rate: 1.125 },
55
- { limit: 10000, rate: 0.562 },
56
- { limit: 100000, rate: 0.281 },
57
- { limit: 1000000, rate: 0.145 }
58
- ];
59
-
60
- const matchingTier = tiers.find(tier => totalUsage < tier.limit);
61
- return matchingTier ? matchingTier.rate : 0.12;
62
- }
20
+ class CoCreateUsage {
21
+ constructor(platformOrgId) {
22
+ this.platformOrgId = platformOrgId; // Master platform organization ID
63
23
 
64
- /**
65
- * Calculates and writes final bandwidth depletion transactions to the platform database.
66
- */
67
- async function sendUsageUpdate(org, organization_id, host) {
68
- if (!server || !server.crud) return;
69
- delete org.debounce;
70
-
71
- org.dataTransferedOut += 250;
72
- org.dataTransferedOutCount++;
73
-
74
- const platformOrganization = server.organization_id;
75
- let organization = await server.crud.send({
76
- method: 'object.read',
77
- // host: server.host,
78
- array: 'organizations',
79
- object: { _id: organization_id },
80
- organization_id: platformOrganization
81
- });
82
-
83
- org.dataTransferedIn += getBytes(platformOrganization);
84
- org.dataTransferedInCount++;
85
-
86
- if (organization && organization.object && organization.object[0]) {
87
- organization = organization.object[0];
88
- } else return;
89
-
90
- if (server.wsManager && server.wsManager.organizations.has(organization_id)) {
91
- const socketOrg = server.wsManager.organizations.get(organization_id);
92
- if (organization.balance <= 0) {
93
- socketOrg.status = false;
94
- socketOrg.organizationBalance = false;
95
- socketOrg.error = 'Your balance has fallen below 0';
96
- } else {
97
- socketOrg.status = true;
98
- socketOrg.organizationBalance = true;
99
- socketOrg.error = '';
100
- }
24
+ // Constants
25
+ this.FLUSH_THRESHOLD_BYTES = 10 * 1024 * 1024; // 10 Megabytes
26
+ this.TENANT_INTERVAL_MS = 60 * 1000; // 1 Minute
27
+
28
+ // Double-buffer map tracking separate platform and tenant blocks per organization
29
+ this.bandwidthMap = new Map();
30
+
31
+ // Initialize the Event Listeners
32
+ this.initEventListeners();
101
33
  }
102
34
 
103
- let timeStamp = new Date(new Date().toISOString());
104
- let isExpired = false;
105
- if (organization.lastDeposit) {
106
- let lastDeposit = new Date(organization.lastDeposit);
107
- isExpired = lastDeposit <= timeStamp.setFullYear(timeStamp.getFullYear() - 1);
35
+ /**
36
+ * Connects directly to process event signals.
37
+ */
38
+ initEventListeners() {
39
+ // Unified Application-Wide System Interceptor
40
+ // Expects an input object: { organization_id, data, type: 'ingress' | 'egress' }
41
+ process.on('usage', (payload) => {
42
+ if (!payload || typeof payload !== 'object') return;
43
+
44
+ const { organization_id, data, type } = payload;
45
+ if (organization_id) {
46
+ this.trackUsage(organization_id, data, type || 'ingress');
47
+ }
48
+ });
49
+
50
+ // Dynamic Eviction Loop: Caught via native process messaging.
51
+ // Triggers clean flushes and timer de-allocations during standard disconnects or orchestrator shutdowns.
52
+ process.on('orgDeleted', async (organization_id) => {
53
+ if (this.bandwidthMap.has(organization_id)) {
54
+ console.log(`[USAGE] Event Received: Flushing pending data pipelines for Org: ${organization_id}`);
55
+ try {
56
+ await this.flushAndClearSingle(organization_id);
57
+ console.log(`[USAGE] Flushing Completed: Memory registry successfully cleared for Org: ${organization_id}`);
58
+ } catch (err) {
59
+ console.error(`[USAGE ERROR] Failed to cleanly flush data during orgDeleted event for ${organization_id}:`, err);
60
+ }
61
+ }
62
+ });
108
63
  }
109
64
 
110
- let isResetDataTransfer = false;
111
- if (organization.modified && organization.modified.on) {
112
- let previousTimeStamp = new Date(organization.modified.on);
113
- if (previousTimeStamp.getMonth() !== timeStamp.getMonth()) {
114
- isResetDataTransfer = true;
115
- server.crud.send({
116
- method: 'object.create',
117
- // host: server.host,
118
- array: 'transactions',
119
- object: {
120
- organization_id,
121
- type: "withdrawal",
122
- dataTransfered: organization.dataTransfered,
123
- previousTimeStamp
65
+ /**
66
+ * High-performance, robust byte-calculation routine.
67
+ * Accurately parses binary files (images, video chunks), text responses, and data objects.
68
+ */
69
+ calculatePayloadBytes(data) {
70
+ if (data === null || data === undefined) return 0;
71
+
72
+ try {
73
+ // Case 1: Raw media streams, files, or binary chunks (Buffers / TypedArrays)
74
+ if (Buffer.isBuffer(data)) {
75
+ return data.length;
76
+ }
77
+ if (data instanceof Uint8Array || data.buffer instanceof ArrayBuffer) {
78
+ return data.byteLength || data.length || 0;
79
+ }
80
+
81
+ // Case 2: Plain Text / Base64 Encoded Strings
82
+ if (typeof data === 'string') {
83
+ return Buffer.byteLength(data, 'utf8');
84
+ }
85
+
86
+ // Case 3: Native JavaScript Objects & Arrays (Database schemas, API JSON packets)
87
+ if (typeof data === 'object') {
88
+ return Buffer.byteLength(JSON.stringify(data), 'utf8');
89
+ }
90
+
91
+ // Case 4: Fallback for primitive datatypes (numbers, booleans, etc.)
92
+ return Buffer.byteLength(String(data), 'utf8');
93
+ } catch (error) {
94
+ console.error('[USAGE ERROR] Failed to safely calculate payload byte scale:', error);
95
+ return 0;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Main entry point to track incoming traffic.
101
+ */
102
+ trackUsage(organization_id, data, direction = 'ingress') {
103
+ const now = Date.now();
104
+ const incomingBytes = this.calculatePayloadBytes(data);
105
+
106
+ // If this is a brand new organization session, initialize our distinct tracking layers
107
+ if (!this.bandwidthMap.has(organization_id)) {
108
+ const tenantTimer = setInterval(() => {
109
+ this.flushTenantWindow(organization_id);
110
+ }, this.TENANT_INTERVAL_MS);
111
+
112
+ this.bandwidthMap.set(organization_id, {
113
+ // Buffer A: Tenant 1-minute accumulator (system_usage)
114
+ tenant: {
115
+ ingressBytes: 0,
116
+ egressBytes: 0,
117
+ totalBytes: 0,
118
+ count: 0,
119
+ sampleStart: now,
120
+ sampleEnd: now
121
+ },
122
+ // Buffer B: Platform master accumulator (system_bandwidth)
123
+ platform: {
124
+ ingressBytes: 0,
125
+ egressBytes: 0,
126
+ totalBytes: 0,
127
+ count: 0,
128
+ sampleStart: now,
129
+ sampleEnd: now
124
130
  },
125
- organization_id: platformOrganization,
126
- timeStamp
131
+ tenantTimer
127
132
  });
128
- organization.dataTransfered = 0;
129
133
  }
130
- }
131
134
 
132
- let dataTransfered = org.dataTransferedIn + org.dataTransferedOut;
133
- dataTransfered = dataTransfered / 1073741824;
134
- dataTransfered = dataTransfered.toFixed(32);
135
- dataTransfered = parseFloat(dataTransfered);
136
-
137
- let rate = getRate(organization.dataTransfered);
138
- let amount = dataTransfered * rate;
139
- amount = -amount;
140
- amount = amount.toFixed(32);
141
- amount = parseFloat(amount);
142
-
143
- let balanceUpdate = {
144
- method: 'object.update',
145
- // host: server.host,
146
- array: 'organizations',
147
- object: { _id: organization_id },
148
- organization_id: platformOrganization,
149
- timeStamp
150
- };
151
-
152
- if (isExpired)
153
- balanceUpdate.object['balance'] = 0;
154
- else
155
- balanceUpdate.object.$inc = { balance: amount };
156
-
157
- if (isResetDataTransfer)
158
- balanceUpdate.object['dataTransfered'] = 0;
159
- else if (!balanceUpdate.object.$inc)
160
- balanceUpdate.object.$inc = { dataTransfered };
161
- else
162
- balanceUpdate.object.$inc.dataTransfered = dataTransfered;
163
-
164
- server.crud.send(balanceUpdate);
165
-
166
- let transaction = {
167
- method: 'object.create',
168
- host,
169
- array: 'transactions',
170
- object: {
171
- organization_id,
172
- type: "withdrawal",
173
- amount,
174
- rate,
175
- dataTransfered,
176
- ...org
177
- },
178
- organization_id,
179
- timeStamp
180
- };
181
-
182
- server.crud.send(transaction);
183
- }
135
+ const stats = this.bandwidthMap.get(organization_id);
184
136
 
185
- /**
186
- * Event-driven handler that accumulates payload footprints and manages write debouncing.
187
- */
188
- async function setBandwidth({ type, data, organization_id }) {
189
- try {
190
- let dataTransfered = getBytes(data);
191
-
192
- if (!dataTransfered || !organization_id)
193
- return;
194
-
195
- let org = organizations.get(organization_id);
196
- if (!org) {
197
- org = {};
198
- org.debounce = setTimeout(() => {
199
- sendUsageUpdate(org, organization_id, data.host);
200
- organizations.delete(organization_id);
201
- }, 60000);
202
-
203
- org.dataTransferedIn = 0;
204
- org.dataTransferedInCount = 0;
205
- org.dataTransferedOut = 0;
206
- org.dataTransferedOutCount = 0;
207
-
208
- organizations.set(organization_id, org);
137
+ // 1. Accumulate Tenant Buffer (Flushes on interval)
138
+ if (direction === 'ingress') {
139
+ stats.tenant.ingressBytes += incomingBytes;
140
+ } else if (direction === 'egress') {
141
+ stats.tenant.egressBytes += incomingBytes;
142
+ }
143
+ stats.tenant.totalBytes += incomingBytes;
144
+ stats.tenant.count += 1;
145
+ stats.tenant.sampleEnd = now;
146
+
147
+ // 2. Accumulate Platform Buffer (Flushes on event triggers / 10MB only)
148
+ if (direction === 'ingress') {
149
+ stats.platform.ingressBytes += incomingBytes;
150
+ } else if (direction === 'egress') {
151
+ stats.platform.egressBytes += incomingBytes;
209
152
  }
153
+ stats.platform.totalBytes += incomingBytes;
154
+ stats.platform.count += 1;
155
+ stats.platform.sampleEnd = now;
210
156
 
211
- if (type === 'in') {
212
- org.dataTransferedIn = dataTransfered;
213
- org.dataTransferedInCount++;
214
- } else if (type === 'out') {
215
- org.dataTransferedOut = dataTransfered;
216
- org.dataTransferedOutCount++;
217
- } else {
218
- console.log('else');
157
+ // 3. Platform Trigger: Flush to the master platform ledger if we hit 10MB
158
+ if (stats.platform.totalBytes >= this.FLUSH_THRESHOLD_BYTES) {
159
+ this.flushPlatformWindow(organization_id);
219
160
  }
161
+ }
220
162
 
221
- } catch (error) {
222
- console.log('Usage error', error);
163
+ /**
164
+ * PIPELINE A: Handles the 1-minute interval flush to the client tenant's database
165
+ */
166
+ async flushTenantWindow(organization_id) {
167
+ const stats = this.bandwidthMap.get(organization_id);
168
+ if (!stats || stats.tenant.totalBytes === 0) return;
169
+
170
+ const tenantData = stats.tenant;
171
+
172
+ // Save exactly one document representing this complete 1-minute segment to their partition
173
+ await this.saveToTenantBilling(organization_id, tenantData);
174
+
175
+ // Reset ONLY the tenant accumulator to begin a fresh 1-minute block
176
+ const now = Date.now();
177
+ stats.tenant = {
178
+ ingressBytes: 0,
179
+ egressBytes: 0,
180
+ totalBytes: 0,
181
+ count: 0,
182
+ sampleStart: now,
183
+ sampleEnd: now
184
+ };
223
185
  }
224
- }
225
186
 
226
- /**
227
- * Auto-initialization loop.
228
- * Silently polls to verify that the unified server orchestrator is fully loaded,
229
- * and that both the database (crud) and socket manager (wsManager) have completed
230
- * their initial handshakes, then attaches active bandwidth monitoring triggers.
231
- */
232
- function init() {
233
- if (server && server.isInitialized && server.crud && server.wsManager) {
234
- server.wsManager.on('setBandwidth', (data) => setBandwidth(data));
235
- } else {
236
- // If the server and WebSocket managers are not yet ready, retry in 500ms
237
- setTimeout(init, 500);
187
+ /**
188
+ * PIPELINE B: Handles targeted platform flushes (when 10MB limit is reached)
189
+ */
190
+ async flushPlatformWindow(organization_id) {
191
+ const stats = this.bandwidthMap.get(organization_id);
192
+ if (!stats || stats.platform.totalBytes === 0) return;
193
+
194
+ const platformData = stats.platform;
195
+
196
+ // Save platform ledger record
197
+ await this.saveToLedgerDatabase(organization_id, platformData);
198
+
199
+ // Reset ONLY the platform long-term memory accumulator
200
+ const now = Date.now();
201
+ stats.platform = {
202
+ ingressBytes: 0,
203
+ egressBytes: 0,
204
+ totalBytes: 0,
205
+ count: 0,
206
+ sampleStart: now,
207
+ sampleEnd: now
208
+ };
238
209
  }
239
- }
240
210
 
241
- // Auto-execute initialization upon module load
242
- init();
211
+ /**
212
+ * Target eviction: Clears timers and flushes all outstanding tenant & platform balances
213
+ */
214
+ async flushAndClearSingle(organization_id) {
215
+ const stats = this.bandwidthMap.get(organization_id);
216
+ if (!stats) return;
217
+
218
+ // De-allocate the 1-minute background loop instantly
219
+ if (stats.tenantTimer) {
220
+ clearInterval(stats.tenantTimer);
221
+ }
222
+
223
+ // Flush any partial unsaved records remaining in either pipeline
224
+ await Promise.all([
225
+ this.flushTenantWindow(organization_id),
226
+ this.flushPlatformWindow(organization_id)
227
+ ]);
228
+
229
+ // Evict from active tracker
230
+ this.bandwidthMap.delete(organization_id);
231
+ }
232
+
233
+ /**
234
+ * CRUD Adapter: Platform Bandwidth Collection (Private time-series log)
235
+ * Writes into the platform's sandbox segment, cataloging target clients in "organization".
236
+ */
237
+ async saveToLedgerDatabase(organization_id, stats) {
238
+ try {
239
+ await crud.send({
240
+ method: 'object.create',
241
+ array: 'system_bandwidth',
242
+ object: {
243
+ organization_id: this.platformOrgId, // Platform workspace sandbox route
244
+ organization: organization_id, // The actual client tenant who generated the bandwidth
245
+ ingressBytes: stats.ingressBytes,
246
+ egressBytes: stats.egressBytes,
247
+ totalBytes: stats.totalBytes,
248
+ totalRequests: stats.count,
249
+ sampleStart: new Date(stats.sampleStart),
250
+ sampleEnd: new Date(stats.sampleEnd),
251
+ durationMs: stats.sampleEnd - stats.sampleStart,
252
+ processed: false // Ready for asynchronous billing calculations
253
+ },
254
+ organization_id: this.platformOrgId // Outermost routing targeting the Platform's DB partition
255
+ });
256
+ } catch (error) {
257
+ console.error(`[DB ERROR] Failed to write to system_bandwidth for ${organization_id}:`, error);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * CRUD Adapter: Tenant Usage Collection (Their 1-Minute Dashboard Stream)
263
+ * Writes directly to the tenant's partition under the safe, system namespace "system_usage"
264
+ */
265
+ async saveToTenantBilling(organization_id, stats) {
266
+ try {
267
+ await crud.send({
268
+ method: 'object.create',
269
+ array: 'system_usage',
270
+ object: {
271
+ organization_id, // Tenant's sandbox segment route
272
+ ingressBytes: stats.ingressBytes,
273
+ egressBytes: stats.egressBytes,
274
+ totalBytes: stats.totalBytes,
275
+ totalRequests: stats.count,
276
+ sampleStart: new Date(stats.sampleStart),
277
+ sampleEnd: new Date(stats.sampleEnd),
278
+ durationMs: stats.sampleEnd - stats.sampleStart
279
+ },
280
+ organization_id: organization_id // Outermost routing targeting the Client's DB partition
281
+ });
282
+ } catch (error) {
283
+ console.error(`[DB ERROR] Failed to write to system_usage for ${organization_id}:`, error);
284
+ }
285
+ }
286
+ }
243
287
 
244
- export default init;
288
+ export default CoCreateUsage;