@adonix.org/cloud-spark 0.0.194 → 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 CHANGED
@@ -1,4 +1,4 @@
1
- # Cloud⚡️Spark
1
+ # CloudSpark
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@adonix.org/cloud-spark.svg?color=blue)](https://www.npmjs.com/package/@adonix.org/cloud-spark)
4
4
  [![Apache 2.0 License](https://badges.adonix.org/License/Apache%202.0?color=blue)](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](#cowboy_hat_face-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](#cowboy_hat_face-wrangler) to create a new project:
55
+ :computer: Use [Wrangler](#partly_sunny-wrangler) to create a new project:
31
56
 
32
57
  ```bash
33
58
  wrangler init
@@ -103,7 +128,7 @@ As shown in the [Quickstart](#rocket-quickstart), BasicWorker is the base class
103
128
  - Support for built-in and custom middleware.
104
129
  - Catching unhandled errors.
105
130
 
106
- Subclasses only need to implement the HTTP methods that their worker will handle. Each method can be overridden independently, and additional functionality such as [middleware](#gear-middleware) can be added as needed.
131
+ Subclasses only need to implement the HTTP methods their worker will handle. Each method can be overridden independently, and additional functionality such as [middleware](#gear-middleware) can be added as needed.
107
132
 
108
133
  Building on the [Quickstart](#rocket-quickstart), what follows is a more complete example:
109
134
 
@@ -426,14 +451,17 @@ class ChatWorker extends RouteWorker {
426
451
  * in wrangler.jsonc
427
452
  */
428
453
  protected upgrade(params: PathParams): Promise<Response> {
429
- const room = params["room"];
430
- const chat = this.env.CHAT;
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 chat.get(chat.idFromName(room)).fetch(this.request);
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
- ## :cowboy_hat_face: Wrangler
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,11 +511,16 @@ interface WebSocketConnection<A extends WSAttachment> {
511
511
  */
512
512
  get attachment(): Readonly<A>;
513
513
  /**
514
- * Attaches a user-defined object to this WebSocket connection.
514
+ * Attaches or updates a user-defined object on this connection.
515
515
  *
516
- * @param attachment - Object containing the metadata to attach.
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.
517
522
  */
518
- attach(attachment: A): void;
523
+ attach(attachment?: Partial<A> | null): void;
519
524
  /**
520
525
  * Sends a message to the connected WebSocket client.
521
526
  *
@@ -1086,7 +1091,7 @@ declare class HttpError extends JsonResponse {
1086
1091
  /**
1087
1092
  * Creates a structured error response without exposing the error
1088
1093
  * details to the client. Links the sent response to the logged
1089
- * error via a generated correlation UUID.
1094
+ * error via a generated correlation ID.
1090
1095
  *
1091
1096
  * Status defaults to 500 Internal Server Error.
1092
1097
  */