Make elysia build/run in cluster mode
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --hot src/index.ts",
|
"dev": "bun run --hot src/index.ts",
|
||||||
"start": "bun run src/index.ts"
|
"start": "bun run src/index.ts",
|
||||||
|
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun-linux-x64 --outfile dist/server src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
|
|||||||
+10
-226
@@ -1,227 +1,11 @@
|
|||||||
import {
|
import cluster from 'node:cluster'
|
||||||
DEFAULT_STANDARDS_DATA,
|
import os from 'node:os'
|
||||||
LevelCalculator,
|
import process from 'node:process'
|
||||||
Standards,
|
|
||||||
} from "@blade-and-brawn/calculator";
|
|
||||||
|
|
||||||
import {
|
if (cluster.isPrimary) {
|
||||||
ActivityPerformanceSchema,
|
for (let i = 0; i < os.availableParallelism(); i++)
|
||||||
PlayerSchema,
|
cluster.fork()
|
||||||
type ActivityPerformance,
|
} else {
|
||||||
type Player,
|
await import('./server')
|
||||||
} from "@blade-and-brawn/domain"
|
console.log(`Worker ${process.pid} started`)
|
||||||
import { cors } from "@elysiajs/cors";
|
}
|
||||||
import { Elysia, NotFoundError, status } from "elysia";
|
|
||||||
import {
|
|
||||||
PrintfulService,
|
|
||||||
SyncService,
|
|
||||||
WebflowService,
|
|
||||||
Printful,
|
|
||||||
Webflow,
|
|
||||||
FetchError,
|
|
||||||
} from "@blade-and-brawn/commerce";
|
|
||||||
import zipcodesUs from "zipcodes-us";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
const levelCalculator = new LevelCalculator(
|
|
||||||
new Standards(DEFAULT_STANDARDS_DATA),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const app = new Elysia()
|
|
||||||
.use(
|
|
||||||
cors({
|
|
||||||
origin: [
|
|
||||||
// WEBSITE
|
|
||||||
// -----------
|
|
||||||
// production
|
|
||||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
|
||||||
// testing
|
|
||||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
|
||||||
|
|
||||||
// PORTAL
|
|
||||||
// -----------
|
|
||||||
// production
|
|
||||||
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
|
||||||
// development
|
|
||||||
"http://localhost:5173",
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
.error({
|
|
||||||
FetchError,
|
|
||||||
})
|
|
||||||
|
|
||||||
.onError(async ({ code, error }) => {
|
|
||||||
switch (code) {
|
|
||||||
case "FetchError":
|
|
||||||
console.error(error.message, await error.parse());
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.onAfterHandle(({ request, set }) => {
|
|
||||||
console.log(
|
|
||||||
JSON.stringify({
|
|
||||||
lvl: "info",
|
|
||||||
msg: "req",
|
|
||||||
method: request.method,
|
|
||||||
path: new URL(request.url).pathname,
|
|
||||||
status: set.status ?? 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
.get("/", () => "Hello world")
|
|
||||||
|
|
||||||
// CALCULATOR
|
|
||||||
|
|
||||||
.post("/calculate", async ({ body }) => {
|
|
||||||
return {
|
|
||||||
levels: levelCalculator.calculate(body.player, body.activityPerformances),
|
|
||||||
};
|
|
||||||
}, {
|
|
||||||
body: z.object({
|
|
||||||
player: PlayerSchema,
|
|
||||||
activityPerformances: z.array(ActivityPerformanceSchema)
|
|
||||||
}),
|
|
||||||
// afterResponse: ({ query, body, responseValue }) => {
|
|
||||||
// if (query.log === "true") {
|
|
||||||
// void fetch("https://kv-logger.xominus.workers.dev", {
|
|
||||||
// method: "POST",
|
|
||||||
// headers: { "content-type": "application/json" },
|
|
||||||
// body: JSON.stringify({
|
|
||||||
// output: responseValue,
|
|
||||||
// player: body.player,
|
|
||||||
// activityPerformances: body.activityPerformances
|
|
||||||
// }),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
|
|
||||||
// COMMERCE
|
|
||||||
// TODO: protect with api key/token
|
|
||||||
.group("/products", (app) =>
|
|
||||||
app
|
|
||||||
.group("/sync", (app) =>
|
|
||||||
app
|
|
||||||
// Sync status
|
|
||||||
.get("/", async ({ }) => SyncService.state)
|
|
||||||
// Full sync
|
|
||||||
.post("/", async ({ }) => {
|
|
||||||
if (SyncService.state.isSyncing)
|
|
||||||
return status(409, { error: "Sync already in progress" });
|
|
||||||
|
|
||||||
await SyncService.sync();
|
|
||||||
return { ok: true };
|
|
||||||
})
|
|
||||||
// Per-product sync
|
|
||||||
.post("/:printfulProductId", async ({ params }) => {
|
|
||||||
if (SyncService.state.isSyncing)
|
|
||||||
return status(409, { error: "Sync already in progress" });
|
|
||||||
|
|
||||||
await SyncService.sync(params.printfulProductId);
|
|
||||||
return { ok: true };
|
|
||||||
}, { params: z.object({ printfulProductId: z.number() }) }),
|
|
||||||
)
|
|
||||||
.get("/:printfulProductId", async ({ params }) => {
|
|
||||||
const printfulProductId = +params.printfulProductId;
|
|
||||||
const printfulProduct =
|
|
||||||
await PrintfulService.Products.get(printfulProductId);
|
|
||||||
if (!printfulProduct) {
|
|
||||||
return new NotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const webflowProductId =
|
|
||||||
printfulProduct.sync_product.external_id.split("-")[0];
|
|
||||||
if (!webflowProductId) {
|
|
||||||
return new NotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const webflowProduct =
|
|
||||||
await WebflowService.Products.get(webflowProductId);
|
|
||||||
|
|
||||||
return { printfulProduct, webflowProduct };
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// WEBHOOKS
|
|
||||||
.post("/webhook/printful", async ({ body }) => {
|
|
||||||
const payload = body as Printful.Webhook.EventPayload;
|
|
||||||
|
|
||||||
switch (payload.type) {
|
|
||||||
case Printful.Webhook.Event.ProductUpdated: {
|
|
||||||
const printfulProduct = payload.data.sync_product;
|
|
||||||
await SyncService.sync(printfulProduct.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Printful.Webhook.Event.ProductDeleted: {
|
|
||||||
await WebflowService.Products.remove(
|
|
||||||
payload.data.sync_product.external_id,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Printful.Webhook.Event.PackageShipped: {
|
|
||||||
const webflowOrderId = payload.data.order.external_id;
|
|
||||||
const shipInfo = payload.data.shipment;
|
|
||||||
|
|
||||||
await WebflowService.Orders.update(webflowOrderId, {
|
|
||||||
shippingTrackingURL: shipInfo.tracking_url,
|
|
||||||
shippingTracking: shipInfo.tracking_number,
|
|
||||||
shippingProvider: shipInfo.carrier,
|
|
||||||
});
|
|
||||||
await WebflowService.Orders.fulfill(webflowOrderId, {
|
|
||||||
sendOrderFulfilledEmail: true,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ok: true };
|
|
||||||
})
|
|
||||||
.post("/webhook/webflow", async ({ request, body, set }) => {
|
|
||||||
if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
|
|
||||||
set.status = 400;
|
|
||||||
return "Invalid signature";
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = body as Webflow.Webhook.EventPayload;
|
|
||||||
|
|
||||||
switch (payload.triggerType) {
|
|
||||||
case Webflow.Webhook.Event.OrderCreated: {
|
|
||||||
const webflowOrder = payload.payload;
|
|
||||||
|
|
||||||
await PrintfulService.Orders.create({
|
|
||||||
external_id: webflowOrder.orderId,
|
|
||||||
// TODO: derive from webflow
|
|
||||||
shipping: "STANDARD",
|
|
||||||
recipient: {
|
|
||||||
name: webflowOrder.shippingAddress.addressee,
|
|
||||||
address1: webflowOrder.shippingAddress.line1,
|
|
||||||
address2: webflowOrder.shippingAddress.line2,
|
|
||||||
city: webflowOrder.shippingAddress.city,
|
|
||||||
state_code: zipcodesUs.find(
|
|
||||||
webflowOrder.shippingAddress.postalCode.split(
|
|
||||||
"-",
|
|
||||||
)[0] ?? "",
|
|
||||||
).stateCode,
|
|
||||||
country_code: webflowOrder.shippingAddress.country,
|
|
||||||
zip: webflowOrder.shippingAddress.postalCode,
|
|
||||||
},
|
|
||||||
items: webflowOrder.purchasedItems.map(
|
|
||||||
(webflowOrderSku) => ({
|
|
||||||
external_variant_id: webflowOrderSku.variantId,
|
|
||||||
quantity: webflowOrderSku.count,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(3000);
|
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_STANDARDS_DATA,
|
||||||
|
LevelCalculator,
|
||||||
|
Standards,
|
||||||
|
} from "@blade-and-brawn/calculator";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActivityPerformanceSchema,
|
||||||
|
PlayerSchema,
|
||||||
|
} from "@blade-and-brawn/domain"
|
||||||
|
import { cors } from "@elysiajs/cors";
|
||||||
|
import { Elysia, NotFoundError, status } from "elysia";
|
||||||
|
import {
|
||||||
|
PrintfulService,
|
||||||
|
SyncService,
|
||||||
|
WebflowService,
|
||||||
|
Printful,
|
||||||
|
Webflow,
|
||||||
|
FetchError,
|
||||||
|
} from "@blade-and-brawn/commerce";
|
||||||
|
import zipcodesUs from "zipcodes-us";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const levelCalculator = new LevelCalculator(
|
||||||
|
new Standards(DEFAULT_STANDARDS_DATA),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cors({
|
||||||
|
origin: [
|
||||||
|
// WEBSITE
|
||||||
|
// -----------
|
||||||
|
// production
|
||||||
|
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
||||||
|
// testing
|
||||||
|
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
||||||
|
|
||||||
|
// PORTAL
|
||||||
|
// -----------
|
||||||
|
// production
|
||||||
|
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||||
|
// development
|
||||||
|
"http://localhost:5173",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
.error({
|
||||||
|
FetchError,
|
||||||
|
})
|
||||||
|
|
||||||
|
.onError(async ({ code, error }) => {
|
||||||
|
switch (code) {
|
||||||
|
case "FetchError":
|
||||||
|
console.error(error.message, await error.parse());
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.onAfterHandle(({ request, set }) => {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
lvl: "info",
|
||||||
|
msg: "req",
|
||||||
|
method: request.method,
|
||||||
|
path: new URL(request.url).pathname,
|
||||||
|
status: set.status ?? 200,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
.get("/", () => "Hello world")
|
||||||
|
|
||||||
|
// CALCULATOR
|
||||||
|
|
||||||
|
.post("/calculate", async ({ body }) => {
|
||||||
|
return {
|
||||||
|
levels: levelCalculator.calculate(body.player, body.activityPerformances),
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
body: z.object({
|
||||||
|
player: PlayerSchema,
|
||||||
|
activityPerformances: z.array(ActivityPerformanceSchema)
|
||||||
|
}),
|
||||||
|
// afterResponse: ({ query, body, responseValue }) => {
|
||||||
|
// if (query.log === "true") {
|
||||||
|
// void fetch("https://kv-logger.xominus.workers.dev", {
|
||||||
|
// method: "POST",
|
||||||
|
// headers: { "content-type": "application/json" },
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// output: responseValue,
|
||||||
|
// player: body.player,
|
||||||
|
// activityPerformances: body.activityPerformances
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
// COMMERCE
|
||||||
|
// TODO: protect with api key/token
|
||||||
|
.group("/products", (app) =>
|
||||||
|
app
|
||||||
|
.group("/sync", (app) =>
|
||||||
|
app
|
||||||
|
// Sync status
|
||||||
|
.get("/", async ({ }) => SyncService.state)
|
||||||
|
// Full sync
|
||||||
|
.post("/", async ({ }) => {
|
||||||
|
if (SyncService.state.isSyncing)
|
||||||
|
return status(409, { error: "Sync already in progress" });
|
||||||
|
|
||||||
|
await SyncService.sync();
|
||||||
|
return { ok: true };
|
||||||
|
})
|
||||||
|
// Per-product sync
|
||||||
|
.post("/:printfulProductId", async ({ params }) => {
|
||||||
|
if (SyncService.state.isSyncing)
|
||||||
|
return status(409, { error: "Sync already in progress" });
|
||||||
|
|
||||||
|
await SyncService.sync(params.printfulProductId);
|
||||||
|
return { ok: true };
|
||||||
|
}, { params: z.object({ printfulProductId: z.number() }) }),
|
||||||
|
)
|
||||||
|
.get("/:printfulProductId", async ({ params }) => {
|
||||||
|
const printfulProductId = +params.printfulProductId;
|
||||||
|
const printfulProduct =
|
||||||
|
await PrintfulService.Products.get(printfulProductId);
|
||||||
|
if (!printfulProduct) {
|
||||||
|
return new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webflowProductId =
|
||||||
|
printfulProduct.sync_product.external_id.split("-")[0];
|
||||||
|
if (!webflowProductId) {
|
||||||
|
return new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const webflowProduct =
|
||||||
|
await WebflowService.Products.get(webflowProductId);
|
||||||
|
|
||||||
|
return { printfulProduct, webflowProduct };
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// WEBHOOKS
|
||||||
|
.post("/webhook/printful", async ({ body }) => {
|
||||||
|
const payload = body as Printful.Webhook.EventPayload;
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case Printful.Webhook.Event.ProductUpdated: {
|
||||||
|
const printfulProduct = payload.data.sync_product;
|
||||||
|
await SyncService.sync(printfulProduct.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Printful.Webhook.Event.ProductDeleted: {
|
||||||
|
await WebflowService.Products.remove(
|
||||||
|
payload.data.sync_product.external_id,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Printful.Webhook.Event.PackageShipped: {
|
||||||
|
const webflowOrderId = payload.data.order.external_id;
|
||||||
|
const shipInfo = payload.data.shipment;
|
||||||
|
|
||||||
|
await WebflowService.Orders.update(webflowOrderId, {
|
||||||
|
shippingTrackingURL: shipInfo.tracking_url,
|
||||||
|
shippingTracking: shipInfo.tracking_number,
|
||||||
|
shippingProvider: shipInfo.carrier,
|
||||||
|
});
|
||||||
|
await WebflowService.Orders.fulfill(webflowOrderId, {
|
||||||
|
sendOrderFulfilledEmail: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
})
|
||||||
|
.post("/webhook/webflow", async ({ request, body, set }) => {
|
||||||
|
if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
|
||||||
|
set.status = 400;
|
||||||
|
return "Invalid signature";
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = body as Webflow.Webhook.EventPayload;
|
||||||
|
|
||||||
|
switch (payload.triggerType) {
|
||||||
|
case Webflow.Webhook.Event.OrderCreated: {
|
||||||
|
const webflowOrder = payload.payload;
|
||||||
|
|
||||||
|
await PrintfulService.Orders.create({
|
||||||
|
external_id: webflowOrder.orderId,
|
||||||
|
// TODO: derive from webflow
|
||||||
|
shipping: "STANDARD",
|
||||||
|
recipient: {
|
||||||
|
name: webflowOrder.shippingAddress.addressee,
|
||||||
|
address1: webflowOrder.shippingAddress.line1,
|
||||||
|
address2: webflowOrder.shippingAddress.line2,
|
||||||
|
city: webflowOrder.shippingAddress.city,
|
||||||
|
state_code: zipcodesUs.find(
|
||||||
|
webflowOrder.shippingAddress.postalCode.split(
|
||||||
|
"-",
|
||||||
|
)[0] ?? "",
|
||||||
|
).stateCode,
|
||||||
|
country_code: webflowOrder.shippingAddress.country,
|
||||||
|
zip: webflowOrder.shippingAddress.postalCode,
|
||||||
|
},
|
||||||
|
items: webflowOrder.purchasedItems.map(
|
||||||
|
(webflowOrderSku) => ({
|
||||||
|
external_variant_id: webflowOrderSku.variantId,
|
||||||
|
quantity: webflowOrderSku.count,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000);
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:api": "bun run --filter @blade-and-brawn/api dev",
|
"dev:api": "bun run --filter @blade-and-brawn/api dev",
|
||||||
|
"build:api": "bun run --filter @blade-and-brawn/api build",
|
||||||
"dev:portal": "bun run --filter @blade-and-brawn/portal dev",
|
"dev:portal": "bun run --filter @blade-and-brawn/portal dev",
|
||||||
"build:portal": "bun run --filter @blade-and-brawn/portal build"
|
"build:portal": "bun run --filter @blade-and-brawn/portal build"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user