Replace # private class members with _ prefix convention for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Isenhower 2026-02-21 16:01:09 -08:00
parent 111118cbb0
commit 94aa3d7ece
7 changed files with 102 additions and 102 deletions

View File

@ -4,29 +4,29 @@ import sirv from 'sirv';
export default class HttpServer
{
/** @member {http.Server} */
#server = null;
_server = null;
get port() {
return this.#server.address().port;
return this._server.address().port;
}
open() {
return new Promise((resolve, reject) => {
if (this.#server) {
if (this._server) {
return resolve();
}
const handler = sirv('./dist');
this.#server = http.createServer(handler);
this.#server.on('listening', () => resolve());
this.#server.listen();
this._server = http.createServer(handler);
this._server.on('listening', () => resolve());
this._server.listen();
});
}
async close() {
if (this.#server) {
await this.#server.close();
this.#server = null;
if (this._server) {
await this._server.close();
this._server = null;
}
}
}

View File

@ -12,43 +12,43 @@ const defaultViewport = {
export default class ScreenshotHelper
{
/** @type {HttpServer} */
#httpServer = null;
_httpServer = null;
/** @type {puppeteer.Browser} */
#browser = null;
_browser = null;
/** @type {puppeteer.Page} */
#page = null;
_page = null;
defaultParams = null;
get isOpen() {
return !!this.#browser;
return !!this._browser;
}
/** @type {puppeteer.Page} */
get page() {
return this.#page;
return this._page;
}
async open() {
await this.close();
// Start the HTTP server
this.#httpServer = new HttpServer;
await this.#httpServer.open();
this._httpServer = new HttpServer;
await this._httpServer.open();
// Connect to Browserless
this.#browser = await puppeteer.connect({
this._browser = await puppeteer.connect({
browserWSEndpoint: process.env.BROWSERLESS_ENDPOINT,
});
// Create a new page and set the viewport
this.#page = await this.#browser.newPage();
this._page = await this._browser.newPage();
await this.applyViewport();
}
async applyViewport(viewport = {}) {
if (this.#page) {
await this.#page.setViewport({
if (this._page) {
await this._page.setViewport({
...defaultViewport,
...viewport,
});
@ -64,7 +64,7 @@ export default class ScreenshotHelper
// Navigate to the URL
let host = process.env.SCREENSHOT_HOST || 'localhost';
let url = new URL(`http://${host}:${this.#httpServer.port}/screenshots/`);
let url = new URL(`http://${host}:${this._httpServer.port}/screenshots/`);
url.hash = path;
let params = {
@ -80,31 +80,31 @@ export default class ScreenshotHelper
.join('&');
}
await this.#page.goto(url, {
await this._page.goto(url, {
waitUntil: 'networkidle0', // Wait until the network is idle
});
// Wait an additional 1000ms
await this.#page.waitForNetworkIdle({ idleTime: 1000 });
await this._page.waitForNetworkIdle({ idleTime: 1000 });
// Take the screenshot
return await this.#page.screenshot();
return await this._page.screenshot();
}
async close() {
if (this.#httpServer) {
await this.#httpServer.close();
if (this._httpServer) {
await this._httpServer.close();
}
this.#httpServer = null;
this._httpServer = null;
if (this.#page) {
await this.#page.close();
if (this._page) {
await this._page.close();
}
this.#page = null;
this._page = null;
if (this.#browser) {
await this.#browser.close();
if (this._browser) {
await this._browser.close();
}
this.#browser = null;
this._browser = null;
}
}

View File

@ -23,16 +23,16 @@ export default class StatusGeneratorManager
}
async sendStatuses(force = false) {
let availableClients = await this.#getAvailableClients();
let availableClients = await this._getAvailableClients();
// Create screenshots in parallel (via Browserless)
let statusPromises = this.#getStatuses(availableClients, force);
let statusPromises = this._getStatuses(availableClients, force);
// Process each client in parallel (while maintaining post order)
await this.#sendStatusesToClients(statusPromises, availableClients);
await this._sendStatusesToClients(statusPromises, availableClients);
}
async #getAvailableClients() {
async _getAvailableClients() {
let clients = [];
for (let client of this.clients) {
@ -47,11 +47,11 @@ export default class StatusGeneratorManager
return clients;
}
#getStatuses(availableClients, force) {
return this.generators.map(generator => this.#getStatus(availableClients, generator, force));
_getStatuses(availableClients, force) {
return this.generators.map(generator => this._getStatus(availableClients, generator, force));
}
async #getStatus(availableClients, generator, force) {
async _getStatus(availableClients, generator, force) {
let screenshotHelper = new ScreenshotHelper;
try {
let clients = [];
@ -82,23 +82,23 @@ export default class StatusGeneratorManager
return null;
}
#sendStatusesToClients(statusPromises, availableClients) {
return Promise.allSettled(availableClients.map(client => this.#sendStatusesToClient(statusPromises, client)));
_sendStatusesToClients(statusPromises, availableClients) {
return Promise.allSettled(availableClients.map(client => this._sendStatusesToClient(statusPromises, client)));
}
async #sendStatusesToClient(statusPromises, client) {
async _sendStatusesToClient(statusPromises, client) {
for (let promise of statusPromises) {
let statusDetails = await promise;
if (statusDetails && statusDetails.clients.includes(client)) {
let { generator, status } = statusDetails;
await this.#sendToClient(generator, status, client);
await this._sendToClient(generator, status, client);
}
}
}
async #sendToClient(generator, status, client) {
async _sendToClient(generator, status, client) {
this.console(generator, client).info('Posting...');
try {
await client.send(status, generator);

View File

@ -9,7 +9,7 @@ export default class BlueskyClient extends Client
key = 'bluesky';
name = 'Bluesky';
#agent;
_agent;
async canSend() {
return process.env.BLUESKY_SERVICE
@ -18,12 +18,12 @@ export default class BlueskyClient extends Client
}
async login() {
if (!this.#agent) {
this.#agent = new BskyAgent({
if (!this._agent) {
this._agent = new BskyAgent({
service: process.env.BLUESKY_SERVICE,
});
await this.#agent.login({
await this._agent.login({
identifier: process.env.BLUESKY_IDENTIFIER,
password: process.env.BLUESKY_PASSWORD,
});
@ -34,9 +34,9 @@ export default class BlueskyClient extends Client
await this.login();
let jpeg = await sharp(avatarBuffer).jpeg().toBuffer();
let uploadResponse = await this.#agent.uploadBlob(jpeg, { encoding: 'image/jpeg' });
let uploadResponse = await this._agent.uploadBlob(jpeg, { encoding: 'image/jpeg' });
await this.#agent.upsertProfile((existing) => {
await this._agent.upsertProfile((existing) => {
return {
...existing,
avatar: uploadResponse.data.blob,
@ -62,7 +62,7 @@ export default class BlueskyClient extends Client
let metadata = await jpeg.metadata();
let buffer = await jpeg.toBuffer();
let response = await this.#agent.uploadBlob(buffer, { encoding: 'image/jpeg' });
let response = await this._agent.uploadBlob(buffer, { encoding: 'image/jpeg' });
return {
image: response.data.blob,
@ -77,9 +77,9 @@ export default class BlueskyClient extends Client
text: status.status,
});
await rt.detectFacets(this.#agent);
await rt.detectFacets(this._agent);
await this.#agent.post({
await this._agent.post({
text: rt.text,
facets: rt.facets,
embed: {

View File

@ -6,26 +6,26 @@ export default class MastodonClient extends Client
key = 'mastodon';
name = 'Mastodon';
#url;
#accessToken;
#visibility;
_url;
_accessToken;
_visibility;
constructor() {
super();
this.#url = process.env.MASTODON_URL;
this.#accessToken = process.env.MASTODON_ACCESS_TOKEN;
this.#visibility = process.env.MASTODON_VISIBILITY || 'public';
this._url = process.env.MASTODON_URL;
this._accessToken = process.env.MASTODON_ACCESS_TOKEN;
this._visibility = process.env.MASTODON_VISIBILITY || 'public';
}
async canSend() {
return this.#url && this.#accessToken;
return this._url && this._accessToken;
}
async updateProfile(avatarBuffer, displayName) {
const masto = await createRestAPIClient({
url: this.#url,
accessToken: this.#accessToken,
url: this._url,
accessToken: this._accessToken,
disableVersionCheck: true,
disableExperimentalWarning: true,
});
@ -45,8 +45,8 @@ export default class MastodonClient extends Client
try {
// Mastodon API
const masto = await createRestAPIClient({
url: this.#url,
accessToken: this.#accessToken,
url: this._url,
accessToken: this._accessToken,
disableVersionCheck: true,
disableExperimentalWarning: true,
});
@ -71,7 +71,7 @@ export default class MastodonClient extends Client
spoilerText: status.contentWrapper,
sensitive: !!status.contentWrapper, // Without the sensitive property the image is still visible
mediaIds,
visibility: this.#visibility,
visibility: this._visibility,
});
} catch (error) {
console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message);

View File

@ -8,9 +8,9 @@ export default class ThreadsClient extends Client {
key = 'threads';
name = 'Threads';
#baseUrl = 'https://graph.threads.net/v1.0';
#tokenCache = new ValueCache('threads.token');
#accessToken;
_baseUrl = 'https://graph.threads.net/v1.0';
_tokenCache = new ValueCache('threads.token');
_accessToken;
get console() {
return this._console ??= prefixedConsole('Social', this.name);
@ -23,32 +23,32 @@ export default class ThreadsClient extends Client {
&& process.env.AWS_S3_ENDPOINT;
}
async #getAccessToken() {
if (this.#accessToken) {
return this.#accessToken;
async _getAccessToken() {
if (this._accessToken) {
return this._accessToken;
}
// Try to use a previously refreshed token from cache
let cached = await this.#tokenCache.getData();
let cached = await this._tokenCache.getData();
if (cached) {
this.#accessToken = cached;
return this.#accessToken;
this._accessToken = cached;
return this._accessToken;
}
// Fall back to the .env token
this.#accessToken = process.env.THREADS_ACCESS_TOKEN;
return this.#accessToken;
this._accessToken = process.env.THREADS_ACCESS_TOKEN;
return this._accessToken;
}
async #refreshToken() {
let currentToken = await this.#getAccessToken();
async _refreshToken() {
let currentToken = await this._getAccessToken();
let params = new URLSearchParams({
grant_type: 'th_refresh_token',
access_token: currentToken,
});
let response = await fetch(`${this.#baseUrl}/refresh_access_token?${params}`);
let response = await fetch(`${this._baseUrl}/refresh_access_token?${params}`);
let data = await response.json();
if (data.error) {
@ -56,11 +56,11 @@ export default class ThreadsClient extends Client {
return;
}
this.#accessToken = data.access_token;
this._accessToken = data.access_token;
// Cache the token with its expiry
let expires = new Date(Date.now() + data.expires_in * 1000);
await this.#tokenCache.setData(data.access_token, expires);
await this._tokenCache.setData(data.access_token, expires);
this.console.log(`Token refreshed, expires ${expires.toISOString()}`);
}
@ -73,30 +73,30 @@ export default class ThreadsClient extends Client {
try {
// Refresh the token if it hasn't been refreshed in the last 24 hours
let lastRefresh = await this.#tokenCache.getCachedAt();
let lastRefresh = await this._tokenCache.getCachedAt();
let oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
if (!lastRefresh || lastRefresh < oneDayAgo) {
await this.#refreshToken();
await this._refreshToken();
}
let accessToken = await this.#getAccessToken();
let accessToken = await this._getAccessToken();
let jpeg = await sharp(status.media[0].file).jpeg().toBuffer();
// Upload image to S3 so it's publicly accessible
let { imageUrl, s3Key } = await this.#uploadImage(jpeg, generator.key);
let { imageUrl, s3Key } = await this._uploadImage(jpeg, generator.key);
try {
// Create a media container
let containerId = await this.#createContainer(status.status, imageUrl, accessToken);
let containerId = await this._createContainer(status.status, imageUrl, accessToken);
// Wait for the container to finish processing
await this.#waitForContainer(containerId, accessToken);
await this._waitForContainer(containerId, accessToken);
// Publish the container
await this.#publish(containerId, accessToken);
await this._publish(containerId, accessToken);
} finally {
// Clean up the temporary image from S3
await this.#deleteImage(s3Key);
await this._deleteImage(s3Key);
}
} catch (error) {
this.console.error(`Failed to post ${generator.key}:`, error.message);
@ -104,7 +104,7 @@ export default class ThreadsClient extends Client {
}
}
async #uploadImage(buffer, key) {
async _uploadImage(buffer, key) {
let s3 = new S3Client({
endpoint: process.env.AWS_S3_ENDPOINT,
region: process.env.AWS_REGION,
@ -130,7 +130,7 @@ export default class ThreadsClient extends Client {
return { imageUrl, s3Key };
}
async #deleteImage(s3Key) {
async _deleteImage(s3Key) {
try {
let s3 = new S3Client({
endpoint: process.env.AWS_S3_ENDPOINT,
@ -150,7 +150,7 @@ export default class ThreadsClient extends Client {
}
}
async #createContainer(text, imageUrl, accessToken) {
async _createContainer(text, imageUrl, accessToken) {
let userId = process.env.THREADS_USER_ID;
let params = new URLSearchParams({
media_type: 'IMAGE',
@ -159,7 +159,7 @@ export default class ThreadsClient extends Client {
access_token: accessToken,
});
let response = await fetch(`${this.#baseUrl}/${userId}/threads`, {
let response = await fetch(`${this._baseUrl}/${userId}/threads`, {
method: 'POST',
body: params,
});
@ -177,7 +177,7 @@ export default class ThreadsClient extends Client {
return data.id;
}
async #waitForContainer(containerId, accessToken) {
async _waitForContainer(containerId, accessToken) {
// Poll container status until it's ready (or timeout after 60s)
let maxAttempts = 12;
@ -187,7 +187,7 @@ export default class ThreadsClient extends Client {
access_token: accessToken,
});
let response = await fetch(`${this.#baseUrl}/${containerId}?${params}`);
let response = await fetch(`${this._baseUrl}/${containerId}?${params}`);
let data = await response.json();
if (data.status === 'FINISHED') {
@ -205,14 +205,14 @@ export default class ThreadsClient extends Client {
throw new Error('Container processing timed out');
}
async #publish(containerId, accessToken) {
async _publish(containerId, accessToken) {
let userId = process.env.THREADS_USER_ID;
let params = new URLSearchParams({
creation_id: containerId,
access_token: accessToken,
});
let response = await fetch(`${this.#baseUrl}/${userId}/threads_publish`, {
let response = await fetch(`${this._baseUrl}/${userId}/threads_publish`, {
method: 'POST',
body: params,
});

View File

@ -7,7 +7,7 @@ export default class TwitterClient extends Client
name = 'Twitter';
/** @member {TwitterApi} */
#api;
_api;
async canSend() {
return process.env.TWITTER_CONSUMER_KEY
@ -17,8 +17,8 @@ export default class TwitterClient extends Client
}
api() {
if (!this.#api) {
this.#api = new TwitterApi({
if (!this._api) {
this._api = new TwitterApi({
appKey: process.env.TWITTER_CONSUMER_KEY,
appSecret: process.env.TWITTER_CONSUMER_SECRET,
accessToken: process.env.TWITTER_ACCESS_TOKEN_KEY,
@ -26,7 +26,7 @@ export default class TwitterClient extends Client
});
}
return this.#api;
return this._api;
}
async send(status, generator) {