druxt 0.25.0-dev.20260922103051 → 0.25.0-dev.20260927110413

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.
package/dist/druxt.esm.js CHANGED
@@ -8,7 +8,113 @@ import merge from 'deepmerge';
8
8
  import md5 from 'md5';
9
9
  import Vue from 'vue';
10
10
 
11
+ const SCOPES = Symbol.for("druxt.processCache");
12
+ const scopes = globalThis[SCOPES] || (globalThis[SCOPES] = new Map());
13
+ const credentialed = new WeakSet();
14
+ const watched = new WeakSet();
15
+ const runtime = { isServer: () => typeof window === "undefined" };
16
+ const DRUPAL_SESSION_COOKIE = "S?SESS[0-9a-f]+";
17
+ const consumerId = (axios) => {
18
+ const headers = ((axios || {}).defaults || {}).headers || {};
19
+ for (const set of [headers, headers.common, headers.get]) {
20
+ const match = Object.entries(set || {}).find(([name]) => name.toLowerCase() === "x-consumer-id");
21
+ if (match && match[1])
22
+ return String(match[1]);
23
+ }
24
+ return "";
25
+ };
26
+ const headersHaveCredentials = (headers, sessionCookie) => {
27
+ const session = new RegExp(`(?:^|;\\s*)(?:${sessionCookie})=`, "i");
28
+ return Object.entries(headers || {}).some(([name, value]) => {
29
+ if (!value || typeof value === "object")
30
+ return false;
31
+ const header = name.toLowerCase();
32
+ return header === "authorization" || header === "cookie" && session.test(String(value));
33
+ });
34
+ };
35
+ const configHasCredentials = (config, sessionCookie = DRUPAL_SESSION_COOKIE) => {
36
+ if (!config)
37
+ return false;
38
+ if (config.auth)
39
+ return true;
40
+ const headers = config.headers || {};
41
+ return headersHaveCredentials(headers, sessionCookie) || headersHaveCredentials(headers.common, sessionCookie) || headersHaveCredentials(headers.get, sessionCookie);
42
+ };
43
+ const hasCredentials = (axios, sessionCookie = DRUPAL_SESSION_COOKIE) => {
44
+ if (!axios)
45
+ return false;
46
+ if (credentialed.has(axios))
47
+ return true;
48
+ return configHasCredentials(axios.defaults, sessionCookie);
49
+ };
50
+ const watchCredentials = (axios, sessionCookie = DRUPAL_SESSION_COOKIE) => {
51
+ const interceptors = ((axios || {}).interceptors || {}).response;
52
+ if (!interceptors || typeof interceptors.use !== "function" || watched.has(axios))
53
+ return;
54
+ watched.add(axios);
55
+ const mark = (config) => {
56
+ if (configHasCredentials(config, sessionCookie))
57
+ credentialed.add(axios);
58
+ };
59
+ interceptors.use((response) => {
60
+ mark((response || {}).config);
61
+ return response;
62
+ }, (error) => {
63
+ mark((error || {}).config);
64
+ return Promise.reject(error);
65
+ });
66
+ };
67
+ const parseCacheLifetime = (headers) => {
68
+ const header = (name) => Object.entries(headers || {}).filter(([key]) => key.toLowerCase() === name).map(([, value]) => String(value)).join(", ");
69
+ const directives = header("cache-control").toLowerCase().split(",").map((d) => d.trim());
70
+ if (directives.some((d) => ["private", "no-store", "no-cache"].includes(d)))
71
+ return 0;
72
+ const vary = header("vary").toLowerCase().split(",").map((v) => v.trim()).filter(Boolean);
73
+ if (vary.some((v) => !["cookie", "x-consumer-id", "accept-encoding"].includes(v)))
74
+ return 0;
75
+ const seconds = (name) => {
76
+ const directive = directives.find((d) => d.startsWith(`${name}=`));
77
+ const value = directive ? directive.slice(name.length + 1) : "";
78
+ return /^\d+$/.test(value) ? Number(value) : NaN;
79
+ };
80
+ const lifetime = directives.some((d) => d.startsWith("s-maxage=")) ? seconds("s-maxage") : seconds("max-age");
81
+ if (Number.isNaN(lifetime))
82
+ return 0;
83
+ const age = parseInt(header("age"), 10) || 0;
84
+ return Math.max(0, lifetime - age);
85
+ };
86
+ const processCache = (scope, { axios, ttl, sessionCookie } = {}) => {
87
+ if (!runtime.isServer() || hasCredentials(axios, sessionCookie))
88
+ return null;
89
+ if (!scopes.has(scope))
90
+ scopes.set(scope, new Map());
91
+ const consumers = scopes.get(scope);
92
+ const consumer = consumerId(axios);
93
+ if (!consumers.has(consumer))
94
+ consumers.set(consumer, new Map());
95
+ const entries = consumers.get(consumer);
96
+ const cap = (seconds) => typeof ttl === "number" ? Math.min(seconds, ttl) : seconds;
97
+ return {
98
+ get(key) {
99
+ const entry = entries.get(key);
100
+ if (!entry)
101
+ return void 0;
102
+ if (Date.now() - entry.created >= cap(entry.seconds) * 1e3)
103
+ return void 0;
104
+ return entry.value;
105
+ },
106
+ set(key, value, seconds) {
107
+ if (cap(seconds) > 0)
108
+ entries.set(key, { value, seconds: cap(seconds), created: Date.now() });
109
+ else
110
+ entries.delete(key);
111
+ return value;
112
+ }
113
+ };
114
+ };
115
+
11
116
  const indexCache = new WeakMap();
