Skip to content

Commit

Permalink
Merge pull request #17 from BretHudson/use-socket-io-rooms
Browse files Browse the repository at this point in the history
Use socket.io rooms & have pages listen to specific files
  • Loading branch information
BretHudson authored Feb 22, 2025
2 parents badb09f + cc7647d commit e0dbb9e
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 45 deletions.
47 changes: 40 additions & 7 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { Server } from 'socket.io';
import { instrument } from '@socket.io/admin-ui';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -18,7 +19,8 @@ console.log('Options: ' + JSON.stringify({ PORT, NODE_ENV }));

const [_nodePath, _scriptPath, ...args] = process.argv;

const [watchPath] = args;
const [_watchPath] = args;
const watchPath = path.join(_watchPath);

const publicPath = path.join(__dirname, '../public');
const server = http.createServer((req, res) => {
Expand All @@ -44,20 +46,50 @@ const server = http.createServer((req, res) => {
showError();
}
});

const adminOrigin = 'https://admin.socket.io';
const io = new Server(server, {
cors: {
origin: '*',
cors: (req, callback) => {
let origin = '*';
if (req.headers.origin === adminOrigin) origin = [adminOrigin];
callback(null, {
origin,
credentials: true,
});
},
});

instrument(io, { auth: false });

let lastJsUpdate = Date.now();
io.on('connection', (client) => {
console.log(`connect\t\tid: ${client.id}`);
const { origin: clientOrigin, pathName, assets } = client.handshake.query;

// TODO(bret): What about .php? or other files?
const paths = [
[watchPath, pathName + '.html'],
[watchPath, pathName, 'index.html'],
[watchPath, pathName],
].map((u) => path.join(...u));

const found = paths.find((p) => fs.existsSync(p) && fs.statSync(p).isFile());
if (!found) throw new Error('???', pathName);

const room = path.relative(watchPath, found);
client.join(room);

console.log(`connect\t\tid: ${client.id}\troom: ${room}`);

client.emit('reload-self', { lastJsUpdate });

client.on('watch-asset', (json) => {
const data = JSON.parse(json);
const room = path.join(data.room);
client.join(room);
});

client.on('disconnect', () => {
console.log(`disconnect\tid: ${client.id}`);
console.log(`disconnect\tid: ${client.id}\troom: ${room}`);
});
});

Expand Down Expand Up @@ -102,9 +134,10 @@ const sendUpdate = (eventType, fileName, contents) => {
const ext = path.extname(fileName);
const event = fileToEventMap[ext];
if (!event) return;
io.sockets.emit(event, { fileName, contents });
const room = path.join(fileName);
io.sockets.to(room).emit(event, { fileName, contents });
console.log(
`[${event}] ${fileName} update emitted (eventType: ${eventType})`,
`[${event}] ${fileName} update emitted to room "${room}" (eventType: ${eventType})`,
);
};

Expand Down
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"author": "Bret Hudson",
"license": "ISC",
"dependencies": {
"@socket.io/admin-ui": "^0.5.1",
"socket.io": "4.8.1"
},
"devDependencies": {
Expand Down
10 changes: 8 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import path from 'node:path';
import { defineConfig, devices } from '@playwright/test';
import { rootPath, SERVER_PORT, siteRootDir, WHR_PORT } from './tests/shared';
import {
rootPath,
SERVER_PORT,
siteRootDir,
tempDir,
WHR_PORT,
} from './tests/shared';

/**
* Read environment variables from file.
Expand Down Expand Up @@ -28,7 +34,7 @@ export default defineConfig({
reporter: 'html',
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
baseURL: `http://localhost:${SERVER_PORT}/${tempDir}/`,

trace: 'on-first-retry',
},
Expand Down
70 changes: 44 additions & 26 deletions public/reloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,9 @@ const updateCSS = (fileName) => {
updateElems(fileName, `link`, 'href');
};

const updateHTML = (fileName, _contents) => {
const names = ['.html', 'index.html', '/index.html'];

const updateHTML = (_contents) => {
let contents = _contents;

// TODO(bret): Revisit this
const targetPath = window.location.origin + '/' + fileName;
const valid = names.some((name) => {
const cur = window.location.origin + window.location.pathname + name;
return cur === targetPath || cur === targetPath.replace('index.html', '');
});

if (!valid) {
log('do not update');
return;
}

warn('updating!');
const script = document.getElementById('__web-hot-reloader');

// TODO(bret): Revisit this - string replacement is highly dependent on how the incoming HTML file is formatted :/
Expand All @@ -79,8 +64,10 @@ const updateHTML = (fileName, _contents) => {
document.write(contents);
document.close();

if (!document.getElementById('__web-hot-reloader'))
document.head.append(script);
document.getElementById('__web-hot-reloader')?.remove();
document.head.append(script);

log('reloaded page');
};

// TODO(bret): Figure out all the places an image could be used
Expand All @@ -106,8 +93,31 @@ const reloadSelf = () => {
};

let lastJsUpdate = null;
const initWebsocket = () => {
const socket = io(origin);
const initWebSocket = async () => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
switch (entry.initiatorType) {
case 'link':
case 'other':
case 'img':
break;
default:
return;
}
const room = entry.name.replace(window.location.origin + '/', '');
const data = JSON.stringify({ room });
socket.emit('watch-asset', data);
});
});
observer.observe({ type: 'resource', buffered: true });

const socket = io(origin, {
query: {
origin: window.location.origin,
pathName: window.location.pathname,
},
});
window['__whr-socket'] = socket;

socket.on('connect', () => {
Expand All @@ -116,22 +126,24 @@ const initWebsocket = () => {

socket.on('css-update', (data) => {
const { fileName } = data;
console.log('css-update', fileName);
updateCSS(fileName);
});

socket.on('html-update', (data) => {
const { fileName, contents } = data;
updateHTML(fileName, contents);
const { contents } = data;
updateHTML(contents);
});

socket.on('image-update', (data) => {
const { fileName } = data;
console.log('image-update', fileName);
updateImage(fileName);
});

socket.on('reload-self', (data) => {
if (lastJsUpdate && lastJsUpdate !== data.lastJsUpdate) {
log('Unloading hot loader, about to disconnect');
log('Shutting down reloader, about to disconnect');
socket.close();
reloadSelf();
}
Expand All @@ -140,19 +152,25 @@ const initWebsocket = () => {

socket.on('disconnect', () => {
log('Socket disconnected');

observer.disconnect();
});

log('Websocket initialized');
log('Socket initialized');
};

const init = () => {
const scriptSrc = `${origin}/socket.io/socket.io.js`;
const scriptElem = document.createElement('script');
scriptElem.onload = () => initWebsocket();
scriptElem.onload = () => {
window.requestAnimationFrame(async () => {
await initWebSocket();
});
};
scriptElem.src = scriptSrc;
document.head.append(scriptElem);

log('Hot loader initialized');
log('Reloader initialized');
};

init();
2 changes: 1 addition & 1 deletion tests/fixtures/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const initPages = async (browser: Browser) => {
);

const page = await browser.newPage();
await page.goto(`http://localhost:${SERVER_PORT}/_template/${url}`);
await page.goto('../_template/' + url);
pageData.defaultTitle = await page.title();
}),
);
Expand Down
16 changes: 14 additions & 2 deletions tests/fixtures/matchers/toBeReloaded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,22 @@ export const expect = baseExpect.extend({
pass: false,
};
}

let handle: Awaited<ReturnType<typeof locator.elementHandle>>;
try {
const handle = await locator.elementHandle();
handle = await locator.elementHandle();
} catch (e) {
return {
message: () => 'could not retrieve element handle',
pass: false,
};
}

try {
const good = await page.waitForFunction(
({ el, attr }) => el?.getAttribute(attr)?.includes('?'),
({ el, attr }) => {
return el?.getAttribute(attr)?.includes('?');
},
{ el: handle, attr },
);

Expand All @@ -50,6 +61,7 @@ export const expect = baseExpect.extend({
pass: Boolean(good),
};
} catch (e) {
console.log(e);
return {
message: () => 'element has not been reloaded via WHR',
pass: false,
Expand Down
3 changes: 2 additions & 1 deletion tests/helpers/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ export class Site {
let url: string | undefined = this.pagePaths[page];
if (url === undefined) throw new Error('ruh roh');
this.currentPage = page;
return this.page.goto(this.serverFilePath.url + url);
const path = this.serverFilePath.url + url;
return this.page.goto(path);
}

async replaceImage(src: string) {
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/server-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const constructServerFilePath = (): ServerFilePath => {
const _path = `test-${count++}-worker-${process.env.TEST_PARALLEL_INDEX}`;
const data = {
path: _path,
url: `http://localhost:${SERVER_PORT}/${tempDir}/${_path}/`,
url: `${_path}/`,
filePath: path.join(tempRoot, _path),
};

Expand Down
Loading

0 comments on commit e0dbb9e

Please sign in to comment.