@editframe/elements 0.18.19-beta.0 → 0.18.20-beta.0

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.
@@ -187,6 +187,7 @@ export declare const deepGetElementsWithFrameTasks: (element: Element, elements?
187
187
  }>) => (HTMLElement & {
188
188
  frameTask: Task;
189
189
  })[];
190
+ export declare const clearTemporalCacheForElement: (element: Element) => void;
190
191
  export declare const shallowGetTemporalElements: (element: Element, temporals?: TemporalMixinInterface[]) => TemporalMixinInterface[];
191
192
  export declare class OwnCurrentTimeController implements ReactiveController {
192
193
  private host;
@@ -15,17 +15,21 @@ const deepGetElementsWithFrameTasks = (element, elements = []) => {
15
15
  return elements;
16
16
  };
17
17
  let temporalCache;
18
+ let modifiedElements = /* @__PURE__ */ new WeakSet();
18
19
  const resetTemporalCache = () => {
19
20
  temporalCache = /* @__PURE__ */ new Map();
21
+ modifiedElements = /* @__PURE__ */ new WeakSet();
20
22
  if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(resetTemporalCache);
21
23
  };
22
24
  resetTemporalCache();
25
+ const clearTemporalCacheForElement = (element) => {
26
+ temporalCache.delete(element);
27
+ modifiedElements.add(element);
28
+ };
23
29
  const shallowGetTemporalElements = (element, temporals = []) => {
24
- const cachedResult = temporalCache.get(element);
25
- if (cachedResult) return cachedResult;
30
+ temporals.length = 0;
26
31
  for (const child of element.children) if (isEFTemporal(child)) temporals.push(child);
27
32
  else shallowGetTemporalElements(child, temporals);
28
- temporalCache.set(element, temporals);
29
33
  return temporals;
30
34
  };
31
35
  var OwnCurrentTimeController = class {
@@ -281,4 +285,4 @@ const EFTemporal = (superClass) => {
281
285
  Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, { value: true });
282
286
  return TemporalMixinClass;
283
287
  };
284
- export { EFTemporal, deepGetElementsWithFrameTasks, flushStartTimeMsCache, isEFTemporal, shallowGetTemporalElements, timegroupContext };
288
+ export { EFTemporal, clearTemporalCacheForElement, deepGetElementsWithFrameTasks, flushStartTimeMsCache, isEFTemporal, shallowGetTemporalElements, timegroupContext };
@@ -53,6 +53,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
53
53
  }
54
54
  #currentTime = 0;
55
55
  #resizeObserver;
56
+ #childObserver;
56
57
  set currentTime(time) {
57
58
  const newTime = Math.max(0, Math.min(time, this.durationMs / 1e3));
58
59
  if (this.isRootTimegroup && this.isFrameUpdateInProgress) {
@@ -128,6 +129,25 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
128
129
  });
129
130
  if (this.parentTimegroup) new TimegroupController(this.parentTimegroup, this);
130
131
  if (this.shouldWrapWithWorkbench()) this.wrapWithWorkbench();
132
+ this.#childObserver = new MutationObserver((mutations) => {
133
+ let shouldUpdate = false;
134
+ for (const mutation of mutations) if (mutation.type === "childList") shouldUpdate = true;
135
+ else if (mutation.type === "attributes") {
136
+ if (mutation.attributeName === "duration" || mutation.attributeName === "mode") shouldUpdate = true;
137
+ }
138
+ if (shouldUpdate) {
139
+ import("./EFTemporal.js").then(({ clearTemporalCacheForElement }) => {
140
+ clearTemporalCacheForElement(this);
141
+ });
142
+ this.requestUpdate();
143
+ }
144
+ });
145
+ this.#childObserver.observe(this, {
146
+ childList: true,
147
+ subtree: true,
148
+ attributes: true,
149
+ attributeFilter: ["duration", "mode"]
150
+ });
131
151
  requestAnimationFrame(() => {
132
152
  this.updateAnimations();
133
153
  });
