Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
65630c2
feat(apps): harden local execution — serialization, Source, edge cases
tyffical Aug 27, 2026
ee42125
fix(apps): forward the real apps-backend runtime's own property shape
tyffical Aug 27, 2026
15db5c1
fix(apps): address PR review findings on $ seeding, test types, and s…
tyffical Aug 27, 2026
113ac9d
fix(apps): reject Map/Set results instead of silently flattening them…
tyffical Aug 27, 2026
1b313cf
fix(apps): re-check installedness on every call instead of caching a …
tyffical Aug 27, 2026
4a8acc5
fix(apps): reject non-finite numbers as a local-execution result
tyffical Aug 27, 2026
6dfa76b
fix(apps): preserve this-binding on a flat apps-backend runtime method
tyffical Aug 27, 2026
7eb0172
fix(apps): reject a Map/Set/non-finite number nested anywhere in a lo…
tyffical Aug 27, 2026
b7cf792
docs(apps): fix testDollar()'s stale reference to a removed helper
tyffical Aug 27, 2026
fe185d4
fix(apps): bound a registration load so it can't permanently poison l…
tyffical Aug 27, 2026
bd03db4
fix(apps): fail loudly on globalThis.$ access outside an active execu…
tyffical Aug 28, 2026
e10db5d
fix(apps): stop conflating a real empty-string JSON key with assertJs…
tyffical Aug 28, 2026
563c050
fix(apps): correct stale concurrency-safety comments on enqueue seria…
tyffical Aug 28, 2026
d7004e2
style(apps): tighten comment prose in local-execution.ts and executio…
tyffical Aug 28, 2026
190a216
fix(apps): stop throwing on typeof $ outside an execution, catch Symb…
tyffical Aug 28, 2026
670ee45
test(apps): cover registration-caching success path and no-false-posi…
tyffical Aug 28, 2026
77d0246
refactor(apps): remove unused EpochGuard.hasActiveScope and forceInva…
tyffical Aug 28, 2026
70831d5
refactor(apps): dedupe the action-catalog/apps-backend once-ever-regi…
tyffical Aug 28, 2026
e069aba
fix(apps): conclude execution scope on early failure, dedupe abandonm…
tyffical Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/plugins/apps/src/vite/execution-epoch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { createEpochGuard } from '@dd/apps-plugin/vite/execution-epoch';

describe('execution-epoch — createEpochGuard', () => {
test('Should invalidate an older scope once a newer one starts', () => {
const guard = createEpochGuard();
const older = guard.start();
expect(older.isCurrent()).toBe(true);

const newer = guard.start();
expect(older.isCurrent()).toBe(false);
expect(newer.isCurrent()).toBe(true);
});

test('Should make concludeIfCurrent a no-op returning false for an already-superseded scope', () => {
const guard = createEpochGuard();
const older = guard.start();
const newer = guard.start();

expect(older.concludeIfCurrent()).toBe(false);
// The newer scope must be unaffected by the older one's no-op conclude.
expect(newer.isCurrent()).toBe(true);
});

test('Should conclude a still-current scope, marking it no longer current', () => {
const guard = createEpochGuard();
const scope = guard.start();

expect(scope.concludeIfCurrent()).toBe(true);
expect(scope.isCurrent()).toBe(false);
});

test('Should make a second concludeIfCurrent call on the same scope a no-op', () => {
const guard = createEpochGuard();
const scope = guard.start();

expect(scope.concludeIfCurrent()).toBe(true);
expect(scope.concludeIfCurrent()).toBe(false);
});

test('Should keep independently-created guards from sharing any state', () => {
const guardA = createEpochGuard();
const guardB = createEpochGuard();

const scopeA = guardA.start();
const scopeB = guardB.start();

guardA.start();
expect(scopeA.isCurrent()).toBe(false);
expect(scopeB.isCurrent()).toBe(true);
});
});
38 changes: 38 additions & 0 deletions packages/plugins/apps/src/vite/execution-epoch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

/** Generation-counter guard so an abandoned scope's late cleanup can't touch a shared resource a newer scope now owns (used by `local-execution.ts`). */
export interface EpochScope {
/** True until a newer scope starts, or this scope is concluded or invalidated. */
isCurrent(): boolean;
/** Marks no scope active and returns true if still current, otherwise a no-op returning false — call in a `finally` to gate cleanup on still owning the resource. */
concludeIfCurrent(): boolean;
}

export interface EpochGuard {
/** Starts a new scope, superseding whichever one was previously active. */
start(): EpochScope;
}

export function createEpochGuard(): EpochGuard {
let currentGeneration = 0;
let activeGeneration: number | null = null;

return {
start() {
const myGeneration = ++currentGeneration;
activeGeneration = myGeneration;
return {
isCurrent: () => activeGeneration === myGeneration,
concludeIfCurrent: () => {
if (activeGeneration === myGeneration) {
activeGeneration = null;
return true;
}
return false;
},
};
},
};
}
Loading
Loading