@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 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
@@ -422,18 +447,22 @@ class ChatWorker extends RouteWorker {
422
447
  /**
423
448
  * Handles WebSocket upgrade requests.
424
449
  *
425
- * Expects a DurableObject binding named CHAT
450
+ * Expects a DurableObject binding named CHAT_ROOM
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
+ * given by the "room" path parameter.
457
+ */
458
+ const stub = this.env.CHAT_ROOM.getByName(params["room"]);
431
459
 
432
460
  /**
433
- * Request has already been validated by the
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 chat.get(chat.idFromName(room)).fetch(this.request);
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
- ## :cowboy_hat_face: Wrangler
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 to this WebSocket connection.
514
+ * Attaches or updates a user-defined object on this connection.
515
515
  *
516
- * @param attachment - Partial object containing metadata to attach,
517
- * or null to clear the attachment.
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.195",
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.0",
57
- "@typescript-eslint/parser": "^8.48.0",
58
- "@vitest/coverage-v8": "^4.0.13",
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.6.2",
63
+ "prettier": "^3.7.3",
64
64
  "tsup": "^8.5.1",
65
65
  "typescript": "^5.9.3",
66
- "typescript-eslint": "^8.48.0",
66
+ "typescript-eslint": "^8.48.1",
67
67
  "vitest": "^4.0.13",
68
- "wrangler": "^4.50.0"
68
+ "wrangler": "^4.51.0"
69
69
  },
70
70
  "dependencies": {
71
71
  "cache-control-parser": "^2.0.6",