117
+ const lifetimes = new WeakMap();
12
118
  class DruxtClient {
13
119
  constructor(baseUrl, options = {}) {
14
120
  this.log = consola.create({ defaults: {
@@ -41,6 +147,8 @@ class DruxtClient {
41
147
  jsonapiResourceConfig: "jsonapi_resource_config--jsonapi_resource_config",
42
148
  ...options
43
149
  };
150
+ if (this.options.cache)
151
+ watchCredentials(this.axios, this.options.cache.sessionCookie);
44
152
  const indexKey = JSON.stringify([baseUrl, this.options.endpoint, this.options.jsonapiResourceConfig]);
45
153
  this.indexKey = indexKey;
46
154
  if (!indexCache.has(this.axios))
@@ -49,6 +157,18 @@ class DruxtClient {
49
157
  this.indexRequests = cache.requests;
50
158
  this.index = cache.index[indexKey] || (cache.index[indexKey] = {});
51
159
  }
160
+ processCache(scope) {
161
+ if (!this.options.cache)
162
+ return null;
163
+ const { ttl, sessionCookie } = this.options.cache;
164
+ return processCache(scope, { axios: this.axios, ttl, sessionCookie });
165
+ }
166
+ cacheLifetime(document) {
167
+ const record = document && typeof document === "object" && lifetimes.get(document);
168
+ if (!record)
169
+ return 0;
170
+ return Math.max(0, record.seconds - Math.floor((Date.now() - record.at) / 1e3));
171
+ }
52
172
  addHeaders(headers) {
53
173
  if (typeof headers === "undefined") {
54
174
  return false;
@@ -142,6 +262,8 @@ class DruxtClient {
142
262
  async get(url, options) {
143
263
  try {
144
264
  const res = await this.axios.get(url, options);
265
+ if (res && res.data && typeof res.data === "object")
266
+ lifetimes.set(res.data, { seconds: parseCacheLifetime(res.headers), at: Date.now() });
145
267
  return res;
146
268
  } catch (err) {
147
269
  this.error(err, { url });
@@ -168,15 +290,26 @@ class DruxtClient {
168
290
  return collections;
169
291
  }
170
292
  async getIndex(resource, prefix) {
293
+ if (!(this.index || {})[prefix]) {
294
+ const shared = this.processCache("index");
295
+ const sharedKey = [this.indexKey, prefix || ""].join(":");
296
+ const stored = shared && shared.get(sharedKey);
297
+ if (stored)
298
+ this.index[prefix] = stored;
299
+ }
171
300
  if (!(this.index || {})[prefix]) {
172
301
  const key = [this.indexKey, prefix || ""].join(":");
173
302
  const request = this.indexRequests[key] || (this.indexRequests[key] = this.fetchIndex(prefix));
303
+ let lifetime;
174
304
  try {
175
- await request;
305
+ lifetime = await request;
176
306
  } finally {
177
307
  if (this.indexRequests[key] === request)
178
308
  delete this.indexRequests[key];
179
309
  }
310
+ const shared = this.processCache("index");
311
+ if (shared && this.index[prefix])
312
+ shared.set(key, this.index[prefix], lifetime);
180
313
  }
181
314
  return resource ? this.index[prefix][resource] || false : this.index[prefix];
182
315
  }
@@ -193,10 +326,13 @@ class DruxtClient {
193
326
  value.href = value.href.replace(baseUrl, "");
194
327
  return [key, value];
195
328
  }));
329
+ const documents = [data];
196
330
  if (index[this.options.jsonapiResourceConfig]) {
197
331
  let resources = [];
198
332
  try {
199
- resources = (await this.get(index[this.options.jsonapiResourceConfig].href)).data.data;
333
+ const config = (await this.get(index[this.options.jsonapiResourceConfig].href)).data;
334
+ resources = config.data;
335
+ documents.push(config);
200
336
  } catch (err) {
201
337
  this.log.warn(err.message);
202
338
  }
@@ -217,6 +353,7 @@ class DruxtClient {
217
353
  }
218
354
  }
219
355
  this.index[prefix] = index;
356
+ return Math.min(...documents.map((document) => this.cacheLifetime(document)));
220
357
  }
221
358
  async getRelated(type, id, related, query, prefix = "") {
222
359
  if (!id || !type || !related) {
@@ -263,7 +400,7 @@ class DruxtClient {
263
400
  }
264
401
 
265
402
  var name = "druxt";
266
- var version = "0.25.0-dev.20260922103051";
403
+ var version = "0.25.0-dev.20260927110413";
267
404
  var description = "The Fully Decoupled Drupal Framework for Nuxt.js.";
268
405
  var keywords = [
269
406
  "cms",
@@ -322,7 +459,7 @@ var dependencies = {
322
459
  };
323
460
  var devDependencies = {
324
461
  "@storybook/addon-docs": "^6.5.16",
325
- "druxt-test-utils": "1.0.0-dev.20260922103051"
462
+ "druxt-test-utils": "1.0.0-dev.20260927110413"
326
463
  };
327
464
  var peerDependencies = {
328
465
  axios: "0.28.0",
@@ -365,6 +502,9 @@ const DruxtNuxtModule = async function(moduleOptions = {}) {
365
502
  ...moduleOptions,
366
503
  ...(this.options || {}).druxt
367
504
  };
505
+ if (options.cache !== false) {
506
+ options.cache = this.options.dev ? false : { ...options.cache };
507
+ }
368
508
  this.options.baseUrl = options.baseUrl = options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl;
369
509
  this.options.endpoint = options.endpoint = options.endpoint.startsWith("/") ? options.endpoint : `/${options.endpoint}`;
370
510
  const druxt = new DruxtClient(options.baseUrl, {
package/dist/druxt.ssr.js CHANGED
@@ -21,7 +21,113 @@ const merge__default = /*#__PURE__*/_interopDefaultLegacy(merge);
21
21
  const md5__default = /*#__PURE__*/_interopDefaultLegacy(md5);
22
22
  const Vue__default = /*#__PURE__*/_interopDefaultLegacy(Vue);
23
23
 
24
+ const SCOPES = Symbol.for("druxt.processCache");
25
+ const scopes = globalThis[SCOPES] || (globalThis[SCOPES] = new Map());
26
+ const credentialed = new WeakSet();
27
+ const watched = new WeakSet();
28
+ const runtime = { isServer: () => typeof window === "undefined" };
29
+ const DRUPAL_SESSION_COOKIE = "S?SESS[0-9a-f]+";
30
+ const consumerId = (axios) => {
31
+ const headers = ((axios || {}).defaults || {}).headers || {};
32
+ for (const set of [headers, headers.common, headers.get]) {
33
+ const match = Object.entries(set || {}).find(([name]) => name.toLowerCase() === "x-consumer-id");
34
+ if (match && match[1])
35
+ return String(match[1]);
36
+ }
37
+ return "";
38
+ };
39
+ const headersHaveCredentials = (headers, sessionCookie) => {
40
+ const session = new RegExp(`(?:^|;\\s*)(?:${sessionCookie})=`, "i");
41
+ return Object.entries(headers || {}).some(([name, value]) => {
42
+ if (!value || typeof value === "object")
43
+ return false;
44
+ const header = name.toLowerCase();
45
+ return header === "authorization" || header === "cookie" && session.test(String(value));
46
+ });
47
+ };
48
+ const configHasCredentials = (config, sessionCookie = DRUPAL_SESSION_COOKIE) => {
49
+ if (!config)
50
+ return false;
51
+ if (config.auth)
52
+ return true;
53
+ const headers = config.headers || {};
54
+ return headersHaveCredentials(headers, sessionCookie) || headersHaveCredentials(headers.common, sessionCookie) || headersHaveCredentials(headers.get, sessionCookie);
55
+ };
56
+ const hasCredentials = (axios, sessionCookie = DRUPAL_SESSION_COOKIE) => {
57
+ if (!axios)
58
+ return false;
59
+ if (credentialed.has(axios))
60
+ return true;
61
+ return configHasCredentials(axios.defaults, sessionCookie);
62
+ };
63
+ const watchCredentials = (axios, sessionCookie = DRUPAL_SESSION_COOKIE) => {
64
+ const interceptors = ((axios || {}).interceptors || {}).response;
65
+ if (!interceptors || typeof interceptors.use !== "function" || watched.has(axios))
66
+ return;
67
+ watched.add(axios);
68
+ const mark = (config) => {
69
+ if (configHasCredentials(config, sessionCookie))
70
+ credentialed.add(axios);
71
+ };
72
+ interceptors.use((response) => {
73
+ mark((response || {}).config);
74
+ return response;
75
+ }, (error) => {
76
+ mark((error || {}).config);
77
+ return Promise.reject(error);
78
+ });
79
+ };
80
+ const parseCacheLifetime = (headers) => {
81
+ const header = (name) => Object.entries(headers || {}).filter(([key]) => key.toLowerCase() === name).map(([, value]) => String(value)).join(", ");
82
+ const directives = header("cache-control").toLowerCase().split(",").map((d) => d.trim());
83
+ if (directives.some((d) => ["private", "no-store", "no-cache"].includes(d)))
84
+ return 0;
85
+ const vary = header("vary").toLowerCase().split(",").map((v) => v.trim()).filter(Boolean);
86
+ if (vary.some((v) => !["cookie", "x-consumer-id", "accept-encoding"].includes(v)))
87
+ return 0;
88
+ const seconds = (name) => {
89
+ const directive = directives.find((d) => d.startsWith(`${name}=`));
90
+ const value = directive ? directive.slice(name.length + 1) : "";
91
+ return /^\d+$/.test(value) ? Number(value) : NaN;
92
+ };
93
+ const lifetime = directives.some((d) => d.startsWith("s-maxage=")) ? seconds("s-maxage") : seconds("max-age");
94
+ if (Number.isNaN(lifetime))
95
+ return 0;
96
+ const age = parseInt(header("age"), 10) || 0;
97
+ return Math.max(0, lifetime - age);
98
+ };
99
+ const processCache = (scope, { axios, ttl, sessionCookie } = {}) => {
100
+ if (!runtime.isServer() || hasCredentials(axios, sessionCookie))
101
+ return null;
102
+ if (!scopes.has(scope))
103
+ scopes.set(scope, new Map());
104
+ const consumers = scopes.get(scope);
105
+ const consumer = consumerId(axios);
106
+ if (!consumers.has(consumer))
107
+ consumers.set(consumer, new Map());
108
+ const entries = consumers.get(consumer);
109
+ const cap = (seconds) => typeof ttl === "number" ? Math.min(seconds, ttl) : seconds;
110
+ return {
111
+ get(key) {
112
+ const entry = entries.get(key);
113
+ if (!entry)
114
+ return void 0;
115
+ if (Date.now() - entry.created >= cap(entry.seconds) * 1e3)
116
+ return void 0;
117
+ return entry.value;
118
+ },
119
+ set(key, value, seconds) {
120
+ if (cap(seconds) > 0)
121
+ entries.set(key, { value, seconds: cap(seconds), created: Date.now() });
122
+ else
123
+ entries.delete(key);
124
+ return value;
125
+ }
126
+ };
127
+ };
128
+
24
129
  const indexCache = new WeakMap();
130
+ const lifetimes = new WeakMap();
25
131
  class DruxtClient {
26
132
  constructor(baseUrl, options = {}) {
27
133
  this.log = consola__default["default"].create({ defaults: {
@@ -54,6 +160,8 @@ class DruxtClient {
54
160
  jsonapiResourceConfig: "jsonapi_resource_config--jsonapi_resource_config",
55
161
  ...options
56
162
  };
163
+ if (this.options.cache)
164
+ watchCredentials(this.axios, this.options.cache.sessionCookie);
57
165
  const indexKey = JSON.stringify([baseUrl, this.options.endpoint, this.options.jsonapiResourceConfig]);
58
166
  this.indexKey = indexKey;
59
167
  if (!indexCache.has(this.axios))
@@ -62,6 +170,18 @@ class DruxtClient {
62
170
  this.indexRequests = cache.requests;
63
171
  this.index = cache.index[indexKey] || (cache.index[indexKey] = {});
64
172
  }
173
+ processCache(scope) {
174
+ if (!this.options.cache)
175
+ return null;
176
+ const { ttl, sessionCookie } = this.options.cache;
177
+ return processCache(scope, { axios: this.axios, ttl, sessionCookie });
178
+ }
179
+ cacheLifetime(document) {
180
+ const record = document && typeof document === "object" && lifetimes.get(document);
181
+ if (!record)
182
+ return 0;
183
+ return Math.max(0, record.seconds - Math.floor((Date.now() - record.at) / 1e3));
184
+ }
65
185
  addHeaders(headers) {
66
186
  if (typeof headers === "undefined") {
67
187
  return false;
@@ -155,6 +275,8 @@ class DruxtClient {
155
275
  async get(url, options) {
156
276
  try {
157
277
  const res = await this.axios.get(url, options);
278
+ if (res && res.data && typeof res.data === "object")
279
+ lifetimes.set(res.data, { seconds: parseCacheLifetime(res.headers), at: Date.now() });
158
280
  return res;
159
281
  } catch (err) {
160
282
  this.error(err, { url });
@@ -181,15 +303,26 @@ class DruxtClient {
181
303
  return collections;
182
304
  }
183
305
  async getIndex(resource, prefix) {
306
+ if (!(this.index || {})[prefix]) {
307
+ const shared = this.processCache("index");
308
+ const sharedKey = [this.indexKey, prefix || ""].join(":");
309
+ const stored = shared && shared.get(sharedKey);
310
+ if (stored)
311
+ this.index[prefix] = stored;
312
+ }
184
313
  if (!(this.index || {})[prefix]) {
185
314
  const key = [this.indexKey, prefix || ""].join(":");
186
315
  const request = this.indexRequests[key] || (this.indexRequests[key] = this.fetchIndex(prefix));
316
+ let lifetime;
187
317
  try {
188
- await request;
318
+ lifetime = await request;
189
319
  } finally {
190
320
  if (this.indexRequests[key] === request)
191
321
  delete this.indexRequests[key];
192
322
  }
323
+ const shared = this.processCache("index");
324
+ if (shared && this.index[prefix])
325
+ shared.set(key, this.index[prefix], lifetime);
193
326
  }
194
327
  return resource ? this.index[prefix][resource] || false : this.index[prefix];
195
328
  }
@@ -206,10 +339,13 @@ class DruxtClient {
206
339
  value.href = value.href.replace(baseUrl, "");
207
340
  return [key, value];
208
341
  }));
342
+ const documents = [data];
209
343
  if (index[this.options.jsonapiResourceConfig]) {
210
344
  let resources = [];
211
345
  try {
212
- resources = (await this.get(index[this.options.jsonapiResourceConfig].href)).data.data;
346
+ const config = (await this.get(index[this.options.jsonapiResourceConfig].href)).data;
347
+ resources = config.data;
348
+ documents.push(config);
213
349
  } catch (err) {
214
350
  this.log.warn(err.message);
215
351
  }
@@ -230,6 +366,7 @@ class DruxtClient {
230
366
  }
231
367
  }
232
368
  this.index[prefix] = index;
369
+ return Math.min(...documents.map((document) => this.cacheLifetime(document)));
233
370
  }
234
371
  async getRelated(type, id, related, query, prefix = "") {
235
372
  if (!id || !type || !related) {
@@ -276,7 +413,7 @@ class DruxtClient {
276
413
  }
277
414
 
278
415
  var name = "druxt";
279
- var version = "0.25.0-dev.20260922103051";
416
+ var version = "0.25.0-dev.20260927110413";
280
417
  var description = "The Fully Decoupled Drupal Framework for Nuxt.js.";
281
418
  var keywords = [
282
419
  "cms",
@@ -335,7 +472,7 @@ var dependencies = {
335
472
  };
336
473
  var devDependencies = {
337
474
  "@storybook/addon-docs": "^6.5.16",
338
- "druxt-test-utils": "1.0.0-dev.20260922103051"
475
+ "druxt-test-utils": "1.0.0-dev.20260927110413"
339
476
  };
340
477
  var peerDependencies = {
341
478
  axios: "0.28.0",
@@ -378,6 +515,9 @@ const DruxtNuxtModule = async function(moduleOptions = {}) {
378
515
  ...moduleOptions,
379
516
  ...(this.options || {}).druxt
380
517
  };
518
+ if (options.cache !== false) {
519
+ options.cache = this.options.dev ? false : { ...options.cache };
520
+ }
381
521
  this.options.baseUrl = options.baseUrl = options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl;
382
522
  this.options.endpoint = options.endpoint = options.endpoint.startsWith("/") ? options.endpoint : `/${options.endpoint}`;
383
523
  const druxt = new DruxtClient(options.baseUrl, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "druxt",
3
- "version": "0.25.0-dev.20260922103051",
3
+ "version": "0.25.0-dev.20260927110413",
4
4
  "description": "The Fully Decoupled Drupal Framework for Nuxt.js.",
5
5
  "keywords": [
6
6
  "cms",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@storybook/addon-docs": "^6.5.16",
62
- "druxt-test-utils": "1.0.0-dev.20260922103051"
62
+ "druxt-test-utils": "1.0.0-dev.20260927110413"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "axios": "0.28.0",