@cloudstreamsoftware/claude-tools 1.0.0 → 1.2.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.
- package/README.md +152 -37
- package/agents/INDEX.md +183 -0
- package/agents/architect.md +247 -0
- package/agents/build-error-resolver.md +555 -0
- package/agents/catalyst-deployer.md +132 -0
- package/agents/code-reviewer.md +121 -0
- package/agents/compliance-auditor.md +148 -0
- package/agents/creator-architect.md +395 -0
- package/agents/deluge-reviewer.md +98 -0
- package/agents/doc-updater.md +471 -0
- package/agents/e2e-runner.md +711 -0
- package/agents/planner.md +122 -0
- package/agents/refactor-cleaner.md +309 -0
- package/agents/security-reviewer.md +582 -0
- package/agents/tdd-guide.md +302 -0
- package/bin/cloudstream-setup.js +16 -6
- package/config/versions.json +63 -0
- package/dist/hooks/hooks.json +209 -0
- package/dist/index.js +47 -0
- package/dist/lib/asset-value.js +609 -0
- package/dist/lib/client-manager.js +300 -0
- package/dist/lib/command-matcher.js +242 -0
- package/dist/lib/cross-session-patterns.js +754 -0
- package/dist/lib/intent-classifier.js +1075 -0
- package/dist/lib/package-manager.js +374 -0
- package/dist/lib/recommendation-engine.js +597 -0
- package/dist/lib/session-memory.js +489 -0
- package/dist/lib/skill-effectiveness.js +486 -0
- package/dist/lib/skill-matcher.js +595 -0
- package/dist/lib/tutorial-metrics.js +242 -0
- package/dist/lib/tutorial-progress.js +209 -0
- package/dist/lib/tutorial-renderer.js +431 -0
- package/dist/lib/utils.js +380 -0
- package/dist/lib/verify-formatter.js +143 -0
- package/dist/lib/workflow-state.js +249 -0
- package/hooks/hooks.json +209 -0
- package/package.json +5 -1
- package/scripts/aggregate-sessions.js +290 -0
- package/scripts/branch-name-validator.js +291 -0
- package/scripts/build.js +101 -0
- package/scripts/commands/client-switch.js +231 -0
- package/scripts/deprecate-skill.js +610 -0
- package/scripts/diagnose.js +324 -0
- package/scripts/doc-freshness.js +168 -0
- package/scripts/generate-weekly-digest.js +393 -0
- package/scripts/health-check.js +270 -0
- package/scripts/hooks/credential-check.js +101 -0
- package/scripts/hooks/evaluate-session.js +81 -0
- package/scripts/hooks/pre-compact.js +66 -0
- package/scripts/hooks/prompt-analyzer.js +276 -0
- package/scripts/hooks/prompt-router.js +422 -0
- package/scripts/hooks/quality-gate-enforcer.js +371 -0
- package/scripts/hooks/session-end.js +156 -0
- package/scripts/hooks/session-start.js +195 -0
- package/scripts/hooks/skill-injector.js +333 -0
- package/scripts/hooks/suggest-compact.js +58 -0
- package/scripts/lib/asset-value.js +609 -0
- package/scripts/lib/client-manager.js +300 -0
- package/scripts/lib/command-matcher.js +242 -0
- package/scripts/lib/cross-session-patterns.js +754 -0
- package/scripts/lib/intent-classifier.js +1075 -0
- package/scripts/lib/package-manager.js +374 -0
- package/scripts/lib/recommendation-engine.js +597 -0
- package/scripts/lib/session-memory.js +489 -0
- package/scripts/lib/skill-effectiveness.js +486 -0
- package/scripts/lib/skill-matcher.js +595 -0
- package/scripts/lib/tutorial-metrics.js +242 -0
- package/scripts/lib/tutorial-progress.js +209 -0
- package/scripts/lib/tutorial-renderer.js +431 -0
- package/scripts/lib/utils.js +380 -0
- package/scripts/lib/verify-formatter.js +143 -0
- package/scripts/lib/workflow-state.js +249 -0
- package/scripts/onboard.js +363 -0
- package/scripts/quarterly-report.js +692 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/sync-upstream.js +391 -0
- package/scripts/test.js +108 -0
- package/scripts/tutorial-runner.js +351 -0
- package/scripts/validate-all.js +201 -0
- package/scripts/verifiers/agents.js +245 -0
- package/scripts/verifiers/config.js +186 -0
- package/scripts/verifiers/environment.js +123 -0
- package/scripts/verifiers/hooks.js +188 -0
- package/scripts/verifiers/index.js +38 -0
- package/scripts/verifiers/persistence.js +140 -0
- package/scripts/verifiers/plugin.js +215 -0
- package/scripts/verifiers/skills.js +209 -0
- package/scripts/verify-setup.js +164 -0
- package/skills/INDEX.md +157 -0
- package/skills/backend-patterns/SKILL.md +586 -0
- package/skills/backend-patterns/catalyst-patterns.md +128 -0
- package/skills/bigquery-patterns/SKILL.md +27 -0
- package/skills/bigquery-patterns/performance-optimization.md +518 -0
- package/skills/bigquery-patterns/query-patterns.md +372 -0
- package/skills/bigquery-patterns/schema-design.md +78 -0
- package/skills/cloudstream-project-template/SKILL.md +20 -0
- package/skills/cloudstream-project-template/structure.md +65 -0
- package/skills/coding-standards/SKILL.md +524 -0
- package/skills/coding-standards/deluge-standards.md +83 -0
- package/skills/compliance-patterns/SKILL.md +28 -0
- package/skills/compliance-patterns/hipaa/audit-requirements.md +251 -0
- package/skills/compliance-patterns/hipaa/baa-process.md +298 -0
- package/skills/compliance-patterns/hipaa/data-archival-strategy.md +387 -0
- package/skills/compliance-patterns/hipaa/phi-handling.md +52 -0
- package/skills/compliance-patterns/pci-dss/saq-a-requirements.md +307 -0
- package/skills/compliance-patterns/pci-dss/tokenization-patterns.md +382 -0
- package/skills/compliance-patterns/pci-dss/zoho-checkout-patterns.md +56 -0
- package/skills/compliance-patterns/soc2/access-controls.md +344 -0
- package/skills/compliance-patterns/soc2/audit-logging.md +458 -0
- package/skills/compliance-patterns/soc2/change-management.md +403 -0
- package/skills/compliance-patterns/soc2/deluge-execution-logging.md +407 -0
- package/skills/consultancy-workflows/SKILL.md +19 -0
- package/skills/consultancy-workflows/client-isolation.md +21 -0
- package/skills/consultancy-workflows/documentation-automation.md +454 -0
- package/skills/consultancy-workflows/handoff-procedures.md +257 -0
- package/skills/consultancy-workflows/knowledge-capture.md +513 -0
- package/skills/consultancy-workflows/time-tracking.md +26 -0
- package/skills/continuous-learning/SKILL.md +84 -0
- package/skills/continuous-learning/config.json +18 -0
- package/skills/continuous-learning/evaluate-session.sh +60 -0
- package/skills/continuous-learning-v2/SKILL.md +126 -0
- package/skills/continuous-learning-v2/config.json +61 -0
- package/skills/frontend-patterns/SKILL.md +635 -0
- package/skills/frontend-patterns/zoho-widget-patterns.md +103 -0
- package/skills/gcp-data-engineering/SKILL.md +36 -0
- package/skills/gcp-data-engineering/bigquery/performance-optimization.md +337 -0
- package/skills/gcp-data-engineering/dataflow/error-handling.md +496 -0
- package/skills/gcp-data-engineering/dataflow/pipeline-patterns.md +444 -0
- package/skills/gcp-data-engineering/dbt/model-organization.md +63 -0
- package/skills/gcp-data-engineering/dbt/testing-patterns.md +503 -0
- package/skills/gcp-data-engineering/medallion-architecture/bronze-layer.md +60 -0
- package/skills/gcp-data-engineering/medallion-architecture/gold-layer.md +311 -0
- package/skills/gcp-data-engineering/medallion-architecture/layer-transitions.md +517 -0
- package/skills/gcp-data-engineering/medallion-architecture/silver-layer.md +305 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/data-extraction.md +543 -0
- package/skills/gcp-data-engineering/zoho-to-gcp/real-time-vs-batch.md +337 -0
- package/skills/security-review/SKILL.md +498 -0
- package/skills/security-review/compliance-checklist.md +53 -0
- package/skills/strategic-compact/SKILL.md +67 -0
- package/skills/tdd-workflow/SKILL.md +413 -0
- package/skills/tdd-workflow/zoho-testing.md +124 -0
- package/skills/tutorial/SKILL.md +249 -0
- package/skills/tutorial/docs/ACCESSIBILITY.md +169 -0
- package/skills/tutorial/lessons/00-philosophy-and-workflow.md +198 -0
- package/skills/tutorial/lessons/01-basics.md +81 -0
- package/skills/tutorial/lessons/02-training.md +86 -0
- package/skills/tutorial/lessons/03-commands.md +109 -0
- package/skills/tutorial/lessons/04-workflows.md +115 -0
- package/skills/tutorial/lessons/05-compliance.md +116 -0
- package/skills/tutorial/lessons/06-zoho.md +121 -0
- package/skills/tutorial/lessons/07-hooks-system.md +277 -0
- package/skills/tutorial/lessons/08-mcp-servers.md +316 -0
- package/skills/tutorial/lessons/09-client-management.md +215 -0
- package/skills/tutorial/lessons/10-testing-e2e.md +260 -0
- package/skills/tutorial/lessons/11-skills-deep-dive.md +272 -0
- package/skills/tutorial/lessons/12-rules-system.md +326 -0
- package/skills/tutorial/lessons/13-golden-standard-graduation.md +213 -0
- package/skills/tutorial/lessons/14-fork-setup-and-sync.md +312 -0
- package/skills/tutorial/lessons/15-living-examples-system.md +221 -0
- package/skills/tutorial/tracks/accelerated/README.md +134 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-1.md +161 -0
- package/skills/tutorial/tracks/accelerated/assessment/checkpoint-2.md +175 -0
- package/skills/tutorial/tracks/accelerated/day-1-core-concepts.md +234 -0
- package/skills/tutorial/tracks/accelerated/day-2-essential-commands.md +270 -0
- package/skills/tutorial/tracks/accelerated/day-3-workflow-mastery.md +305 -0
- package/skills/tutorial/tracks/accelerated/day-4-compliance-zoho.md +304 -0
- package/skills/tutorial/tracks/accelerated/day-5-hooks-skills.md +344 -0
- package/skills/tutorial/tracks/accelerated/day-6-client-testing.md +386 -0
- package/skills/tutorial/tracks/accelerated/day-7-graduation.md +369 -0
- package/skills/zoho-patterns/CHANGELOG.md +108 -0
- package/skills/zoho-patterns/SKILL.md +446 -0
- package/skills/zoho-patterns/analytics/dashboard-patterns.md +352 -0
- package/skills/zoho-patterns/analytics/zoho-to-bigquery-pipeline.md +427 -0
- package/skills/zoho-patterns/catalyst/appsail-deployment.md +349 -0
- package/skills/zoho-patterns/catalyst/context-close-patterns.md +354 -0
- package/skills/zoho-patterns/catalyst/cron-batch-processing.md +374 -0
- package/skills/zoho-patterns/catalyst/function-patterns.md +439 -0
- package/skills/zoho-patterns/creator/form-design.md +304 -0
- package/skills/zoho-patterns/creator/publish-api-patterns.md +313 -0
- package/skills/zoho-patterns/creator/widget-integration.md +306 -0
- package/skills/zoho-patterns/creator/workflow-automation.md +253 -0
- package/skills/zoho-patterns/deluge/api-patterns.md +468 -0
- package/skills/zoho-patterns/deluge/batch-processing.md +403 -0
- package/skills/zoho-patterns/deluge/cross-app-integration.md +356 -0
- package/skills/zoho-patterns/deluge/error-handling.md +423 -0
- package/skills/zoho-patterns/deluge/syntax-reference.md +65 -0
- package/skills/zoho-patterns/integration/cors-proxy-architecture.md +426 -0
- package/skills/zoho-patterns/integration/crm-books-native-sync.md +277 -0
- package/skills/zoho-patterns/integration/oauth-token-management.md +461 -0
- package/skills/zoho-patterns/integration/zoho-flow-patterns.md +334 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Catalyst Cron Batch Processing
|
|
2
|
+
|
|
3
|
+
## Critical Constraints
|
|
4
|
+
|
|
5
|
+
> **TIMEOUT:** Cron functions have a 15-minute execution limit. Plan batch operations to complete within this window.
|
|
6
|
+
|
|
7
|
+
> **MANDATORY:** Every Cron function MUST call `context.close()` in ALL exit paths. See context-close-patterns.md for details.
|
|
8
|
+
|
|
9
|
+
## Cron Function Structure
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
const catalyst = require("zcatalyst-sdk-node");
|
|
13
|
+
|
|
14
|
+
module.exports = async (cronDetails, context) => {
|
|
15
|
+
const app = catalyst.initialize(context);
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
const MAX_RUNTIME_MS = 14 * 60 * 1000; // 14 minutes (1 min buffer)
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const results = await processInBatches(app, startTime, MAX_RUNTIME_MS);
|
|
21
|
+
console.log("Batch complete:", JSON.stringify(results));
|
|
22
|
+
context.close();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error("Cron failed:", error.message);
|
|
25
|
+
await notifyAdminOnFailure(app, error);
|
|
26
|
+
context.close();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Chunking Patterns for Large Datasets
|
|
32
|
+
|
|
33
|
+
### Pattern 1: Offset-Based Pagination
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
async function processInBatches(app, startTime, maxRuntime) {
|
|
37
|
+
const BATCH_SIZE = 100;
|
|
38
|
+
let offset = await getLastOffset(app); // Resume from last run
|
|
39
|
+
let processedCount = 0;
|
|
40
|
+
let errors = [];
|
|
41
|
+
|
|
42
|
+
while (true) {
|
|
43
|
+
// Safety: Check remaining time
|
|
44
|
+
if (Date.now() - startTime > maxRuntime) {
|
|
45
|
+
console.warn("Approaching timeout, saving progress at offset:", offset);
|
|
46
|
+
await saveProgress(app, offset, processedCount, errors);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fetch next batch
|
|
51
|
+
const records = await fetchBatch(app, offset, BATCH_SIZE);
|
|
52
|
+
|
|
53
|
+
if (records.length === 0) {
|
|
54
|
+
// All records processed - reset offset for next run
|
|
55
|
+
await saveProgress(app, 0, processedCount, errors);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Process each record
|
|
60
|
+
for (const record of records) {
|
|
61
|
+
try {
|
|
62
|
+
await processRecord(app, record);
|
|
63
|
+
processedCount++;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
errors.push({
|
|
66
|
+
recordId: record.ROWID,
|
|
67
|
+
error: error.message
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
offset += BATCH_SIZE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { processedCount, errorCount: errors.length, errors };
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Pattern 2: Cursor-Based (For APIs with Cursor Pagination)
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
async function processWithCursor(app, startTime, maxRuntime) {
|
|
83
|
+
let cursor = await getSavedCursor(app); // null on first run
|
|
84
|
+
let processedCount = 0;
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
if (Date.now() - startTime > maxRuntime) {
|
|
88
|
+
await saveCursor(app, cursor);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await fetchWithCursor(cursor);
|
|
93
|
+
const { data, next_cursor, has_more } = response;
|
|
94
|
+
|
|
95
|
+
for (const item of data) {
|
|
96
|
+
await processItem(app, item);
|
|
97
|
+
processedCount++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!has_more) {
|
|
101
|
+
await saveCursor(app, null); // Reset for next full run
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
cursor = next_cursor;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { processedCount, lastCursor: cursor };
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Pattern 3: Date-Window Chunking
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
async function processDateWindow(app) {
|
|
116
|
+
// Process records modified since last successful run
|
|
117
|
+
const lastRunDate = await getLastSuccessfulRun(app);
|
|
118
|
+
const now = new Date();
|
|
119
|
+
|
|
120
|
+
const criteria = `Modified_Date >= '${lastRunDate.toISOString()}' AND Modified_Date < '${now.toISOString()}'`;
|
|
121
|
+
|
|
122
|
+
const records = await fetchAllPages(app, criteria);
|
|
123
|
+
|
|
124
|
+
let processed = 0;
|
|
125
|
+
for (const record of records) {
|
|
126
|
+
await syncToExternalSystem(record);
|
|
127
|
+
processed++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Only update last-run timestamp on full success
|
|
131
|
+
await setLastSuccessfulRun(app, now);
|
|
132
|
+
|
|
133
|
+
return { processed, window: { from: lastRunDate, to: now } };
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## State Management Between Runs
|
|
138
|
+
|
|
139
|
+
Use Catalyst Data Store (ZCQL) to persist state:
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
// Save processing state
|
|
143
|
+
async function saveProgress(app, offset, processedCount, errors) {
|
|
144
|
+
const dataStore = app.datastore();
|
|
145
|
+
const table = dataStore.table("BatchState");
|
|
146
|
+
|
|
147
|
+
const existingRows = await app.zcql().executeZCQLQuery(
|
|
148
|
+
"SELECT ROWID FROM BatchState WHERE job_name = 'sync_invoices'"
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const stateData = {
|
|
152
|
+
job_name: "sync_invoices",
|
|
153
|
+
last_offset: offset,
|
|
154
|
+
last_run: new Date().toISOString(),
|
|
155
|
+
processed_count: processedCount,
|
|
156
|
+
error_count: errors.length,
|
|
157
|
+
errors_json: JSON.stringify(errors.slice(0, 50)), // Cap stored errors
|
|
158
|
+
status: errors.length > 0 ? "partial" : "complete"
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (existingRows.length > 0) {
|
|
162
|
+
stateData.ROWID = existingRows[0].BatchState.ROWID;
|
|
163
|
+
await table.updateRow(stateData);
|
|
164
|
+
} else {
|
|
165
|
+
await table.insertRow(stateData);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Retrieve last state
|
|
170
|
+
async function getLastOffset(app) {
|
|
171
|
+
const result = await app.zcql().executeZCQLQuery(
|
|
172
|
+
"SELECT last_offset FROM BatchState WHERE job_name = 'sync_invoices'"
|
|
173
|
+
);
|
|
174
|
+
return result.length > 0 ? parseInt(result[0].BatchState.last_offset) : 0;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error Notification Setup
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
async function notifyAdminOnFailure(app, error, jobName = "batch_job") {
|
|
182
|
+
// Option 1: Email via Catalyst Mail
|
|
183
|
+
const mailService = app.email();
|
|
184
|
+
await mailService.sendMail({
|
|
185
|
+
from_email: "noreply@your-catalyst-project.com",
|
|
186
|
+
to_email: ["admin@company.com"],
|
|
187
|
+
subject: `[ALERT] Cron Job Failed: ${jobName}`,
|
|
188
|
+
content: `
|
|
189
|
+
<h3>Batch Processing Failure</h3>
|
|
190
|
+
<p><strong>Job:</strong> ${jobName}</p>
|
|
191
|
+
<p><strong>Error:</strong> ${error.message}</p>
|
|
192
|
+
<p><strong>Stack:</strong> <pre>${error.stack}</pre></p>
|
|
193
|
+
<p><strong>Time:</strong> ${new Date().toISOString()}</p>
|
|
194
|
+
`,
|
|
195
|
+
content_type: "html"
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Option 2: Zoho Cliq notification
|
|
199
|
+
const axios = require("axios");
|
|
200
|
+
await axios.post(process.env.CLIQ_WEBHOOK_URL, {
|
|
201
|
+
text: `*Cron Job Failed:* ${jobName}\nError: ${error.message}\nTime: ${new Date().toISOString()}`
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Retry Logic
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
async function processWithRetry(fn, record, maxRetries = 3) {
|
|
210
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
return await fn(record);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (attempt === maxRetries) {
|
|
215
|
+
throw error; // Final attempt failed
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Exponential backoff
|
|
219
|
+
const delay = Math.pow(2, attempt) * 500;
|
|
220
|
+
console.warn(`Attempt ${attempt} failed for record ${record.ROWID}, retrying in ${delay}ms`);
|
|
221
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
222
|
+
|
|
223
|
+
// Don't retry non-transient errors
|
|
224
|
+
if (error.status === 400 || error.status === 404) {
|
|
225
|
+
throw error; // Bad data, retry won't help
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Avoiding Duplicate Processing
|
|
233
|
+
|
|
234
|
+
### Idempotency Pattern
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
async function processRecordIdempotent(app, record) {
|
|
238
|
+
const idempotencyKey = `${record.ROWID}_${record.Modified_Date}`;
|
|
239
|
+
|
|
240
|
+
// Check if already processed
|
|
241
|
+
const existing = await app.zcql().executeZCQLQuery(
|
|
242
|
+
`SELECT ROWID FROM ProcessedLog WHERE idempotency_key = '${idempotencyKey}'`
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (existing.length > 0) {
|
|
246
|
+
console.log(`Skipping already-processed record: ${idempotencyKey}`);
|
|
247
|
+
return { skipped: true };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Process the record
|
|
251
|
+
const result = await doActualProcessing(record);
|
|
252
|
+
|
|
253
|
+
// Mark as processed
|
|
254
|
+
await app.datastore().table("ProcessedLog").insertRow({
|
|
255
|
+
idempotency_key: idempotencyKey,
|
|
256
|
+
record_id: record.ROWID,
|
|
257
|
+
processed_at: new Date().toISOString(),
|
|
258
|
+
result: JSON.stringify(result)
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return { skipped: false, result };
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Locking Pattern (Prevent Concurrent Runs)
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
async function acquireLock(app, jobName, ttlMinutes = 16) {
|
|
269
|
+
const lockExpiry = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString();
|
|
270
|
+
|
|
271
|
+
// Check for existing active lock
|
|
272
|
+
const existing = await app.zcql().executeZCQLQuery(
|
|
273
|
+
`SELECT ROWID, lock_expiry FROM CronLocks WHERE job_name = '${jobName}'`
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (existing.length > 0) {
|
|
277
|
+
const expiry = new Date(existing[0].CronLocks.lock_expiry);
|
|
278
|
+
if (expiry > new Date()) {
|
|
279
|
+
console.warn("Job already running, skipping this execution");
|
|
280
|
+
return false; // Lock held by another run
|
|
281
|
+
}
|
|
282
|
+
// Expired lock - update it
|
|
283
|
+
await app.datastore().table("CronLocks").updateRow({
|
|
284
|
+
ROWID: existing[0].CronLocks.ROWID,
|
|
285
|
+
lock_expiry: lockExpiry,
|
|
286
|
+
locked_at: new Date().toISOString()
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
await app.datastore().table("CronLocks").insertRow({
|
|
290
|
+
job_name: jobName,
|
|
291
|
+
lock_expiry: lockExpiry,
|
|
292
|
+
locked_at: new Date().toISOString()
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return true; // Lock acquired
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function releaseLock(app, jobName) {
|
|
300
|
+
const existing = await app.zcql().executeZCQLQuery(
|
|
301
|
+
`SELECT ROWID FROM CronLocks WHERE job_name = '${jobName}'`
|
|
302
|
+
);
|
|
303
|
+
if (existing.length > 0) {
|
|
304
|
+
await app.datastore().table("CronLocks").deleteRow(existing[0].CronLocks.ROWID);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Complete Cron Function Template
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
const catalyst = require("zcatalyst-sdk-node");
|
|
313
|
+
|
|
314
|
+
module.exports = async (cronDetails, context) => {
|
|
315
|
+
const app = catalyst.initialize(context);
|
|
316
|
+
const JOB_NAME = "invoice_sync";
|
|
317
|
+
const startTime = Date.now();
|
|
318
|
+
const MAX_RUNTIME = 14 * 60 * 1000;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
// Prevent concurrent execution
|
|
322
|
+
const lockAcquired = await acquireLock(app, JOB_NAME);
|
|
323
|
+
if (!lockAcquired) {
|
|
324
|
+
console.log("Another instance running, exiting");
|
|
325
|
+
context.close();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Main processing
|
|
330
|
+
const results = await processInBatches(app, startTime, MAX_RUNTIME);
|
|
331
|
+
|
|
332
|
+
// Report results
|
|
333
|
+
console.log(`Completed: ${results.processedCount} processed, ${results.errorCount} errors`);
|
|
334
|
+
|
|
335
|
+
if (results.errorCount > 0) {
|
|
336
|
+
await notifyAdminOnFailure(app, new Error(
|
|
337
|
+
`${results.errorCount} records failed: ${JSON.stringify(results.errors.slice(0, 5))}`
|
|
338
|
+
), JOB_NAME);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await releaseLock(app, JOB_NAME);
|
|
342
|
+
context.close();
|
|
343
|
+
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error("Fatal error:", error);
|
|
346
|
+
await notifyAdminOnFailure(app, error, JOB_NAME);
|
|
347
|
+
await releaseLock(app, JOB_NAME);
|
|
348
|
+
context.close();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Cron Schedule Configuration
|
|
354
|
+
|
|
355
|
+
Set in `catalyst-config.json`:
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"cron": {
|
|
360
|
+
"function_name": "invoice_sync_cron",
|
|
361
|
+
"cron_expression": "0 */2 * * *",
|
|
362
|
+
"description": "Sync invoices every 2 hours"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
| Expression | Schedule |
|
|
368
|
+
|-----------|----------|
|
|
369
|
+
| `0 * * * *` | Every hour |
|
|
370
|
+
| `*/15 * * * *` | Every 15 minutes |
|
|
371
|
+
| `0 */2 * * *` | Every 2 hours |
|
|
372
|
+
| `0 6 * * *` | Daily at 6 AM |
|
|
373
|
+
| `0 0 * * 0` | Weekly on Sunday midnight |
|
|
374
|
+
| `0 0 1 * *` | First of every month |
|