@adonix.org/cloud-spark 0.0.195 → 1.0.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/README.md +308 -10
- package/dist/index.d.ts +7 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Cloud
|
|
1
|
+
# Cloud⚡Spark
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@adonix.org/cloud-spark)
|
|
4
4
|
[](https://github.com/adonix-org/cloud-spark/blob/main/LICENSE)
|
|
@@ -11,12 +11,37 @@
|
|
|
11
11
|
|
|
12
12
|
CloudSpark provides a logical foundation for building Cloudflare Workers. It works well for simple workers or projects that grow in complexity, helping keep code organized and functionality scalable. It is lightweight and designed to let you focus on the logic that powers your worker.
|
|
13
13
|
|
|
14
|
-
:bulb: If you are new to _Cloudflare Workers_, create a free [Cloudflare account](https://dash.cloudflare.com/sign-up) and install their command line interface [Wrangler](#
|
|
14
|
+
:bulb: If you are new to _Cloudflare Workers_, create a free [Cloudflare account](https://dash.cloudflare.com/sign-up) and install their command line interface [Wrangler](#partly_sunny-wrangler).
|
|
15
15
|
|
|
16
16
|
Detailed worker documentation can also be found [here](https://developers.cloudflare.com/workers/).
|
|
17
17
|
|
|
18
18
|
<br>
|
|
19
19
|
|
|
20
|
+
## :books: Contents
|
|
21
|
+
|
|
22
|
+
- [Install](#package-install)
|
|
23
|
+
|
|
24
|
+
- [Quickstart](#rocket-quickstart)
|
|
25
|
+
|
|
26
|
+
- [Basic Worker](#arrow_right-basic-worker)
|
|
27
|
+
|
|
28
|
+
- [Route Worker](#twisted_rightwards_arrows-route-worker)
|
|
29
|
+
|
|
30
|
+
- [Middleware](#gear-middleware)
|
|
31
|
+
- [CORS](#cors)
|
|
32
|
+
- [Cache](#cache)
|
|
33
|
+
- [WebSocket](#websocket)
|
|
34
|
+
- [Custom](#custom)
|
|
35
|
+
- [Ordering](#ordering)
|
|
36
|
+
|
|
37
|
+
- [WebSockets](#left_right_arrow-web-sockets)
|
|
38
|
+
|
|
39
|
+
- [Wrangler](#partly_sunny-wrangler)
|
|
40
|
+
|
|
41
|
+
- [Links](#link-links)
|
|
42
|
+
|
|
43
|
+
<br>
|
|
44
|
+
|
|
20
45
|
## :package: Install
|
|
21
46
|
|
|
22
47
|
```bash
|
|
@@ -27,7 +52,7 @@ npm install @adonix.org/cloud-spark
|
|
|
27
52
|
|
|
28
53
|
## :rocket: Quickstart
|
|
29
54
|
|
|
30
|
-
:computer: Use [Wrangler](#
|
|
55
|
+
:computer: Use [Wrangler](#partly_sunny-wrangler) to create a new project:
|
|
31
56
|
|
|
32
57
|
```bash
|
|
33
58
|
wrangler init
|
|
@@ -422,18 +447,22 @@ class ChatWorker extends RouteWorker {
|
|
|
422
447
|
/**
|
|
423
448
|
* Handles WebSocket upgrade requests.
|
|
424
449
|
*
|
|
425
|
-
* Expects a DurableObject binding named
|
|
450
|
+
* Expects a DurableObject binding named CHAT_ROOM
|
|
426
451
|
* in wrangler.jsonc
|
|
427
452
|
*/
|
|
428
453
|
protected upgrade(params: PathParams): Promise<Response> {
|
|
429
|
-
|
|
430
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Get the Durable Object stub for the chat room
|
|
456
|
+
* given by the "room" path parameter.
|
|
457
|
+
*/
|
|
458
|
+
const stub = this.env.CHAT_ROOM.getByName(params["room"]);
|
|
431
459
|
|
|
432
460
|
/**
|
|
433
|
-
*
|
|
434
|
-
* WebSocket middleware
|
|
461
|
+
* The request has already been validated by the
|
|
462
|
+
* WebSocket middleware, so dispatch the WebSocket
|
|
463
|
+
* upgrade request to the Durable Object.
|
|
435
464
|
*/
|
|
436
|
-
return
|
|
465
|
+
return stub.fetch(this.request);
|
|
437
466
|
}
|
|
438
467
|
}
|
|
439
468
|
|
|
@@ -443,6 +472,8 @@ class ChatWorker extends RouteWorker {
|
|
|
443
472
|
export default ChatWorker.ignite();
|
|
444
473
|
```
|
|
445
474
|
|
|
475
|
+
:bulb: See the complete WebSocket example [here](#left_right_arrow-web-sockets).
|
|
476
|
+
|
|
446
477
|
### Custom
|
|
447
478
|
|
|
448
479
|
Create custom middleware by implementing the [Middleware](https://github.com/adonix-org/cloud-spark/blob/main/src/interfaces/middleware.ts) interface and its single _handle_ method, then register it with your worker. Within your middleware, you can inspect requests and modify responses or short-circuit processing entirely.
|
|
@@ -519,13 +550,261 @@ export function poweredby(name?: string): Middleware {
|
|
|
519
550
|
}
|
|
520
551
|
```
|
|
521
552
|
|
|
553
|
+
### Ordering
|
|
554
|
+
|
|
555
|
+
The order in which middleware is registered by a worker can matter depending on the implementation. It helps to visualize ordering as _top-down_ for requests and _bottom-up_ for responses.
|
|
556
|
+
|
|
557
|
+
Here is a what a full `GET` request flow with middleware `A`, `B`, and `C` could look like:
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
Full
|
|
561
|
+
|
|
562
|
+
Request Response
|
|
563
|
+
↓ this.use(A) ↑
|
|
564
|
+
↓ this.use(B) ↑
|
|
565
|
+
↓ this.use(C) ↑
|
|
566
|
+
→ get() →
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Now imagine `B` middleware returns a response early and short-circuits the flow:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
Short Circuit B
|
|
574
|
+
|
|
575
|
+
Request Response
|
|
576
|
+
↓ this.use(A) ↑
|
|
577
|
+
↓ this.use(B) →
|
|
578
|
+
this.use(C)
|
|
579
|
+
get()
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
In this scenario, neither middleware `C` nor the worker's `get()` method executes. This is exactly what you want, for example, when using the [Cache](#cache) middleware. If a valid response is found in the cache, that response can and should be returned immediately.
|
|
583
|
+
|
|
584
|
+
However, this illustrates that different behavior can occur depending on the order of middleware registration.
|
|
585
|
+
|
|
586
|
+
We can use the built-in [Cache](#cache) and [CORS](#cors) middleware as a more concrete example:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
/**
|
|
590
|
+
* This version results in CORS response headers stored in
|
|
591
|
+
* the cache. On the first cacheable response, CORS middleware
|
|
592
|
+
* applies its response headers BEFORE caching.
|
|
593
|
+
*/
|
|
594
|
+
this.use(cache());
|
|
595
|
+
this.use(cors());
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* This version results in CORS response headers NOT stored
|
|
599
|
+
* in the cache, which is likely preferred. Fresh CORS headers
|
|
600
|
+
* are added to every response regardless of cache status.
|
|
601
|
+
*/
|
|
602
|
+
this.use(cors());
|
|
603
|
+
this.use(cache());
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
The difference in behavior becomes clear when disabling the CORS middleware on the worker. In the first version, CORS headers remain on all cached responses until the cached entry expires. In the second version, disabling CORS takes effect immediately; all responses, cached or not, will no longer include CORS headers.
|
|
607
|
+
|
|
522
608
|
<br>
|
|
523
609
|
|
|
524
610
|
## :left_right_arrow: Web Sockets
|
|
525
611
|
|
|
612
|
+
Simplify [WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#_top) connection management with CloudSpark. Features include:
|
|
613
|
+
|
|
614
|
+
- Type-safe session metadata
|
|
615
|
+
- Support for [Hibernation WebSocket API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api) (recommended)
|
|
616
|
+
- Support for [Standard WebSocket API](https://developers.cloudflare.com/workers/runtime-apis/websockets/)
|
|
617
|
+
- [Middleware](#websocket) for Upgrade request validation
|
|
618
|
+
- Standardized WebSocketUpgrade response
|
|
619
|
+
|
|
620
|
+
The following is a simple chat with hibernation example:
|
|
621
|
+
|
|
622
|
+
:page_facing_up: wrangler.jsonc
|
|
623
|
+
|
|
624
|
+
```jsonc
|
|
625
|
+
/**
|
|
626
|
+
* Remember to rerun 'wrangler types' after you change your
|
|
627
|
+
* wrangler.json file.
|
|
628
|
+
*/
|
|
629
|
+
{
|
|
630
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
631
|
+
"name": "chat-room",
|
|
632
|
+
"main": "src/index.ts",
|
|
633
|
+
"compatibility_date": "2025-11-01",
|
|
634
|
+
"observability": {
|
|
635
|
+
"enabled": true,
|
|
636
|
+
},
|
|
637
|
+
"durable_objects": {
|
|
638
|
+
"bindings": [
|
|
639
|
+
{
|
|
640
|
+
"name": "CHAT_ROOM",
|
|
641
|
+
"class_name": "ChatRoom",
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
"migrations": [
|
|
646
|
+
{
|
|
647
|
+
"tag": "v1",
|
|
648
|
+
"new_sqlite_classes": ["ChatRoom"],
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
:page_facing_up: index.ts
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
import { DurableObject } from "cloudflare:workers";
|
|
658
|
+
|
|
659
|
+
import {
|
|
660
|
+
GET,
|
|
661
|
+
PathParams,
|
|
662
|
+
RouteWorker,
|
|
663
|
+
websocket,
|
|
664
|
+
WebSocketSessions,
|
|
665
|
+
WebSocketUpgrade,
|
|
666
|
+
} from "@adonix.org/cloud-spark";
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Metadata attached to each session.
|
|
670
|
+
*/
|
|
671
|
+
interface Profile {
|
|
672
|
+
name: string;
|
|
673
|
+
lastActive: number;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export class ChatRoom extends DurableObject {
|
|
677
|
+
/**
|
|
678
|
+
* Manage all active connections for this room.
|
|
679
|
+
*/
|
|
680
|
+
protected readonly sessions = new WebSocketSessions<Profile>();
|
|
681
|
+
|
|
682
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
683
|
+
super(ctx, env);
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Restore all active connections on wake from
|
|
687
|
+
* hibernation.
|
|
688
|
+
*/
|
|
689
|
+
this.sessions.restoreAll(this.ctx.getWebSockets());
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
public override fetch(request: Request): Promise<Response> {
|
|
693
|
+
/**
|
|
694
|
+
* For demo purposes, get the user's name from the `name`
|
|
695
|
+
* query parameter.
|
|
696
|
+
*/
|
|
697
|
+
const name = new URL(request.url).searchParams.get("name") ?? "Anonymous";
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Create a new connection and initialize its `Profile`
|
|
701
|
+
* attachment.
|
|
702
|
+
*/
|
|
703
|
+
const con = this.sessions.create({
|
|
704
|
+
name,
|
|
705
|
+
lastActive: Date.now(),
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Accept the WebSocket with recommended hibernation enabled.
|
|
710
|
+
*
|
|
711
|
+
* To accept without hibernation, use `con.accept()` and
|
|
712
|
+
* `con.addEventListener()` methods instead.
|
|
713
|
+
*/
|
|
714
|
+
const client = con.acceptWebSocket(this.ctx);
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Return the upgrade response with the client WebSocket.
|
|
718
|
+
*/
|
|
719
|
+
return new WebSocketUpgrade(client).response();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Send a message to all active sessions.
|
|
724
|
+
*/
|
|
725
|
+
public broadcast(message: string): void {
|
|
726
|
+
for (const session of this.sessions) {
|
|
727
|
+
session.send(message);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
public override webSocketMessage(ws: WebSocket, message: string): void {
|
|
732
|
+
/**
|
|
733
|
+
* Get the sender's WebSocket session from the active sessions.
|
|
734
|
+
*/
|
|
735
|
+
const con = this.sessions.get(ws);
|
|
736
|
+
if (!con) return;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Update the sender's `Profile` with current `lastActive` time.
|
|
740
|
+
*/
|
|
741
|
+
con.attach({ lastActive: Date.now() });
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Broadcast the message to all sessions, prefixed with the
|
|
745
|
+
* sender’s name.
|
|
746
|
+
*/
|
|
747
|
+
this.broadcast(`${con.attachment.name}: ${message}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
public override webSocketClose(ws: WebSocket, code: number, reason: string): void {
|
|
751
|
+
/**
|
|
752
|
+
* Closes and removes the WebSocket from active sessions.
|
|
753
|
+
*/
|
|
754
|
+
this.sessions.close(ws, code, reason);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
class ChatWorker extends RouteWorker {
|
|
759
|
+
protected override init(): void {
|
|
760
|
+
/**
|
|
761
|
+
* Define the WebSocket connection route.
|
|
762
|
+
*/
|
|
763
|
+
this.route(GET, "/chat/:room", this.upgrade);
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Register the middleware to validate WebSocket
|
|
767
|
+
* connection requests.
|
|
768
|
+
*/
|
|
769
|
+
this.use(websocket("/chat/:room"));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private upgrade(params: PathParams): Promise<Response> {
|
|
773
|
+
/**
|
|
774
|
+
* Get the Durable Object stub for the chat room
|
|
775
|
+
* given by the "room" path parameter.
|
|
776
|
+
*/
|
|
777
|
+
const stub = this.env.CHAT_ROOM.getByName(params["room"]);
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Dispatch the WebSocket upgrade request to the
|
|
781
|
+
* Durable Object.
|
|
782
|
+
*/
|
|
783
|
+
return stub.fetch(this.request);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Connects ChatWorker to the Cloudflare runtime.
|
|
789
|
+
*/
|
|
790
|
+
export default ChatWorker.ignite();
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
:computer: To run this chat example locally:
|
|
794
|
+
|
|
795
|
+
```bash
|
|
796
|
+
wrangler dev
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
:bulb: Apps like [Postman](https://www.postman.com/downloads/) can be used to create and join local chat rooms for testing:
|
|
800
|
+
|
|
801
|
+
```
|
|
802
|
+
ws://localhost:8787/chat/fencing?name=Inigo
|
|
803
|
+
```
|
|
804
|
+
|
|
526
805
|
<br>
|
|
527
806
|
|
|
528
|
-
## :
|
|
807
|
+
## :partly_sunny: Wrangler
|
|
529
808
|
|
|
530
809
|
First, create a **FREE** [Cloudflare account](https://dash.cloudflare.com/sign-up).
|
|
531
810
|
|
|
@@ -548,3 +827,22 @@ wrangler init
|
|
|
548
827
|
```
|
|
549
828
|
|
|
550
829
|
[Install](#package-install) Cloud Spark
|
|
830
|
+
|
|
831
|
+
<br>
|
|
832
|
+
|
|
833
|
+
## :link: Links
|
|
834
|
+
|
|
835
|
+
- [Cloudflare - Home](https://www.cloudflare.com)
|
|
836
|
+
- [Cloudflare - Dashboard](https://dash.cloudflare.com)
|
|
837
|
+
- [Wrangler](https://developers.cloudflare.com/workers/wrangler/)
|
|
838
|
+
- [Workers](https://developers.cloudflare.com/workers/)
|
|
839
|
+
- [Workers - SDK](https://github.com/cloudflare/workers-sdk)
|
|
840
|
+
- [Hibernation WebSocket API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api)
|
|
841
|
+
- [Standard WebSocket API](https://developers.cloudflare.com/workers/runtime-apis/websockets/)
|
|
842
|
+
- [Postman](https://www.postman.com/downloads/)
|
|
843
|
+
- [http-status-codes](https://github.com/prettymuchbryce/http-status-codes)
|
|
844
|
+
- [path-to-regexp](https://github.com/pillarjs/path-to-regexp)
|
|
845
|
+
|
|
846
|
+
##
|
|
847
|
+
|
|
848
|
+
### [:arrow_up:](#books-contents)
|
package/dist/index.d.ts
CHANGED
|
@@ -511,10 +511,14 @@ interface WebSocketConnection<A extends WSAttachment> {
|
|
|
511
511
|
*/
|
|
512
512
|
get attachment(): Readonly<A>;
|
|
513
513
|
/**
|
|
514
|
-
* Attaches a user-defined object
|
|
514
|
+
* Attaches or updates a user-defined object on this connection.
|
|
515
515
|
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
516
|
+
* Passing a partial object merges the new properties into the existing
|
|
517
|
+
* attachment, leaving other fields unchanged. Pass `null` to clear
|
|
518
|
+
* the attachment entirely.
|
|
519
|
+
*
|
|
520
|
+
* @param attachment - Partial object containing metadata to attach or update,
|
|
521
|
+
* or `null` to clear the attachment.
|
|
518
522
|
*/
|
|
519
523
|
attach(attachment?: Partial<A> | null): void;
|
|
520
524
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonix.org/cloud-spark",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Ignite your Cloudflare Workers with a type-safe library for rapid development.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -53,19 +53,19 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@eslint/js": "^9.39.1",
|
|
55
55
|
"@types/node": "^24.10.1",
|
|
56
|
-
"@typescript-eslint/eslint-plugin": "^8.48.
|
|
57
|
-
"@typescript-eslint/parser": "^8.48.
|
|
58
|
-
"@vitest/coverage-v8": "^4.0.
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
|
57
|
+
"@typescript-eslint/parser": "^8.48.1",
|
|
58
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
59
59
|
"eslint": "^9.39.1",
|
|
60
60
|
"eslint-plugin-import": "^2.32.0",
|
|
61
61
|
"globals": "^16.5.0",
|
|
62
62
|
"jiti": "^2.6.1",
|
|
63
|
-
"prettier": "^3.
|
|
63
|
+
"prettier": "^3.7.3",
|
|
64
64
|
"tsup": "^8.5.1",
|
|
65
65
|
"typescript": "^5.9.3",
|
|
66
|
-
"typescript-eslint": "^8.48.
|
|
66
|
+
"typescript-eslint": "^8.48.1",
|
|
67
67
|
"vitest": "^4.0.13",
|
|
68
|
-
"wrangler": "^4.
|
|
68
|
+
"wrangler": "^4.51.0"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"cache-control-parser": "^2.0.6",
|