@adonix.org/cloud-spark 0.0.195 → 1.0.0
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 +304 -7
- package/dist/index.d.ts +7 -3
- package/package.json +2 -2
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
|
|
@@ -426,14 +451,17 @@ class ChatWorker extends RouteWorker {
|
|
|
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
|
+
* defined by the "room" path parameter.
|
|
457
|
+
*/
|
|
458
|
+
const stub = this.env.CHAT_ROOM.getByName(params["room"]);
|
|
431
459
|
|
|
432
460
|
/**
|
|
433
461
|
* Request has already been validated by the
|
|
434
462
|
* WebSocket middleware.
|
|
435
463
|
*/
|
|
436
|
-
return
|
|
464
|
+
return stub.fetch(this.request);
|
|
437
465
|
}
|
|
438
466
|
}
|
|
439
467
|
|
|
@@ -443,6 +471,8 @@ class ChatWorker extends RouteWorker {
|
|
|
443
471
|
export default ChatWorker.ignite();
|
|
444
472
|
```
|
|
445
473
|
|
|
474
|
+
:bulb: See the complete WebSocket example [here](#left_right_arrow-web-sockets).
|
|
475
|
+
|
|
446
476
|
### Custom
|
|
447
477
|
|
|
448
478
|
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 +549,261 @@ export function poweredby(name?: string): Middleware {
|
|
|
519
549
|
}
|
|
520
550
|
```
|
|
521
551
|
|
|
552
|
+
### Ordering
|
|
553
|
+
|
|
554
|
+
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.
|
|
555
|
+
|
|
556
|
+
Here is a what a full `GET` request flow with middleware `A`, `B`, and `C` could look like:
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
Full
|
|
560
|
+
|
|
561
|
+
Request Response
|
|
562
|
+
↓ this.use(A) ↑
|
|
563
|
+
↓ this.use(B) ↑
|
|
564
|
+
↓ this.use(C) ↑
|
|
565
|
+
→ get() →
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Now imagine `B` middleware returns a response early and short-circuits the flow:
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
Short Circuit B
|
|
573
|
+
|
|
574
|
+
Request Response
|
|
575
|
+
↓ this.use(A) ↑
|
|
576
|
+
↓ this.use(B) →
|
|
577
|
+
this.use(C)
|
|
578
|
+
get()
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
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.
|
|
582
|
+
|
|
583
|
+
However, this illustrates that different behavior can occur depending on the order of middleware registration.
|
|
584
|
+
|
|
585
|
+
We can use the built-in [Cache](#cache) and [CORS](#cors) middleware as a more concrete example:
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
/**
|
|
589
|
+
* This version results in CORS response headers stored in
|
|
590
|
+
* the cache. On the first cacheable response, CORS middleware
|
|
591
|
+
* applies its response headers BEFORE caching.
|
|
592
|
+
*/
|
|
593
|
+
this.use(cache());
|
|
594
|
+
this.use(cors());
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* This version results in CORS response headers NOT stored
|
|
598
|
+
* in the cache, which is likely preferred. Fresh CORS headers
|
|
599
|
+
* are added to every response regardless of cache status.
|
|
600
|
+
*/
|
|
601
|
+
this.use(cors());
|
|
602
|
+
this.use(cache());
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
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 version expires. In the second version, disabling CORS takes effect immediately—all responses, cached or not, will no longer include CORS headers.
|
|
606
|
+
|
|
522
607
|
<br>
|
|
523
608
|
|
|
524
609
|
## :left_right_arrow: Web Sockets
|
|
525
610
|
|
|
611
|
+
Simplify [WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#_top) connection management with CloudSpark. Features include:
|
|
612
|
+
|
|
613
|
+
- Type-safe session metadata
|
|
614
|
+
- Support for [Hibernation WebSocket API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api) (recommended)
|
|
615
|
+
- Support for [Standard WebSocket API](https://developers.cloudflare.com/workers/runtime-apis/websockets/)
|
|
616
|
+
- [Middleware](#websocket) for Upgrade request validation
|
|
617
|
+
- Standardized WebSocketUpgrade response
|
|
618
|
+
|
|
619
|
+
The following is a simple chat with hibernation example:
|
|
620
|
+
|
|
621
|
+
:page_facing_up: wrangler.jsonc
|
|
622
|
+
|
|
623
|
+
```jsonc
|
|
624
|
+
/**
|
|
625
|
+
* Remember to rerun 'wrangler types' after you change your
|
|
626
|
+
* wrangler.json file.
|
|
627
|
+
*/
|
|
628
|
+
{
|
|
629
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
630
|
+
"name": "chat-room",
|
|
631
|
+
"main": "src/index.ts",
|
|
632
|
+
"compatibility_date": "2025-11-01",
|
|
633
|
+
"observability": {
|
|
634
|
+
"enabled": true,
|
|
635
|
+
},
|
|
636
|
+
"durable_objects": {
|
|
637
|
+
"bindings": [
|
|
638
|
+
{
|
|
639
|
+
"name": "CHAT_ROOM",
|
|
640
|
+
"class_name": "ChatRoom",
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
"migrations": [
|
|
645
|
+
{
|
|
646
|
+
"tag": "v1",
|
|
647
|
+
"new_sqlite_classes": ["ChatRoom"],
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
:page_facing_up: index.ts
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
import { DurableObject } from "cloudflare:workers";
|
|
657
|
+
|
|
658
|
+
import {
|
|
659
|
+
GET,
|
|
660
|
+
PathParams,
|
|
661
|
+
RouteWorker,
|
|
662
|
+
websocket,
|
|
663
|
+
WebSocketSessions,
|
|
664
|
+
WebSocketUpgrade,
|
|
665
|
+
} from "@adonix.org/cloud-spark";
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Metadata attached to each session.
|
|
669
|
+
*/
|
|
670
|
+
interface Profile {
|
|
671
|
+
name: string;
|
|
672
|
+
lastActive: number;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export class ChatRoom extends DurableObject {
|
|
676
|
+
/**
|
|
677
|
+
* Manage all active connections for this room.
|
|
678
|
+
*/
|
|
679
|
+
protected readonly sessions = new WebSocketSessions<Profile>();
|
|
680
|
+
|
|
681
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
682
|
+
super(ctx, env);
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Restore all active connections on wake from
|
|
686
|
+
* hibernation.
|
|
687
|
+
*/
|
|
688
|
+
this.sessions.restoreAll(this.ctx.getWebSockets());
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
public override fetch(request: Request): Promise<Response> {
|
|
692
|
+
/**
|
|
693
|
+
* For demo purposes, get the user's name from the `name`
|
|
694
|
+
* query parameter.
|
|
695
|
+
*/
|
|
696
|
+
const name = new URL(request.url).searchParams.get("name") ?? "Anonymous";
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Create a new connection and initialize its `Profile`
|
|
700
|
+
* attachment.
|
|
701
|
+
*/
|
|
702
|
+
const con = this.sessions.create({
|
|
703
|
+
name,
|
|
704
|
+
lastActive: Date.now(),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Accept the WebSocket with recommended hibernation enabled.
|
|
709
|
+
*
|
|
710
|
+
* To accept without hibernation, use `con.accept()` and
|
|
711
|
+
* con.addEventListener() methods instead.
|
|
712
|
+
*/
|
|
713
|
+
const client = con.acceptWebSocket(this.ctx);
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Return the upgrade response with the client WebSocket.
|
|
717
|
+
*/
|
|
718
|
+
return new WebSocketUpgrade(client).response();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Send a message to all active sessions.
|
|
723
|
+
*/
|
|
724
|
+
public broadcast(message: string): void {
|
|
725
|
+
for (const session of this.sessions) {
|
|
726
|
+
session.send(message);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
public override webSocketMessage(ws: WebSocket, message: string): void {
|
|
731
|
+
/**
|
|
732
|
+
* Get the sender's WebSocket session from the active sessions.
|
|
733
|
+
*/
|
|
734
|
+
const con = this.sessions.get(ws);
|
|
735
|
+
if (!con) return;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Update the sender's `Profile` with current `lastActive` time.
|
|
739
|
+
*/
|
|
740
|
+
con.attach({ lastActive: Date.now() });
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Broadcast the message to all sessions, prefixed with the
|
|
744
|
+
* sender’s name.
|
|
745
|
+
*/
|
|
746
|
+
this.broadcast(`${con.attachment.name}: ${message}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
public override webSocketClose(ws: WebSocket, code: number, reason: string): void {
|
|
750
|
+
/**
|
|
751
|
+
* Closes and removes the WebSocket from active sessions.
|
|
752
|
+
*/
|
|
753
|
+
this.sessions.close(ws, code, reason);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
class ChatWorker extends RouteWorker {
|
|
758
|
+
protected override init(): void {
|
|
759
|
+
/**
|
|
760
|
+
* Define the WebSocket connection route.
|
|
761
|
+
*/
|
|
762
|
+
this.route(GET, "/chat/:room", this.upgrade);
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Register the middleware to validate WebSocket
|
|
766
|
+
* connection requests.
|
|
767
|
+
*/
|
|
768
|
+
this.use(websocket("/chat/:room"));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private upgrade(params: PathParams): Promise<Response> {
|
|
772
|
+
/**
|
|
773
|
+
* Get the Durable Object stub for the chat room
|
|
774
|
+
* given by the "room" path parameter.
|
|
775
|
+
*/
|
|
776
|
+
const stub = this.env.CHAT_ROOM.getByName(params["room"]);
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Dispatch the WebSocket upgrade request to the
|
|
780
|
+
* Durable Object.
|
|
781
|
+
*/
|
|
782
|
+
return stub.fetch(this.request);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Connects ChatWorker to the Cloudflare runtime.
|
|
788
|
+
*/
|
|
789
|
+
export default ChatWorker.ignite();
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
:computer: To run this chat example locally:
|
|
793
|
+
|
|
794
|
+
```bash
|
|
795
|
+
wrangler dev
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
:bulb: Apps like [Postman](https://www.postman.com/downloads/) can be used to create and join local chat rooms for testing:
|
|
799
|
+
|
|
800
|
+
```
|
|
801
|
+
ws://localhost:8787/chat/fencing?name=Inigo
|
|
802
|
+
```
|
|
803
|
+
|
|
526
804
|
<br>
|
|
527
805
|
|
|
528
|
-
## :
|
|
806
|
+
## :partly_sunny: Wrangler
|
|
529
807
|
|
|
530
808
|
First, create a **FREE** [Cloudflare account](https://dash.cloudflare.com/sign-up).
|
|
531
809
|
|
|
@@ -548,3 +826,22 @@ wrangler init
|
|
|
548
826
|
```
|
|
549
827
|
|
|
550
828
|
[Install](#package-install) Cloud Spark
|
|
829
|
+
|
|
830
|
+
<br>
|
|
831
|
+
|
|
832
|
+
## :link: Links
|
|
833
|
+
|
|
834
|
+
- [Cloudflare - Home](https://www.cloudflare.com)
|
|
835
|
+
- [Cloudflare - Dashboard](https://dash.cloudflare.com)
|
|
836
|
+
- [Wrangler](https://developers.cloudflare.com/workers/wrangler/)
|
|
837
|
+
- [Workers](https://developers.cloudflare.com/workers/)
|
|
838
|
+
- [Workers - SDK](https://github.com/cloudflare/workers-sdk)
|
|
839
|
+
- [Hibernation WebSocket API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api)
|
|
840
|
+
- [Standard WebSocket API](https://developers.cloudflare.com/workers/runtime-apis/websockets/)
|
|
841
|
+
- [Postman](https://www.postman.com/downloads/)
|
|
842
|
+
- [http-status-codes](https://github.com/prettymuchbryce/http-status-codes)
|
|
843
|
+
- [path-to-regexp](https://github.com/pillarjs/path-to-regexp)
|
|
844
|
+
|
|
845
|
+
##
|
|
846
|
+
|
|
847
|
+
### [: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": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"typescript": "^5.9.3",
|
|
66
66
|
"typescript-eslint": "^8.48.0",
|
|
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",
|