tina4ruby 3.0.0 → 3.9.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +120 -32
  3. data/lib/tina4/auth.rb +137 -27
  4. data/lib/tina4/auto_crud.rb +55 -3
  5. data/lib/tina4/cli.rb +228 -28
  6. data/lib/tina4/cors.rb +1 -1
  7. data/lib/tina4/database.rb +230 -26
  8. data/lib/tina4/database_result.rb +122 -8
  9. data/lib/tina4/dev_mailbox.rb +1 -1
  10. data/lib/tina4/env.rb +1 -1
  11. data/lib/tina4/frond.rb +314 -7
  12. data/lib/tina4/gallery/queue/meta.json +1 -1
  13. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +314 -16
  14. data/lib/tina4/localization.rb +1 -1
  15. data/lib/tina4/messenger.rb +111 -33
  16. data/lib/tina4/middleware.rb +349 -1
  17. data/lib/tina4/migration.rb +132 -11
  18. data/lib/tina4/orm.rb +149 -18
  19. data/lib/tina4/public/js/tina4-dev-admin.min.js +1 -1
  20. data/lib/tina4/public/js/tina4js.min.js +47 -0
  21. data/lib/tina4/query_builder.rb +374 -0
  22. data/lib/tina4/queue.rb +219 -61
  23. data/lib/tina4/queue_backends/lite_backend.rb +42 -7
  24. data/lib/tina4/queue_backends/mongo_backend.rb +126 -0
  25. data/lib/tina4/rack_app.rb +200 -11
  26. data/lib/tina4/request.rb +14 -1
  27. data/lib/tina4/response.rb +26 -0
  28. data/lib/tina4/response_cache.rb +446 -29
  29. data/lib/tina4/router.rb +127 -0
  30. data/lib/tina4/service_runner.rb +1 -1
  31. data/lib/tina4/session.rb +6 -1
  32. data/lib/tina4/session_handlers/database_handler.rb +66 -0
  33. data/lib/tina4/swagger.rb +1 -1
  34. data/lib/tina4/templates/errors/404.twig +2 -2
  35. data/lib/tina4/templates/errors/500.twig +1 -1
  36. data/lib/tina4/validator.rb +174 -0
  37. data/lib/tina4/version.rb +1 -1
  38. data/lib/tina4/websocket.rb +23 -4
  39. data/lib/tina4/websocket_backplane.rb +118 -0
  40. data/lib/tina4.rb +126 -5
  41. metadata +40 -3
data/lib/tina4/orm.rb CHANGED
@@ -85,19 +85,118 @@ module Tina4
85
85
  end
86
86
  end
87
87
 
88
- def find(id_or_filter = nil, filter = nil)
88
+ # Create a fluent QueryBuilder pre-configured for this model's table and database.
89
+ #
90
+ # Usage:
91
+ # results = User.query.where("active = ?", [1]).order_by("name").get
92
+ #
93
+ # @return [Tina4::QueryBuilder]
94
+ def query
95
+ QueryBuilder.from(table_name, db: db)
96
+ end
97
+
98
+ def find(id_or_filter = nil, filter = nil, **kwargs)
99
+ include_list = kwargs.delete(:include)
100
+
89
101
  # find(id) — find by primary key
90
102
  # find(filter_hash) — find by criteria
91
- if id_or_filter.is_a?(Hash)
103
+ # find(name: "Alice") — keyword args as filter hash
104
+ result = if id_or_filter.is_a?(Hash)
92
105
  find_by_filter(id_or_filter)
93
106
  elsif filter.is_a?(Hash)
94
107
  find_by_filter(filter)
108
+ elsif !kwargs.empty?
109
+ find_by_filter(kwargs)
95
110
  else
96
111
  find_by_id(id_or_filter)
97
112
  end
113
+
114
+ if include_list && result
115
+ instances = result.is_a?(Array) ? result : [result]
116
+ eager_load(instances, include_list)
117
+ end
118
+ result
98
119
  end
99
120
 
