@tstdl/base 0.93.139 → 0.93.141

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.
Files changed (218) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.d.ts +1 -1
  28. package/application/application.js +3 -3
  29. package/application/providers.d.ts +20 -2
  30. package/application/providers.js +34 -7
  31. package/audit/README.md +267 -0
  32. package/audit/module.d.ts +5 -0
  33. package/audit/module.js +9 -1
  34. package/authentication/README.md +288 -0
  35. package/authentication/client/authentication.service.d.ts +12 -11
  36. package/authentication/client/authentication.service.js +21 -21
  37. package/authentication/client/http-client.middleware.js +2 -2
  38. package/authentication/server/module.d.ts +5 -0
  39. package/authentication/server/module.js +9 -1
  40. package/authentication/tests/authentication.api-controller.test.js +1 -1
  41. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  42. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  43. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  44. package/authentication/tests/authentication.client-service.test.js +1 -1
  45. package/browser/README.md +401 -0
  46. package/cancellation/README.md +156 -0
  47. package/cancellation/tests/coverage.test.d.ts +1 -0
  48. package/cancellation/tests/coverage.test.js +49 -0
  49. package/cancellation/tests/leak.test.js +24 -29
  50. package/cancellation/tests/token.test.d.ts +1 -0
  51. package/cancellation/tests/token.test.js +136 -0
  52. package/cancellation/token.d.ts +53 -177
  53. package/cancellation/token.js +132 -208
  54. package/circuit-breaker/postgres/module.d.ts +1 -0
  55. package/circuit-breaker/postgres/module.js +5 -1
  56. package/context/README.md +174 -0
  57. package/cookie/README.md +161 -0
  58. package/css/README.md +157 -0
  59. package/data-structures/README.md +320 -0
  60. package/decorators/README.md +140 -0
  61. package/distributed-loop/README.md +231 -0
  62. package/distributed-loop/distributed-loop.js +1 -1
  63. package/document-management/README.md +403 -0
  64. package/document-management/server/configure.js +5 -1
  65. package/document-management/server/module.d.ts +1 -1
  66. package/document-management/server/module.js +1 -1
  67. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  68. package/document-management/server/services/document-management.service.js +9 -7
  69. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  70. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  71. package/document-management/tests/document-management-core.test.js +2 -7
  72. package/document-management/tests/document-management.api.test.js +6 -7
  73. package/document-management/tests/document-statistics.service.test.js +11 -12
  74. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  75. package/document-management/tests/document.service.test.js +3 -3
  76. package/document-management/tests/enum-helpers.test.js +2 -3
  77. package/dom/README.md +213 -0
  78. package/enumerable/README.md +259 -0
  79. package/enumeration/README.md +121 -0
  80. package/errors/README.md +267 -0
  81. package/examples/document-management/main.d.ts +1 -0
  82. package/examples/document-management/main.js +14 -11
  83. package/file/README.md +191 -0
  84. package/formats/README.md +210 -0
  85. package/function/README.md +144 -0
  86. package/http/README.md +318 -0
  87. package/http/client/adapters/undici.adapter.js +1 -1
  88. package/http/client/http-client-request.d.ts +6 -5
  89. package/http/client/http-client-request.js +8 -9
  90. package/http/server/node/node-http-server.js +1 -2
  91. package/image-service/README.md +137 -0
  92. package/injector/README.md +491 -0
  93. package/intl/README.md +113 -0
  94. package/json-path/README.md +182 -0
  95. package/jsx/README.md +154 -0
  96. package/key-value-store/README.md +191 -0
  97. package/key-value-store/postgres/module.d.ts +1 -0
  98. package/key-value-store/postgres/module.js +5 -1
  99. package/lock/README.md +249 -0
  100. package/lock/postgres/module.d.ts +1 -0
  101. package/lock/postgres/module.js +5 -1
  102. package/lock/web/web-lock.js +119 -47
  103. package/logger/README.md +287 -0
  104. package/mail/README.md +256 -0
  105. package/mail/module.d.ts +5 -1
  106. package/mail/module.js +11 -6
  107. package/memory/README.md +144 -0
  108. package/message-bus/README.md +244 -0
  109. package/message-bus/message-bus-base.js +1 -1
  110. package/module/README.md +182 -0
  111. package/module/module.d.ts +1 -1
  112. package/module/module.js +77 -17
  113. package/module/modules/web-server.module.js +3 -4
  114. package/notification/server/module.d.ts +1 -0
  115. package/notification/server/module.js +5 -1
  116. package/notification/tests/notification-flow.test.js +2 -2
  117. package/notification/tests/notification-type.service.test.js +24 -15
  118. package/object-storage/README.md +300 -0
  119. package/openid-connect/README.md +274 -0
  120. package/orm/README.md +423 -0
  121. package/orm/decorators.d.ts +5 -1
  122. package/orm/decorators.js +1 -1
  123. package/orm/server/drizzle/schema-converter.js +17 -30
  124. package/orm/server/encryption.d.ts +0 -1
  125. package/orm/server/encryption.js +1 -4
  126. package/orm/server/index.d.ts +1 -6
  127. package/orm/server/index.js +1 -6
  128. package/orm/server/migration.d.ts +19 -0
  129. package/orm/server/migration.js +72 -0
  130. package/orm/server/repository.d.ts +1 -1
  131. package/orm/server/transaction.d.ts +5 -10
  132. package/orm/server/transaction.js +22 -26
  133. package/orm/server/transactional.js +3 -3
  134. package/orm/tests/database-migration.test.d.ts +1 -0
  135. package/orm/tests/database-migration.test.js +82 -0
  136. package/orm/tests/encryption.test.js +3 -4
  137. package/orm/utils.d.ts +17 -2
  138. package/orm/utils.js +49 -1
  139. package/package.json +9 -6
  140. package/password/README.md +164 -0
  141. package/pdf/README.md +246 -0
  142. package/polyfills.js +1 -0
  143. package/pool/README.md +198 -0
  144. package/process/README.md +237 -0
  145. package/promise/README.md +252 -0
  146. package/promise/cancelable-promise.js +1 -1
  147. package/random/README.md +193 -0
  148. package/rate-limit/postgres/module.d.ts +1 -0
  149. package/rate-limit/postgres/module.js +5 -1
  150. package/reflection/README.md +305 -0
  151. package/reflection/decorator-data.js +11 -12
  152. package/rpc/README.md +386 -0
  153. package/rxjs-utils/README.md +262 -0
  154. package/schema/README.md +342 -0
  155. package/serializer/README.md +342 -0
  156. package/signals/implementation/README.md +134 -0
  157. package/sse/README.md +278 -0
  158. package/task-queue/README.md +293 -0
  159. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  160. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  161. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  162. package/task-queue/postgres/module.d.ts +1 -0
  163. package/task-queue/postgres/module.js +5 -1
  164. package/task-queue/postgres/schemas.d.ts +9 -6
  165. package/task-queue/postgres/schemas.js +4 -3
  166. package/task-queue/postgres/task-queue.d.ts +4 -13
  167. package/task-queue/postgres/task-queue.js +462 -355
  168. package/task-queue/postgres/task.model.d.ts +12 -5
  169. package/task-queue/postgres/task.model.js +51 -25
  170. package/task-queue/task-context.d.ts +2 -2
  171. package/task-queue/task-context.js +8 -8
  172. package/task-queue/task-queue.d.ts +53 -19
  173. package/task-queue/task-queue.js +121 -55
  174. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  175. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  176. package/task-queue/tests/complex.test.js +45 -229
  177. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  178. package/task-queue/tests/coverage-branch.test.js +407 -0
  179. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  180. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  181. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  182. package/task-queue/tests/dag-dependencies.test.js +41 -0
  183. package/task-queue/tests/dependencies.test.js +28 -26
  184. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  185. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  186. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  187. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  188. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  189. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  190. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  191. package/task-queue/tests/queue.test.js +128 -8
  192. package/task-queue/tests/worker.test.js +39 -16
  193. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  194. package/task-queue/tests/zombie-parent.test.js +45 -0
  195. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  196. package/task-queue/tests/zombie-recovery.test.js +51 -0
  197. package/templates/README.md +287 -0
  198. package/test5.js +5 -5
  199. package/testing/README.md +157 -0
  200. package/testing/integration-setup.d.ts +4 -4
  201. package/testing/integration-setup.js +54 -29
  202. package/text/README.md +346 -0
  203. package/text/localization.service.js +2 -2
  204. package/threading/README.md +238 -0
  205. package/types/README.md +311 -0
  206. package/utils/README.md +322 -0
  207. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  208. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  209. package/utils/async-iterable-helpers/take-until.js +4 -4
  210. package/utils/backoff.js +89 -30
  211. package/utils/file-reader.js +1 -2
  212. package/utils/retry-with-backoff.js +1 -1
  213. package/utils/timer.d.ts +1 -1
  214. package/utils/timer.js +5 -7
  215. package/utils/timing.d.ts +1 -1
  216. package/utils/timing.js +2 -4
  217. package/utils/z-base32.d.ts +1 -0
  218. package/utils/z-base32.js +1 -0
