@cocreate/usage 1.2.1 → 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.
@@ -1,8 +1,10 @@
1
1
  name: Automated Workflow
2
+
2
3
  on:
3
4
  push:
4
5
  branches:
5
6
  - master
7
+
6
8
  jobs:
7
9
  about:
8
10
  runs-on: ubuntu-latest
@@ -18,52 +20,37 @@ jobs:
18
20
  with:
19
21
  direction: overwrite-github
20
22
  githubToken: "${{ secrets.GITHUB }}"
23
+
21
24
  release:
22
25
  runs-on: ubuntu-latest
23
26
  steps:
24
27
  - name: Checkout
25
- uses: actions/checkout@v3
26
- - name: Setup Node.js
27
- uses: actions/setup-node@v3
28
+ uses: actions/checkout@v4
28
29
  with:
29
- node-version: 14
30
- - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v3
32
- id: semantic
30
+ fetch-depth: 0 # Required so semantic-release can trace git tags/history
31
+
32
+ - name: Setup Node.js
33
+ uses: actions/setup-node@v4
33
34
  with:
34
- extra_plugins: |
35
- @semantic-release/changelog
35
+ node-version: 22
36
+
37
+ - name: Install Semantic Release & Plugins
38
+ # Installs semantic-release and its plugins on the runner
39
+ run: |
40
+ npm install -g semantic-release \
41
+ @semantic-release/changelog \
42
+ @semantic-release/npm \
43
+ @semantic-release/github \
36
44
  @semantic-release/git
37
- @semantic-release/github
45
+
46
+ - name: Run Semantic Release (Native)
47
+ id: semantic
48
+ # This will automatically pick up your export default config file in the repository root
49
+ run: npx semantic-release
38
50
  env:
39
- GITHUB_TOKEN: "${{ secrets.GITHUB }}"
51
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
40
52
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
53
+
41
54
  outputs:
42
55
  new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
