@decocms/start 0.19.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.
Files changed (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
@@ -0,0 +1,222 @@
1
+ # CDP Connection Guide
2
+
3
+ How to connect to a Deno pod's Chrome DevTools Protocol inspector for memory debugging.
4
+
5
+ ## Prerequisites
6
+
7
+ - `kubectl` configured with cluster access
8
+ - Python 3 with `websockets` package (`pip3 install websockets`)
9
+ - Pod must be running Deno with inspector enabled (Deco sites expose port 9229 by default)
10
+
11
+ ## Step 1: Identify the Pod
12
+
13
+ ```bash
14
+ # List pods in the site namespace
15
+ kubectl get pods -n sites-<sitename> -o wide
16
+
17
+ # Check current memory usage
18
+ kubectl top pods -n sites-<sitename>
19
+ ```
20
+
21
+ ## Step 2: Port-Forward
22
+
23
+ ```bash
24
+ # Forward local 9229 to pod's inspector port
25
+ kubectl port-forward -n sites-<sitename> <pod-name> 9229:9229
26
+
27
+ # If port 9229 is busy, use a different local port
28
+ kubectl port-forward -n sites-<sitename> <pod-name> 19229:9229
29
+ ```
30
+
31
+ **Keep this running in a separate terminal.** Port-forwards drop frequently — check if it's alive before running scripts.
32
+
33
+ ## Step 3: Get WebSocket URL
34
+
35
+ ```bash
36
+ curl -s http://127.0.0.1:9229/json/list | jq '.[0].webSocketDebuggerUrl'
37
+ ```
38
+
39
+ This returns something like:
40
+ ```
41
+ ws://127.0.0.1:9229/ws/b9cf0f05-6e67-4ad6-865f-f418f6b4856c
42
+ ```
43
+
44
+ **The UUID changes every time the pod restarts.** Always fetch a fresh URL.
45
+
46
+ ## Step 4: Connect via Python
47
+
48
+ ```python
49
+ import asyncio, json, websockets
50
+
51
+ WS = "ws://127.0.0.1:9229/ws/<UUID>"
52
+ MSG_ID = 0
53
+
54
+ async def send_cmd(ws, method, params=None):
55
+ global MSG_ID
56
+ MSG_ID += 1
57
+ msg = {"id": MSG_ID, "method": method}
58
+ if params:
59
+ msg["params"] = params
60
+ await ws.send(json.dumps(msg))
61
+ for _ in range(10000):
62
+ raw = await asyncio.wait_for(ws.recv(), timeout=30)
63
+ data = json.loads(raw)
64
+ if data.get("id") == MSG_ID:
65
+ return data
66
+ return None
67
+
68
+ async def evaluate(ws, expr):
69
+ r = await send_cmd(ws, "Runtime.evaluate", {
70
+ "expression": expr,
71
+ "contextId": 1, # IMPORTANT: always use contextId: 1
72
+ "returnByValue": True,
73
+ "awaitPromise": True,
74
+ "timeout": 30000,
75
+ })
76
+ if r and "result" in r and "result" in r["result"]:
77
+ return r["result"]["result"].get("value")
78
+ return None
79
+
80
+ async def main():
81
+ async with websockets.connect(WS, max_size=50*1024*1024) as ws:
82
+ await send_cmd(ws, "Runtime.enable")
83
+ # Your analysis code here...
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ ## Common Mistakes and Pitfalls
89
+
90
+ ### 1. Missing `contextId: 1`
91
+
92
+ **Symptom:** `Runtime.evaluate` returns empty results or "Cannot find default execution context".
93
+
94
+ **Cause:** After `Runtime.enable`, Deno emits thousands of `Runtime.consoleAPICalled` events that flood the event loop. Without explicit `contextId`, the evaluation may target the wrong context.
95
+
96
+ **Fix:** Always pass `contextId: 1` in every `Runtime.evaluate` call:
97
+ ```python
98
+ await send_cmd(ws, "Runtime.evaluate", {
99
+ "expression": expr,
100
+ "contextId": 1, # <-- THIS IS REQUIRED
101
+ ...
102
+ })
103
+ ```
104
+
105
+ ### 2. queryObjects returns under `objects`, not `result`
106
+
107
+ **Symptom:** `KeyError: 'result'` when accessing `queryObjects` response.
108
+
109
+ **Cause:** Deno's V8 inspector returns the array under `result.objects`, not `result.result` like Chrome does.
110
+
111
+ **Fix:**
112
+ ```python
113
+ # WRONG (Chrome-style):
114
+ arr_id = qr["result"]["result"].get("objectId")
115
+
116
+ # CORRECT (Deno-style):
117
+ arr_id = qr["result"].get("objects", {}).get("objectId")
118
+ ```
119
+
120
+ Full pattern:
121
+ ```python
122
+ async def query_objects(ws, proto_expr):
123
+ proto_r = await send_cmd(ws, "Runtime.evaluate", {
124
+ "expression": proto_expr,
125
+ "contextId": 1,
126
+ })
127
+ proto_id = proto_r["result"].get("result", {}).get("objectId")
128
+ if not proto_id:
129
+ return None
130
+ qr = await send_cmd(ws, "Runtime.queryObjects", {"prototypeObjectId": proto_id})
131
+ if not qr or "result" not in qr:
132
+ return None
133
+ return qr["result"].get("objects", {}).get("objectId")
134
+ ```
135
+
136
+ ### 3. Event flooding drowns responses
137
+
138
+ **Symptom:** `send_cmd` never receives the response, times out.
139
+
140
+ **Cause:** `Runtime.enable` triggers a flood of `Runtime.consoleAPICalled` events (can be thousands). The event drain loop in `send_cmd` may exhaust its iteration limit before finding the response.
141
+
142
+ **Fix:** Increase the drain limit:
143
+ ```python
144
+ for _ in range(10000): # Use 10000, not 100
145
+ raw = await asyncio.wait_for(ws.recv(), timeout=30)
146
+ data = json.loads(raw)
147
+ if data.get("id") == MSG_ID:
148
+ return data
149
+ ```
150
+
151
+ ### 4. Port-forward drops silently
152
+
153
+ **Symptom:** `ConnectionClosedError: no close frame` or `ConnectionRefusedError`.
154
+
155
+ **Cause:** `kubectl port-forward` drops connections under load or after inactivity.
156
+
157
+ **Fix:**
158
+ 1. Check if port-forward process is still running
159
+ 2. Re-establish port-forward
160
+ 3. Get a fresh WebSocket URL (same pod, new connection)
161
+ 4. Script should handle reconnection gracefully
162
+
163
+ ### 5. WebSocket message too large
164
+
165
+ **Symptom:** `PayloadTooBig` error when taking heap snapshots.
166
+
167
+ **Fix:** Increase `max_size` on the WebSocket connection:
168
+ ```python
169
+ async with websockets.connect(WS, max_size=100*1024*1024) as ws:
170
+ ```
171
+
172
+ ### 6. Deno 2.x API changes
173
+
174
+ Some APIs changed in Deno 2.x:
175
+ - `Deno.resources()` → **removed** in Deno 2.x. Use `/proc/self/fd` instead.
176
+ - `caches.keys()` → may not be available as a function. Use `caches.open(name)` and `cache.keys()` instead.
177
+
178
+ ### 7. /proc access requires permissions
179
+
180
+ **Symptom:** "Requires all access" error when reading `/proc/self/status` or `/proc/self/maps`.
181
+
182
+ **Cause:** Deno's permission system blocks filesystem reads outside allowed paths.
183
+
184
+ **Workaround:** Use `Deno.memoryUsage()` instead — it doesn't require extra permissions and gives RSS, heap, and external memory.
185
+
186
+ ### 8. Heap snapshot node_fields empty
187
+
188
+ **Symptom:** Heap snapshot `snapshot.node_fields` is an empty array.
189
+
190
+ **Cause:** Deno/V8 stores `node_fields` under `snapshot.meta` or uses a different layout than Chrome.
191
+
192
+ **Fix:** Infer field count from `len(nodes) / node_count`. Standard V8 uses 6 or 7 fields per node:
193
+ ```python
194
+ field_count = len(nodes) // node_count
195
+ # 6 fields: type, name, id, self_size, edge_count, trace_node_id
196
+ # 7 fields: + detachedness
197
+ ```
198
+
199
+ ### 9. callFunctionOn for object analysis
200
+
201
+ After `queryObjects`, use `callFunctionOn` to analyze the returned array:
202
+ ```python
203
+ async def call_on(ws, obj_id, func):
204
+ r = await send_cmd(ws, "Runtime.callFunctionOn", {
205
+ "objectId": obj_id,
206
+ "functionDeclaration": func,
207
+ "returnByValue": True,
208
+ })
209
+ if not r or "result" not in r:
210
+ return None
211
+ return r["result"].get("result", {}).get("value")
212
+
213
+ # Example: count Response objects and check bodyUsed
214
+ body_info = await call_on(ws, resp_array_id, """function() {
215
+ let used = 0, notUsed = 0;
216
+ for (let i = 0; i < this.length; i++) {
217
+ if (this[i].bodyUsed) used++;
218
+ else notUsed++;
219
+ }
220
+ return JSON.stringify({used, notUsed});
221
+ }""")
222
+ ```
@@ -0,0 +1,362 @@
1
+ # Memory Analysis Procedures
2
+
3
+ Step-by-step procedures for analyzing memory in Deno pods via CDP.
4
+
5
+ ## Procedure 1: Quick Memory Check
6
+
7
+ **Goal:** Determine if memory is a leak or lazy GC. Takes 2 minutes.
8
+
9
+ ```python
10
+ # 1. Get memory BEFORE GC
11
+ mem_before = await evaluate(ws, "JSON.stringify(Deno.memoryUsage())")
12
+
13
+ # 2. Force GC
14
+ await send_cmd(ws, "HeapProfiler.collectGarbage")
15
+ await asyncio.sleep(0.5)
16
+ await send_cmd(ws, "HeapProfiler.collectGarbage") # twice for thoroughness
17
+
18
+ # 3. Get memory AFTER GC
19
+ mem_after = await evaluate(ws, "JSON.stringify(Deno.memoryUsage())")
20
+ ```
21
+
22
+ **Interpretation:**
23
+ - RSS drops >30%? → Lazy GC, not a leak. **Recommend reducing `--max-old-space-size`.**
24
+ - RSS drops <10%? → Real retained memory. Continue to Procedure 2.
25
+
26
+ **Recommendation for lazy GC:**
27
+ If most memory is reclaimable by GC, the pod doesn't have a leak — V8 is just being lazy about collecting garbage. Reduce the max old space size so GC triggers more frequently:
28
+
29
+ ```bash
30
+ # In the deployment or Deno flags:
31
+ --v8-flags=--max-old-space-size=512
32
+ # or even 256 for small sites
33
+ ```
34
+
35
+ This keeps RSS predictable without affecting performance. V8's incremental GC is fast (typically <10ms pauses) so more frequent runs have negligible impact on request latency.
36
+
37
+ ## Procedure 2: Object Leak Detection
38
+
39
+ **Goal:** Find leaked Response/Request objects and unconsumed bodies.
40
+
41
+ ### Check Response Objects
42
+
43
+ ```python
44
+ resp_id = await query_objects(ws, "Response.prototype")
45
+ if resp_id:
46
+ info = await call_on(ws, resp_id, """function() {
47
+ let used = 0, notUsed = 0;
48
+ const leakedUrls = [];
49
+ for (let i = 0; i < this.length; i++) {
50
+ if (this[i].bodyUsed) used++;
51
+ else {
52
+ notUsed++;
53
+ if (leakedUrls.length < 20)
54
+ leakedUrls.push({
55
+ url: this[i].url.substring(0, 120),
56
+ status: this[i].status
57
+ });
58
+ }
59
+ }
60
+ return JSON.stringify({total: this.length, used, notUsed, leakedUrls});
61
+ }""")
62
+ ```
63
+
64
+ **Interpretation:**
65
+ - `notUsed` < 5? → Normal (in-flight requests)
66
+ - `notUsed` > 50? → **Response body leak.** Bodies are fetched but never consumed (`.text()`, `.json()`, `.arrayBuffer()`, or `.body.cancel()`).
67
+ - Check the URLs to identify which code path is leaking
68
+
69
+ ### Check Request Objects
70
+
71
+ ```python
72
+ req_id = await query_objects(ws, "Request.prototype")
73
+ if req_id:
74
+ info = await call_on(ws, req_id, """function() {
75
+ const hosts = {};
76
+ for (let i = 0; i < this.length; i++) {
77
+ try {
78
+ const h = new URL(this[i].url).host;
79
+ hosts[h] = (hosts[h] || 0) + 1;
80
+ } catch(e) {}
81
+ }
82
+ return JSON.stringify({total: this.length, hosts});
83
+ }""")
84
+ ```
85
+
86
+ **Interpretation:**
87
+ - Hundreds of Request objects to the same host → possible fetch loop or unbounded cache
88
+ - `localhost` requests → SSR self-fetches (normal for Fresh)
89
+
90
+ ## Procedure 3: ArrayBuffer Analysis
91
+
92
+ **Goal:** Identify large memory consumers in ArrayBuffers.
93
+
94
+ ```python
95
+ ab_id = await query_objects(ws, "ArrayBuffer.prototype")
96
+ if ab_id:
97
+ info = await call_on(ws, ab_id, """function() {
98
+ let totalBytes = 0;
99
+ const buckets = {
100
+ '0-1KB': 0, '1-10KB': 0, '10-100KB': 0,
101
+ '100KB-1MB': 0, '1-10MB': 0, '10MB+': 0
102
+ };
103
+ const large = [];
104
+ for (let i = 0; i < this.length; i++) {
105
+ const sz = this[i].byteLength;
106
+ totalBytes += sz;
107
+ if (sz < 1024) buckets['0-1KB']++;
108
+ else if (sz < 10240) buckets['1-10KB']++;
109
+ else if (sz < 102400) buckets['10-100KB']++;
110
+ else if (sz < 1048576) buckets['100KB-1MB']++;
111
+ else if (sz < 10485760) buckets['1-10MB']++;
112
+ else buckets['10MB+']++;
113
+
114
+ if (sz > 100000 && large.length < 20) {
115
+ try {
116
+ const preview = new TextDecoder().decode(
117
+ new Uint8Array(this[i], 0, Math.min(200, sz))
118
+ );
119
+ large.push({sizeMB: sz/1024/1024, preview});
120
+ } catch(e) {
121
+ large.push({sizeMB: sz/1024/1024, preview: '(binary)'});
122
+ }
123
+ }
124
+ }
125
+ return JSON.stringify({
126
+ count: this.length,
127
+ totalMB: totalBytes/1024/1024,
128
+ buckets,
129
+ large
130
+ });
131
+ }""")
132
+ ```
133
+
134
+ **Known ArrayBuffer patterns:**
135
+ - **~304 MB static buffer** — V8/ICU internal data. Always present. Ignore it.
136
+ - **`resourceMetrics` JSON buffers (0.3-0.6 MB each)** — OpenTelemetry export batches accumulating. Minor but grows over time.
137
+ - **Large JSON buffers (>1 MB)** — ProductListingPage or similar API responses. If appearing in PAIRS, might indicate response body read + original buffer both retained.
138
+ - **`data:application/json;base64,...`** — Source maps. Normal, proportional to loaded modules.
139
+ - **`<!DOCTYPE html>...`** — Rendered HTML pages. If many, SSR cache might be unbounded.
140
+
141
+ ## Procedure 4: Heap Snapshot
142
+
143
+ **Goal:** Get a comprehensive view of all heap objects.
144
+
145
+ ```python
146
+ await send_cmd(ws, "HeapProfiler.enable")
147
+
148
+ MSG_ID += 1
149
+ snap_id = MSG_ID
150
+ await ws.send(json.dumps({
151
+ "id": snap_id,
152
+ "method": "HeapProfiler.takeHeapSnapshot",
153
+ "params": {"reportProgress": False, "treatGlobalObjectsAsRoots": True}
154
+ }))
155
+
156
+ chunks = []
157
+ for _ in range(200000):
158
+ raw = await asyncio.wait_for(ws.recv(), timeout=120)
159
+ data = json.loads(raw)
160
+ if data.get("method") == "HeapProfiler.addHeapSnapshotChunk":
161
+ chunks.append(data["params"]["chunk"])
162
+ elif data.get("id") == snap_id:
163
+ break
164
+
165
+ snapshot = json.loads("".join(chunks))
166
+ ```
167
+
168
+ **Parsing the snapshot:**
169
+
170
+ ```python
171
+ snap_meta = snapshot.get("snapshot", {})
172
+ node_count = snap_meta.get("node_count", 0)
173
+ nodes = snapshot.get("nodes", [])
174
+ strings = snapshot.get("strings", [])
175
+
176
+ # Infer field count (node_fields may be empty in Deno)
177
+ field_count = len(nodes) // node_count # typically 6 or 7
178
+
179
+ # V8 node type indices (standard order):
180
+ # 0=hidden, 1=array, 2=string, 3=object, 4=code,
181
+ # 5=closure, 6=regexp, 7=number, 8=native,
182
+ # 9=synthetic, 10=concatenated string, 11=sliced string,
183
+ # 12=symbol, 13=bigint, 14=object shape
184
+
185
+ # Aggregate by type
186
+ type_agg = {}
187
+ for i in range(0, node_count * field_count, field_count):
188
+ node_type = nodes[i] # index 0 = type
189
+ name_idx = nodes[i + 1] # index 1 = name (string table index)
190
+ self_size = nodes[i + 3] # index 3 = self_size
191
+ # aggregate...
192
+ ```
193
+
194
+ **What to look for in the snapshot:**
195
+ - `string` type >100 MB → HTML pages or JSON cached in memory
196
+ - `native` (type 8) → ArrayBuffers (cross-reference with Procedure 3)
197
+ - `closure` count very high → possible listener/callback leak
198
+ - `object` with specific names → identify which data structures hold memory
199
+
200
+ ## Procedure 5: Additional Checks
201
+
202
+ ### Open File Descriptors
203
+
204
+ ```python
205
+ fds = await evaluate(ws, """
206
+ (async () => {
207
+ try {
208
+ let count = 0;
209
+ const types = {socket: 0, pipe: 0, file: 0, other: 0};
210
+ for await (const entry of Deno.readDir('/proc/self/fd')) {
211
+ count++;
212
+ try {
213
+ const link = await Deno.readLink('/proc/self/fd/' + entry.name);
214
+ if (link.startsWith('socket:')) types.socket++;
215
+ else if (link.startsWith('pipe:')) types.pipe++;
216
+ else if (link.startsWith('/')) types.file++;
217
+ else types.other++;
218
+ } catch(e) { types.other++; }
219
+ }
220
+ return JSON.stringify({count, types});
221
+ } catch(e) { return JSON.stringify({error: e.message}); }
222
+ })()
223
+ """)
224
+ ```
225
+
226
+ - 50-100 FDs → normal
227
+ - 500+ FDs → possible connection leak or file handle leak
228
+
229
+ ### Map/Set Objects (potential caches)
230
+
231
+ ```python
232
+ map_id = await query_objects(ws, "Map.prototype")
233
+ if map_id:
234
+ info = await call_on(ws, map_id, """function() {
235
+ const large = [];
236
+ for (let i = 0; i < this.length; i++) {
237
+ if (this[i].size > 10) {
238
+ let keys = [];
239
+ let j = 0;
240
+ for (const k of this[i].keys()) {
241
+ if (j++ >= 3) break;
242
+ keys.push(String(k).substring(0, 80));
243
+ }
244
+ large.push({size: this[i].size, keys});
245
+ }
246
+ }
247
+ large.sort((a,b) => b.size - a.size);
248
+ return JSON.stringify({total: this.length, large: large.slice(0, 15)});
249
+ }""")
250
+ ```
251
+
252
+ ### Deno Module Cache Size
253
+
254
+ ```python
255
+ cache_info = await evaluate(ws, """
256
+ (async () => {
257
+ const denoDir = Deno.env.get('DENO_DIR') || '/app/deco/deno_dir';
258
+ let count = 0, totalSize = 0;
259
+ async function walk(dir, depth) {
260
+ if (depth > 3) return;
261
+ try {
262
+ for await (const entry of Deno.readDir(dir)) {
263
+ if (entry.isDirectory) await walk(dir + '/' + entry.name, depth + 1);
264
+ else {
265
+ count++;
266
+ try {
267
+ const s = await Deno.stat(dir + '/' + entry.name);
268
+ totalSize += s.size;
269
+ } catch(e) {}
270
+ }
271
+ }
272
+ } catch(e) {}
273
+ }
274
+ await walk(denoDir, 0);
275
+ return JSON.stringify({denoDir, files: count, sizeMB: totalSize/1024/1024});
276
+ })()
277
+ """)
278
+ ```
279
+
280
+ ### Deno Version
281
+
282
+ ```python
283
+ ver = await evaluate(ws, """
284
+ JSON.stringify({
285
+ deno: Deno.version,
286
+ pid: Deno.pid,
287
+ hostname: Deno.hostname(),
288
+ })
289
+ """)
290
+ ```
291
+
292
+ ## Procedure 6: LRU Cache Inspection
293
+
294
+ **Important context:** Deco's LRU cache stores `true` (a boolean) as the value — it's a metadata index for the filesystem cache, NOT storing response bodies in memory. The `calculatedSize` is the sum of tracked Content-Length values (metadata tracking), not actual memory consumption.
295
+
296
+ ```python
297
+ lru = await evaluate(ws, """
298
+ (() => {
299
+ const results = [];
300
+ function findLRU(obj, path, depth) {
301
+ if (depth > 3 || !obj) return;
302
+ try {
303
+ for (const key of Object.keys(obj)) {
304
+ try {
305
+ const val = obj[key];
306
+ if (val && typeof val === 'object' &&
307
+ typeof val.max === 'number' &&
308
+ typeof val.size === 'number' &&
309
+ typeof val.calculatedSize === 'number') {
310
+ results.push({
311
+ path: path + '.' + key,
312
+ size: val.size,
313
+ calcSizeMB: val.calculatedSize / 1024 / 1024,
314
+ max: val.max,
315
+ maxSizeMB: val.maxSize ? val.maxSize / 1024 / 1024 : null,
316
+ });
317
+ }
318
+ if (depth < 2) findLRU(val, path + '.' + key, depth + 1);
319
+ } catch(e) {}
320
+ }
321
+ } catch(e) {}
322
+ }
323
+ findLRU(globalThis, 'globalThis', 0);
324
+ return JSON.stringify(results);
325
+ })()
326
+ """)
327
+ ```
328
+
329
+ **Interpretation:**
330
+ - `size` = number of entries in the LRU
331
+ - `calculatedSize` = sum of Content-Length values tracked (metadata, NOT actual memory)
332
+ - `max` = maximum number of entries
333
+ - `maxSize` = maximum calculatedSize before eviction
334
+
335
+ ## Summary: What's Normal vs What's a Leak
336
+
337
+ | Metric | Normal Range | Concern Threshold |
338
+ |--------|-------------|-------------------|
339
+ | RSS after GC | 500-1500 MB | >2 GB or growing continuously |
340
+ | Heap used | 100-300 MB | >500 MB after GC |
341
+ | Response objects (bodyUsed=false) | <10 | >50 |
342
+ | ArrayBuffers (excl. static 304MB) | <100 MB | >500 MB |
343
+ | Open FDs | 50-100 | >500 |
344
+ | Promises | 1000-5000 | >50000 |
345
+ | RSS drop after GC | 10-50% | If <5%, memory is truly retained |
346
+
347
+ ## Typical Healthy Memory Profile (after GC)
348
+
349
+ For a large Deco/Fresh site (e.g., fila-store, farmrio):
350
+
351
+ ```
352
+ RSS: ~1500 MB
353
+ ├── Heap: ~150 MB (JS objects)
354
+ ├── External: ~350 MB (ArrayBuffers, ~304MB is static V8)
355
+ └── Native: ~1000 MB (Deno runtime + JIT + modules)
356
+ ├── V8 JIT compiled code: ~300-500 MB
357
+ ├── Deno Rust runtime: ~200-300 MB
358
+ ├── Module cache (on disk): ~100-200 MB
359
+ └── Libraries + thread stacks: ~100 MB
360
+ ```
361
+
362
+ The ~1 GB native gap is expected for apps loading thousands of modules. It's stable and does not grow over time.