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.
- checksums.yaml +4 -4
- data/README.md +120 -32
- data/lib/tina4/auth.rb +137 -27
- data/lib/tina4/auto_crud.rb +55 -3
- data/lib/tina4/cli.rb +228 -28
- data/lib/tina4/cors.rb +1 -1
- data/lib/tina4/database.rb +230 -26
- data/lib/tina4/database_result.rb +122 -8
- data/lib/tina4/dev_mailbox.rb +1 -1
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/frond.rb +314 -7
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +314 -16
- data/lib/tina4/localization.rb +1 -1
- data/lib/tina4/messenger.rb +111 -33
- data/lib/tina4/middleware.rb +349 -1
- data/lib/tina4/migration.rb +132 -11
- data/lib/tina4/orm.rb +149 -18
- data/lib/tina4/public/js/tina4-dev-admin.min.js +1 -1
- data/lib/tina4/public/js/tina4js.min.js +47 -0
- data/lib/tina4/query_builder.rb +374 -0
- data/lib/tina4/queue.rb +219 -61
- data/lib/tina4/queue_backends/lite_backend.rb +42 -7
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -0
- data/lib/tina4/rack_app.rb +200 -11
- data/lib/tina4/request.rb +14 -1
- data/lib/tina4/response.rb +26 -0
- data/lib/tina4/response_cache.rb +446 -29
- data/lib/tina4/router.rb +127 -0
- data/lib/tina4/service_runner.rb +1 -1
- data/lib/tina4/session.rb +6 -1
- data/lib/tina4/session_handlers/database_handler.rb +66 -0
- data/lib/tina4/swagger.rb +1 -1
- data/lib/tina4/templates/errors/404.twig +2 -2
- data/lib/tina4/templates/errors/500.twig +1 -1
- data/lib/tina4/validator.rb +174 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +23 -4
- data/lib/tina4/websocket_backplane.rb +118 -0
- data/lib/tina4.rb +126 -5
- metadata +40 -3
data/lib/tina4/orm.rb
CHANGED
|
@@ -85,19 +85,118 @@ module Tina4
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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,
|
|
123
|
-
results = db.fetch(sql, params, limit: limit,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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);})();
|