100
- def where(conditions, params = [])
121
+ # Eager load relationships for a collection of instances (prevents N+1).
122
+ # include is an array of relationship names, supporting dot notation for nesting.
123
+ def eager_load(instances, include_list)
124
+ return if instances.nil? || instances.empty?
125
+
126
+ # Group includes: top-level and nested
127
+ top_level = {}
128
+ include_list.each do |inc|
129
+ parts = inc.to_s.split(".", 2)
130
+ rel_name = parts[0].to_sym
131
+ top_level[rel_name] ||= []
132
+ top_level[rel_name] << parts[1] if parts.length > 1
133
+ end
134
+
135
+ top_level.each do |rel_name, nested|
136
+ rel = relationship_definitions[rel_name]
137
+ next unless rel
138
+
139
+ klass = Object.const_get(rel[:class_name])
140
+ pk = primary_key_field || :id
141
+
142
+ case rel[:type]
143
+ when :has_one, :has_many
144
+ fk = rel[:foreign_key] || "#{name.split('::').last.downcase}_id"
145
+ pk_values = instances.map { |inst| inst.__send__(pk) }.compact.uniq
146
+ next if pk_values.empty?
147
+
148
+ placeholders = pk_values.map { "?" }.join(",")
149
+ sql = "SELECT * FROM #{klass.table_name} WHERE #{fk} IN (#{placeholders})"
150
+ results = klass.db.fetch(sql, pk_values)
151
+ related_records = results.map { |row| klass.from_hash(row) }
152
+
153
+ # Eager load nested
154
+ klass.eager_load(related_records, nested) unless nested.empty?
155
+
156
+ # Group by FK
157
+ grouped = {}
158
+ related_records.each do |record|
159
+ fk_val = record.__send__(fk.to_sym) if record.respond_to?(fk.to_sym)
160
+ (grouped[fk_val] ||= []) << record
161
+ end
162
+
163
+ instances.each do |inst|
164
+ pk_val = inst.__send__(pk)
165
+ records = grouped[pk_val] || []
166
+ if rel[:type] == :has_one
167
+ inst.instance_variable_get(:@relationship_cache)[rel_name] = records.first
168
+ else
169
+ inst.instance_variable_get(:@relationship_cache)[rel_name] = records
170
+ end
171
+ end
172
+
173
+ when :belongs_to
174
+ fk = rel[:foreign_key] || "#{rel_name}_id"
175
+ fk_values = instances.map { |inst|
176
+ inst.respond_to?(fk.to_sym) ? inst.__send__(fk.to_sym) : nil
177
+ }.compact.uniq
178
+ next if fk_values.empty?
179
+
180
+ related_pk = klass.primary_key_field || :id
181
+ placeholders = fk_values.map { "?" }.join(",")
182
+ sql = "SELECT * FROM #{klass.table_name} WHERE #{related_pk} IN (#{placeholders})"
183
+ results = klass.db.fetch(sql, fk_values)
184
+ related_records = results.map { |row| klass.from_hash(row) }
185
+
186
+ klass.eager_load(related_records, nested) unless nested.empty?
187
+
188
+ lookup = {}
189
+ related_records.each { |r| lookup[r.__send__(related_pk)] = r }
190
+
191
+ instances.each do |inst|
192
+ fk_val = inst.respond_to?(fk.to_sym) ? inst.__send__(fk.to_sym) : nil
193
+ inst.instance_variable_get(:@relationship_cache)[rel_name] = lookup[fk_val]
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ def where(conditions, params = [], include: nil)
101
200
  sql = "SELECT * FROM #{table_name}"
102
201
  if soft_delete
103
202
  sql += " WHERE (#{soft_delete_field} IS NULL OR #{soft_delete_field} = 0) AND (#{conditions})"
@@ -105,23 +204,28 @@ module Tina4
105
204
  sql += " WHERE #{conditions}"
106
205
  end
107
206
  results = db.fetch(sql, params)
108
- results.map { |row| from_hash(row) }
207
+ instances = results.map { |row| from_hash(row) }
208
+ eager_load(instances, include) if include
209
+ instances
109
210
  end
