11import express , { type Request , type Response } from "express" ;
2+ import { v4 as uuidv4 } from "uuid" ;
23
34import { SpatiadClient } from "@spatiad/sdk" ;
45import {
@@ -8,72 +9,278 @@ import {
89
910const client = new SpatiadClient ( "http://localhost:3000" ) ;
1011
11- const run = async ( ) => {
12- const controller = new AbortController ( ) ;
13- setTimeout ( ( ) => controller . abort ( ) , 5_000 ) ;
12+ // Simulate multiple drivers with location updates
13+ interface Driver {
14+ id : string ;
15+ latitude : number ;
16+ longitude : number ;
17+ category : string ;
18+ }
1419
15- const app = express ( ) ;
16- const webhookSecret = process . env . SPATIAD_WEBHOOK_SECRET ?? "dev-secret" ;
20+ const drivers : Driver [ ] = [
21+ { id : uuidv4 ( ) , latitude : 38.433 , longitude : 26.768 , category : "tow_truck" } ,
22+ { id : uuidv4 ( ) , latitude : 38.435 , longitude : 26.769 , category : "tow_truck" } ,
23+ { id : uuidv4 ( ) , latitude : 38.431 , longitude : 26.767 , category : "tow_truck" } ,
24+ { id : uuidv4 ( ) , latitude : 38.436 , longitude : 26.770 , category : "tow_truck" } ,
25+ ] ;
1726
18- app . post (
19- "/webhooks/spatiad" ,
20- spatiadWebhookJson ( ) ,
21- verifySpatiadWebhook ( { secret : webhookSecret } ) ,
22- ( req : Request , res : Response ) => {
23- console . log ( "verified webhook payload" , req . body ) ;
24- res . status ( 204 ) . end ( ) ;
27+ async function registerDrivers ( ) {
28+ console . log ( "\n📍 Registering drivers..." ) ;
29+ for ( const driver of drivers ) {
30+ await client . upsertDriver ( {
31+ driverId : driver . id ,
32+ category : driver . category ,
33+ status : "Available" ,
34+ position : { latitude : driver . latitude , longitude : driver . longitude } ,
35+ } ) ;
36+ console . log ( ` ✓ Driver ${ driver . id . slice ( 0 , 8 ) } at (${ driver . latitude } , ${ driver . longitude } )` ) ;
37+ }
38+ }
39+
40+ async function demonstrateOfferCreation ( ) {
41+ console . log ( "\n🚗 Creating offer for job..." ) ;
42+
43+ const jobId = uuidv4 ( ) ;
44+ const response = await client . createOffer ( {
45+ jobId,
46+ category : "tow_truck" ,
47+ pickup : { latitude : 38.433 , longitude : 26.768 } ,
48+ dropoff : { latitude : 38.44 , longitude : 26.78 } ,
49+ initialRadiusKm : 0.5 ,
50+ maxRadiusKm : 5 ,
51+ timeoutSeconds : 30 ,
52+ retry : {
53+ maxAttempts : 3 ,
54+ backoffMs : 100
2555 }
26- ) ;
56+ } ) ;
2757
28- app . listen ( 4000 , ( ) => {
29- console . log ( "example webhook server listening on :4000" ) ;
58+ console . log ( ` ✓ Offer created: ${ response . offer_id } ` ) ;
59+ return jobId ;
60+ }
61+
62+ async function demonstrateRadiusExpansion ( ) {
63+ console . log ( "\n📡 Testing radius expansion..." ) ;
64+
65+ // Create a driver far away
66+ const farDriver = {
67+ id : uuidv4 ( ) ,
68+ latitude : 38.45 ,
69+ longitude : 26.80 ,
70+ category : "tow_truck"
71+ } ;
72+
73+ await client . upsertDriver ( {
74+ driverId : farDriver . id ,
75+ category : farDriver . category ,
76+ status : "Available" ,
77+ position : { latitude : farDriver . latitude , longitude : farDriver . longitude } ,
3078 } ) ;
79+ console . log ( ` ✓ Registered far driver at (${ farDriver . latitude } , ${ farDriver . longitude } )` ) ;
3180
81+ const jobId = uuidv4 ( ) ;
3282 const response = await client . createOffer ( {
33- jobId : "33333333-3333-3333-3333-333333333333" ,
83+ jobId,
3484 category : "tow_truck" ,
3585 pickup : { latitude : 38.433 , longitude : 26.768 } ,
3686 dropoff : { latitude : 38.44 , longitude : 26.78 } ,
37- initialRadiusKm : 1 ,
38- maxRadiusKm : 5 ,
39- timeoutSeconds : 20 ,
87+ initialRadiusKm : 0.1 , // Very small initial radius
88+ maxRadiusKm : 5 , // Will expand to find the far driver
89+ timeoutSeconds : 30 ,
4090 retry : {
4191 maxAttempts : 3 ,
4292 backoffMs : 100
4393 }
4494 } ) ;
4595
46- console . log ( "offer response" , response ) ;
96+ console . log ( ` ✓ Offer created via radius expansion: ${ response . offer_id } ` ) ;
97+ return jobId ;
98+ }
4799
100+ async function demonstrateJobStatus ( jobId : string ) {
101+ console . log ( "\n📊 Checking job status..." ) ;
102+
103+ const status = await client . getJobStatus ( {
104+ jobId,
105+ } ) ;
106+
107+ console . log ( ` ✓ Job ${ jobId . slice ( 0 , 8 ) } ` ) ;
108+ console . log ( ` State: ${ status . state } ` ) ;
109+ console . log ( ` Matched Driver: ${ status . matched_driver_id || "none" } ` ) ;
110+ console . log ( ` Matched Offer: ${ status . matched_offer_id || "none" } ` ) ;
111+ }
112+
113+ async function demonstrateJobEvents ( jobId : string ) {
114+ console . log ( "\n📋 Fetching job events..." ) ;
115+
48116 const events = await client . getJobEvents ( {
49- jobId : "33333333-3333-3333-3333-333333333333" ,
50- limit : 20 ,
51- kinds : [ "offer_created" , "match_confirmed" ]
117+ jobId,
118+ limit : 50 ,
119+ kinds : [ "job_registered" , " offer_created", "match_confirmed" ]
52120 } ) ;
53121
54- console . log ( "filtered events" , events ) ;
122+ if ( events . events . length === 0 ) {
123+ console . log ( ` ℹ️ No events found` ) ;
124+ return ;
125+ }
55126
56- const allFilteredEvents = await client . getJobEventsAllPages ( {
57- jobId : "33333333-3333-3333-3333-333333333333" ,
58- limit : 10 ,
59- maxPages : 5 ,
60- maxEvents : 100 ,
61- kinds : [ "offer_created" , "match_confirmed" ] ,
62- signal : controller . signal ,
63- retry : {
64- maxAttempts : 4 ,
65- backoffMs : 120 ,
66- maxBackoffMs : 1000
127+ console . log ( ` ✓ Found ${ events . events . length } event(s):` ) ;
128+ for ( const event of events . events ) {
129+ console . log ( ` - ${ event . kind } at ${ event . at } ` ) ;
130+ }
131+ }
132+
133+ async function demonstrateInputValidation ( ) {
134+ console . log ( "\n⚠️ Testing input validation..." ) ;
135+
136+ const testCases = [
137+ {
138+ name : "Invalid category (too long)" ,
139+ config : {
140+ jobId : uuidv4 ( ) ,
141+ category : "a" . repeat ( 51 ) , // > 50 chars
142+ pickup : { latitude : 38.433 , longitude : 26.768 } ,
143+ initialRadiusKm : 1 ,
144+ maxRadiusKm : 5 ,
145+ timeoutSeconds : 20 ,
146+ }
147+ } ,
148+ {
149+ name : "Invalid coordinates (out of range)" ,
150+ config : {
151+ jobId : uuidv4 ( ) ,
152+ category : "tow_truck" ,
153+ pickup : { latitude : 91 , longitude : 26.768 } , // > 90
154+ initialRadiusKm : 1 ,
155+ maxRadiusKm : 5 ,
156+ timeoutSeconds : 20 ,
157+ }
158+ } ,
159+ {
160+ name : "Invalid radius (initial > max)" ,
161+ config : {
162+ jobId : uuidv4 ( ) ,
163+ category : "tow_truck" ,
164+ pickup : { latitude : 38.433 , longitude : 26.768 } ,
165+ initialRadiusKm : 10 ,
166+ maxRadiusKm : 5 , // < initial
167+ timeoutSeconds : 20 ,
168+ }
169+ } ,
170+ {
171+ name : "Invalid timeout (0 seconds)" ,
172+ config : {
173+ jobId : uuidv4 ( ) ,
174+ category : "tow_truck" ,
175+ pickup : { latitude : 38.433 , longitude : 26.768 } ,
176+ initialRadiusKm : 1 ,
177+ maxRadiusKm : 5 ,
178+ timeoutSeconds : 0 , // Invalid
179+ }
67180 } ,
68- onPage : ( page , index ) => {
69- console . log ( "page fetched" , index , page . events . length , page . next_before_cursor ) ;
181+ ] ;
182+
183+ for ( const testCase of testCases ) {
184+ try {
185+ // @ts -ignore - intentionally passing invalid config
186+ await client . createOffer ( testCase . config ) ;
187+ console . log ( ` ✗ ${ testCase . name } : Should have failed but didn't` ) ;
188+ } catch ( error ) {
189+ const err = error as any ;
190+ console . log ( ` ✓ ${ testCase . name } : Rejected` ) ;
191+ if ( err . message ) {
192+ console . log ( ` Error: ${ err . message . slice ( 0 , 60 ) } ...` ) ;
193+ }
194+ }
195+ }
196+ }
197+
198+ async function demonstrateOffersAndCancellation ( ) {
199+ console . log ( "\n❌ Testing offer/job operations..." ) ;
200+
201+ const jobId = uuidv4 ( ) ;
202+
203+ // Create offer
204+ const offerResponse = await client . createOffer ( {
205+ jobId,
206+ category : "tow_truck" ,
207+ pickup : { latitude : 38.433 , longitude : 26.768 } ,
208+ dropoff : { latitude : 38.44 , longitude : 26.78 } ,
209+ initialRadiusKm : 1 ,
210+ maxRadiusKm : 5 ,
211+ timeoutSeconds : 30 ,
212+ } ) ;
213+
214+ console . log ( ` ✓ Offer created: ${ offerResponse . offer_id ?. slice ( 0 , 8 ) } ` ) ;
215+
216+ // Check job status is pending/searching
217+ const statusBefore = await client . getJobStatus ( { jobId } ) ;
218+ console . log ( ` ✓ Job state before: ${ statusBefore . state } ` ) ;
219+
220+ // Try to cancel offer
221+ if ( offerResponse . offer_id && offerResponse . offer_id !== "00000000-0000-0000-0000-000000000000" ) {
222+ await client . cancelOffer ( { offerId : offerResponse . offer_id } ) ;
223+ console . log ( ` ✓ Offer cancelled` ) ;
224+ }
225+
226+ // Check status after
227+ const statusAfter = await client . getJobStatus ( { jobId } ) ;
228+ console . log ( ` ✓ Job state after: ${ statusAfter . state } ` ) ;
229+ }
230+
231+ const run = async ( ) => {
232+ const app = express ( ) ;
233+ const webhookSecret = process . env . SPATIAD_WEBHOOK_SECRET ?? "dev-secret" ;
234+
235+ // Webhook receiver
236+ app . post (
237+ "/webhooks/spatiad" ,
238+ spatiadWebhookJson ( ) ,
239+ verifySpatiadWebhook ( { secret : webhookSecret } ) ,
240+ ( req : Request , res : Response ) => {
241+ console . log ( "\n✅ Webhook received:" , {
242+ event : ( req . body as any ) . event ,
243+ jobId : ( req . body as any ) . job_id ?. slice ( 0 , 8 ) ,
244+ driverId : ( req . body as any ) . driver_id ?. slice ( 0 , 8 ) ,
245+ } ) ;
246+ res . status ( 204 ) . end ( ) ;
70247 }
248+ ) ;
249+
250+ app . listen ( 4000 , ( ) => {
251+ console . log ( "🎯 Webhook receiver listening on :4000" ) ;
71252 } ) ;
72253
73- console . log ( "all filtered events" , allFilteredEvents . length ) ;
254+ try {
255+ console . log ( "═══════════════════════════════════════" ) ;
256+ console . log ( " Spatiad Demo - Multi-Driver Dispatch" ) ;
257+ console . log ( "═══════════════════════════════════════" ) ;
258+
259+ // Run all demonstrations
260+ await registerDrivers ( ) ;
261+
262+ const jobId = await demonstrateOfferCreation ( ) ;
263+ await demonstrateJobStatus ( jobId ) ;
264+ await demonstrateJobEvents ( jobId ) ;
265+
266+ await demonstrateRadiusExpansion ( ) ;
267+
268+ await demonstrateInputValidation ( ) ;
269+
270+ await demonstrateOffersAndCancellation ( ) ;
271+
272+ console . log ( "\n═══════════════════════════════════════" ) ;
273+ console . log ( " Demo Completed Successfully ✓" ) ;
274+ console . log ( "═══════════════════════════════════════\n" ) ;
275+
276+ } catch ( error ) {
277+ console . error ( "❌ Demo failed:" , error ) ;
278+ } finally {
279+ process . exit ( 0 ) ;
280+ }
74281} ;
75282
76283run ( ) . catch ( ( error ) => {
77- console . error ( "example failed" , error ) ;
284+ console . error ( "❌ Example failed" , error ) ;
78285 process . exitCode = 1 ;
79286} ) ;
0 commit comments