1 module libwasm.router; 2 3 import memutils.hashmap; 4 import libwasm.promise; 5 import libwasm.types; 6 import libwasm.bindings.MouseEvent; 7 import libwasm.bindings.Event; 8 import std.traits : isPointer, isAggregateType; 9 import memutils.vector; 10 import memutils.scoped; 11 import optional; 12 13 // Match setup in compile 14 15 @safe nothrow: 16 17 enum Direction 18 { 19 Entering, 20 Leaving, 21 Always 22 } 23 24 struct RouterEvent 25 { 26 HashMap!(string, string) parameters; 27 string prevURL; 28 string newURL; 29 } 30 /* 31 @entering!"/this/path/:id/" Promise!Any routeHandler(RouterEvent ev) { 32 33 } 34 35 todo: Write Promise on ThreadMemAllocator 36 37 38 39 https://github.com/CyberShadow/ae/blob/master/utils/promise/package.d 40 41 */ 42 43 private enum maxRouteParameters = 64; 44 45 private class Route 46 { 47 @safe nothrow: 48 49 Array!char pattern; 50 Array!char active_url; 51 Optional!(Promise!void) delegate(ref RouterEvent ev) entering_cb; 52 Optional!(Promise!void) delegate(ref RouterEvent ev) leaving_cb; 53 Optional!(Promise!void) delegate(ref RouterEvent ev) always_cb; 54 this(Array!char _pattern) { pattern = _pattern; } 55 ~this() { 56 //console.log("Route destroyed for pattern"); 57 //console.log(pattern[]); 58 } 59 60 bool matches(string url, ref HashMap!(string, string) params) @trusted 61 { 62 size_t i, j; 63 //console.log("Matches"); 64 //console.log(url); 65 // store parameters until a full match is confirmed 66 import memutils.ct; 67 68 Tuple!(string, string)[maxRouteParameters] tmpparams; 69 size_t tmppparams_length = 0; 70 71 for (i = 0, j = 0; i < url.length && j < pattern.length;) 72 { 73 if (pattern[j] == '*') 74 { 75 foreach (t; tmpparams[0 .. tmppparams_length]) 76 params[cast(string) t[0]] = cast(string) t[1]; 77 return true; 78 } 79 if (url[i] == pattern[j]) 80 { 81 i++; 82 j++; 83 } 84 else if (pattern[j] == ':') 85 { 86 j++; 87 string name = skipPathNode(pattern[], j); 88 string match = skipPathNode(url, i); 89 assert(tmppparams_length < maxRouteParameters, "Maximum number of route parameters exceeded."); 90 tmpparams[tmppparams_length++] = tuple(name, match); 91 } 92 else 93 return false; 94 } 95 96 if ((j < pattern.length && pattern[j] == '*') || (i == url.length && j == pattern.length)) 97 { 98 foreach (t; tmpparams[0 .. tmppparams_length]) 99 params[cast(string) t[0]] = cast(string) t[1]; 100 return true; 101 } 102 103 return false; 104 } 105 106 bool matches(string url) 107 { 108 //console.log("matches"); 109 //console.log(url); 110 size_t i, j; 111 112 for (i = 0, j = 0; i < url.length && j < pattern.length;) 113 { 114 if (pattern[j] == '*') 115 { 116 return true; 117 } 118 if (url[i] == pattern[j]) 119 { 120 i++; 121 j++; 122 } 123 else if (pattern[j] == ':') 124 { 125 j++; 126 while (i < url.length && url[i] != '/') 127 i++; 128 while (j < pattern.length && pattern[j] != '/') 129 j++; 130 } 131 else 132 return false; 133 } 134 135 if ((j < pattern.length && pattern[j] == '*') || (i == url.length && j == pattern.length)) 136 { 137 return true; 138 } 139 140 return false; 141 } 142 143 static: 144 private string skipPathNode()(string str, ref size_t idx) 145 { 146 size_t start = idx; 147 while (idx < str.length && str[idx] != '/') 148 idx++; 149 return str[start .. idx]; 150 } 151 152 private string skipPathNode()(ref string str) 153 { 154 size_t idx = 0; 155 auto ret = skipPathNode(str, idx); 156 str = str[idx .. $]; 157 return ret; 158 } 159 } 160 161 class URLRouter 162 { 163 @safe nothrow: 164 165 private 166 { 167 bool m_is_setup; 168 Array!char m_base_path; 169 Array!Route m_routes; 170 // Array!Route m_routesForAlways; 171 172 Array!Route m_activeRoutes; 173 Array!char m_currentURL; 174 175 // defined when concurrent URL changes occur and promises are still pending 176 bool m_busy; 177 Array!char m_pendingURL; 178 179 Array!char m_title; 180 ManagedPool m_pool; 181 } 182 183 this() { 184 m_pool = ManagedPool(64*1024); 185 setupRouter(); 186 } 187 188 ~this() 189 { 190 191 console.error("Destroying router"); 192 193 } 194 195 void setTitle()(string title) @trusted 196 { 197 m_title = Array!char(title); 198 } 199 200 string getTitle()() @trusted 201 { 202 return m_title[]; 203 } 204 205 void setBasePath()(string base_url) 206 { 207 m_base_path = Array!char(base_url); 208 } 209 210 string getBasePath()() @trusted 211 { 212 return m_base_path[]; 213 } 214 215 void delegate(Handle) getDelegate() { 216 return (Handle hndl) { 217 //console.log("In delegate"); 218 string path = libwasm_get__string(hndl); 219 //console.log("Got path"); 220 //console.log(path); 221 navigateTo(path); 222 //console.log("Done"); 223 libwasm_removeObject(hndl); 224 }; 225 } 226 227 private void setupRouter() 228 { 229 if (!m_is_setup) 230 { 231 import libwasm.dom : document, window; 232 import libwasm.bindings.Location; 233 import libwasm.bindings.Window; 234 import libwasm.bindings.EventHandler; 235 //console.log("Allocating delegate"); 236 exportDelegate("navigate_to", getDelegate()); 237 //console.log("Allocated delegate"); 238 239 auto onpopstate = EventHandler(cast(EventHandlerNonNull)(&onPopState)); 240 window().onpopstate(onpopstate); 241 m_is_setup = true; 242 } 243 } 244 245 // struct PromiseIterator { 246 private 247 { 248 @safe nothrow: 249 250 Array!Route leaving_candidates; 251 Array!Route entering_candidates; 252 Array!char newPath; 253 254 void setupIterator(Array!Route _leaving_candidates, Array!Route _entering_candidates, Array!char _newPath) 255 { 256 leaving_candidates = _leaving_candidates; 257 //console.log("Setting up iterator with n entering candidates"); 258 //console.log(_entering_candidates.length); 259 entering_candidates = _entering_candidates; 260 newPath = _newPath; 261 } 262 263 void iterate()() @trusted 264 { 265 bool still_busy; 266 import libwasm.bindings.Console; 267 268 auto scoped = ScopedPool(m_pool); 269 //console.log("Iterate: "); 270 //console.log(m_title[]); 271 //console.log(newPath[]); 272 //console.log(newPath[] == "/home"); 273 if (!leaving_candidates.empty) 274 { 275 //console.log("Leaving candidates n:"); 276 //console.log(leaving_candidates.length); 277 auto r = leaving_candidates.back; 278 leaving_candidates.removeBack(); 279 if (!r.matches(newPath[])) 280 { 281 RouterEvent ev; 282 ev.newURL = newPath[]; 283 ev.prevURL = m_currentURL[]; 284 r.matches(r.active_url[], ev.parameters); 285 286 Optional!(Promise!void) promise; 287 if (r.leaving_cb) promise = r.leaving_cb(ev); 288 r.active_url.clear(); 289 // remove from activeRoutes 290 if (!promise.empty) 291 { 292 promise.front.then(&iterate); 293 still_busy = true; 294 } 295 } 296 } 297 else if (!entering_candidates.empty) 298 { 299 //console.log("We have entering candidates"); 300 RouterEvent ev; 301 auto r = entering_candidates.front; 302 entering_candidates.removeFront(); 303 304 //console.log(newPath[]); 305 bool found; 306 foreach (route; m_activeRoutes[]) { 307 if (route.pattern[] == r.pattern[]) { 308 found = true; 309 break; 310 } 311 } 312 if (!found) 313 { 314 //console.log("We cannot find this active route"); 315 //console.log(newPath[]); 316 if (r.matches(newPath[], ev.parameters)) 317 { 318 //console.log(newPath[]); 319 ev.newURL = newPath[]; 320 ev.prevURL = m_currentURL[]; 321 //console.log(newPath[]); 322 323 Optional!(Promise!void) promise; 324 if (r.entering_cb) promise = r.entering_cb(ev); 325 r.active_url[] = cast(char[]) newPath[]; 326 //console.log("Added to active url"); 327 //console.log(newPath[]); 328 m_activeRoutes ~= r; 329 if (!promise.empty) 330 { 331 //console.log("Promise was not empty"); 332 promise.front.then(&iterate); 333 still_busy = true; 334 } 335 } 336 } else { 337 338 //console.log("We found this active route"); 339 } 340 } 341 else 342 { 343 //console.log("Both entering and leaving candidates were empty"); 344 m_busy = false; 345 346 import libwasm.bindings.Window; 347 import libwasm.bindings.History; 348 import libwasm.dom : window; 349 350 //console.log("Pushing state: "); 351 //console.log(m_title[]); 352 //console.log(newPath[]); 353 if (m_is_setup) 354 window().history().pushState(null, m_title[], Optional!string(newPath[])); 355 356 // we finished iteration 357 if (!m_pendingURL.empty()) 358 { 359 auto url = m_pendingURL; 360 m_pendingURL.clear(); 361 navigateTo(url[]); 362 } 363 return; 364 } 365 366 if (!still_busy) 367 iterate(); 368 369 // todo: detect finished iteration and start over for the pendingURL if need be 370 371 } 372 //} 373 } 374 375 /// Adds a new route for requests matching the specified HTTP method and pattern. 376 void register(string path)(Optional!(Promise!void) delegate( 377 ref RouterEvent ev) nothrow @safe cb, Direction direction) 378 { 379 auto scoped = ScopedPool(m_pool); 380 //console.log("Registering"); 381 //console.log(path); 382 //static assert(path.count(':') <= maxRouteParameters, "Too many route parameters"); 383 bool found; 384 foreach (route; m_routes[]) 385 { 386 if (route.pattern[] == path) 387 { 388 final switch (direction) 389 { 390 case Direction.Entering: 391 //console.log("Is entering cb"); 392 route.entering_cb = cb; 393 break; 394 case Direction.Leaving: 395 route.leaving_cb = cb; 396 break; 397 case Direction.Always: 398 route.always_cb = cb; 399 break; 400 } 401 found = true; 402 break; 403 } 404 } 405 if (!found) 406 { 407 auto route = new Route(Array!char(path)); 408 final switch (direction) 409 { 410 case Direction.Entering: 411 //console.log("Is entering cb"); 412 route.entering_cb = cb; 413 break; 414 case Direction.Leaving: 415 //console.log("Is leaving cb"); 416 route.leaving_cb = cb; 417 break; 418 case Direction.Always: 419 route.always_cb = cb; 420 break; 421 } 422 m_routes ~= route; 423 } 424 425 } 426 427 /// Handles a HTTP request by dispatching it to the registered route handlers. 428 void navigateTo(string new_url) 429 { 430 //console.log("navigating to1"); 431 auto scoped = ScopedPool(m_pool); 432 //console.log("navigating to"); 433 //console.log(new_url); 434 //console.log(m_base_path.length); 435 if (new_url.length < m_base_path.length || (m_base_path.length > 0 && new_url[0 .. m_base_path.length] != m_base_path[])) 436 return; 437 new_url = new_url[m_base_path.length .. $]; 438 439 // queue path changes due to promises 440 if (m_busy) 441 { 442 m_pendingURL = Array!char(new_url); 443 return; 444 } 445 446 m_busy = true; 447 448 //console.log("Setup Iterator"); 449 //console.log(m_routes.length); 450 setupIterator(Array!Route(m_activeRoutes[]), Array!Route(m_routes[]), Array!char(new_url)); 451 //console.log("Iterate"); 452 iterate(); 453 454 } 455 456 void handleLinkEvent()(MouseEvent ev) 457 { 458 auto scoped = ScopedPool(m_pool); 459 import libwasm.bindings.HTMLLinkElement; 460 import libwasm.bindings.Node; 461 462 auto pool = ScopedPool(); 463 auto target = Node(ev.target().front); 464 if (target.nodeName() == "A") 465 { 466 auto el = target.as!HTMLLinkElement; 467 if (!el.getAttribute("internal").empty()) 468 { 469 string link_url = el.href(); 470 navigateTo(link_url); 471 ev.preventDefault(); 472 } 473 } 474 } 475 476 Any onPopState(Event ev) 477 { 478 import libwasm.dom : document; 479 import libwasm.bindings.Location; 480 import libwasm.bindings.Window; 481 482 auto scoped = ScopedPool(m_pool); 483 navigateTo(document().location().front.pathname()); 484 return Any.init; 485 } 486 487 } 488 489 void registerRoutes(T)(auto ref T t) @trusted 490 { 491 import std.meta : AliasSeq; 492 import std.traits : hasUDA, isCallable, getUDAs; 493 import libwasm.dom : compile; 494 495 static foreach (i; __traits(allMembers, T)) 496 { 497 { 498 alias sym = __traits(getMember, t, i); 499 static if (isPointer!(typeof(sym))) 500 alias ChildType = PointerTarget!(typeof(sym)); 501 else 502 alias ChildType = typeof(sym); 503 enum isPublic = __traits(getProtection, sym) == "public"; 504 static if (hasUDA!(sym, entering)) 505 { 506 alias udas = getUDAs!(sym, entering); 507 static foreach (uda; udas) 508 { 509 static if (is(uda : entering!path, string path)) 510 { 511 // callMember behind the scenes 512 g_router.register!path(cast(Optional!(Promise!void) delegate( 513 ref RouterEvent ev) nothrow @safe)&__traits(getMember, t, i), Direction 514 .Entering); 515 } 516 } 517 } 518 else static if (hasUDA!(sym, leaving)) 519 { 520 alias udas = getUDAs!(sym, leaving); 521 static foreach (uda; udas) 522 { 523 static if (is(uda : leaving!path, string path)) 524 { 525 // callMember behind the scenes 526 g_router.register!path(cast(Optional!(Promise!void) delegate( 527 ref RouterEvent ev) nothrow @safe)&__traits(getMember, t, i), Direction 528 .Leaving); 529 } 530 } 531 } 532 /* 533 else static if (hasUDA!(sym, always)) { 534 alias udas = getUDAs!(field, always); 535 static foreach (uda; udas) { 536 static if (is(uda : always!path, string path)) { 537 // callMember behind the scenes 538 g_router.register!path(__traits(getMember, t, sym), Direction.Always); 539 } 540 } 541 }*/ 542 else static if (isPublic && isAggregateType!ChildType && hasUDA!(sym, child) && !isCallable!( 543 typeof(sym))) 544 { 545 static if (isPointer!(typeof(sym))) 546 registerRoutes(*__traits(getMember, t, i)); 547 else 548 registerRoutes(__traits(getMember, t, i)); 549 } 550 } 551 } 552 } 553 554 void setupRouter()() 555 { 556 g_router = new URLRouter(); 557 } 558 559 URLRouter router() 560 { 561 return g_router; 562 } 563 564 private URLRouter g_router;