@tummycrypt/acuity-middleware 0.1.0 → 0.1.1
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/adapters/acuity-scraper.d.ts +8 -0
- package/dist/adapters/acuity-scraper.d.ts.map +1 -0
- package/dist/adapters/acuity-scraper.js +8 -0
- package/dist/adapters/acuity-scraper.js.map +1 -0
- package/dist/adapters/types.d.ts +8 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +8 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/acuity-wizard.d.ts +49 -0
- package/dist/middleware/acuity-wizard.d.ts.map +1 -0
- package/dist/middleware/acuity-wizard.js +265 -0
- package/dist/middleware/acuity-wizard.js.map +1 -0
- package/dist/middleware/browser-service.d.ts +53 -0
- package/dist/middleware/browser-service.d.ts.map +1 -0
- package/dist/middleware/browser-service.js +105 -0
- package/dist/middleware/browser-service.js.map +1 -0
- package/dist/middleware/errors.d.ts +58 -0
- package/dist/middleware/errors.d.ts.map +1 -0
- package/dist/middleware/errors.js +43 -0
- package/dist/middleware/errors.js.map +1 -0
- package/{src/middleware/index.ts → dist/middleware/index.d.ts} +5 -52
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +38 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +26 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +65 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/remote-adapter.d.ts +45 -0
- package/dist/middleware/remote-adapter.d.ts.map +1 -0
- package/dist/middleware/remote-adapter.js +178 -0
- package/dist/middleware/remote-adapter.js.map +1 -0
- package/dist/middleware/selector-health.d.ts +44 -0
- package/dist/middleware/selector-health.d.ts.map +1 -0
- package/dist/middleware/selector-health.js +144 -0
- package/dist/middleware/selector-health.js.map +1 -0
- package/dist/middleware/selectors.d.ts +108 -0
- package/dist/middleware/selectors.d.ts.map +1 -0
- package/dist/middleware/selectors.js +249 -0
- package/dist/middleware/selectors.js.map +1 -0
- package/dist/middleware/server.d.ts +34 -0
- package/dist/middleware/server.d.ts.map +1 -0
- package/dist/middleware/server.js +377 -0
- package/dist/middleware/server.js.map +1 -0
- package/dist/middleware/service-resolver.d.ts +46 -0
- package/dist/middleware/service-resolver.d.ts.map +1 -0
- package/dist/middleware/service-resolver.js +274 -0
- package/dist/middleware/service-resolver.js.map +1 -0
- package/dist/middleware/slot-parser.d.ts +29 -0
- package/dist/middleware/slot-parser.d.ts.map +1 -0
- package/dist/middleware/slot-parser.js +50 -0
- package/dist/middleware/slot-parser.js.map +1 -0
- package/dist/middleware/steps/__tests__/fixtures.d.ts +14 -0
- package/dist/middleware/steps/__tests__/fixtures.d.ts.map +1 -0
- package/dist/middleware/steps/__tests__/fixtures.js +204 -0
- package/dist/middleware/steps/__tests__/fixtures.js.map +1 -0
- package/dist/middleware/steps/bypass-payment.d.ts +54 -0
- package/dist/middleware/steps/bypass-payment.d.ts.map +1 -0
- package/dist/middleware/steps/bypass-payment.js +164 -0
- package/dist/middleware/steps/bypass-payment.js.map +1 -0
- package/dist/middleware/steps/extract-business.d.ts +93 -0
- package/dist/middleware/steps/extract-business.d.ts.map +1 -0
- package/dist/middleware/steps/extract-business.js +170 -0
- package/dist/middleware/steps/extract-business.js.map +1 -0
- package/dist/middleware/steps/extract.d.ts +41 -0
- package/dist/middleware/steps/extract.d.ts.map +1 -0
- package/dist/middleware/steps/extract.js +128 -0
- package/dist/middleware/steps/extract.js.map +1 -0
- package/dist/middleware/steps/fill-form.d.ts +45 -0
- package/dist/middleware/steps/fill-form.d.ts.map +1 -0
- package/dist/middleware/steps/fill-form.js +262 -0
- package/dist/middleware/steps/fill-form.js.map +1 -0
- package/dist/middleware/steps/index.d.ts +12 -0
- package/dist/middleware/steps/index.d.ts.map +1 -0
- package/dist/middleware/steps/index.js +12 -0
- package/dist/middleware/steps/index.js.map +1 -0
- package/dist/middleware/steps/navigate.d.ts +51 -0
- package/dist/middleware/steps/navigate.d.ts.map +1 -0
- package/dist/middleware/steps/navigate.js +391 -0
- package/dist/middleware/steps/navigate.js.map +1 -0
- package/dist/middleware/steps/read-availability.d.ts +37 -0
- package/dist/middleware/steps/read-availability.d.ts.map +1 -0
- package/dist/middleware/steps/read-availability.js +298 -0
- package/dist/middleware/steps/read-availability.js.map +1 -0
- package/dist/middleware/steps/read-slots.d.ts +33 -0
- package/dist/middleware/steps/read-slots.d.ts.map +1 -0
- package/dist/middleware/steps/read-slots.js +295 -0
- package/dist/middleware/steps/read-slots.js.map +1 -0
- package/dist/middleware/steps/read-via-url.d.ts +39 -0
- package/dist/middleware/steps/read-via-url.d.ts.map +1 -0
- package/dist/middleware/steps/read-via-url.js +141 -0
- package/dist/middleware/steps/read-via-url.js.map +1 -0
- package/dist/middleware/steps/submit.d.ts +22 -0
- package/dist/middleware/steps/submit.d.ts.map +1 -0
- package/dist/middleware/steps/submit.js +112 -0
- package/dist/middleware/steps/submit.js.map +1 -0
- package/dist/middleware/wizard-calendar.d.ts +37 -0
- package/dist/middleware/wizard-calendar.d.ts.map +1 -0
- package/dist/middleware/wizard-calendar.js +177 -0
- package/dist/middleware/wizard-calendar.js.map +1 -0
- package/dist/middleware/wizard-service.d.ts +30 -0
- package/dist/middleware/wizard-service.d.ts.map +1 -0
- package/dist/middleware/wizard-service.js +89 -0
- package/dist/middleware/wizard-service.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/{src/server.ts → dist/server.js} +1 -0
- package/dist/server.js.map +1 -0
- package/package.json +16 -4
- package/.github/workflows/build-paper.yml +0 -39
- package/.github/workflows/ci.yml +0 -37
- package/Dockerfile +0 -53
- package/docs/blog-post.mdx +0 -240
- package/docs/paper/IEEEtran.bst +0 -2409
- package/docs/paper/IEEEtran.cls +0 -6347
- package/docs/paper/acuity-middleware-paper.tex +0 -375
- package/docs/paper/balance.sty +0 -87
- package/docs/paper/references.bib +0 -231
- package/docs/paper.md +0 -400
- package/flake.nix +0 -32
- package/modal-app.py +0 -82
- package/src/adapters/acuity-scraper.ts +0 -543
- package/src/adapters/types.ts +0 -193
- package/src/core/types.ts +0 -325
- package/src/index.ts +0 -75
- package/src/middleware/acuity-wizard.ts +0 -456
- package/src/middleware/browser-service.ts +0 -183
- package/src/middleware/errors.ts +0 -70
- package/src/middleware/remote-adapter.ts +0 -246
- package/src/middleware/selectors.ts +0 -308
- package/src/middleware/server.ts +0 -372
- package/src/middleware/steps/bypass-payment.ts +0 -226
- package/src/middleware/steps/extract.ts +0 -174
- package/src/middleware/steps/fill-form.ts +0 -359
- package/src/middleware/steps/index.ts +0 -27
- package/src/middleware/steps/navigate.ts +0 -537
- package/src/middleware/steps/read-availability.ts +0 -399
- package/src/middleware/steps/read-slots.ts +0 -405
- package/src/middleware/steps/submit.ts +0 -168
- package/tsconfig.json +0 -25
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export scraper from @tummycrypt/scheduling-kit.
|
|
3
|
+
*
|
|
4
|
+
* @deprecated — see scheduling-kit's acuity-scraper.ts for deprecation details.
|
|
5
|
+
* Prefer extract-business.ts (BUSINESS object) and wizard steps (readAvailableDates, readTimeSlots).
|
|
6
|
+
*/
|
|
7
|
+
export { AcuityScraper, createScraperAdapter, scrapeServicesOnce, scrapeAvailabilityOnce, type ScraperConfig, type ScrapedService, type ScrapedAvailability, type ScrapedTimeSlot, } from '@tummycrypt/scheduling-kit/adapters';
|
|
8
|
+
//# sourceMappingURL=acuity-scraper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acuity-scraper.d.ts","sourceRoot":"","sources":["../../src/adapters/acuity-scraper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export scraper from @tummycrypt/scheduling-kit.
|
|
3
|
+
*
|
|
4
|
+
* @deprecated — see scheduling-kit's acuity-scraper.ts for deprecation details.
|
|
5
|
+
* Prefer extract-business.ts (BUSINESS object) and wizard steps (readAvailableDates, readTimeSlots).
|
|
6
|
+
*/
|
|
7
|
+
export { AcuityScraper, createScraperAdapter, scrapeServicesOnce, scrapeAvailabilityOnce, } from '@tummycrypt/scheduling-kit/adapters';
|
|
8
|
+
//# sourceMappingURL=acuity-scraper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acuity-scraper.js","sourceRoot":"","sources":["../../src/adapters/acuity-scraper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,GAKvB,MAAM,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export adapter types from @tummycrypt/scheduling-kit.
|
|
3
|
+
*
|
|
4
|
+
* Previously this file was a full copy of scheduling-kit's adapter interface.
|
|
5
|
+
* Consolidated to prevent type drift.
|
|
6
|
+
*/
|
|
7
|
+
export * from '@tummycrypt/scheduling-kit/adapters';
|
|
8
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export adapter types from @tummycrypt/scheduling-kit.
|
|
3
|
+
*
|
|
4
|
+
* Previously this file was a full copy of scheduling-kit's adapter interface.
|
|
5
|
+
* Consolidated to prevent type drift.
|
|
6
|
+
*/
|
|
7
|
+
export * from '@tummycrypt/scheduling-kit/adapters';
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export core types from @tummycrypt/scheduling-kit.
|
|
3
|
+
*
|
|
4
|
+
* We selectively re-export only the types module (not utils/pipelines)
|
|
5
|
+
* to avoid pulling in fp-ts runtime code that has ESM import issues
|
|
6
|
+
* in the published package.
|
|
7
|
+
*/
|
|
8
|
+
export type { AcuityError, CalComError, PaymentError, ValidationError, ReservationError, IdempotencyError, InfrastructureError, SchedulingError, SchedulingResult, SchedulingReader, Service, Provider, TimeSlot, AvailableDate, ClientInfo, BookingRequest, Booking, BookingStatus, PaymentStatus, SlotReservation, PaymentIntent, PaymentResult, RefundResult, SchedulingConfig, } from '@tummycrypt/scheduling-kit/core';
|
|
9
|
+
export { Errors } from '@tummycrypt/scheduling-kit/core';
|
|
10
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,YAAY,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,UAAU,EACV,cAAc,EACd,OAAO,EACP,aAAa,EACb,aAAa,EACb,eAAe,EACf,aAAa,EACb,aAAa,EACb,YAAY,EACZ,gBAAgB,GACjB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tummycrypt/acuity-middleware
|
|
3
|
+
*
|
|
4
|
+
* Playwright-based Acuity booking middleware server.
|
|
5
|
+
* Proxies booking operations through browser automation.
|
|
6
|
+
*/
|
|
7
|
+
export type { Service, Provider, TimeSlot, AvailableDate, Booking, BookingRequest, ClientInfo, SchedulingError, SchedulingResult, BookingStatus, PaymentStatus, } from './core/types.js';
|
|
8
|
+
export { Errors } from './core/types.js';
|
|
9
|
+
export type { SchedulingAdapter } from './adapters/types.js';
|
|
10
|
+
export { createWizardAdapter, type WizardAdapterConfig, } from './middleware/acuity-wizard.js';
|
|
11
|
+
export { createRemoteWizardAdapter, type RemoteAdapterConfig, } from './middleware/remote-adapter.js';
|
|
12
|
+
export { BrowserService, BrowserServiceLive, BrowserServiceTest, defaultBrowserConfig, type BrowserConfig, type BrowserServiceShape, } from './middleware/browser-service.js';
|
|
13
|
+
export { BrowserError, SelectorError, WizardStepError, CouponError, toSchedulingError, type MiddlewareError, } from './middleware/errors.js';
|
|
14
|
+
export { Selectors, resolveSelector, resolve, probeSelector, probe, healthCheck, type SelectorKey, type ResolvedSelector, } from './middleware/selectors.js';
|
|
15
|
+
export { createScraperAdapter, AcuityScraper, type ScraperConfig, } from './adapters/acuity-scraper.js';
|
|
16
|
+
export { server } from './middleware/server.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,YAAY,EACX,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,OAAO,EACP,cAAc,EACd,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,aAAa,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG7D,OAAO,EACN,mBAAmB,EACnB,KAAK,mBAAmB,GACxB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACN,yBAAyB,EACzB,KAAK,mBAAmB,GACxB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,mBAAmB,GACxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACN,YAAY,EACZ,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,KAAK,eAAe,GACpB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACN,SAAS,EACT,eAAe,EACf,OAAO,EACP,aAAa,EACb,KAAK,EACL,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACrB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EACN,oBAAoB,EACpB,aAAa,EACb,KAAK,aAAa,GAClB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tummycrypt/acuity-middleware
|
|
3
|
+
*
|
|
4
|
+
* Playwright-based Acuity booking middleware server.
|
|
5
|
+
* Proxies booking operations through browser automation.
|
|
6
|
+
*/
|
|
7
|
+
export { Errors } from './core/types.js';
|
|
8
|
+
// Middleware exports
|
|
9
|
+
export { createWizardAdapter, } from './middleware/acuity-wizard.js';
|
|
10
|
+
export { createRemoteWizardAdapter, } from './middleware/remote-adapter.js';
|
|
11
|
+
export { BrowserService, BrowserServiceLive, BrowserServiceTest, defaultBrowserConfig, } from './middleware/browser-service.js';
|
|
12
|
+
export { BrowserError, SelectorError, WizardStepError, CouponError, toSchedulingError, } from './middleware/errors.js';
|
|
13
|
+
export { Selectors, resolveSelector, resolve, probeSelector, probe, healthCheck, } from './middleware/selectors.js';
|
|
14
|
+
// Scraper
|
|
15
|
+
export { createScraperAdapter, AcuityScraper, } from './adapters/acuity-scraper.js';
|
|
16
|
+
// Server
|
|
17
|
+
export { server } from './middleware/server.js';
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAKzC,qBAAqB;AACrB,OAAO,EACN,mBAAmB,GAEnB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACN,yBAAyB,GAEzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GAGpB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACN,YAAY,EACZ,aAAa,EACb,eAAe,EACf,WAAW,EACX,iBAAiB,GAEjB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACN,SAAS,EACT,eAAe,EACf,OAAO,EACP,aAAa,EACb,KAAK,EACL,WAAW,GAGX,MAAM,2BAA2B,CAAC;AAEnC,UAAU;AACV,OAAO,EACN,oBAAoB,EACpB,aAAa,GAEb,MAAM,8BAA8B,CAAC;AAEtC,SAAS;AACT,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acuity Wizard Adapter
|
|
3
|
+
*
|
|
4
|
+
* Full SchedulingAdapter implementation that uses Effect TS middleware
|
|
5
|
+
* to puppeteer the Acuity booking wizard.
|
|
6
|
+
*
|
|
7
|
+
* Read operations:
|
|
8
|
+
* - getServices: returns static service catalog (if provided) or falls back to scraper
|
|
9
|
+
* - getAvailableDates: Effect program navigates wizard calendar (verified selectors)
|
|
10
|
+
* - getAvailableSlots: Effect program clicks day tile and reads time buttons
|
|
11
|
+
*
|
|
12
|
+
* Write operations:
|
|
13
|
+
* - createBooking/createBookingWithPaymentRef: Effect TS middleware via Playwright
|
|
14
|
+
*
|
|
15
|
+
* This is the bridge between Effect TS (middleware layer) and
|
|
16
|
+
* fp-ts (pipeline/adapter layer).
|
|
17
|
+
*/
|
|
18
|
+
import type { SchedulingAdapter } from '../adapters/types.js';
|
|
19
|
+
import type { Service } from '../core/types.js';
|
|
20
|
+
import { type BrowserConfig } from './browser-service.js';
|
|
21
|
+
import { type RemoteAdapterConfig } from './remote-adapter.js';
|
|
22
|
+
export interface WizardAdapterConfig extends Partial<BrowserConfig> {
|
|
23
|
+
/** Base URL for the Acuity scheduling page */
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
/** Pre-configured coupon code for payment bypass (from ACUITY_BYPASS_COUPON env) */
|
|
26
|
+
couponCode?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Execution mode:
|
|
29
|
+
* - 'local': Run Playwright in-process (default, requires Chromium installed)
|
|
30
|
+
* - 'remote': Proxy all operations to a remote middleware server via HTTP
|
|
31
|
+
*/
|
|
32
|
+
mode?: 'local' | 'remote';
|
|
33
|
+
/** Remote adapter config (required when mode is 'remote') */
|
|
34
|
+
remote?: RemoteAdapterConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Static service catalog. If provided, getServices() returns this list
|
|
37
|
+
* without launching a browser. Recommended for known service sets to avoid
|
|
38
|
+
* expensive browser launches for read-only operations.
|
|
39
|
+
*/
|
|
40
|
+
services?: readonly Service[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a full SchedulingAdapter that puppeteers the Acuity wizard.
|
|
44
|
+
*
|
|
45
|
+
* Read operations delegate to the existing scraper.
|
|
46
|
+
* Write operations use Effect TS middleware via Playwright.
|
|
47
|
+
*/
|
|
48
|
+
export declare const createWizardAdapter: (config: WizardAdapterConfig) => SchedulingAdapter;
|
|
49
|
+
//# sourceMappingURL=acuity-wizard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acuity-wizard.d.ts","sourceRoot":"","sources":["../../src/middleware/acuity-wizard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAGX,OAAO,EAEP,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAsB,KAAK,aAAa,EAAwB,MAAM,sBAAsB,CAAC;AAEpG,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAmB1F,MAAM,WAAW,mBAAoB,SAAQ,OAAO,CAAC,aAAa,CAAC;IAClE,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC9B;AAgHD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,mBAAmB,KAAG,iBAuPjE,CAAC"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acuity Wizard Adapter
|
|
3
|
+
*
|
|
4
|
+
* Full SchedulingAdapter implementation that uses Effect TS middleware
|
|
5
|
+
* to puppeteer the Acuity booking wizard.
|
|
6
|
+
*
|
|
7
|
+
* Read operations:
|
|
8
|
+
* - getServices: returns static service catalog (if provided) or falls back to scraper
|
|
9
|
+
* - getAvailableDates: Effect program navigates wizard calendar (verified selectors)
|
|
10
|
+
* - getAvailableSlots: Effect program clicks day tile and reads time buttons
|
|
11
|
+
*
|
|
12
|
+
* Write operations:
|
|
13
|
+
* - createBooking/createBookingWithPaymentRef: Effect TS middleware via Playwright
|
|
14
|
+
*
|
|
15
|
+
* This is the bridge between Effect TS (middleware layer) and
|
|
16
|
+
* fp-ts (pipeline/adapter layer).
|
|
17
|
+
*/
|
|
18
|
+
import { Effect, pipe } from 'effect';
|
|
19
|
+
import { createScraperAdapter } from '../adapters/acuity-scraper.js';
|
|
20
|
+
import { Errors } from '../core/types.js';
|
|
21
|
+
import { BrowserServiceLive, defaultBrowserConfig } from './browser-service.js';
|
|
22
|
+
import { toSchedulingError } from './errors.js';
|
|
23
|
+
import { createRemoteWizardAdapter } from './remote-adapter.js';
|
|
24
|
+
import { navigateToBooking, fillFormFields, bypassPayment, generateCouponCode, submitBooking, extractConfirmation, toBooking, readAvailableDates, readTimeSlots, } from './steps/index.js';
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// EFFECT PROGRAMS
|
|
27
|
+
// =============================================================================
|
|
28
|
+
/**
|
|
29
|
+
* Full booking creation via the Acuity wizard.
|
|
30
|
+
*
|
|
31
|
+
* Flow:
|
|
32
|
+
* 1. Click through wizard: service → Book → calendar → time → continue
|
|
33
|
+
* 2. Fill client form (standard + intake fields) → "Continue to Payment" → /payment page
|
|
34
|
+
* 3. Apply 100% gift certificate on payment page (coupon toggle → enter code → Apply)
|
|
35
|
+
* 4. Click "PAY & CONFIRM" at $0 total
|
|
36
|
+
* 5. Extract confirmation data
|
|
37
|
+
*/
|
|
38
|
+
const createBookingWithPaymentRefProgram = (request, paymentRef, paymentProcessor, couponCode, service) => Effect.scoped(Effect.gen(function* () {
|
|
39
|
+
// Step 1: Navigate through wizard to client form
|
|
40
|
+
const serviceName = service?.name ?? request.serviceId;
|
|
41
|
+
const nav = yield* navigateToBooking({
|
|
42
|
+
serviceName,
|
|
43
|
+
datetime: request.datetime,
|
|
44
|
+
client: request.client,
|
|
45
|
+
});
|
|
46
|
+
if (nav.landingStep !== 'client-form') {
|
|
47
|
+
return yield* Effect.fail({
|
|
48
|
+
_tag: 'WizardStepError',
|
|
49
|
+
step: 'navigate',
|
|
50
|
+
message: `Wizard landed on '${nav.landingStep}' instead of client form. Service or datetime may be unavailable.`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Step 2: Fill form fields (standard + intake) and advance to /payment page
|
|
54
|
+
yield* fillFormFields({
|
|
55
|
+
client: request.client,
|
|
56
|
+
customFields: request.client.customFields,
|
|
57
|
+
});
|
|
58
|
+
// Step 3: Apply gift certificate on the payment page (total → $0)
|
|
59
|
+
yield* bypassPayment(couponCode);
|
|
60
|
+
// Step 4: Click "PAY & CONFIRM" at $0 total
|
|
61
|
+
yield* submitBooking();
|
|
62
|
+
// Step 5: Extract confirmation
|
|
63
|
+
const confirmation = yield* extractConfirmation();
|
|
64
|
+
return toBooking(confirmation, request, paymentRef, paymentProcessor, service
|
|
65
|
+
? {
|
|
66
|
+
name: service.name,
|
|
67
|
+
duration: service.duration,
|
|
68
|
+
price: service.price,
|
|
69
|
+
currency: service.currency,
|
|
70
|
+
}
|
|
71
|
+
: undefined);
|
|
72
|
+
}));
|
|
73
|
+
/**
|
|
74
|
+
* Simple booking creation (no payment bypass needed).
|
|
75
|
+
* For card payments that go through Acuity's normal flow.
|
|
76
|
+
*/
|
|
77
|
+
const createBookingProgram = (request, serviceName) => Effect.scoped(Effect.gen(function* () {
|
|
78
|
+
const nav = yield* navigateToBooking({
|
|
79
|
+
serviceName: serviceName ?? request.serviceId,
|
|
80
|
+
datetime: request.datetime,
|
|
81
|
+
client: request.client,
|
|
82
|
+
});
|
|
83
|
+
if (nav.landingStep !== 'client-form') {
|
|
84
|
+
return yield* Effect.fail({
|
|
85
|
+
_tag: 'WizardStepError',
|
|
86
|
+
step: 'navigate',
|
|
87
|
+
message: `Wizard landed on '${nav.landingStep}' instead of client form.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
yield* fillFormFields({
|
|
91
|
+
client: request.client,
|
|
92
|
+
customFields: request.client.customFields,
|
|
93
|
+
});
|
|
94
|
+
// No payment bypass - form fills and advances to payment page
|
|
95
|
+
// where Acuity handles card payment normally
|
|
96
|
+
yield* submitBooking();
|
|
97
|
+
const confirmation = yield* extractConfirmation();
|
|
98
|
+
return toBooking(confirmation, request, '', 'acuity');
|
|
99
|
+
}));
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// ADAPTER FACTORY
|
|
102
|
+
// =============================================================================
|
|
103
|
+
/**
|
|
104
|
+
* Create a full SchedulingAdapter that puppeteers the Acuity wizard.
|
|
105
|
+
*
|
|
106
|
+
* Read operations delegate to the existing scraper.
|
|
107
|
+
* Write operations use Effect TS middleware via Playwright.
|
|
108
|
+
*/
|
|
109
|
+
export const createWizardAdapter = (config) => {
|
|
110
|
+
// Remote mode: delegate everything to the middleware server
|
|
111
|
+
if (config.mode === 'remote') {
|
|
112
|
+
if (!config.remote) {
|
|
113
|
+
throw new Error('WizardAdapterConfig.remote is required when mode is "remote"');
|
|
114
|
+
}
|
|
115
|
+
return createRemoteWizardAdapter({
|
|
116
|
+
...config.remote,
|
|
117
|
+
couponCode: config.remote.couponCode ?? config.couponCode,
|
|
118
|
+
services: config.services,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Local mode: run Playwright in-process
|
|
122
|
+
const browserConfig = {
|
|
123
|
+
...defaultBrowserConfig,
|
|
124
|
+
...config,
|
|
125
|
+
};
|
|
126
|
+
const layer = BrowserServiceLive(browserConfig);
|
|
127
|
+
// Provide browser layer and map MiddlewareError → SchedulingError
|
|
128
|
+
const runWizard = (effect) => pipe(Effect.scoped(effect), Effect.provide(layer), Effect.mapError(toSchedulingError));
|
|
129
|
+
// Static services from config (avoids browser launch for service listing)
|
|
130
|
+
const staticServices = config.services ? [...config.services] : null;
|
|
131
|
+
// Scraper fallback for services if no static list provided
|
|
132
|
+
const scraperConfig = {
|
|
133
|
+
baseUrl: config.baseUrl,
|
|
134
|
+
headless: browserConfig.headless,
|
|
135
|
+
timeout: browserConfig.timeout,
|
|
136
|
+
userAgent: browserConfig.userAgent,
|
|
137
|
+
executablePath: browserConfig.executablePath,
|
|
138
|
+
launchArgs: browserConfig.launchArgs ? [...browserConfig.launchArgs] : undefined,
|
|
139
|
+
};
|
|
140
|
+
const scraper = createScraperAdapter(scraperConfig);
|
|
141
|
+
// Cache services for getService lookups
|
|
142
|
+
let cachedServices = staticServices;
|
|
143
|
+
// Helper: resolve service name from ID using cached services
|
|
144
|
+
const resolveServiceName = (serviceId) => cachedServices?.find((s) => s.id === serviceId)?.name;
|
|
145
|
+
return {
|
|
146
|
+
name: 'acuity-wizard',
|
|
147
|
+
// -----------------------------------------------------------------------
|
|
148
|
+
// Read operations
|
|
149
|
+
// -----------------------------------------------------------------------
|
|
150
|
+
getServices: () => {
|
|
151
|
+
if (staticServices) {
|
|
152
|
+
return Effect.succeed(staticServices);
|
|
153
|
+
}
|
|
154
|
+
// Fallback to scraper (may fail with broken selectors)
|
|
155
|
+
return pipe(scraper.getServices(), Effect.tap((services) => Effect.sync(() => {
|
|
156
|
+
cachedServices = services;
|
|
157
|
+
})));
|
|
158
|
+
},
|
|
159
|
+
getService: (serviceId) => {
|
|
160
|
+
if (cachedServices) {
|
|
161
|
+
const found = cachedServices.find((s) => s.id === serviceId);
|
|
162
|
+
return found
|
|
163
|
+
? Effect.succeed(found)
|
|
164
|
+
: Effect.fail(Errors.acuity('NOT_FOUND', `Service ${serviceId} not found`));
|
|
165
|
+
}
|
|
166
|
+
return pipe(scraper.getServices(), Effect.flatMap((services) => {
|
|
167
|
+
cachedServices = services;
|
|
168
|
+
const found = services.find((s) => s.id === serviceId);
|
|
169
|
+
return found
|
|
170
|
+
? Effect.succeed(found)
|
|
171
|
+
: Effect.fail(Errors.acuity('NOT_FOUND', `Service ${serviceId} not found`));
|
|
172
|
+
}));
|
|
173
|
+
},
|
|
174
|
+
getProviders: () => Effect.succeed([
|
|
175
|
+
{
|
|
176
|
+
id: '1',
|
|
177
|
+
name: 'Default Provider',
|
|
178
|
+
email: 'provider@example.com',
|
|
179
|
+
description: 'Primary provider',
|
|
180
|
+
timezone: 'America/New_York',
|
|
181
|
+
},
|
|
182
|
+
]),
|
|
183
|
+
getProvider: () => Effect.succeed({
|
|
184
|
+
id: '1',
|
|
185
|
+
name: 'Default Provider',
|
|
186
|
+
email: 'provider@example.com',
|
|
187
|
+
description: 'Primary provider',
|
|
188
|
+
timezone: 'America/New_York',
|
|
189
|
+
}),
|
|
190
|
+
getProvidersForService: () => Effect.succeed([
|
|
191
|
+
{
|
|
192
|
+
id: '1',
|
|
193
|
+
name: 'Default Provider',
|
|
194
|
+
email: 'provider@example.com',
|
|
195
|
+
description: 'Primary provider',
|
|
196
|
+
timezone: 'America/New_York',
|
|
197
|
+
},
|
|
198
|
+
]),
|
|
199
|
+
getAvailableDates: (params) => {
|
|
200
|
+
const serviceName = resolveServiceName(params.serviceId);
|
|
201
|
+
if (!serviceName) {
|
|
202
|
+
return Effect.fail(Errors.acuity('NOT_FOUND', `Cannot resolve service name for ID ${params.serviceId}. Provide static services in config.`));
|
|
203
|
+
}
|
|
204
|
+
return runWizard(Effect.scoped(readAvailableDates({
|
|
205
|
+
serviceName,
|
|
206
|
+
// Don't pass appointmentTypeId — our internal IDs don't match Acuity URL IDs
|
|
207
|
+
targetMonth: params.startDate?.slice(0, 7),
|
|
208
|
+
monthsToScan: 2,
|
|
209
|
+
})));
|
|
210
|
+
},
|
|
211
|
+
getAvailableSlots: (params) => {
|
|
212
|
+
const serviceName = resolveServiceName(params.serviceId);
|
|
213
|
+
if (!serviceName) {
|
|
214
|
+
return Effect.fail(Errors.acuity('NOT_FOUND', `Cannot resolve service name for ID ${params.serviceId}. Provide static services in config.`));
|
|
215
|
+
}
|
|
216
|
+
return runWizard(Effect.scoped(readTimeSlots({
|
|
217
|
+
serviceName,
|
|
218
|
+
date: params.date,
|
|
219
|
+
})));
|
|
220
|
+
},
|
|
221
|
+
checkSlotAvailability: (params) => {
|
|
222
|
+
const serviceName = resolveServiceName(params.serviceId);
|
|
223
|
+
if (!serviceName) {
|
|
224
|
+
return Effect.fail(Errors.acuity('NOT_FOUND', `Cannot resolve service name for ID ${params.serviceId}`));
|
|
225
|
+
}
|
|
226
|
+
return pipe(runWizard(Effect.scoped(readTimeSlots({
|
|
227
|
+
serviceName,
|
|
228
|
+
date: params.datetime.split('T')[0],
|
|
229
|
+
}))), Effect.map((slots) => {
|
|
230
|
+
// Slots return local time (no TZ suffix: "2026-03-07T14:00:00").
|
|
231
|
+
// Request datetime should also be local, but normalize both by
|
|
232
|
+
// stripping any trailing Z or offset for comparison.
|
|
233
|
+
const normalize = (dt) => dt.replace(/([+-]\d{2}:\d{2}|Z)$/, '');
|
|
234
|
+
const requestNorm = normalize(params.datetime);
|
|
235
|
+
return slots.some((s) => s.available && normalize(s.datetime) === requestNorm);
|
|
236
|
+
}));
|
|
237
|
+
},
|
|
238
|
+
// -----------------------------------------------------------------------
|
|
239
|
+
// Reservation - not supported (pipeline has graceful fallback)
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
createReservation: () => Effect.fail(Errors.reservation('BLOCK_FAILED', 'Reservations not supported by wizard adapter')),
|
|
242
|
+
releaseReservation: () => Effect.succeed(undefined),
|
|
243
|
+
// -----------------------------------------------------------------------
|
|
244
|
+
// Write operations - Effect TS middleware
|
|
245
|
+
// -----------------------------------------------------------------------
|
|
246
|
+
createBooking: (request) => {
|
|
247
|
+
const serviceName = cachedServices?.find((s) => s.id === request.serviceId)?.name;
|
|
248
|
+
return runWizard(createBookingProgram(request, serviceName));
|
|
249
|
+
},
|
|
250
|
+
createBookingWithPaymentRef: (request, paymentRef, paymentProcessor) => {
|
|
251
|
+
const coupon = config.couponCode ?? generateCouponCode(paymentRef, paymentProcessor);
|
|
252
|
+
const service = cachedServices?.find((s) => s.id === request.serviceId);
|
|
253
|
+
return runWizard(createBookingWithPaymentRefProgram(request, paymentRef, paymentProcessor, coupon, service));
|
|
254
|
+
},
|
|
255
|
+
getBooking: () => Effect.fail(Errors.acuity('NOT_IMPLEMENTED', 'Get booking not yet supported via wizard')),
|
|
256
|
+
cancelBooking: () => Effect.fail(Errors.acuity('NOT_IMPLEMENTED', 'Cancel not yet supported via wizard')),
|
|
257
|
+
rescheduleBooking: () => Effect.fail(Errors.acuity('NOT_IMPLEMENTED', 'Reschedule not yet supported via wizard')),
|
|
258
|
+
// -----------------------------------------------------------------------
|
|
259
|
+
// Client - pass-through (client data comes from our form, not Acuity)
|
|
260
|
+
// -----------------------------------------------------------------------
|
|
261
|
+
findOrCreateClient: (client) => Effect.succeed({ id: `local-${client.email}`, isNew: true }),
|
|
262
|
+
getClientByEmail: () => Effect.succeed(null),
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=acuity-wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acuity-wizard.js","sourceRoot":"","sources":["../../src/middleware/acuity-wizard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserService Layer
|
|
3
|
+
*
|
|
4
|
+
* Effect TS Layer providing managed Playwright browser lifecycle.
|
|
5
|
+
* The browser and pages are acquired/released via Effect's Scope,
|
|
6
|
+
* ensuring proper cleanup even on errors or interruptions.
|
|
7
|
+
*/
|
|
8
|
+
import { Context, Effect, Layer, Scope } from 'effect';
|
|
9
|
+
import type { Page } from 'playwright-core';
|
|
10
|
+
import { BrowserError } from './errors.js';
|
|
11
|
+
export interface BrowserConfig {
|
|
12
|
+
/** Base URL for the Acuity scheduling page */
|
|
13
|
+
readonly baseUrl: string;
|
|
14
|
+
/** Run browser in headless mode (default: true) */
|
|
15
|
+
readonly headless: boolean;
|
|
16
|
+
/** Default timeout for page operations in ms (default: 30000) */
|
|
17
|
+
readonly timeout: number;
|
|
18
|
+
/** User agent string */
|
|
19
|
+
readonly userAgent: string;
|
|
20
|
+
/** Take screenshot on failure (default: true) */
|
|
21
|
+
readonly screenshotOnFailure: boolean;
|
|
22
|
+
/** Directory for failure screenshots */
|
|
23
|
+
readonly screenshotDir: string;
|
|
24
|
+
/** Path to Chromium executable (for Lambda/serverless) */
|
|
25
|
+
readonly executablePath?: string;
|
|
26
|
+
/** Additional chromium.launch() args (e.g., Lambda sandbox flags) */
|
|
27
|
+
readonly launchArgs?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export declare const defaultBrowserConfig: BrowserConfig;
|
|
30
|
+
export interface BrowserServiceShape {
|
|
31
|
+
/** Get a managed Page (scoped - auto-closed when scope ends) */
|
|
32
|
+
readonly acquirePage: Effect.Effect<Page, BrowserError, Scope.Scope>;
|
|
33
|
+
/** Take a screenshot of the most recently created page */
|
|
34
|
+
readonly screenshot: (label: string) => Effect.Effect<Buffer, BrowserError>;
|
|
35
|
+
/** The browser configuration */
|
|
36
|
+
readonly config: BrowserConfig;
|
|
37
|
+
}
|
|
38
|
+
declare const BrowserService_base: Context.TagClass<BrowserService, "scheduling-kit/BrowserService", BrowserServiceShape>;
|
|
39
|
+
export declare class BrowserService extends BrowserService_base {
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a live BrowserService layer backed by Playwright Chromium.
|
|
43
|
+
* The browser is launched once when the layer is created and closed
|
|
44
|
+
* when the layer scope ends.
|
|
45
|
+
*/
|
|
46
|
+
export declare const BrowserServiceLive: (config?: Partial<BrowserConfig>) => Layer.Layer<BrowserService, BrowserError>;
|
|
47
|
+
/**
|
|
48
|
+
* A mock BrowserService for unit tests.
|
|
49
|
+
* Does not launch a real browser.
|
|
50
|
+
*/
|
|
51
|
+
export declare const BrowserServiceTest: (config?: Partial<BrowserConfig>) => Layer.Layer<BrowserService>;
|
|
52
|
+
export {};
|
|
53
|
+
//# sourceMappingURL=browser-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-service.d.ts","sourceRoot":"","sources":["../../src/middleware/browser-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvD,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,MAAM,WAAW,aAAa;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wBAAwB;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,wCAAwC;IACxC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,0DAA0D;IAC1D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,eAAO,MAAM,oBAAoB,EAAE,aAQlC,CAAC;AAMF,MAAM,WAAW,mBAAmB;IACnC,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACrE,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC5E,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAC/B;;AAED,qBAAa,cAAe,SAAQ,mBAGjC;CAAG;AAMN;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAQ,OAAO,CAAC,aAAa,CAAM,KACjC,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,YAAY,CAqF1C,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAQ,OAAO,CAAC,aAAa,CAAM,KACjC,KAAK,CAAC,KAAK,CAAC,cAAc,CAW5B,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserService Layer
|
|
3
|
+
*
|
|
4
|
+
* Effect TS Layer providing managed Playwright browser lifecycle.
|
|
5
|
+
* The browser and pages are acquired/released via Effect's Scope,
|
|
6
|
+
* ensuring proper cleanup even on errors or interruptions.
|
|
7
|
+
*/
|
|
8
|
+
import { Context, Effect, Layer } from 'effect';
|
|
9
|
+
import { BrowserError } from './errors.js';
|
|
10
|
+
export const defaultBrowserConfig = {
|
|
11
|
+
baseUrl: 'https://MassageIthaca.as.me',
|
|
12
|
+
headless: true,
|
|
13
|
+
timeout: 30000,
|
|
14
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
15
|
+
screenshotOnFailure: true,
|
|
16
|
+
screenshotDir: '/tmp/scheduling-kit-screenshots',
|
|
17
|
+
};
|
|
18
|
+
export class BrowserService extends Context.Tag('scheduling-kit/BrowserService')() {
|
|
19
|
+
}
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// LIVE IMPLEMENTATION
|
|
22
|
+
// =============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Create a live BrowserService layer backed by Playwright Chromium.
|
|
25
|
+
* The browser is launched once when the layer is created and closed
|
|
26
|
+
* when the layer scope ends.
|
|
27
|
+
*/
|
|
28
|
+
export const BrowserServiceLive = (config = {}) => {
|
|
29
|
+
const cfg = { ...defaultBrowserConfig, ...config };
|
|
30
|
+
return Layer.scoped(BrowserService, Effect.gen(function* () {
|
|
31
|
+
// Dynamic import - try playwright-core first (lighter, no bundled browsers),
|
|
32
|
+
// fall back to full playwright which includes its own Chromium
|
|
33
|
+
const chromium = yield* Effect.tryPromise({
|
|
34
|
+
try: async () => {
|
|
35
|
+
try {
|
|
36
|
+
const pw = await import('playwright-core');
|
|
37
|
+
return pw.chromium;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
const pw = await import('playwright-core');
|
|
42
|
+
return pw.chromium;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
catch: () => new BrowserError({
|
|
46
|
+
reason: 'PLAYWRIGHT_MISSING',
|
|
47
|
+
cause: new Error('playwright-core or playwright is required for the wizard middleware. ' +
|
|
48
|
+
'Install with: pnpm add playwright-core'),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
// Launch browser (released when scope ends)
|
|
52
|
+
const browser = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
53
|
+
try: () => chromium.launch({
|
|
54
|
+
headless: cfg.headless,
|
|
55
|
+
executablePath: cfg.executablePath,
|
|
56
|
+
args: cfg.launchArgs ? [...cfg.launchArgs] : undefined,
|
|
57
|
+
}),
|
|
58
|
+
catch: (e) => new BrowserError({ reason: 'LAUNCH_FAILED', cause: e }),
|
|
59
|
+
}), (browser) => Effect.promise(() => browser.close()).pipe(Effect.ignoreLogged));
|
|
60
|
+
// Create a single managed page for the layer's lifetime.
|
|
61
|
+
// All step programs share this page within a scope.
|
|
62
|
+
const page = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
63
|
+
try: async () => {
|
|
64
|
+
const p = await browser.newPage({ userAgent: cfg.userAgent });
|
|
65
|
+
p.setDefaultTimeout(cfg.timeout);
|
|
66
|
+
return p;
|
|
67
|
+
},
|
|
68
|
+
catch: (e) => new BrowserError({ reason: 'PAGE_FAILED', cause: e }),
|
|
69
|
+
}), (p) => Effect.promise(() => p.close()).pipe(Effect.ignoreLogged));
|
|
70
|
+
// acquirePage returns the singleton page — cleanup is handled by the
|
|
71
|
+
// layer scope above. The no-op release keeps the Scope type requirement
|
|
72
|
+
// so callers still need Effect.scoped.
|
|
73
|
+
const acquirePage = Effect.acquireRelease(Effect.succeed(page), () => Effect.void);
|
|
74
|
+
const screenshot = (label) => Effect.tryPromise({
|
|
75
|
+
try: async () => {
|
|
76
|
+
if (page.isClosed()) {
|
|
77
|
+
throw new Error('No active page for screenshot');
|
|
78
|
+
}
|
|
79
|
+
const buffer = await page.screenshot({
|
|
80
|
+
path: `${cfg.screenshotDir}/${label}-${Date.now()}.png`,
|
|
81
|
+
fullPage: true,
|
|
82
|
+
});
|
|
83
|
+
return buffer;
|
|
84
|
+
},
|
|
85
|
+
catch: (e) => new BrowserError({ reason: 'SCREENSHOT_FAILED', cause: e }),
|
|
86
|
+
});
|
|
87
|
+
return { acquirePage, screenshot, config: cfg };
|
|
88
|
+
}));
|
|
89
|
+
};
|
|
90
|
+
// =============================================================================
|
|
91
|
+
// TEST IMPLEMENTATION
|
|
92
|
+
// =============================================================================
|
|
93
|
+
/**
|
|
94
|
+
* A mock BrowserService for unit tests.
|
|
95
|
+
* Does not launch a real browser.
|
|
96
|
+
*/
|
|
97
|
+
export const BrowserServiceTest = (config = {}) => {
|
|
98
|
+
const cfg = { ...defaultBrowserConfig, ...config };
|
|
99
|
+
return Layer.succeed(BrowserService, {
|
|
100
|
+
acquirePage: Effect.die(new Error('BrowserServiceTest: acquirePage called - use a real browser for integration tests')),
|
|
101
|
+
screenshot: () => Effect.succeed(Buffer.from('mock-screenshot')),
|
|
102
|
+
config: cfg,
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=browser-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-service.js","sourceRoot":"","sources":["../../src/middleware/browser-service.ts"],"names":[],"mappings":"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"}
|