@ductape/sdk 0.3.6 → 0.3.8
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/dist/context/execution-context.d.ts +2 -0
- package/dist/context/execution-context.js +1 -1
- package/dist/context/execution-context.js.map +1 -1
- package/dist/features/features.service.d.ts +20 -2
- package/dist/features/features.service.js +63 -24
- package/dist/features/features.service.js.map +1 -1
- package/dist/features/index.d.ts +1 -0
- package/dist/features/index.js +4 -2
- package/dist/features/index.js.map +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.js +68 -10
- package/dist/index.js.map +1 -1
- package/dist/logs/logs.service.js +4 -2
- package/dist/logs/logs.service.js.map +1 -1
- package/dist/logs/logs.types.d.ts +3 -0
- package/dist/logs/logs.types.js +1 -0
- package/dist/logs/logs.types.js.map +1 -1
- package/dist/processor/services/processor.service.d.ts +2 -2
- package/dist/products/services/products.service.d.ts +1 -0
- package/dist/products/services/products.service.js +31 -0
- package/dist/products/services/products.service.js.map +1 -1
- package/dist/resilience/healthcheck.service.js +25 -6
- package/dist/resilience/healthcheck.service.js.map +1 -1
- package/dist/resilience/rate-limit.service.d.ts +30 -3
- package/dist/resilience/rate-limit.service.js +117 -30
- package/dist/resilience/rate-limit.service.js.map +1 -1
- package/dist/storage/types/index.d.ts +4 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +255 -2
|
@@ -20,6 +20,8 @@ export interface DuctapeAssetSpan {
|
|
|
20
20
|
error_message?: string;
|
|
21
21
|
}
|
|
22
22
|
export interface DuctapeExecutionContext {
|
|
23
|
+
/** Probe internals are represented by one healthcheck observation, not API logs. */
|
|
24
|
+
healthcheck_probe?: boolean;
|
|
23
25
|
workspace_id?: string;
|
|
24
26
|
product_id?: string;
|
|
25
27
|
product_tag?: string;
|
|
@@ -33,7 +33,7 @@ function runWithExecutionContext(context, fn) {
|
|
|
33
33
|
function executionContextFields(context = currentExecutionContext()) {
|
|
34
34
|
if (!context)
|
|
35
35
|
return {};
|
|
36
|
-
const { session: _secretSession, baggage: _baggage, asset_span_sink: _assetSpanSink, kind: execution_kind } = context, fields = __rest(context, ["session", "baggage", "asset_span_sink", "kind"]);
|
|
36
|
+
const { session: _secretSession, baggage: _baggage, asset_span_sink: _assetSpanSink, healthcheck_probe: _healthcheckProbe, kind: execution_kind } = context, fields = __rest(context, ["session", "baggage", "asset_span_sink", "healthcheck_probe", "kind"]);
|
|
37
37
|
return Object.assign(Object.assign({}, fields), { execution_kind });
|
|
38
38
|
}
|
|
39
39
|
async function withAssetSpan(asset, fn) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution-context.js","sourceRoot":"","sources":["../../src/context/execution-context.ts"],"names":[],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"execution-context.js","sourceRoot":"","sources":["../../src/context/execution-context.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,20 @@ export declare class FeatureError extends Error {
|
|
|
24
24
|
* Main Feature Service class
|
|
25
25
|
* Provides unified interface for feature management and execution
|
|
26
26
|
*/
|
|
27
|
+
export interface FeaturePublicationReceipt {
|
|
28
|
+
tag: string;
|
|
29
|
+
operation?: 'create' | 'update';
|
|
30
|
+
write: 'not_attempted' | 'unknown' | 'acknowledged';
|
|
31
|
+
verified: boolean;
|
|
32
|
+
compensation: 'not_needed' | 'retained' | 'unknown' | 'restored';
|
|
33
|
+
}
|
|
34
|
+
export declare class FeaturePublicationError extends Error {
|
|
35
|
+
readonly product: string;
|
|
36
|
+
readonly receipts: FeaturePublicationReceipt[];
|
|
37
|
+
readonly cause: unknown;
|
|
38
|
+
readonly code = "FEATURE_PUBLICATION_FAILED";
|
|
39
|
+
constructor(message: string, product: string, receipts: FeaturePublicationReceipt[], cause: unknown);
|
|
40
|
+
}
|
|
27
41
|
export declare class FeatureService {
|
|
28
42
|
/** Service configuration */
|
|
29
43
|
private config;
|
|
@@ -31,6 +45,8 @@ export declare class FeatureService {
|
|
|
31
45
|
private productBuilders;
|
|
32
46
|
/** Local feature configurations */
|
|
33
47
|
private localConfigs;
|
|
48
|
+
private localKey;
|
|
49
|
+
private localFeatures;
|
|
34
50
|
/** Feature API service for backend communication */
|
|
35
51
|
private featureApiService;
|
|
36
52
|
/** LogService instance for logging operations */
|
|
@@ -207,8 +223,9 @@ export declare class FeatureService {
|
|
|
207
223
|
/** Compile several definitions without performing any remote writes. */
|
|
208
224
|
compileMany(definitions: IDefineFeatureOptions<any, any>[]): Promise<IProductFeature[]>;
|
|
209
225
|
/**
|
|
210
|
-
* Publish
|
|
211
|
-
*
|
|
226
|
+
* Publish with content verification and best-effort restoration of existing records.
|
|
227
|
+
* New records are retained. Failures expose per-feature receipts on the error;
|
|
228
|
+
* this is not an atomic transaction, and rejected writes may have persisted.
|
|
212
229
|
*/
|
|
213
230
|
publishMany(productTag: string, definitions: IDefineFeatureOptions<any, any>[], options?: {
|
|
214
231
|
dryRun?: boolean;
|
|
@@ -216,6 +233,7 @@ export declare class FeatureService {
|
|
|
216
233
|
product: string;
|
|
217
234
|
dry_run: boolean;
|
|
218
235
|
features: IProductFeature[];
|
|
236
|
+
receipts: FeaturePublicationReceipt[];
|
|
219
237
|
}>;
|
|
220
238
|
/**
|
|
221
239
|
* Execute a feature
|
|
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
};
|
|
46
46
|
var _a, _b;
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.featureService = exports.FeatureCompilationError = exports.FeatureService = exports.FeatureError = void 0;
|
|
48
|
+
exports.featureService = exports.FeatureCompilationError = exports.FeatureService = exports.FeaturePublicationError = exports.FeatureError = void 0;
|
|
49
49
|
const crypto_1 = require("crypto");
|
|
50
50
|
const feature_control_flow_validator_1 = require("./feature-control-flow.validator");
|
|
51
51
|
const functions_1 = require("../functions");
|
|
@@ -110,11 +110,26 @@ class FeatureError extends Error {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
exports.FeatureError = FeatureError;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
class FeaturePublicationError extends Error {
|
|
114
|
+
constructor(message, product, receipts, cause) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.product = product;
|
|
117
|
+
this.receipts = receipts;
|
|
118
|
+
this.cause = cause;
|
|
119
|
+
this.code = 'FEATURE_PUBLICATION_FAILED';
|
|
120
|
+
this.name = 'FeaturePublicationError';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.FeaturePublicationError = FeaturePublicationError;
|
|
117
124
|
class FeatureService {
|
|
125
|
+
localKey(tag, product) {
|
|
126
|
+
return JSON.stringify([product !== null && product !== void 0 ? product : null, tag]);
|
|
127
|
+
}
|
|
128
|
+
localFeatures(product) {
|
|
129
|
+
return [...this.localConfigs.entries()]
|
|
130
|
+
.filter(([key]) => JSON.parse(key)[0] === (product !== null && product !== void 0 ? product : null))
|
|
131
|
+
.map(([, feature]) => feature);
|
|
132
|
+
}
|
|
118
133
|
/**
|
|
119
134
|
* Create a new FeatureService instance
|
|
120
135
|
* @param config - Optional configuration for authentication and workspace context
|
|
@@ -443,7 +458,7 @@ class FeatureService {
|
|
|
443
458
|
options: featureData.options,
|
|
444
459
|
envs: featureData.envs,
|
|
445
460
|
};
|
|
446
|
-
this.localConfigs.set(featureData.tag, localConfig);
|
|
461
|
+
this.localConfigs.set(this.localKey(featureData.tag, productTag), localConfig);
|
|
447
462
|
console.log('[FeatureService] create SUCCESS', { productTag, tag: featureData.tag });
|
|
448
463
|
return persisted;
|
|
449
464
|
}
|
|
@@ -468,7 +483,7 @@ class FeatureService {
|
|
|
468
483
|
if (!productTag) {
|
|
469
484
|
const localCount = this.localConfigs.size;
|
|
470
485
|
console.log('[FeatureService] fetchAll no productTag, returning local configs', { count: localCount });
|
|
471
|
-
return
|
|
486
|
+
return this.localFeatures();
|
|
472
487
|
}
|
|
473
488
|
// Fetch from API using ProductBuilder
|
|
474
489
|
try {
|
|
@@ -483,7 +498,7 @@ class FeatureService {
|
|
|
483
498
|
catch (error) {
|
|
484
499
|
// Fall back to local configs if API fails
|
|
485
500
|
console.log('[FeatureService] fetchAll API failed, falling back to local', { productTag, error: (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error });
|
|
486
|
-
return
|
|
501
|
+
return this.localFeatures(productTag);
|
|
487
502
|
}
|
|
488
503
|
}
|
|
489
504
|
/**
|
|
@@ -507,7 +522,7 @@ class FeatureService {
|
|
|
507
522
|
if (feature) {
|
|
508
523
|
const config = this.productFeatureToConfig(feature);
|
|
509
524
|
// Update local cache
|
|
510
|
-
this.localConfigs.set(featureTag, config);
|
|
525
|
+
this.localConfigs.set(this.localKey(featureTag, productTag), config);
|
|
511
526
|
console.log('[FeatureService] fetch SUCCESS from API', { featureTag, productTag, stepsCount: (_a = config.steps) === null || _a === void 0 ? void 0 : _a.length });
|
|
512
527
|
return config;
|
|
513
528
|
}
|
|
@@ -517,10 +532,10 @@ class FeatureService {
|
|
|
517
532
|
catch (error) {
|
|
518
533
|
// Fall back to local config
|
|
519
534
|
console.log('[FeatureService] fetch API failed, trying local', { featureTag, productTag, error: (_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error });
|
|
520
|
-
return this.localConfigs.get(featureTag) || null;
|
|
535
|
+
return this.localConfigs.get(this.localKey(featureTag, productTag)) || null;
|
|
521
536
|
}
|
|
522
537
|
}
|
|
523
|
-
const local = this.localConfigs.get(featureTag) || null;
|
|
538
|
+
const local = this.localConfigs.get(this.localKey(featureTag)) || null;
|
|
524
539
|
console.log('[FeatureService] fetch from local only', { featureTag, found: !!local });
|
|
525
540
|
return local;
|
|
526
541
|
}
|
|
@@ -542,10 +557,10 @@ class FeatureService {
|
|
|
542
557
|
const builder = await this.getProductBuilder(productTag);
|
|
543
558
|
await builder.updateFeature(featureTag, updates);
|
|
544
559
|
// Update local cache
|
|
545
|
-
const existing = this.localConfigs.get(featureTag);
|
|
560
|
+
const existing = this.localConfigs.get(this.localKey(featureTag, productTag));
|
|
546
561
|
if (existing) {
|
|
547
562
|
const updated = Object.assign(Object.assign({}, existing), { name: updates.name || existing.name, description: (_a = updates.description) !== null && _a !== void 0 ? _a : existing.description, input: (_b = updates.input) !== null && _b !== void 0 ? _b : existing.input, output: (_c = updates.output) !== null && _c !== void 0 ? _c : existing.output, steps: updates.steps || existing.steps, signals: (_d = updates.signals) !== null && _d !== void 0 ? _d : existing.signals, queries: (_e = updates.queries) !== null && _e !== void 0 ? _e : existing.queries, options: (_f = updates.options) !== null && _f !== void 0 ? _f : existing.options, envs: (_g = updates.envs) !== null && _g !== void 0 ? _g : existing.envs });
|
|
548
|
-
this.localConfigs.set(featureTag, updated);
|
|
563
|
+
this.localConfigs.set(this.localKey(featureTag, productTag), updated);
|
|
549
564
|
}
|
|
550
565
|
console.log('[FeatureService] update SUCCESS', { featureTag, productTag });
|
|
551
566
|
}
|
|
@@ -563,7 +578,7 @@ class FeatureService {
|
|
|
563
578
|
const builder = await this.getProductBuilder(productTag);
|
|
564
579
|
await builder.deleteFeature(featureTag);
|
|
565
580
|
// Remove from local cache
|
|
566
|
-
this.localConfigs.delete(featureTag);
|
|
581
|
+
this.localConfigs.delete(this.localKey(featureTag, productTag));
|
|
567
582
|
console.log('[FeatureService] delete SUCCESS', { featureTag, productTag });
|
|
568
583
|
}
|
|
569
584
|
// ==================== CODE-FIRST API ====================
|
|
@@ -679,7 +694,7 @@ class FeatureService {
|
|
|
679
694
|
options: schema.options,
|
|
680
695
|
envs: schema.envs,
|
|
681
696
|
};
|
|
682
|
-
this.localConfigs.set(schema.tag, localConfig);
|
|
697
|
+
this.localConfigs.set(this.localKey(schema.tag, options.product), localConfig);
|
|
683
698
|
console.log('[FeatureService] define SUCCESS', { tag: options.tag, product: options.product });
|
|
684
699
|
// Return the defined feature with compile method
|
|
685
700
|
return {
|
|
@@ -699,34 +714,50 @@ class FeatureService {
|
|
|
699
714
|
return Promise.all(definitions.map((definition) => new FeatureCompiler(definition).compileAsync()));
|
|
700
715
|
}
|
|
701
716
|
/**
|
|
702
|
-
* Publish
|
|
703
|
-
*
|
|
717
|
+
* Publish with content verification and best-effort restoration of existing records.
|
|
718
|
+
* New records are retained. Failures expose per-feature receipts on the error;
|
|
719
|
+
* this is not an atomic transaction, and rejected writes may have persisted.
|
|
704
720
|
*/
|
|
705
721
|
async publishMany(productTag, definitions, options = {}) {
|
|
706
722
|
const schemas = await this.compileMany(definitions);
|
|
723
|
+
const receipts = schemas.map(schema => ({
|
|
724
|
+
tag: schema.tag, write: 'not_attempted', verified: false, compensation: 'not_needed',
|
|
725
|
+
}));
|
|
707
726
|
if (options.dryRun)
|
|
708
|
-
return { product: productTag, dry_run: true, features: schemas };
|
|
727
|
+
return { product: productTag, dry_run: true, features: schemas, receipts };
|
|
709
728
|
const builder = await this.getProductBuilder(productTag);
|
|
710
729
|
const previous = new Map();
|
|
711
730
|
const changed = [];
|
|
712
731
|
try {
|
|
713
|
-
for (const schema of schemas) {
|
|
732
|
+
for (const [index, schema] of schemas.entries()) {
|
|
733
|
+
const receipt = receipts[index];
|
|
714
734
|
const existing = await builder.fetchFeature(schema.tag);
|
|
715
735
|
previous.set(schema.tag, existing);
|
|
736
|
+
receipt.operation = existing ? 'update' : 'create';
|
|
737
|
+
receipt.write = 'unknown';
|
|
738
|
+
receipt.compensation = existing ? 'not_needed' : 'retained';
|
|
739
|
+
changed.push(schema.tag);
|
|
716
740
|
if (existing)
|
|
717
741
|
await builder.updateFeature(schema.tag, schema);
|
|
718
742
|
else
|
|
719
743
|
await builder.createFeature(schema, true);
|
|
744
|
+
receipt.write = 'acknowledged';
|
|
720
745
|
const readBack = await builder.fetchFeature(schema.tag);
|
|
721
746
|
if (!readBack)
|
|
722
747
|
throw new Error(`Feature ${schema.tag} is missing after publish`);
|
|
723
|
-
|
|
748
|
+
if (this.computeFeatureFingerprint(readBack) !== this.computeFeatureFingerprint(schema)) {
|
|
749
|
+
throw new Error(`Feature ${schema.tag} content differs after publish`);
|
|
750
|
+
}
|
|
751
|
+
receipt.verified = true;
|
|
752
|
+
this.localConfigs.set(this.localKey(schema.tag, productTag), this.productFeatureToConfig(readBack));
|
|
724
753
|
}
|
|
725
|
-
return { product: productTag, dry_run: false, features: schemas };
|
|
754
|
+
return { product: productTag, dry_run: false, features: schemas, receipts };
|
|
726
755
|
}
|
|
727
756
|
catch (error) {
|
|
728
757
|
for (const tag of changed.reverse()) {
|
|
729
758
|
const old = previous.get(tag);
|
|
759
|
+
const receipt = receipts.find(item => item.tag === tag);
|
|
760
|
+
this.localConfigs.delete(this.localKey(tag, productTag));
|
|
730
761
|
try {
|
|
731
762
|
// Restore definitions that existed before this publication attempt.
|
|
732
763
|
// Never delete a newly created definition as compensation: a
|
|
@@ -734,14 +765,22 @@ class FeatureService {
|
|
|
734
765
|
// the write is durable. Automatically tombstoning that record turns
|
|
735
766
|
// an observation failure into data loss and caused entire feature
|
|
736
767
|
// catalogues to disappear after repeated retries.
|
|
737
|
-
if (old)
|
|
768
|
+
if (old) {
|
|
769
|
+
receipt.compensation = 'unknown';
|
|
738
770
|
await builder.updateFeature(tag, old);
|
|
771
|
+
const restored = await builder.fetchFeature(tag);
|
|
772
|
+
if (!restored || this.computeFeatureFingerprint(restored) !== this.computeFeatureFingerprint(old)) {
|
|
773
|
+
throw new Error(`Feature ${tag} restoration could not be verified`);
|
|
774
|
+
}
|
|
775
|
+
receipt.compensation = 'restored';
|
|
776
|
+
this.localConfigs.set(this.localKey(tag, productTag), this.productFeatureToConfig(restored));
|
|
777
|
+
}
|
|
739
778
|
}
|
|
740
779
|
catch (rollbackError) {
|
|
741
780
|
console.error('[Ductape] Feature batch rollback failed', { product: productTag, tag, rollbackError });
|
|
742
781
|
}
|
|
743
782
|
}
|
|
744
|
-
throw error;
|
|
783
|
+
throw new FeaturePublicationError(error instanceof Error ? error.message : String(error), productTag, receipts, error);
|
|
745
784
|
}
|
|
746
785
|
}
|
|
747
786
|
// ==================== WORKFLOW EXECUTION ====================
|
|
@@ -779,7 +818,7 @@ class FeatureService {
|
|
|
779
818
|
// Prefer locally cached feature (from a recent define()) so step conditions and full schema are preserved
|
|
780
819
|
const builder = await this.getProductBuilder(options.product);
|
|
781
820
|
console.log('[FeatureService] execute fetching feature', { tag: options.tag, product: options.product });
|
|
782
|
-
const localConfig = this.localConfigs.get(options.tag);
|
|
821
|
+
const localConfig = this.localConfigs.get(this.localKey(options.tag, options.product));
|
|
783
822
|
let feature = localConfig
|
|
784
823
|
? Object.assign(Object.assign({}, localConfig), { tag: localConfig.tag, name: localConfig.name, steps: localConfig.steps }) : null;
|
|
785
824
|
if (feature) {
|