serviceworkerapp.pas 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. {
  2. A service worker functions like a proxy server, allowing to modify requests
  3. and responses, and replace them with items from its own cache, and more.
  4. }
  5. unit ServiceWorkerApp;
  6. {$mode objfpc}
  7. interface
  8. uses
  9. Classes, SysUtils, Types, JS, weborworker, webworker, CustApp;
  10. type
  11. { TServiceWorkerApplication }
  12. TServiceWorkerApplication = class(TCustomApplication)
  13. private
  14. FFallbackURL: string;
  15. protected
  16. FCacheName: string;
  17. FResources: TStringDynArray;
  18. procedure PutInCache(Request: TJSRequest; Response: TJSResponse); async; virtual;
  19. function CacheFirst(Request: TJSRequest; PreloadResponsePromise: TJSPromise;
  20. FallbackUrl: string): jsvalue; async; virtual;
  21. function EnableNavigationPreload: jsvalue; async; virtual;
  22. procedure DeleteCache(key: string); async; virtual;
  23. function DeleteOldCaches: jsvalue; async; virtual;
  24. procedure SetFallbackURL(const AValue: string); virtual;
  25. procedure DoRun; override;
  26. procedure Activate(Event: TJSExtendableEvent); virtual;
  27. procedure Install(Event: TJSExtendableEvent); virtual;
  28. procedure Fetch(FetchEvent: TJSFetchEvent); virtual;
  29. function GetConsoleApplication: boolean; override;
  30. function GetLocation: String; override;
  31. public
  32. procedure GetEnvironmentList(List{%H-}: TStrings; NamesOnly{%H-}: Boolean); override;
  33. procedure ShowException(E: Exception); override;
  34. procedure HandleException(Sender: TObject); override;
  35. property CacheName: string read FCacheName;
  36. property FallbackURL: string read FFallbackURL write SetFallbackURL;
  37. property Resources: TStringDynArray read FResources;
  38. end;
  39. implementation
  40. var
  41. EnvNames: TJSObject;
  42. procedure ReloadEnvironmentStrings;
  43. var
  44. I : Integer;
  45. S,N : String;
  46. A,P : TStringDynArray;
  47. begin
  48. if Assigned(EnvNames) then
  49. FreeAndNil(EnvNames);
  50. EnvNames:=TJSObject.new;
  51. S:=self_.Location.search;
  52. S:=Copy(S,2,Length(S)-1);
  53. A:=TJSString(S).split('&');
  54. for I:=0 to Length(A)-1 do
  55. begin
  56. P:=TJSString(A[i]).split('=');
  57. N:=LowerCase(decodeURIComponent(P[0]));
  58. if Length(P)=2 then
  59. EnvNames[N]:=decodeURIComponent(P[1])
  60. else if Length(P)=1 then
  61. EnvNames[N]:=''
  62. end;
  63. end;
  64. function MyGetEnvironmentVariable(Const EnvVar: String): String;
  65. Var
  66. aName : String;
  67. begin
  68. aName:=Lowercase(EnvVar);
  69. if EnvNames.hasOwnProperty(aName) then
  70. Result:=String(EnvNames[aName])
  71. else
  72. Result:='';
  73. end;
  74. function MyGetEnvironmentVariableCount: Integer;
  75. begin
  76. Result:=length(TJSOBject.getOwnPropertyNames(envNames));
  77. end;
  78. function MyGetEnvironmentString(Index: Integer): String;
  79. begin
  80. Result:=String(EnvNames[TJSOBject.getOwnPropertyNames(envNames)[Index]]);
  81. end;
  82. { TServiceWorkerApplication }
  83. procedure TServiceWorkerApplication.SetFallbackURL(const AValue: string);
  84. begin
  85. if FFallbackURL=AValue then Exit;
  86. FFallbackURL:=AValue;
  87. end;
  88. procedure TServiceWorkerApplication.PutInCache(Request: TJSRequest;
  89. Response: TJSResponse);
  90. var
  91. Cache: TJSCache;
  92. begin
  93. Cache := await(TJSCache,self_.Caches.open(CacheName));
  94. await(TJSCache,Cache.put(Request, Response));
  95. end;
  96. function TServiceWorkerApplication.CacheFirst(Request: TJSRequest;
  97. PreloadResponsePromise: TJSPromise; FallbackUrl: string): jsvalue;
  98. var
  99. ResponseFromCache, PreloadResponse, ResponseFromNetwork, FallbackResponse: TJSResponse;
  100. begin
  101. Result:=nil;
  102. // First try to get the resource from the cache
  103. ResponseFromCache := await(TJSResponse,self_.caches.match(Request));
  104. if Assigned(ResponseFromCache) then
  105. exit(ResponseFromCache);
  106. // Next try to use (and cache) the preloaded response, if it's there
  107. PreloadResponse := await(TJSResponse,PreloadResponsePromise);
  108. if Assigned(PreloadResponse) then
  109. begin
  110. //console.info('using preload response: '+String(JSValue(PreloadResponse)));
  111. putInCache(Request, PreloadResponse.clone());
  112. exit(PreloadResponse);
  113. end;
  114. // Next try to get the resource from the network
  115. try
  116. ResponseFromNetwork := await(TJSResponse,self_.fetch(Request));
  117. // response may be used only once
  118. // we need to save clone to put one copy in cache
  119. // and serve second one
  120. PutInCache(Request, ResponseFromNetwork.clone());
  121. exit(ResponseFromNetwork);
  122. except
  123. FallbackResponse := await(TJSResponse,self_.caches.match(FallbackUrl));
  124. if Assigned(FallbackResponse) then
  125. exit(FallbackResponse);
  126. // when even the fallback response is not available,
  127. // there is nothing we can do, but we must always
  128. // return a Response object
  129. Result:=TJSResponse.new('Network error happened', js.new([
  130. 'status', 408,
  131. 'headers',
  132. js.new(['Content-Type', 'text/plain' ])
  133. ]) );
  134. end;
  135. end;
  136. function TServiceWorkerApplication.EnableNavigationPreload: jsvalue;
  137. begin
  138. Result:=nil;
  139. if jsvalue(serviceWorker.registration.navigationPreload) then
  140. // Enable navigation preloads!
  141. await(serviceWorker.registration.navigationPreload.enable());
  142. end;
  143. procedure TServiceWorkerApplication.DeleteCache(key: string);
  144. begin
  145. await(boolean,Self_.caches.delete(key));
  146. end;
  147. function TServiceWorkerApplication.DeleteOldCaches: jsvalue;
  148. var
  149. CacheKeepList: TStringDynArray;
  150. CachesToDelete, KeyList: TJSArray;
  151. begin
  152. CacheKeepList := [CacheName];
  153. KeyList := await(TJSArray,self_.caches.keys());
  154. CachesToDelete := keyList.filter(
  155. function (key: JSValue; index: NativeInt; anArray : TJSArray) : Boolean
  156. begin
  157. Result:=not TJSArray(CacheKeepList).includes(key);
  158. end);
  159. Result:=await(jsvalue,TJSPromise.all(CachesToDelete.map(TJSArrayMapEvent(@DeleteCache))));
  160. end;
  161. procedure TServiceWorkerApplication.DoRun;
  162. begin
  163. ServiceWorker.addEventListener('activate', @Activate);
  164. ServiceWorker.addEventListener('install', @Install);
  165. ServiceWorker.addEventListener('fetch', @Fetch);
  166. end;
  167. procedure TServiceWorkerApplication.Activate(Event: TJSExtendableEvent);
  168. begin
  169. Event.waitUntil(EnableNavigationPreload());
  170. Event.waitUntil(DeleteOldCaches());
  171. end;
  172. procedure TServiceWorkerApplication.Install(Event: TJSExtendableEvent);
  173. begin
  174. Event.waitUntil(
  175. self_.Caches.Open(CacheName)._then(
  176. TJSPromiseResolver(procedure(Cache: TJSCache)
  177. begin
  178. Cache.addAll(Resources);
  179. end))
  180. );
  181. end;
  182. procedure TServiceWorkerApplication.Fetch(FetchEvent: TJSFetchEvent);
  183. begin
  184. FetchEvent.RespondWith(CacheFirst(FetchEvent.request,
  185. FetchEvent.PreloadResponse,FallbackURL) );
  186. end;
  187. function TServiceWorkerApplication.GetConsoleApplication: boolean;
  188. begin
  189. Result:=true;
  190. end;
  191. function TServiceWorkerApplication.GetLocation: String;
  192. begin
  193. Result:='';
  194. end;
  195. procedure TServiceWorkerApplication.GetEnvironmentList(List: TStrings;
  196. NamesOnly: Boolean);
  197. begin
  198. end;
  199. procedure TServiceWorkerApplication.ShowException(E: Exception);
  200. Var
  201. S : String;
  202. begin
  203. if (E<>nil) then
  204. S:=E.ClassName+': '+E.Message
  205. else if ExceptObjectJS then
  206. s:=TJSObject(ExceptObjectJS).toString;
  207. console.log('Unhandled exception caught:'+S);
  208. end;
  209. procedure TServiceWorkerApplication.HandleException(Sender: TObject);
  210. begin
  211. if ExceptObject is Exception then
  212. ShowException(ExceptObject);
  213. inherited HandleException(Sender);
  214. end;
  215. initialization
  216. IsConsole:=true;
  217. ReloadEnvironmentStrings;
  218. OnGetEnvironmentVariable:=@MyGetEnvironmentVariable;
  219. OnGetEnvironmentVariableCount:=@MyGetEnvironmentVariableCount;
  220. OnGetEnvironmentString:=@MyGetEnvironmentString;
  221. end.