@@ -135,6 +155,7 @@ let EFTimegroup = class EFTimegroup$1 extends EFTemporal(LitElement) {
135
155
  disconnectedCallback() {
136
156
  super.disconnectedCallback();
137
157
  this.#resizeObserver?.disconnect();
158
+ this.#childObserver?.disconnect();
138
159
  }
139
160
  get storageKey() {
140
161
  if (!this.id) throw new Error("Timegroup must have an id to use localStorage.");
@@ -18,9 +18,9 @@ export declare const RenderInfo: z.ZodObject<{
18
18
  efImage: string[];
19
19
  }>;
20
20
  }, "strip", z.ZodTypeAny, {
21
+ durationMs: number;
21
22
  width: number;
22
23
  height: number;
23
- durationMs: number;
24
24
  fps: number;
25
25
  assets: {
26
26
  efMedia: Record<string, any>;
@@ -28,9 +28,9 @@ export declare const RenderInfo: z.ZodObject<{
28
28
  efImage: string[];
29
29
  };
30
30
  }, {
31
+ durationMs: number;
31
32
  width: number;
32
33
  height: number;
33
- durationMs: number;
34
34
  fps: number;
35
35
  assets: {
36
36
  efMedia: Record<string, any>;
@@ -2,9 +2,14 @@ import { LitElement } from 'lit';
2
2
  declare const TestContext_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
3
3
  declare class TestContext extends TestContext_base {
4
4
  }
5
+ declare const TestContextElement_base: (new (...args: any[]) => import('./ContextMixin.js').ContextMixinInterface) & typeof LitElement;
6
+ declare class TestContextElement extends TestContextElement_base {
7
+ render(): import('../elements/EFTimegroup.js').EFTimegroup & Element;
8
+ }
5
9
  declare global {
6
10
  interface HTMLElementTagNameMap {
7
11
  "test-context": TestContext;
12
+ "test-context-reactivity": TestContextElement;
8
13
  }
9
14
  }
10
15
  export {};
@@ -24,14 +24,22 @@ function ContextMixin(superClass) {
24
24
  init.headers ||= {};
25
25
  Object.assign(init.headers, { "Content-Type": "application/json" });
26
26
  if (this.signingURL) {
27
- if (!this.#URLTokens[url]) this.#URLTokens[url] = fetch(this.signingURL, {
27
+ const now = Date.now();
28
+ const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
29
+ const tokenExpiration = this.#URLTokenExpirations[cacheKey] || 0;
30
+ if (!this.#URLTokens[cacheKey] || now >= tokenExpiration) this.#URLTokens[cacheKey] = fetch(this.signingURL, {
28
31
  method: "POST",
29
- body: JSON.stringify({ url })
32
+ body: JSON.stringify(signingPayload)
30
33
  }).then(async (response) => {
31
- if (response.ok) return (await response.json()).token;
34
+ if (response.ok) {
35
+ const tokenData = await response.json();
36
+ const token = tokenData.token;
37
+ this.#URLTokenExpirations[cacheKey] = this.#parseTokenExpiration(token);
38
+ return token;
39
+ }
32
40
  throw new Error(`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`);
33
41
  });
34
- const urlToken = await this.#URLTokens[url];
42
+ const urlToken = await this.#URLTokens[cacheKey];
35
43
  Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
36
44
  } else init.credentials = "include";
37
45
  return fetch(url, init);
@@ -51,7 +59,76 @@ function ContextMixin(superClass) {
51
59
  set apiHost(value) {
52
60
  this.#apiHost = value;
53
61
  }
62
+ get durationMs() {
63
+ return this.targetTimegroup?.durationMs ?? 0;
64
+ }
65
+ get endTimeMs() {
66
+ return this.targetTimegroup?.endTimeMs ?? 0;
67
+ }
54
68
  #URLTokens = {};
69
+ #URLTokenExpirations = {};
70
+ /**
71
+ * Generate a cache key for URL token based on signing strategy
72
+ *
73
+ * Uses unified prefix + parameter matching approach:
74
+ * - For transcode URLs: signs base "/api/v1/transcode" + params like {url: "source.mp4"}
75
+ * - For regular URLs: signs full URL with empty params {}
76
+ * - All validation uses prefix matching + exhaustive parameter matching
77
+ * - Multiple transcode segments with same source share one token (reduces round-trips)
78
+ */
79
+ #getTokenCacheKey(url) {
80
+ try {
81
+ const urlObj = new URL(url);
82
+ if (urlObj.pathname.includes("/api/v1/transcode/")) {
83
+ const urlParam = urlObj.searchParams.get("url");
84
+ if (urlParam) {
85
+ const basePath = `${urlObj.origin}/api/v1/transcode`;
86
+ const cacheKey = `${basePath}?url=${urlParam}`;
87
+ return {
88
+ cacheKey,
89
+ signingPayload: {
90
+ url: basePath,
91
+ params: { url: urlParam }
92
+ }
93
+ };
94
+ }
95
+ }
96
+ return {
97
+ cacheKey: url,
98
+ signingPayload: { url }
99
+ };
100
+ } catch {
101
+ return {
102
+ cacheKey: url,
103
+ signingPayload: { url }
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * Parse JWT token to extract safe expiration time (with buffer)
109
+ * @param token JWT token string
110
+ * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails
111
+ */
112
+ #parseTokenExpiration(token) {
113
+ try {
114
+ const parts = token.split(".");
115
+ if (parts.length !== 3) return 0;
116
+ const payload = parts[1];
117
+ if (!payload) return 0;
118
+ const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
119
+ const parsed = JSON.parse(decoded);
120
+ const exp = parsed.exp;
121
+ const iat = parsed.iat;
122
+ if (!exp) return 0;
123
+ const lifetimeSeconds = iat ? exp - iat : 3600;
124
+ const tenPercentBufferMs = lifetimeSeconds * .1 * 1e3;
125
+ const fiveMinutesMs = 5 * 60 * 1e3;
126
+ const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);
127
+ return exp * 1e3 - bufferMs;
128
+ } catch {
129
+ return 0;
130
+ }
131
+ }
55
132
  #signingURL;
56
133
  /**
57
134
  * A URL that will be used to generated signed tokens for accessing media files from the
@@ -66,10 +143,20 @@ function ContextMixin(superClass) {
66
143
  #FPS = 30;
67
144
  #MS_PER_FRAME = 1e3 / this.#FPS;
68
145
  #timegroupObserver = new MutationObserver((mutations) => {
146
+ let shouldUpdate = false;
69
147
  for (const mutation of mutations) if (mutation.type === "childList") {
70
148
  const newTimegroup = this.querySelector("ef-timegroup");
71
- if (newTimegroup !== this.targetTimegroup) this.targetTimegroup = newTimegroup;
149
+ if (newTimegroup !== this.targetTimegroup) {
150
+ this.targetTimegroup = newTimegroup;
151
+ shouldUpdate = true;
152
+ }
153
+ } else if (mutation.type === "attributes") {
154
+ if (mutation.attributeName === "duration" || mutation.attributeName === "mode" || mutation.target instanceof Element && (mutation.target.tagName === "EF-TIMEGROUP" || mutation.target.closest("ef-timegroup"))) shouldUpdate = true;
72
155
  }
156
+ if (shouldUpdate) queueMicrotask(() => {
157
+ this.requestUpdate();
158
+ if (this.targetTimegroup) this.targetTimegroup.requestUpdate();
159
+ });
73
160
  });
74
161
  connectedCallback() {
75
162
  super.connectedCallback();
@@ -85,6 +172,8 @@ function ContextMixin(superClass) {
85
172
  super.disconnectedCallback();
86
173
  this.#timegroupObserver.disconnect();
87
174
  this.stopPlayback();
175
+ this.#URLTokens = {};
176
+ this.#URLTokenExpirations = {};
88
177
  }
89
178
  update(changedProperties) {
90
179
  if (changedProperties.has("playing")) if (this.playing) this.startPlayback();
@@ -194,6 +283,8 @@ function ContextMixin(superClass) {
194
283
  })], ContextElement.prototype, "apiHost", null);
195
284
  _decorate([provide({ context: efContext })], ContextElement.prototype, "efContext", void 0);
196
285
  _decorate([provide({ context: targetTimegroupContext }), state()], ContextElement.prototype, "targetTimegroup", void 0);
286
+ _decorate([state()], ContextElement.prototype, "durationMs", null);
287
+ _decorate([state()], ContextElement.prototype, "endTimeMs", null);
197
288
  _decorate([provide({ context: fetchContext })], ContextElement.prototype, "fetch", void 0);
198
289
  _decorate([property({
199
290
  type: String,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.18.19-beta.0",
3
+ "version": "0.18.20-beta.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -27,7 +27,7 @@
27
27
  "license": "UNLICENSED",
28
28
  "dependencies": {
29
29
  "@bramus/style-observer": "^1.3.0",
30
- "@editframe/assets": "0.18.19-beta.0",
30
+ "@editframe/assets": "0.18.20-beta.0",
31
31
  "@lit/context": "^1.1.2",
32
32
  "@lit/task": "^1.0.1",
33
33
  "d3": "^7.9.0",
@@ -236,22 +236,32 @@ export const deepGetElementsWithFrameTasks = (
236
236
  };
237
237
 
238
238
  let temporalCache: Map<Element, TemporalMixinInterface[]>;
239
+ let modifiedElements = new WeakSet<Element>();
240
+
239
241
  const resetTemporalCache = () => {
240
242
  temporalCache = new Map();
243
+ modifiedElements = new WeakSet();
241
244
  if (typeof requestAnimationFrame !== "undefined") {
242
245
  requestAnimationFrame(resetTemporalCache);
243
246
  }
244
247
  };
245
248
  resetTemporalCache();
246
249
 
250
+ export const clearTemporalCacheForElement = (element: Element) => {
251
+ temporalCache.delete(element);
252
+ modifiedElements.add(element);
253
+ };
254
+
247
255
  export const shallowGetTemporalElements = (
248
256
  element: Element,
249
257
  temporals: TemporalMixinInterface[] = [],
250
258
  ) => {
251
- const cachedResult = temporalCache.get(element);
252
- if (cachedResult) {
253
- return cachedResult;
254
- }
259
+ // Temporarily disable caching to ensure reactivity works correctly
260
+ // TODO: Implement proper cache invalidation mechanism
261
+
262
+ // Clear the temporals array to ensure fresh results
263
+ temporals.length = 0;
264
+
255
265
  for (const child of element.children) {
256
266
  if (isEFTemporal(child)) {
257
267
  temporals.push(child);
@@ -259,7 +269,7 @@ export const shallowGetTemporalElements = (
259
269
  shallowGetTemporalElements(child, temporals);
260
270
  }
261
271
  }
262
- temporalCache.set(element, temporals);
272
+
263
273
  return temporals;
264
274
  };
265
275
 
@@ -73,6 +73,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
73
73
  fit: "none" | "contain" | "cover" = "none";
74
74
 
75
75
  #resizeObserver?: ResizeObserver;
76
+ #childObserver?: MutationObserver;
76
77
 
77
78
  @property({ type: Number, attribute: "currenttime" })
78
79
  set currentTime(time: number) {
@@ -185,6 +186,44 @@ export class EFTimegroup extends EFTemporal(LitElement) {
185
186
  this.wrapWithWorkbench();
186
187
  }
187
188
 
189
+ // Set up observer to detect child changes that affect duration
190
+ this.#childObserver = new MutationObserver((mutations) => {
191
+ let shouldUpdate = false;
192
+
193
+ for (const mutation of mutations) {
194
+ if (mutation.type === "childList") {
195
+ // Child added/removed - this affects duration for contain/sequence modes
196
+ shouldUpdate = true;
197
+ } else if (mutation.type === "attributes") {
198
+ // Attribute changes that might affect duration
199
+ if (
200
+ mutation.attributeName === "duration" ||
201
+ mutation.attributeName === "mode"
202
+ ) {
203
+ shouldUpdate = true;
204
+ }
205
+ }
206
+ }
207
+
208
+ if (shouldUpdate) {
209
+ // Clear the temporal cache for this element to ensure childTemporals is up to date
210
+ import("./EFTemporal.js").then(({ clearTemporalCacheForElement }) => {
211
+ clearTemporalCacheForElement(this);
212
+ });
213
+
214
+ // Trigger an update to recalculate computed properties
215
+ this.requestUpdate();
216
+ }
217
+ });
218
+
219
+ // Observe this element for child changes
220
+ this.#childObserver.observe(this, {
221
+ childList: true,
222
+ subtree: true,
223
+ attributes: true,
224
+ attributeFilter: ["duration", "mode"],
225
+ });
226
+
188
227
  requestAnimationFrame(() => {
189
228
  this.updateAnimations();
190
229
  });
@@ -193,6 +232,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
193
232
  disconnectedCallback() {
194
233
  super.disconnectedCallback();
195
234
  this.#resizeObserver?.disconnect();
235
+ this.#childObserver?.disconnect();
196
236
  }
197
237
 
198
238
  get storageKey() {