@@ -0,0 +1,401 @@
1
+ # Browser Module
2
+
3
+ A high-level, controller-based wrapper for Playwright, designed to simplify web automation, scraping, and testing tasks within a dependency-injection-friendly architecture.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Features](#-features)
8
+ - [Core Concepts](#core-concepts)
9
+ - [🚀 Basic Usage](#-basic-usage)
10
+ - [🔧 Advanced Topics](#-advanced-topics)
11
+ - [Global Configuration](#global-configuration)
12
+ - [Persistent Contexts](#persistent-contexts)
13
+ - [Element Interaction & Filtering](#element-interaction--filtering)
14
+ - [PDF Rendering](#pdf-rendering)
15
+ - [Logging & Debugging](#logging--debugging)
16
+ - [Smart Scrolling](#smart-scrolling)
17
+ - [Frame & Popup Handling](#frame--popup-handling)
18
+ - [Element Lifecycle & Existence](#element-lifecycle--existence)
19
+ - [📚 API](#-api)
20
+
21
+ ## ✨ Features
22
+
23
+ - **Hierarchical Controllers:** Encapsulates Playwright entities (`Browser`, `Page`, `Locator`) into managed controllers (`BrowserController`, `PageController`, `ElementController`) for a clean, object-oriented API.
24
+ - **Dependency Injection Ready:** Designed to be resolved and managed by the `@tstdl/base/injector`, simplifying lifecycle and dependency management.
25
+ - **Unified Element API:** `ElementController` provides a consistent interface over Playwright's `Locator` and `ElementHandle`, supporting chaining and advanced filtering.
26
+ - **Pop-up & Frame Handling:** Seamlessly handle multiple tabs and `<iframe>` elements with dedicated controllers and automatic tracking.
27
+ - **Enhanced Capabilities:** Built-in helpers for PDF generation (buffer and stream), smart scrolling, and automatic logger attachment.
28
+ - **Robust Resource Management:** Implements `AsyncDisposable` for reliable cleanup of browsers and contexts.
29
+
30
+ ## Core Concepts
31
+
32
+ The module is built around a hierarchy of controllers. Each controller wraps a specific part of the browser automation lifecycle:
33
+
34
+ 1. **`BrowserService`**: The singleton entry point. It manages the creation and lifecycle of `BrowserController` instances.
35
+ 2. **`BrowserController`**: Represents a browser process (e.g., Chromium). It creates isolated `BrowserContextController` sessions.
36
+ 3. **`BrowserContextController`**: Represents an isolated session (incognito-like) with its own cookies and storage. It creates `PageController` tabs.
37
+ 4. **`PageController`**: Represents a single tab. It provides methods for navigation, evaluation, and finding elements.
38
+ 5. **`ElementController`**: Represents a DOM element (via Locator or Handle). It provides methods for interaction (click, type, etc.) and inspection.
39
+
40
+ ## 🚀 Basic Usage
41
+
42
+ Here is a typical workflow: launching a browser, navigating to a page, interacting with elements, and cleaning up.
43
+
44
+ ```typescript
45
+ import { BrowserService } from '@tstdl/base/browser';
46
+ import { inject } from '@tstdl/base/injector';
47
+
48
+ // Typically resolved via dependency injection in your application
49
+ const browserService = inject(BrowserService);
50
+
51
+ // Launch a new browser instance (Chromium by default)
52
+ const browser = await browserService.newBrowser({ headless: false });
53
+
54
+ try {
55
+ // Create a new isolated context and a page
56
+ const context = await browser.newContext();
57
+ const page = await context.newPage();
58
+
59
+ // Navigate to a website
60
+ await page.navigate('https://playwright.dev/');
61
+
62
+ // Locate an element using accessibility roles
63
+ const getStartedLink = page.getByRole('link', { name: 'Get started' });
64
+
65
+ // Interact with the element
66
+ await getStartedLink.click();
67
+
68
+ // Wait for navigation or URL change
69
+ await page.waitForUrl('**/docs/intro');
70
+ console.log('Current URL:', page.url());
71
+
72
+ // Find elements by text
73
+ const title = page.getByText('Installation');
74
+
75
+ if (await title.isVisible()) {
76
+ console.log('Installation guide loaded.');
77
+ }
78
+ } finally {
79
+ // Closes the browser and all associated contexts/pages
80
+ await browser.close();
81
+ }
82
+ ```
83
+
84
+ ## 🔧 Advanced Topics
85
+
86
+ ### Global Configuration
87
+
88
+ You can configure default options for all browsers launched by the `BrowserService`. This is typically done during application bootstrap.
89
+
90
+ ```typescript
91
+ import { configureBrowser } from '@tstdl/base/browser';
92
+ import { chromium } from 'playwright';
93
+
94
+ configureBrowser({
95
+ // Optionally provide specific browser binaries
96
+ browsers: { chromium },
97
+ options: {
98
+ defaultNewBrowserOptions: {
99
+ browser: 'chromium',
100
+ headless: 'new',
101
+ windowSize: { width: 1920, height: 1080 },
102
+ defaultNewContextOptions: {
103
+ locale: 'en-US',
104
+ colorScheme: 'dark',
105
+ viewport: { width: 1920, height: 1080 },
106
+ },
107
+ },
108
+ },
109
+ });
110
+ ```
111
+
112
+ ### Persistent Contexts
113
+
114
+ Persistent contexts store session data (cookies, local storage) on disk, allowing you to maintain login states across restarts.
115
+
116
+ ```typescript
117
+ import { BrowserService } from '@tstdl/base/browser';
118
+ import { inject } from '@tstdl/base/injector';
119
+
120
+ const browserService = inject(BrowserService);
121
+
122
+ // Data will be saved to './user-data'
123
+ const context = await browserService.newPersistentContext('./user-data', {
124
+ headless: false,
125
+ });
126
+
127
+ const page = await context.newPage();
128
+ await page.navigate('https://github.com/login');
129
+
130
+ // ... perform login manually or automatically ...
131
+
132
+ // Close the context to save state
133
+ await context.close();
134
+
135
+ // Next time you run this, the session will be restored
136
+ ```
137
+
138
+ ### Element Interaction & Filtering
139
+
140
+ The `ElementController` supports advanced filtering and chaining to precisely target elements.
141
+
142
+ ```typescript
143
+ import type { PageController } from '@tstdl/base/browser';
144
+
145
+ async function selectProduct(page: PageController, productName: string) {
146
+ // Find a list item that contains the specific product name
147
+ const productCard = page.getByRole('listitem').filter({ hasText: productName });
148
+
149
+ // Within that card, find the "Add to Cart" button
150
+ const addButton = productCard.locate(page.getByRole('button', { name: 'Add to Cart' }));
151
+
152
+ if (await addButton.isEnabled()) {
153
+ await addButton.click();
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### PDF Rendering
159
+
160
+ `PageController` includes a convenient method to render the current page as a PDF.
161
+
162
+ ```typescript
163
+ import { writeFile } from 'node:fs/promises';
164
+ import type { PageController } from '@tstdl/base/browser';
165
+
166
+ async function saveAsPdf(page: PageController) {
167
+ await page.navigate('https://example.com/report');
168
+
169
+ const pdfBuffer = await page.renderPdf({
170
+ format: 'A4',
171
+ printBackground: true,
172
+ margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
173
+ displayHeaderFooter: true,
174
+ footerTemplate: '<div style="font-size: 10px; text-align: center; width: 100%;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
175
+ });
176
+
177
+ await writeFile('report.pdf', pdfBuffer);
178
+ }
179
+ ```
180
+
181
+ ### Logging & Debugging
182
+
183
+ Attach a logger to a context or page to automatically capture console messages, page errors, and network requests.
184
+
185
+ ```typescript
186
+ import type { BrowserContextController } from '@tstdl/base/browser';
187
+ import { Logger } from '@tstdl/base/logger';
188
+ import { inject } from '@tstdl/base/injector';
189
+
190
+ const logger = inject(Logger);
191
+
192
+ function debugSession(context: BrowserContextController) {
193
+ // All pages created in this context will now log to the provided logger
194
+ context.attachLogger(logger);
195
+
196
+ // Console logs from the browser will appear in your node logs
197
+ // Network requests will be logged at verbose level
198
+ }
199
+ ```
200
+
201
+ ### Smart Scrolling
202
+
203
+ The `scrollTo` method intelligently scrolls the viewport to a specific element or coordinate, handling complex scrolling containers.
204
+
205
+ ```typescript
206
+ import type { PageController } from '@tstdl/base/browser';
207
+
208
+ async function scrollToFooter(page: PageController) {
209
+ const footer = page.getByRole('contentinfo');
210
+
211
+ // Smoothly scrolls until the element is in view
212
+ await page.scrollTo(footer);
213
+ }
214
+ ```
215
+
216
+ ### Frame & Popup Handling
217
+
218
+ Interacting with `<iframe>` elements or popups is straightforward using dedicated controllers.
219
+
220
+ ```typescript
221
+ import type { PageController } from '@tstdl/base/browser';
222
+
223
+ async function handleIframe(page: PageController) {
224
+ // Access an iframe by name, url, or index
225
+ const newsletterFrame = page.frame({ url: /.*newsletter.*/ });
226
+
227
+ // Use the same ElementController API inside the frame
228
+ await newsletterFrame.getByPlaceholder('Email').fill('user@example.com');
229
+ await newsletterFrame.getByRole('button', { name: 'Subscribe' }).click();
230
+ }
231
+
232
+ async function handlePopups(page: PageController) {
233
+ // Trigger an action that opens a popup
234
+ await page.getByRole('link', { name: 'Help' }).click();
235
+
236
+ // Find the newly opened page
237
+ const [helpPage] = await page.opened();
238
+
239
+ if (helpPage) {
240
+ console.log('Opened help page:', helpPage.url());
241
+ await helpPage.close();
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Element Lifecycle & Existence
247
+
248
+ `ElementController` provides helpful methods to check for elements without throwing errors on timeout immediately.
249
+
250
+ ```typescript
251
+ import type { PageController } from '@tstdl/base/browser';
252
+
253
+ async function checkStatus(page: PageController) {
254
+ const successMessage = page.getByText('Success!');
255
+
256
+ // Check if it exists and is visible within a short timeout
257
+ if (await successMessage.exists({ timeout: 2000 })) {
258
+ console.log('Operation completed successfully.');
259
+ } else {
260
+ console.warn('Success message did not appear.');
261
+ }
262
+ }
263
+ ```
264
+
265
+ ## 📚 API
266
+
267
+ ### `BrowserService`
268
+
269
+ Singleton service for managing browser instances.
270
+
271
+ | Method | Description |
272
+ | :------------------------------------ | :-------------------------------------------------------------------- |
273
+ | `newBrowser(options?)` | Launches a new `BrowserController`. |
274
+ | `newPersistentContext(dir, options?)` | Launches a browser with a persistent context for saving session data. |
275
+ | `waitForNoBrowsers()` | Waits until all managed browsers are closed. |
276
+ | `dispose()` | Closes all browsers and contexts created by this service. |
277
+
278
+ ### `BrowserController`
279
+
280
+ Controls a single browser process.
281
+
282
+ | Method | Description |
283
+ | :--------------------- | :-------------------------------------------------- |
284
+ | `newContext(options?)` | Creates a new, isolated `BrowserContextController`. |
285
+ | `waitForNoContexts()` | Waits until all contexts are closed. |
286
+ | `close()` | Closes the browser and all its contexts. |
287
+ | `waitForClose()` | Waits for the browser to close. |
288
+
289
+ ### `BrowserContextController`
290
+
291
+ Controls an isolated browser session.
292
+
293
+ | Method | Description |
294
+ | :----------------------------- | :------------------------------------------------------- |
295
+ | `newPage(options?)` | Creates a new `PageController` (tab). |
296
+ | `pages()` | Returns all open pages in this context. |
297
+ | `getControllerByPage(page)` | Wraps a native Playwright `Page` in a `PageController`. |
298
+ | `getState()` | Returns the storage state (cookies, localStorage). |
299
+ | `setExtraHttpHeaders(headers)` | Sets headers for all requests in this context. |
300
+ | `attachLogger(logger)` | Attaches a logger to monitor console and network events. |
301
+ | `waitForNoPages()` | Waits until all pages are closed. |
302
+ | `close()` | Closes the context and all pages. |
303
+
304
+ ### `PageController`
305
+
306
+ Controls a single tab. Extends `DocumentController`.
307
+
308
+ | Method | Description |
309
+ | :----------------------------- | :----------------------------------------------------- |
310
+ | `url()` | Returns the current URL. |
311
+ | `opened()` | Returns pages opened by this page (e.g., popups). |
312
+ | `opener()` | Returns the page that opened this one. |
313
+ | `navigate(url, options?)` | Navigates to a URL. |
314
+ | `waitForUrl(url, options?)` | Waits for the URL to match a predicate or string. |
315
+ | `waitForLoadState(state?)` | Waits for the page to reach a specific load state. |
316
+ | `setContent(html, options?)` | Sets the page content. |
317
+ | `renderPdf(options?)` | Renders the page as a PDF `Uint8Array`. |
318
+ | `renderPdfStream(options?)` | Renders the page as a PDF `ReadableStream`. |
319
+ | `scroll(deltaX, deltaY)` | Scrolls using the mouse wheel. |
320
+ | `scrollTo(target)` | Scrolls to a coordinate or `ElementController`. |
321
+ | `frame(selector)` | Gets a `FrameController` for an iframe. |
322
+ | `close()` | Closes the page. |
323
+ | `attachLogger(logger)` | Attaches a logger to this specific page. |
324
+
325
+ ### `FrameController`
326
+
327
+ Controls a single iframe. Extends `DocumentController`.
328
+
329
+ | Method | Description |
330
+ | :------------------ | :---------------------------------- |
331
+ | `getPage()` | Returns the page containing this frame. |
332
+ | `getFrame(selector)`| Gets a child frame. |
333
+
334
+ ### `DocumentController`
335
+
336
+ Base class for `PageController` and `FrameController`. Extends `LocatorController`.
337
+
338
+ | Method | Description |
339
+ | :---------------------------- | :--------------------------------------------------- |
340
+ | `evaluate(fn, arg?)` | Executes JavaScript in the context of the document. |
341
+ | `frames()` | Returns all child frames. |
342
+ | `getControllerByFrame(frame)` | Wraps a native Playwright `Frame` in a controller. |
343
+ | `waitForElement(selector)` | Waits for an element to appear in the DOM. |
344
+ | `locateInFrame(selector)` | Returns a `LocatorController` for an iframe. |
345
+
346
+ ### `ElementController`
347
+
348
+ Wraps a Playwright `Locator` or `ElementHandle`. Extends `LocatorController`.
349
+
350
+ | Method | Description |
351
+ | :------------------------------ | :---------------------------------------------------------- |
352
+ | `exists(options?)` | Checks if the element exists and is visible (configurable). |
353
+ | `count()` | Returns the number of matching elements. |
354
+ | `isVisible()` | Checks if the element is visible. |
355
+ | `isHidden()` | Checks if the element is hidden. |
356
+ | `isEnabled()` | Checks if the element is enabled. |
357
+ | `isDisabled()` | Checks if the element is disabled. |
358
+ | `isChecked()` | Checks if the element is checked (radio/checkbox). |
359
+ | `isEditable()` | Checks if the element is editable. |
360
+ | `click(options?)` | Clicks the element. |
361
+ | `dblclick(options?)` | Double-clicks the element. |
362
+ | `fill(text, options?)` | Fills an input. |
363
+ | `clear(options?)` | Clears an input. |
364
+ | `type(text, options?)` | Types text character by character. |
365
+ | `press(key, options?)` | Simulates a key press. |
366
+ | `check(options?)` | Checks a checkbox or radio button. |
367
+ | `uncheck(options?)` | Unchecks a checkbox or radio button. |
368
+ | `setChecked(checked, options?)` | Sets the checked state. |
369
+ | `selectOption(values, options?)`| Selects one or multiple options in a `<select>`. |
370
+ | `setInputFiles(files, options?)`| Sets files for a file input. |
371
+ | `hover(options?)` | Hovers over the element. |
372
+ | `focus(options?)` | Focuses the element. |
373
+ | `tap(options?)` | Taps the element (touch). |
374
+ | `selectText(options?)` | Selects the text in the element. |
375
+ | `inputValue()` | Gets the current value of an input. |
376
+ | `evaluate(fn, arg?)` | Executes JavaScript in the context of the element. |
377
+ | `boundingBox()` | Returns the element's bounding box (x, y, width, height). |
378
+ | `scrollIntoViewIfNeeded()` | Scrolls the element into view if it's not visible. |
379
+ | `filter(filter)` | Narrow down the selection based on text or descendants. |
380
+ | `locate(selector)` | Finds descendant elements. |
381
+ | `and(element)` | Combines two locators (intersection). |
382
+ | `or(element)` | Combines two locators (union). |
383
+ | `first()` | Returns the first matching element. |
384
+ | `last()` | Returns the last matching element. |
385
+ | `nth(index)` | Returns the nth matching element. |
386
+ | `waitFor(state?)` | Waits for a specific state (visible, attached, etc.). |
387
+ | `getAll()` | Returns an array of controllers for all matching elements. |
388
+
389
+ ### `LocatorController`
390
+
391
+ Base class for searching elements.
392
+
393
+ | Method | Description |
394
+ | :--------------------- | :--------------------------------------------- |
395
+ | `getBySelector(sel)` | Finds element(s) by CSS selector. |
396
+ | `getByRole(role)` | Finds element(s) by ARIA role. |
397
+ | `getByLabel(text)` | Finds element(s) by associated label text. |
398
+ | `getByPlaceholder(tx)` | Finds element(s) by placeholder text. |
399
+ | `getByText(text)` | Finds element(s) by text content. |
400
+ | `getByAltText(text)` | Finds element(s) by alt attribute. |
401
+ | `getByTitle(text)` | Finds element(s) by title attribute. |
@@ -0,0 +1,156 @@
1
+ # Cancellation (`@tstdl/base/cancellation`)
2
+
3
+ A powerful and flexible cancellation signaling module for TypeScript, built on the native `AbortSignal` API. It provides a robust mechanism for managing cancellation in asynchronous operations, offering seamless integration with Promises, Observables, and the native `AbortController`.
4
+
5
+ ## ✨ Features
6
+
7
+ - **Native Integration:** Built on top of the native `AbortSignal` for maximum compatibility with modern web and Node.js APIs.
8
+ - **One-way Lifecycle:** Signals are one-way (once set, they stay set), simplifying state management and reasoning.
9
+ - **Hierarchical:** Create child signals that inherit cancellation state from parents.
10
+ - **Promise-friendly:** `await` a signal to pause execution until it is cancelled.
11
+ - **Resource Management:** Automatic cleanup via the `Disposable` pattern (`Symbol.dispose`).
12
+ - **Explicit Read/Write Separation:** Use `CancellationToken` to control state and `CancellationSignal` for a read-only view.
13
+
14
+ ## Core Concepts
15
+
16
+ The module revolves around two main classes:
17
+
18
+ 1. **`CancellationToken`**: A mutable cancellation source. It provides a `.set()` method to trigger cancellation.
19
+ 2. **`CancellationSignal`**: A read-only view of a cancellation state. It allows checking if cancellation has occurred and waiting for it.
20
+
21
+ A `CancellationToken` _is a_ `CancellationSignal`, but it has the additional ability to trigger the cancellation.
22
+
23
+ The state of a signal is:
24
+
25
+ - **Unset** (`isUnset` is true): The default state; the operation should continue.
26
+ - **Set** (`isSet` is true): The cancelled state; the operation should stop.
27
+
28
+ Once a signal is **Set**, it cannot be **Unset**.
29
+
30
+ ## 🚀 Basic Usage
31
+
32
+ Create a `CancellationToken` to manage the lifecycle of an operation.
33
+
34
+ ```typescript
35
+ import { CancellationToken } from '@tstdl/base/cancellation';
36
+
37
+ const token = new CancellationToken();
38
+
39
+ console.log(token.isSet); // false
40
+ console.log(token.isUnset); // true
41
+
42
+ // Pass the read-only signal to a consumer
43
+ doWork(token.signal);
44
+
45
+ // Some time later...
46
+ token.set('User requested cancellation'); // Set the token to a cancelled state
47
+
48
+ console.log(token.isSet); // true
49
+ console.log(token.reason); // Error: Operation cancelled (cause: User requested cancellation)
50
+
51
+ function doWork(signal: CancellationSignal) {
52
+ if (signal.isSet) {
53
+ return;
54
+ }
55
+ // ... perform work
56
+ }
57
+ ```
58
+
59
+ ## 🔧 Advanced Topics
60
+
61
+ ### Promise Integration
62
+
63
+ You can `await` a signal using the `.wait()` method. It returns a promise that resolves when the signal is set. By default, it throws the cancellation reason as an error.
64
+
65
+ ```typescript
66
+ async function longRunningTask(signal: CancellationSignal) {
67
+ console.log('Task started.');
68
+
69
+ try {
70
+ // Wait for the signal to be set (will throw)
71
+ await signal.wait();
72
+ } catch (error) {
73
+ console.log('Task was cancelled:', error.message);
74
+ return;
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Hierarchical Cancellation (Inheritance)
80
+
81
+ Create a hierarchy of signals. When a parent signal is set, all its children are also set.
82
+
83
+ ```typescript
84
+ const parent = new CancellationToken();
85
+
86
+ // child1 inherits from parent
87
+ const child1 = parent.fork();
88
+
89
+ // child2 inherits from parent and another source
90
+ const child2 = new CancellationToken([parent, otherSignal]);
91
+
92
+ parent.set();
93
+ console.log(child1.isSet); // true
94
+ console.log(child2.isSet); // true
95
+ ```
96
+
97
+ ### Native `AbortSignal` Interoperability
98
+
99
+ `CancellationSignal` wraps a native `AbortSignal` and can be created from any `AbortSignal` or `AbortController`.
100
+
101
+ ```typescript
102
+ const controller = new AbortController();
103
+ const signal = CancellationSignal.from(controller.signal);
104
+
105
+ // Use the wrapped signal with fetch
106
+ const response = await fetch(url, { signal: signal.abortSignal });
107
+ ```
108
+
109
+ ### Timeouts
110
+
111
+ Easily create signals that automatically trigger after a timeout.
112
+
113
+ ```typescript
114
+ const signal = parentSignal.withTimeout(5000); // Set after 5 seconds or if parent is set
115
+ ```
116
+
117
+ ### Cleanup and Disposal
118
+
119
+ `CancellationSignal` and `CancellationToken` implement the `Disposable` interface. Disposing of a signal will abort its internal controller, effectively setting it.
120
+
121
+ ```typescript
122
+ using token = new CancellationToken();
123
+ // token will be disposed (and thus set) when the scope is exited
124
+ ```
125
+
126
+ ## 📚 API Reference
127
+
128
+ ### `CancellationSignal` (Read-only)
129
+
130
+ | Member | Type | Description |
131
+ | :----------------- | :------------------- | :------------------------------------------------------------------------ |
132
+ | `isSet` | `boolean` | Whether the signal is set (cancelled). |
133
+ | `isUnset` | `boolean` | Whether the signal is not yet set. |
134
+ | `reason` | `any` | The reason for cancellation (usually an `Error`). |
135
+ | `abortSignal` | `AbortSignal` | The underlying native `AbortSignal`. |
136
+ | `wait(options?)` | `Promise<void>` | Returns a promise that resolves when the signal is set. |
137
+ | `throwIfSet()` | `void` | Throws the `reason` if the signal is set. |
138
+ | `fork()` | `CancellationToken` | Creates a new `CancellationToken` that inherits from this signal. |
139
+ | `inherit(sources)` | `CancellationSignal` | Creates a new signal that is set when this or any of the sources are set. |
140
+ | `withTimeout(ms)` | `CancellationSignal` | Creates a new signal that is set after a timeout. |
141
+ | `dispose()` | `void` | Manually disposes of the signal (sets it). |
142
+
143
+ ### `CancellationToken` (Writable)
144
+
145
+ Extends `CancellationSignal`.
146
+
147
+ | Member | Type | Description |
148
+ | :---------------------- | :------------------- | :-------------------------------------------------------------- |
149
+ | `constructor(parents?)` | - | Creates a new token, optionally inheriting from parent sources. |
150
+ | `set(reason?)` | `void` | Triggers cancellation with an optional reason. |
151
+ | `signal` | `CancellationSignal` | Returns a read-only view of this token. |
152
+
153
+ ### `CancellationSource` (Type)
154
+
155
+ A union type representing any object that can be used as a source for cancellation:
156
+ `CancellationToken | CancellationSignal | AbortController | AbortSignal`
@@ -0,0 +1 @@
1
+ import '../../polyfills.js';
@@ -0,0 +1,49 @@
1
+ import '../../polyfills.js';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import * as core from '../../core.js';
4
+ import { CancellationToken, sourcesToAbortSignal } from '../token.js';
5
+ describe('Cancellation Coverage Extra', () => {
6
+ it('should hit dev mode branch (true)', () => {
7
+ vi.spyOn(core, 'isDevMode').mockReturnValue(true);
8
+ const token = new CancellationToken();
9
+ expect(token.isSet).toBe(false);
10
+ });
11
+ it('should hit dev mode branch (false)', () => {
12
+ vi.spyOn(core, 'isDevMode').mockReturnValue(false);
13
+ const token = new CancellationToken();
14
+ expect(token.isSet).toBe(false);
15
+ });
16
+ it('should hit throw: true branch in wait', async () => {
17
+ const token = new CancellationToken();
18
+ token.set('reason');
19
+ await expect(token.wait({ throw: true })).rejects.toThrow();
20
+ });
21
+ it('should hit waitThrow', async () => {
22
+ const token = new CancellationToken();
23
+ token.set('reason');
24
+ await expect(token.waitThrow()).rejects.toThrow();
25
+ });
26
+ it('should hit signals.length == 1 branch', () => {
27
+ const controller = new AbortController();
28
+ const signal = sourcesToAbortSignal(controller.signal);
29
+ expect(signal).toBe(controller.signal);
30
+ });
31
+ it('should hit signals.length > 1 branch', () => {
32
+ const controller1 = new AbortController();
33
+ const controller2 = new AbortController();
34
+ const signal = sourcesToAbortSignal([controller1.signal, controller2.signal]);
35
+ expect(signal).not.toBe(controller1.signal);
36
+ expect(signal).not.toBe(controller2.signal);
37
+ });
38
+ it('should hit sourcesToAbortSignal error branch', () => {
39
+ expect(() => sourcesToAbortSignal([])).toThrow('At least one cancellation source must be provided');
40
+ });
41
+ it('should hit unsubscribe branch in subscribe', () => {
42
+ const token = new CancellationToken();
43
+ const next = vi.fn();
44
+ const subscription = token.subscribe({ next });
45
+ subscription.unsubscribe();
46
+ token.set();
47
+ expect(next).not.toHaveBeenCalled();
48
+ });
49
+ });
@@ -1,40 +1,35 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { CancellationToken } from '../token.js';
2
+ import { CancellationSignal, CancellationToken } from '../token.js';
3
3
  describe('CancellationToken Memory Leak', () => {
4
- it('should not leak subscriptions in connect', () => {
4
+ it('should not leak when using inherit and disposing', () => {
5
5
  const parent = new CancellationToken();
6
- // @ts-ignore
7
- const initialParentSubscribers = parent._stateSubject.observers.length;
8
6
  for (let i = 0; i < 1000; i++) {
9
- const child = new CancellationToken();
10
- parent.connect(child, { once: true });
11
- child.set();
7
+ const child = parent.inherit(new CancellationToken());
8
+ child.dispose();
12
9
  }
13
- // @ts-ignore
14
- expect(parent._stateSubject.observers.length).toBeLessThan(initialParentSubscribers + 10);
10
+ expect(parent.isSet).toBe(false);
15
11
  });
16
- it('should not leak when child is completed', () => {
12
+ it('should follow parent signal', () => {
17
13
  const parent = new CancellationToken();
18
- const child = new CancellationToken();
19
- parent.connect(child);
20
- // @ts-ignore
21
- expect(parent._stateSubject.observers.length).toBe(1);
22
- child.complete();
23
- // @ts-ignore
24
- expect(parent._stateSubject.observers.length).toBe(0);
14
+ const child = new CancellationToken(parent);
15
+ expect(child.isSet).toBe(false);
16
+ parent.set();
17
+ expect(child.isSet).toBe(true);
25
18
  });
26
- it('should not leak when parent is completed', () => {
19
+ it('should not affect parent when child is set', () => {
27
20
  const parent = new CancellationToken();
28
- const child = new CancellationToken();
29
- parent.connect(child);
30
- // @ts-ignore
31
- expect(parent._stateSubject.observers.length).toBeGreaterThan(0);
32
- // @ts-ignore
33
- expect(child._stateSubject.observers.length).toBeGreaterThan(0);
34
- parent.complete();
35
- // @ts-ignore
36
- expect(parent._stateSubject.observers.length).toBe(0);
37
- // @ts-ignore
38
- expect(child._stateSubject.observers.length).toBe(0);
21
+ const child = new CancellationToken(parent);
22
+ expect(parent.isSet).toBe(false);
23
+ child.set();
24
+ expect(parent.isSet).toBe(false);
25
+ expect(child.isSet).toBe(true);
26
+ });
27
+ it('should clean up when disposed', () => {
28
+ const parent = new CancellationToken();
29
+ const signal = CancellationSignal.from(parent);
30
+ expect(signal.isSet).toBe(false);
31
+ signal.dispose();
32
+ expect(signal.isSet).toBe(true);
33
+ expect(parent.isSet).toBe(false);
39
34
  });
40
35
  });
@@ -0,0 +1 @@
1
+ import '../../polyfills.js';