110
211
 
111
- def all(limit: nil, offset: nil, skip: nil, order_by: nil)
212
+ def all(limit: nil, offset: nil, order_by: nil, include: nil)
112
213
  sql = "SELECT * FROM #{table_name}"
113
214
  if soft_delete
114
215
  sql += " WHERE #{soft_delete_field} IS NULL OR #{soft_delete_field} = 0"
115
216
  end
116
217
  sql += " ORDER BY #{order_by}" if order_by
117
- effective_offset = offset || skip
118
- results = db.fetch(sql, [], limit: limit, skip: effective_offset)
119
- results.map { |row| from_hash(row) }
218
+ results = db.fetch(sql, [], limit: limit, offset: offset)
219
+ instances = results.map { |row| from_hash(row) }
220
+ eager_load(instances, include) if include
221
+ instances
120
222
  end
121
223
 
122
- def select(sql, params = [], limit: nil, skip: nil)
123
- results = db.fetch(sql, params, limit: limit, skip: skip)
124
- results.map { |row| from_hash(row) }
224
+ def select(sql, params = [], limit: nil, offset: nil, include: nil)
225
+ results = db.fetch(sql, params, limit: limit, offset: offset)
226
+ instances = results.map { |row| from_hash(row) }
227
+ eager_load(instances, include) if include
228
+ instances
125
229
  end
126
230
 
127
231
  def count(conditions = nil, params = [])
@@ -148,9 +252,9 @@ module Tina4
148
252
  result
149
253
  end
150
254
 
151
- def with_trashed(conditions = "1=1", params = [], limit: 20, skip: 0)
255
+ def with_trashed(conditions = "1=1", params = [], limit: 20, offset: 0)
152
256
  sql = "SELECT * FROM #{table_name} WHERE #{conditions}"
153
- results = db.fetch(sql, params, limit: limit, skip: skip)
257
+ results = db.fetch(sql, params, limit: limit, offset: offset)
154
258
  results.map { |row| from_hash(row) }
155
259
  end
156
260
 
@@ -195,7 +299,7 @@ module Tina4
195
299
  end
196
300
 
197
301
  def scope(name, filter_sql, params = [])
198
- define_singleton_method(name) do |limit: 20, skip: 0|
302
+ define_singleton_method(name) do |limit: 20, offset: 0|
199
303
  where(filter_sql, params)
200
304
  end
201
305
  end
@@ -255,6 +359,7 @@ module Tina4
255
359
 
256
360
  def save
257
361
  @errors = []
362
+ @relationship_cache = {} # Clear relationship cache on save
258
363
  validate_fields
259
364
  return false unless @errors.empty?
260
365
 
@@ -342,6 +447,7 @@ module Tina4
342
447
  pk = self.class.primary_key_field || :id
343
448
  id ||= __send__(pk)
344
449
  return false unless id
450
+ @relationship_cache = {} # Clear relationship cache on reload
345
451
 