43
- new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
44
- upload:
45
- runs-on: ubuntu-latest
46
- needs: release
47
- if: needs.release.outputs.new_release_published == 'true'
48
- env:
49
- VERSION: "${{ needs.release.outputs.new_release_version }}"
50
- steps:
51
- - name: Checkout
52
- uses: actions/checkout@v3
53
- - name: Setup Node.js
54
- uses: actions/setup-node@v3
55
- with:
56
- node-version: 16
57
- - name: Set npm registry auth
58
- run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
59
- - name: Install dependencies
60
- run: yarn install
61
-
62
- - name: Set Environment Variables
63
- run: |
64
- echo "organization_id=${{ secrets.COCREATE_ORGANIZATION_ID }}" >> $GITHUB_ENV
65
- echo "key=${{ secrets.COCREATE_KEY }}" >> $GITHUB_ENV
66
- echo "host=${{ secrets.COCREATE_HOST }}" >> $GITHUB_ENV
67
- - name: CoCreate Upload
68
- run: coc upload
69
-
56
+ new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
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
+
9
+ # [1.3.0](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.2.2...v1.3.0) (2026-07-17)
10
+
11
+
12
+ ### Features
13
+
14
+ * update automated workflow and release configuration for improved semantic release handling ([47aa8c2](https://github.com/CoCreate-app/CoCreate-usage/commit/47aa8c2941f8393a0ca55cbf6f66ae4df1172dea))
15
+
16
+ ## [1.2.2](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.2.1...v1.2.2) (2026-07-14)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * remove unnecessary console logs in init function for cleaner output ([af5f039](https://github.com/CoCreate-app/CoCreate-usage/commit/af5f03958fa5e84008411e99186bcdea438e7087))
22
+ * removed post install ([933f2b6](https://github.com/CoCreate-app/CoCreate-usage/commit/933f2b6b9a7a551afb7e2723bfd02de9a72f2019))
23
+ * semantic version handling. Reorganize .gitignore for improved clarity and structure ([9d67031](https://github.com/CoCreate-app/CoCreate-usage/commit/9d67031f6144a5b767ff0a2b6d487b14cbbd33aa))
24
+ * update module export to ES6 syntax in release.config.js ([8908cb7](https://github.com/CoCreate-app/CoCreate-usage/commit/8908cb7ea61ceb65d1151909e8706c0a54046903))
25
+ * update organization ID reference and host variable in sendUsageUpdate function ([4f4cc11](https://github.com/CoCreate-app/CoCreate-usage/commit/4f4cc11f9d0757b27c71f180ce83895fb2c436cd))
26
+ * update package.json to use ESM and restructure entry points ([c96c5db](https://github.com/CoCreate-app/CoCreate-usage/commit/c96c5db22a621d2af2a151d2173ffaa265c03f6e))
27
+
1
28
  ## [1.2.1](https://github.com/CoCreate-app/CoCreate-usage/compare/v1.2.0...v1.2.1) (2025-05-01)
2
29
 
3
30
 
package/package.json CHANGED
@@ -1,18 +1,10 @@
1
1
  {
2
2
  "name": "@cocreate/usage",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "CoCreate-usage",
5
5
  "keywords": [
6
6
  "cocreate-usage",
7
- "cocreate",
8
- "low-code-framework",
9
- "no-code-framework",
10
- "cocreatejs",
11
- "cocreatejs-component",
12
- "cocreate-framework",
13
- "no-code",
14
7
  "low-code",
15
- "collaborative-framework",
16
8
  "realtime",
17
9
  "realtime-framework",
18
10
  "collaboration",
@@ -20,6 +12,7 @@
20
12
  "html5-framework",
21
13
  "javascript-framework"
22
14
  ],
15
+ "type": "module",
23
16
  "main": "./src/index.js",
24
17
  "publishConfig": {
25
18
  "access": "public"
@@ -36,7 +29,6 @@
36
29
  "homepage": "https://cocreate.app/docs/CoCreate-usage",
37
30
  "scripts": {
38
31
  "demo": "PORT=5000 node demo/server.js",
39
- "test": "echo \"Error: no test specified\" && exit 1",
40
- "postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
32
+ "test": "echo \"Error: no test specified\" && exit 1"
41
33
  }
42
34
  }
package/release.config.js CHANGED
@@ -1,4 +1,4 @@
1
- module.exports = {
1
+ export default {
2
2
  dryRun: false,
3
3
  branches: ["master"],
4
4
  plugins: [
@@ -10,12 +10,20 @@ module.exports = {
10
10
  changelogFile: "CHANGELOG.md",
11
11
  },
12
12
  ],
13
- "@semantic-release/npm",
14
- "@semantic-release/github",
13
+ "@semantic-release/npm",
15
14
  [
16
- "@semantic-release/git",
15
+ "@semantic-release/github",
17
16
  {
17
+ successComment: false,
18
+ failTitle: false,
19
+ },
20
+ ],
21
+ [
22
+ "@semantic-release/git",
23
+ {
24
+ // Only stage and commit the changelog and package.json
18
25
  assets: ["CHANGELOG.md", "package.json"],
26
+ message: "chore(release): ${nextRelease.version} [skip ci]",
19
27
  },
20
28
  ],
21
29
  ],
package/src/index.js CHANGED
@@ -1,208 +1,288 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import crud from '@cocreate/crud-server';
19
+
1
20
  class CoCreateUsage {
2
- constructor(crud) {
3
- this.wsManager = crud.wsManager;
4
- this.crud = crud;
5
- this.init();
6
- this.organizations = new Map()
7
- }
21
+ constructor(platformOrgId) {
22
+ this.platformOrgId = platformOrgId; // Master platform organization ID
8
23
 
9
- init() {
10
- if (this.wsManager) {
11
- this.wsManager.on('setBandwidth', (data) => this.setBandwidth(data));
12
- }
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();
13
33
  }
14
34
 
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
+ });
63
+ }
15
64
 
16
- async setBandwidth({ type, data, organization_id }) {
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
+
17
72
  try {
18
- let dataTransfered = this.getBytes(data)
19
-
20
- if (!dataTransfered || !organization_id)
21
- return
22
- // TODO: set duration
23
- let org = this.organizations.get(organization_id)
24
- if (!org) {
25
- let self = this
26
-
27
- org = {}
28
- org.debounce = setTimeout(() => {
29
- self.send(org, organization_id, data.host); // Call the callback to display the count
30
- self.organizations.delete(organization_id)
31
- }, 60000);
32
-
33
- org.dataTransferedIn = 0;
34
- org.dataTransferedInCount = 0;
35
- org.dataTransferedOut = 0;
36
- org.dataTransferedOutCount = 0;
37
-
38
- this.organizations.set(organization_id, org)
73
+ // Case 1: Raw media streams, files, or binary chunks (Buffers / TypedArrays)
74
+ if (Buffer.isBuffer(data)) {
75
+ return data.length;
39
76
  }
40
-
41
- if (type === 'in') {
42
- org.dataTransferedIn = dataTransfered;
43
- org.dataTransferedInCount++
44
- } else if (type === 'out') {
45
- org.dataTransferedOut = dataTransfered;
46
- org.dataTransferedOutCount++
47
- } else {
48
- console.log('else')
77
+ if (data instanceof Uint8Array || data.buffer instanceof ArrayBuffer) {
78
+ return data.byteLength || data.length || 0;
49
79
  }
50
-
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');
51
93
  } catch (error) {
52
- console.log('Usage error', error)
94
+ console.error('[USAGE ERROR] Failed to safely calculate payload byte scale:', error);
95
+ return 0;
53
96
  }
54
97
  }
55
98
 
56
-
57
- async send(org, organization_id, host) {
58
- delete org.debounce
59
-
60
- org.dataTransferedOut += 250
61
- org.dataTransferedOutCount++
62
-
63
- const platformOrganization = this.crud.config.organization_id
64
- let organization = await this.crud.send({
65
- method: 'object.read',
66
- host: this.crud.config.host,
67
- array: 'organizations',
68
- object: { _id: organization_id },
69
- organization_id: platformOrganization
70
- })
71
-
72
- org.dataTransferedIn += this.getBytes(platformOrganization)
73
- org.dataTransferedInCount++
74
-
75
- if (organization && organization.object && organization.object[0]) {
76
- organization = organization.object[0]
77
- } else return
78
-
79
- //TODO: if (organization.transactionInterval) // set in global map to use with timeout 1 sec intervals to 3600 sec
80
- if (this.wsManager.organizations.has(organization_id)) {
81
- if (organization.balance <= 0) {
82
- this.wsManager.organizations.get(organization_id).status = false
83
- this.wsManager.organizations.get(organization_id).organizationBalance = false
84
- this.wsManager.organizations.get(organization_id).error = 'Your balance has fallen bellow 0'
85
-
86
- } else {
87
- this.wsManager.organizations.get(organization_id).status = true
88
- this.wsManager.organizations.get(organization_id).organizationBalance = true
89
- this.wsManager.organizations.get(organization_id).error = ''
90
- }
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
130
+ },
131
+ tenantTimer
132
+ });
91
133
  }
92
134
 
93
- let timeStamp = new Date(new Date().toISOString());
94
- let isExpired = false
95
- if (organization.lastDeposit) {
96
- let lastDeposit = new Date(organization.lastDeposit)
97
- isExpired = lastDeposit <= timeStamp.setFullYear(timeStamp.getFullYear() - 1)
98
- }
135
+ const stats = this.bandwidthMap.get(organization_id);
99
136
 
100
- let isResetDataTransfer = false
101
- if (organization.modified && organization.modified.on) {
102
- let previousTimeStamp = new Date(organization.modified.on)
103
- if (previousTimeStamp.getMonth() !== timeStamp.getMonth()) {
104
- isResetDataTransfer = true
105
- this.crud.send({
106
- method: 'object.create',
107
- host: this.crud.config.host,
108
- array: 'transactions',
109
- object: {
110
- organization_id,
111
- type: "withdrawal", // deposit, credit, withdrawal, debit
112
- dataTransfered: organization.dataTransfered,
113
- previousTimeStamp
114
- },
115
- organization_id: platformOrganization,
116
- timeStamp
117
- });
118
- organization.dataTransfered = 0
119
- }
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;
120
142
  }
121
-
122
- let dataTransfered = org.dataTransferedIn + org.dataTransferedOut
123
- dataTransfered = dataTransfered / 1073741824;
124
- dataTransfered = dataTransfered.toFixed(32);
125
- dataTransfered = parseFloat(dataTransfered);
126
-
127
- let rate = this.getRate(organization.dataTransfered)
128
- let amount = dataTransfered * rate
129
- amount = -amount
130
- amount = amount.toFixed(32)
131
- amount = parseFloat(amount)
132
-
133
- let balanceUpdate = {
134
- method: 'object.update',
135
- host: this.crud.config.host,
136
- array: 'organizations',
137
- object: { _id: organization_id },
138
- organization_id: platformOrganization,
139
- timeStamp
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;
140
152
  }
153
+ stats.platform.totalBytes += incomingBytes;
154
+ stats.platform.count += 1;
155
+ stats.platform.sampleEnd = now;
141
156
 
142
- if (isExpired)
143
- balanceUpdate.object['balance'] = 0
144
- else
145
- balanceUpdate.object.$inc = { balance: amount }
146
-
147
- if (isResetDataTransfer)
148
- balanceUpdate.object['dataTransfered'] = 0
149
- else if (!balanceUpdate.object.$inc)
150
- balanceUpdate.object.$inc = { dataTransfered }
151
- else
152
- balanceUpdate.object.$inc.dataTransfered = dataTransfered
153
-
154
- // console.log('balanceUpdate: ', balanceUpdate)
155
-
156
- this.crud.send(balanceUpdate)
157
-
158
- let transaction = {
159
- method: 'object.create',
160
- host,
161
- array: 'transactions',
162
- object: {
163
- organization_id,
164
- type: "withdrawal", // deposit, credit, withdrawal, debit
165
- amount,
166
- rate,
167
- dataTransfered,
168
- ...org
169
- },
170
- organization_id,
171
- timeStamp
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);
172
160
  }
161
+ }
173
162
 
174
- this.crud.send(transaction);
175
-
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
+ };
176
185
  }
177
186
 
178
- getBytes(data) {
179
- if (typeof data === 'number')
180
- return data;
181
- else if (data instanceof Buffer)
182
- return data.byteLength || 0;
183
- else if (data instanceof String || typeof data === 'string')
184
- return Buffer.byteLength(data, 'utf8') || 0;
185
- else if (typeof data === 'object')
186
- return Buffer.byteLength(JSON.stringify(data), 'utf8') || 0;
187
- else return 0
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
+ };
188
209
  }
189
210
 
190
- getRate(totalUsage = 0) {
191
- const tiers = [
192
- { limit: 1, rate: 10 },
193
- { limit: 10, rate: 5 },
194
- { limit: 100, rate: 2.5 },
195
- { limit: 1000, rate: 1.125 },
196
- { limit: 10000, rate: 0.562 },
197
- { limit: 100000, rate: 0.281 },
198
- { limit: 1000000, rate: 0.145 }
199
- ];
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
+ ]);
200
228
 
201
- const matchingTier = tiers.find(tier => totalUsage < tier.limit);
229
+ // Evict from active tracker
230
+ this.bandwidthMap.delete(organization_id);
231
+ }
202
232
 
203
- return matchingTier ? matchingTier.rate : 0.12;
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
+ }
204
259
  }
205
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
+ }
206
286
  }
207
287
 
208
- module.exports = CoCreateUsage;
288
+ export default CoCreateUsage;