346
452
  result = self.class.db.fetch_one(
347
453
  "SELECT * FROM #{self.class.table_name} WHERE #{pk} = ?", [id]
@@ -366,12 +472,37 @@ module Tina4
366
472
  @errors
367
473
  end
368
474
 
369
- # Convert to hash using Ruby attribute names
370
- def to_h
475
+ # Convert to hash using Ruby attribute names.
476
+ # Optionally include relationships via the include keyword.
477
+ def to_h(include: nil)
371
478
  hash = {}
372
479
  self.class.field_definitions.each_key do |name|
373
480
  hash[name] = __send__(name)
374
481
  end
482
+
483
+ if include
484
+ # Group includes: top-level and nested
485
+ top_level = {}
486
+ include.each do |inc|
487
+ parts = inc.to_s.split(".", 2)
488
+ rel_name = parts[0].to_sym
489
+ top_level[rel_name] ||= []
490
+ top_level[rel_name] << parts[1] if parts.length > 1
491
+ end
492
+
493
+ top_level.each do |rel_name, nested|
494
+ next unless self.class.relationship_definitions.key?(rel_name)
495
+ related = __send__(rel_name)
496
+ if related.nil?
497
+ hash[rel_name] = nil
498
+ elsif related.is_a?(Array)
499
+ hash[rel_name] = related.map { |r| r.to_h(include: nested.empty? ? nil : nested) }
500
+ else
501
+ hash[rel_name] = related.to_h(include: nested.empty? ? nil : nested)
502
+ end
503
+ end
504
+ end
505
+
375
506
  hash
376
507
  end
377
508
 
@@ -385,8 +516,8 @@ module Tina4
385
516
 
386
517
  alias to_list to_array
387
518
 
388
- def to_json(*_args)
389
- JSON.generate(to_h)
519
+ def to_json(include: nil, **_args)
520
+ JSON.generate(to_h(include: include))
390
521
  end
391
522
 
392
523
  def to_s
@@ -21,7 +21,7 @@ document.getElementById('routes-count').textContent = d.count;
21
21
  document.getElementById('routes-body').innerHTML = d.routes.map(r => `
22
22
  <tr>
23
23
  <td><span class="method method-${r.method.toLowerCase()}">${r.method}</span></td>
24
- <td class="path">${r.path || r.pattern || ''}</td>
24
+ <td class="path"><a href="${r.path || r.pattern || ''}" target="_blank" title="${r.method !== 'GET' ? r.method + ' route — may not respond to browser GET' : 'Open in new tab'}" style="color:inherit;text-decoration:underline dotted;${r.method !== 'GET' ? 'opacity:0.7' : ''}">${r.path || r.pattern || ''}</a></td>
25
25
  <td>${r.auth_required || r.secure ? '<span class="badge-pill bg-reserved">auth</span>' : '<span class="badge-pill bg-success">open</span>'}</td>
26
26
  <td class="text-sm text-muted">${r.handler || ''} ${r.module ? '<small>(' + r.module + ')</small>' : ''}</td>
27
27
  </tr>`).join('');
@@ -0,0 +1,47 @@
1
+ "use strict";var Tina4=(()=>{var H=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var ge=Object.getOwnPropertyNames;var he=Object.prototype.hasOwnProperty;var me=(e,n)=>{for(var t in n)H(e,t,{get:n[t],enumerable:!0})},ye=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of ge(n))!he.call(e,r)&&r!==t&&H(e,r,{get:()=>n[r],enumerable:!(o=pe(n,r))||o.enumerable});return e};var ve=e=>ye(H({},"__esModule",{value:!0}),e);var Ae={};me(Ae,{Tina4Element:()=>I,api:()=>ie,batch:()=>q,computed:()=>z,effect:()=>m,html:()=>X,isSignal:()=>w,navigate:()=>L,pwa:()=>le,route:()=>oe,router:()=>re,signal:()=>h,ws:()=>ue});var C=null,_=null,M=null;function b(e){M=e}function J(){return M}var B=null,V=null;var N=0,P=new Set;function h(e,n){let t=e,o=new Set,r={_t4:!0,get value(){if(C&&(o.add(C),_)){let a=C;_.push(()=>o.delete(a))}return t},set value(a){if(Object.is(a,t))return;let i=t;if(t=a,r._debugInfo&&r._debugInfo.updateCount++,V&&V(r,i,a),N>0)for(let s of o)P.add(s);else{let s;for(let c of[...o])try{c()}catch(l){s===void 0&&(s=l)}if(s!==void 0)throw s}},_subscribe(a){return o.add(a),()=>{o.delete(a)}},peek(){return t}};return B&&(r._debugInfo={label:n,createdAt:Date.now(),updateCount:0,subs:o},B(r,n)),r}function z(e){let n=h(void 0);return m(()=>{n.value=e()}),{_t4:!0,get value(){return n.value},set value(t){throw new Error("[tina4] computed signals are read-only")},_subscribe(t){return n._subscribe(t)},peek(){return n.peek()}}}function m(e){let n=!1,t=[],o=()=>{if(n)return;for(let s of t)s();t=[];let a=C,i=_;C=o,_=t;try{e()}finally{C=a,_=i}};o();let r=()=>{n=!0;for(let a of t)a();t=[]};return M&&M.push(r),r}function q(e){N++;try{e()}finally{if(N--,N===0){let n=[...P];P.clear();let t;for(let o of n)try{o()}catch(r){t===void 0&&(t=r)}if(t!==void 0)throw t}}}function w(e){return e!==null&&typeof e=="object"&&e._t4===!0}var Q=new WeakMap,D="t4:";function X(e,...n){let t=Q.get(e);if(!t){t=document.createElement("template");let i="";for(let s=0;s<e.length;s++)i+=e[s],s<n.length&&(Te(i)?i+=`__t4_${s}__`:i+=`<!--${D}${s}-->`);t.innerHTML=i,Q.set(e,t)}let o=t.content.cloneNode(!0),r=be(o);for(let{marker:i,index:s}of r)ke(i,n[s]);let a=we(o);for(let i of a)Ce(i,n);return o}function be(e){let n=[];return W(e,t=>{if(t.nodeType===8){let o=t.data;if(o&&o.startsWith(D)){let r=parseInt(o.slice(D.length),10);n.push({marker:t,index:r})}}}),n}function we(e){let n=[];return W(e,t=>{t.nodeType===1&&n.push(t)}),n}function W(e,n){let t=e.childNodes;for(let o=0;o<t.length;o++){let r=t[o];n(r),W(r,n)}}function ke(e,n){let t=e.parentNode;if(t)if(w(n)){let o=document.createTextNode("");t.replaceChild(o,e),m(()=>{o.data=String(n.value??"")})}else if(typeof n=="function"){let o=document.createComment("");t.replaceChild(o,e);let r=[],a=[];m(()=>{for(let d of a)d();a=[];let i=[],s=J();b(i);let c=n();b(s),a=i;for(let d of r)d.parentNode?.removeChild(d);r=[];let l=F(c),f=o.parentNode;if(f)for(let d of l)f.insertBefore(d,o),r.push(d)})}else if(Y(n))t.replaceChild(n,e);else if(n instanceof Node)t.replaceChild(n,e);else if(Array.isArray(n)){let o=document.createDocumentFragment();for(let r of n){let a=F(r);for(let i of a)o.appendChild(i)}t.replaceChild(o,e)}else{let o=document.createTextNode(String(n??""));t.replaceChild(o,e)}}function Ce(e,n){let t=[];for(let o of Array.from(e.attributes)){let r=o.name,a=o.value;if(r.startsWith("@")){let s=r.slice(1),c=a.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];typeof l=="function"&&e.addEventListener(s,f=>q(()=>l(f)))}t.push(r);continue}if(r.startsWith("?")){let s=r.slice(1),c=a.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];if(w(l)){let f=l;m(()=>{f.value?e.setAttribute(s,""):e.removeAttribute(s)})}else typeof l=="function"?m(()=>{l()?e.setAttribute(s,""):e.removeAttribute(s)}):l&&e.setAttribute(s,"")}t.push(r);continue}if(r.startsWith(".")){let s=r.slice(1),c=a.match(/__t4_(\d+)__/);if(c){let l=n[parseInt(c[1],10)];w(l)?m(()=>{e[s]=l.value}):e[s]=l}t.push(r);continue}let i=a.match(/__t4_(\d+)__/);if(i){let s=n[parseInt(i[1],10)];if(w(s)){let c=s;m(()=>{e.setAttribute(r,String(c.value??""))})}else typeof s=="function"?m(()=>{e.setAttribute(r,String(s()??""))}):e.setAttribute(r,String(s??""))}}for(let o of t)e.removeAttribute(o)}function F(e){if(e==null||e===!1)return[];if(Y(e))return Array.from(e.childNodes);if(e instanceof Node)return[e];if(Array.isArray(e)){let n=[];for(let t of e)n.push(...F(t));return n}return[document.createTextNode(String(e))]}function Y(e){return e!=null&&typeof e=="object"&&e.nodeType===11}function Te(e){let n=!1,t=!1,o=!1;for(let r=0;r<e.length;r++){let a=e[r];a==="<"&&!n&&!t&&(o=!0),a===">"&&!n&&!t&&(o=!1),o&&(a==='"'&&!n&&(t=!t),a==="'"&&!t&&(n=!n))}return o}var Z=null,ee=null;var I=class extends HTMLElement{constructor(){super();this._props={};this._rendered=!1;let t=this.constructor;this._root=t.shadow?this.attachShadow({mode:"open"}):this;for(let[o,r]of Object.entries(t.props))this._props[o]=h(this._coerce(this.getAttribute(o),r))}static{this.props={}}static{this.styles=""}static{this.shadow=!0}static get observedAttributes(){return Object.keys(this.props)}connectedCallback(){if(this._rendered)return;this._rendered=!0;let t=this.constructor;if(t.styles&&t.shadow&&this._root instanceof ShadowRoot){let r=document.createElement("style");r.textContent=t.styles,this._root.appendChild(r)}let o=this.render();o&&this._root.appendChild(o),this.onMount(),Z&&Z(this)}disconnectedCallback(){this.onUnmount(),ee&&ee(this)}attributeChangedCallback(t,o,r){let i=this.constructor.props[t];i&&this._props[t]&&(this._props[t].value=this._coerce(r,i))}prop(t){if(!this._props[t])throw new Error(`[tina4] Prop '${t}' not declared in static props of <${this.tagName.toLowerCase()}>`);return this._props[t]}emit(t,o){this.dispatchEvent(new CustomEvent(t,{bubbles:!0,composed:!0,...o}))}onMount(){}onUnmount(){}_coerce(t,o){return o===Boolean?t!==null:o===Number?t!==null?Number(t):0:t??""}};var U=[],T=null,S="history",_e=!1,E=[],O=[],te=0;function oe(e,n){let t=[],o;e==="*"?o=".*":o=e.replace(/\{(\w+)\}/g,(a,i)=>(t.push(i),"([^/]+)"));let r=new RegExp(`^${o}$`);typeof n=="function"?U.push({pattern:e,regex:r,paramNames:t,handler:n}):U.push({pattern:e,regex:r,paramNames:t,handler:n.handler,guard:n.guard})}function L(e,n){if(S==="hash")if(n?.replace){let t=new URL(location.href);t.hash="#"+e,history.replaceState(null,"",t.toString()),R()}else location.hash="#"+e;else n?.replace?history.replaceState(null,"",e):history.pushState(null,"",e),R()}function R(){if(!T)return;let e=performance.now(),n=++te,t=S==="hash"?location.hash.slice(1)||"/":location.pathname;for(let o of U){let r=t.match(o.regex);if(!r)continue;let a={};if(o.paramNames.forEach((c,l)=>{a[c]=decodeURIComponent(r[l+1])}),o.guard){let c=o.guard();if(c===!1)return;if(typeof c=="string"){L(c,{replace:!0});return}}for(let c of O)c();O=[],T.innerHTML="";let i=[];b(i);let s=o.handler(a);if(s instanceof Promise)s.then(c=>{if(b(null),n!==te){for(let f of i)f();return}ne(T,c),O=i;let l=performance.now()-e;for(let f of E)f({path:t,params:a,pattern:o.pattern,durationMs:l})});else{b(null),ne(T,s),O=i;let c=performance.now()-e;for(let l of E)l({path:t,params:a,pattern:o.pattern,durationMs:c})}return}}function ne(e,n){n instanceof DocumentFragment||n instanceof Node?e.replaceChildren(n):typeof n=="string"?e.innerHTML=n:n!=null&&e.replaceChildren(document.createTextNode(String(n)))}var re={start(e){if(T=document.querySelector(e.target),!T)throw new Error(`[tina4] Router target '${e.target}' not found in DOM`);S=e.mode??"history",_e=!0,window.addEventListener("popstate",R),S==="hash"&&window.addEventListener("hashchange",R),document.addEventListener("click",n=>{if(n.metaKey||n.ctrlKey||n.shiftKey||n.altKey)return;let t=n.target.closest("a[href]");if(!t||t.origin!==location.origin||t.hasAttribute("target")||t.hasAttribute("download")||t.getAttribute("rel")?.includes("external"))return;n.preventDefault();let o=S==="hash"?t.getAttribute("href"):t.pathname;L(o)}),R()},on(e,n){return E.push(n),()=>{let t=E.indexOf(n);t>=0&&E.splice(t,1)}}};var y={baseUrl:"",auth:!1,tokenKey:"tina4_token",headers:{}},j=[],K=[],Se=0;function se(){try{return localStorage.getItem(y.tokenKey)}catch{return null}}function Ee(e){try{localStorage.setItem(y.tokenKey,e)}catch{}}async function x(e,n,t,o){let r={method:e,headers:{"Content-Type":"application/json",...y.headers}};if(y.auth){let d=se();d&&(r.headers.Authorization=`Bearer ${d}`)}if(t!==void 0&&e!=="GET"){let d=typeof t=="object"&&t!==null?{...t}:t;if(y.auth&&typeof d=="object"&&d!==null){let p=se();p&&(d.formToken=p)}r.body=JSON.stringify(d)}if(o?.headers&&Object.assign(r.headers,o.headers),o?.params){let d=Object.entries(o.params).map(([p,v])=>`${encodeURIComponent(p)}=${encodeURIComponent(String(v))}`).join("&");n+=(n.includes("?")?"&":"?")+d}let a=y.baseUrl+n;r._url=a,r._requestId=++Se;for(let d of j){let p=d(r);p&&(r=p)}let i=await fetch(a,r),s=i.headers.get("FreshToken");s&&Ee(s);let c=i.headers.get("Content-Type")??"",l;c.includes("json")?l=await i.json():l=await i.text();let f={status:i.status,data:l,ok:i.ok,headers:i.headers,_requestId:r._requestId};for(let d of K){let p=d(f);p&&(f=p)}if(!i.ok)throw f;return f.data}var ie={configure(e){Object.assign(y,e)},get(e,n){return x("GET",e,void 0,n)},post(e,n,t){return x("POST",e,n,t)},put(e,n,t){return x("PUT",e,n,t)},patch(e,n,t){return x("PATCH",e,n,t)},delete(e,n){return x("DELETE",e,void 0,n)},intercept(e,n){e==="request"?j.push(n):K.push(n)},_reset(){y.baseUrl="",y.auth=!1,y.tokenKey="tina4_token",y.headers={},j.length=0,K.length=0}};function ae(e){let n=e.cacheStrategy??"network-first",t=JSON.stringify(e.precache??[]),o=e.offlineRoute?`'${e.offlineRoute}'`:"null";return`
2
+ const CACHE = 'tina4-v1';
3
+ const PRECACHE = ${t};
4
+ const OFFLINE = ${o};
5
+
6
+ self.addEventListener('install', (e) => {
7
+ e.waitUntil(
8
+ caches.open(CACHE).then((c) => c.addAll(PRECACHE)).then(() => self.skipWaiting())
9
+ );
10
+ });
11
+
12
+ self.addEventListener('activate', (e) => {
13
+ e.waitUntil(self.clients.claim());
14
+ });
15
+
16
+ self.addEventListener('fetch', (e) => {
17
+ const req = e.request;
18
+ if (req.method !== 'GET') return;
19
+
20
+ ${n==="cache-first"?`
21
+ e.respondWith(
22
+ caches.match(req).then((cached) => cached || fetch(req).then((res) => {
23
+ const clone = res.clone();
24
+ caches.open(CACHE).then((c) => c.put(req, clone));
25
+ return res;
26
+ })).catch(() => OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
27
+ );`:n==="stale-while-revalidate"?`
28
+ e.respondWith(
29
+ caches.match(req).then((cached) => {
30
+ const fetched = fetch(req).then((res) => {
31
+ caches.open(CACHE).then((c) => c.put(req, res.clone()));
32
+ return res;
33
+ });
34
+ return cached || fetched;
35
+ }).catch(() => OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
36
+ );`:`
37
+ e.respondWith(
38
+ fetch(req).then((res) => {
39
+ const clone = res.clone();
40
+ caches.open(CACHE).then((c) => c.put(req, clone));
41
+ return res;
42
+ }).catch(() => caches.match(req).then((cached) =>
43
+ cached || (OFFLINE ? caches.match(OFFLINE) : new Response('Offline', { status: 503 }))
44
+ ))
45
+ );`}
46
+ });
47
+ `.trim()}function ce(e){let n={name:e.name,short_name:e.shortName??e.name,start_url:"/",display:e.display??"standalone",background_color:e.backgroundColor??"#ffffff",theme_color:e.themeColor??"#000000"};return e.icon&&(n.icons=[{src:e.icon,sizes:"192x192",type:"image/png"},{src:e.icon,sizes:"512x512",type:"image/png"}]),n}var le={register(e){let n=ce(e),t=new Blob([JSON.stringify(n)],{type:"application/json"}),o=document.createElement("link");o.rel="manifest",o.href=URL.createObjectURL(t),document.head.appendChild(o);let r=document.querySelector('meta[name="theme-color"]');if(r||(r=document.createElement("meta"),r.name="theme-color",document.head.appendChild(r)),r.content=e.themeColor??"#000000","serviceWorker"in navigator){let a=ae(e),i=new Blob([a],{type:"text/javascript"}),s=URL.createObjectURL(i);navigator.serviceWorker.register(s).catch(c=>{console.warn("[tina4] Service worker registration failed:",c)})}},generateServiceWorker(e){return ae(e)},generateManifest(e){return ce(e)}};var Re={reconnect:!0,reconnectDelay:1e3,reconnectMaxDelay:3e4,reconnectAttempts:1/0,protocols:[]};function xe(e,n={}){let t={...Re,...n},o=h("connecting"),r=h(!1),a=h(null),i=h(null),s=h(0),c={message:[],open:[],close:[],error:[]},l=null,f=!1,d=t.reconnectDelay,p=null,v=0;function de(u){if(typeof u!="string")return u;try{return JSON.parse(u)}catch{return u}}function $(){o.value=v>0?"reconnecting":"connecting";try{l=new WebSocket(e,t.protocols)}catch{o.value="closed",r.value=!1;return}l.onopen=()=>{o.value="open",r.value=!0,i.value=null,v=0,d=t.reconnectDelay,s.value=0;for(let u of c.open)u()},l.onmessage=u=>{let g=de(u.data);a.value=g;for(let k of c.message)k(g)},l.onclose=u=>{o.value="closed",r.value=!1;for(let g of c.close)g(u.code,u.reason);!f&&t.reconnect&&v<t.reconnectAttempts&&fe()},l.onerror=u=>{i.value=u;for(let g of c.error)g(u)}}function fe(){v++,s.value=v,o.value="reconnecting",p=setTimeout(()=>{p=null,$()},d),d=Math.min(d*2,t.reconnectMaxDelay)}let G={status:o,connected:r,lastMessage:a,error:i,reconnectCount:s,send(u){if(!l||l.readyState!==WebSocket.OPEN)throw new Error("[tina4] WebSocket is not connected");let g=typeof u=="string"?u:JSON.stringify(u);l.send(g)},on(u,g){return c[u].push(g),()=>{let k=c[u],A=k.indexOf(g);A>=0&&k.splice(A,1)}},pipe(u,g){let k=A=>{u.value=g(A,u.value)};return G.on("message",k)},close(u,g){f=!0,p&&(clearTimeout(p),p=null),l&&l.close(u??1e3,g??""),o.value="closed",r.value=!1}};return $(),G}var ue={connect:xe};return ve(Ae);})();