@lunora/platform-node 1.0.0-alpha.128 → 1.0.0-alpha.129

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/README.md CHANGED
@@ -17,3 +17,10 @@ Portability was a claim, not a construction check — `PLATFORM_MATRICES` held e
17
17
  ## Scope
18
18
 
19
19
  This is a **spike**, not a production target: it is not wired into `lunora dev`, has no deploy driver, and several capabilities are honestly rated `emulated`/`unsupported` in its capability matrix entry (see `@lunora/platform`'s `NODE_CAPABILITIES`). Wiring it into the dev server is the payoff and a follow-up, not this change.
20
+
21
+ ## On-demand profiling
22
+
23
+ `createNodeProfileHandler({ token })` serves an on-demand CPU or heap profile
24
+ (gzip pprof) behind a bearer-guarded route the app mounts itself. The full
25
+ description, including what each profile type does and does not show, is in
26
+ [`docs/index.mdx`](./docs/index.mdx#on-demand-profiling).
package/dist/index.d.mts CHANGED
@@ -525,6 +525,13 @@ declare const createNodePlatform: <Queues extends Record<string, {
525
525
  }> = Record<string, never>, Workflows extends Record<string, {
526
526
  isLunoraWorkflow: true;
527
527
  }> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
528
+ interface NodeProfileHandlerOptions {
529
+ /** The bearer a caller must present. Required and non-empty: an empty token would make the endpoint open to anyone. */
530
+ token: string;
531
+ }
532
+ type NodeProfileHandler = (request: Request) => Promise<Response>;
533
+ /** The public handler: the real profiler, guarded by `token`. */
534
+ declare const createNodeProfileHandler: ({ token }: NodeProfileHandlerOptions) => NodeProfileHandler;
528
535
  /** Options for {@link createNodeR2Bucket}. */
529
536
  interface NodeR2BucketOptions {
530
537
  /** The bucket directory — created on first write. Objects live here, one file per key. */
@@ -736,4 +743,4 @@ type NodeGlobalStore,
736
743
  * server, which is why both statements above hold at once. See
737
744
  * `plans/234-node-host-findings.md`.
738
745
  */
739
- type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
746
+ type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
package/dist/index.d.ts CHANGED
@@ -525,6 +525,13 @@ declare const createNodePlatform: <Queues extends Record<string, {
525
525
  }> = Record<string, never>, Workflows extends Record<string, {
526
526
  isLunoraWorkflow: true;
527
527
  }> = Record<string, never>>(options?: NodePlatformOptions<Queues, Workflows>) => NodePlatform<Queues, Workflows>;
528
+ interface NodeProfileHandlerOptions {
529
+ /** The bearer a caller must present. Required and non-empty: an empty token would make the endpoint open to anyone. */
530
+ token: string;
531
+ }
532
+ type NodeProfileHandler = (request: Request) => Promise<Response>;
533
+ /** The public handler: the real profiler, guarded by `token`. */
534
+ declare const createNodeProfileHandler: ({ token }: NodeProfileHandlerOptions) => NodeProfileHandler;
528
535
  /** Options for {@link createNodeR2Bucket}. */
529
536
  interface NodeR2BucketOptions {
530
537
  /** The bucket directory — created on first write. Objects live here, one file per key. */
@@ -736,4 +743,4 @@ type NodeGlobalStore,
736
743
  * server, which is why both statements above hold at once. See
737
744
  * `plans/234-node-host-findings.md`.
738
745
  */
739
- type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
746
+ type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createNodeGlobalStore as r,createNodeSqlExec as t}from"./packem_shared/createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-D29HvrOz.mjs";import{createNodeQueueHost as x}from"./packem_shared/createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as p}from"./packem_shared/createNodeR2Bucket-ChHs23n-.mjs";import{createNodeSchedulerHost as l}from"./packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as h}from"./packem_shared/createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as k}from"./packem_shared/createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeShardState as w}from"./packem_shared/createNodeShardState-D6XMWbFs.mjs";import{createNodeSocketHost as W}from"./packem_shared/createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as g}from"./packem_shared/createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as q}from"./packem_shared/createNodeWorkflowStore-CL7RYP6H.mjs";export{r as createNodeGlobalStore,f as createNodePlatform,x as createNodeQueueHost,p as createNodeR2Bucket,l as createNodeSchedulerHost,h as createNodeShardHost,d as createNodeShardKvStore,k as createNodeShardRegistry,w as createNodeShardState,W as createNodeSocketHost,t as createNodeSqlExec,g as createNodeWorkflowHost,q as createNodeWorkflowStore};
1
+ import{createNodeGlobalStore as r,createNodeSqlExec as t}from"./packem_shared/createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as d}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as f}from"./packem_shared/createNodePlatform-D29HvrOz.mjs";import{createNodeProfileHandler as x}from"./packem_shared/createNodeProfileHandler-MX5xKPg0.mjs";import{createNodeQueueHost as p}from"./packem_shared/createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as l}from"./packem_shared/createNodeR2Bucket-ChHs23n-.mjs";import{createNodeSchedulerHost as H}from"./packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as k}from"./packem_shared/createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as i}from"./packem_shared/createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeShardState as P}from"./packem_shared/createNodeShardState-D6XMWbFs.mjs";import{createNodeSocketHost as W}from"./packem_shared/createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as g}from"./packem_shared/createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as q}from"./packem_shared/createNodeWorkflowStore-CL7RYP6H.mjs";export{r as createNodeGlobalStore,f as createNodePlatform,x as createNodeProfileHandler,p as createNodeQueueHost,l as createNodeR2Bucket,H as createNodeSchedulerHost,k as createNodeShardHost,d as createNodeShardKvStore,i as createNodeShardRegistry,P as createNodeShardState,W as createNodeSocketHost,t as createNodeSqlExec,g as createNodeWorkflowHost,q as createNodeWorkflowStore};
@@ -0,0 +1 @@
1
+ import{timingSafeEqual as B,createHash as L}from"node:crypto";import{LunoraError as l,toErrorBody as H}from"@lunora/errors";import{Session as $}from"node:inspector";import{gzipSync as C}from"node:zlib";import{StringTable as x,Profile as j}from"pprof-format";const z="(anonymous)",T=e=>e>=0?e+1:0,R=e=>{const t=new x,n=[],o=[],p=new Map,i=new Map,f=(r,a)=>{const c=`${r}\0${a.url}\0${String(a.lineNumber)}\0${String(a.columnNumber)}`,s=p.get(c);if(s!==void 0)return s;const u=n.length+1;return p.set(c,u),n.push({filename:t.dedup(a.url),id:u,name:t.dedup(r),startLine:T(a.lineNumber),systemName:t.dedup(r)}),u},h=r=>{const a=r.functionName===""?z:r.functionName,c=f(a,r),s=T(r.lineNumber),u=T(r.columnNumber),y=`${String(c)}\0${String(s)}\0${String(u)}`,I=i.get(y);if(I!==void 0)return I;const w=o.length+1;return i.set(y,w),o.push({id:w,line:[{column:u,functionId:c,line:s}]}),w},S=new Map,U=r=>{const a=S.get(r);if(a!==void 0)return a;const c=[];let s=e.frames.get(r);for(;s?.parentId!==void 0;)c.push(h(s.callFrame)),s=e.frames.get(s.parentId);return S.set(r,c),c},g=new Map;for(const r of e.samples){const a=U(r.nodeId),c=a.join(","),s=g.get(c);s===void 0?g.set(c,{locationId:a,values:[...r.values]}):s.values=s.values.map((u,y)=>u+(r.values[y]??0))}const k=[...g.values()].map(r=>({locationId:r.locationId,value:r.values}));return new j({durationNanos:e.durationNanos,function:n,location:o,period:e.period,periodType:{type:t.dedup(e.periodType.type),unit:t.dedup(e.periodType.unit)},sample:k,sampleType:e.sampleTypes.map(r=>({type:t.dedup(r.type),unit:t.dedup(r.unit)})),stringTable:t,timeNanos:e.startNanos})},W=e=>{const t=new Map;for(const n of e.nodes)for(const o of n.children??[])t.set(o,n.id);return new Map(e.nodes.map(n=>[n.id,{callFrame:n.callFrame,parentId:t.get(n.id)}]))},Y=e=>{const t=new Map,n=[{node:e,parentId:void 0}];for(let o=n.pop();o!==void 0;o=n.pop()){t.set(o.node.id,{callFrame:o.node.callFrame,parentId:o.parentId});for(const p of o.node.children)n.push({node:p,parentId:o.node.id})}return t},G=[{type:"samples",unit:"count"},{type:"cpu",unit:"nanoseconds"}],Q=[{type:"space",unit:"bytes"}],P=e=>e*1e6,J=(e,t)=>R({durationNanos:(e.endTime-e.startTime)*1e3,frames:W(e),period:t.intervalMicros*1e3,periodType:{type:"cpu",unit:"nanoseconds"},samples:e.samples.map((n,o)=>({nodeId:n,values:[1,(e.timeDeltas[o]??0)*1e3]})),sampleTypes:G,startNanos:P(t.startedAtMs)}),V=(e,t)=>R({durationNanos:P(t.durationMs),frames:Y(e.head),period:t.samplingIntervalBytes,periodType:{type:"space",unit:"bytes"},samples:e.samples.map(n=>({nodeId:n.nodeId,values:[n.size]})),sampleTypes:Q,startNanos:P(t.startedAtMs)}),M=e=>new Uint8Array(C(e.encode())),_=1e3,v=5e4,D=["cpu","heap"],K=e=>typeof e=="string"&&D.includes(e),E=1e3,A=32768;let N=!1;const b=e=>new Promise(t=>{setTimeout(t,e)}),d=(e,t,n)=>new Promise((o,p)=>{e.post(t,n,(i,f)=>{i===null?o(f):p(i)})}),F=e=>{const{durationMs:t,profileType:n}=e;if(!K(n))throw new l("BAD_REQUEST",`profile type must be one of ${D.join(", ")}`);if(typeof t!="number"||!Number.isInteger(t)||t<_||t>v)throw new l("BAD_REQUEST",`duration must be an integer from ${String(_)} to ${String(v)} ms`);return{durationMs:t,profileType:n}},X=async e=>{const t=new $,n=Date.now();let o;t.connect();try{if(e.profileType==="cpu"){await d(t,"Profiler.enable"),await d(t,"Profiler.setSamplingInterval",{interval:E}),await d(t,"Profiler.start"),o="cpu",await b(e.durationMs);const{profile:i}=await d(t,"Profiler.stop");return o=void 0,M(J(i,{intervalMicros:E,startedAtMs:n}))}await d(t,"HeapProfiler.startSampling",{samplingInterval:A}),o="heap",await b(e.durationMs);const{profile:p}=await d(t,"HeapProfiler.stopSampling");return o=void 0,M(V(p,{durationMs:e.durationMs,samplingIntervalBytes:A,startedAtMs:n}))}finally{o==="cpu"?await d(t,"Profiler.stop").catch(()=>{}):o==="heap"&&await d(t,"HeapProfiler.stopSampling").catch(()=>{}),t.disconnect()}},Z=()=>({capture:async e=>{const t=F(e);if(N)throw new l("CONFLICT","a profile capture is already running in this process",{hint:"Wait for the running capture to finish, then retry."});N=!0;try{return await X(t)}finally{N=!1}}}),q=/^Bearer\s+(?<token>\S+)$/iu,O=e=>L("sha256").update(e,"utf8").digest(),m=(e,t={})=>{const{body:n,status:o}=H(e);return Response.json({error:n},{headers:t,status:o})},ee=e=>{const t=e.headers.get("authorization");return t===null?void 0:q.exec(t)?.groups?.token},te=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ne=(e,t)=>{if(e.length===0)throw new l("ADMIN_TOKEN_NOT_CONFIGURED","createNodeProfileHandler needs a non-empty token; refusing to expose the profiler without one");const n=O(e);return async o=>{const p=ee(o);if(p===void 0)return m(new l("UNAUTHORIZED","send the bearer token as `Authorization: Bearer <token>`"),{"WWW-Authenticate":"Bearer"});if(!B(O(p),n))return m(new l("FORBIDDEN","the bearer token is not accepted"));if(o.method!=="POST")return m(new l("METHOD_NOT_ALLOWED","the profile endpoint accepts POST only"),{Allow:"POST"});let i;try{i=await o.json()}catch{return m(new l("BAD_REQUEST","the body must be JSON: { duration_ms, profile_type }"))}if(!te(i))return m(new l("BAD_REQUEST","the body must be a JSON object: { duration_ms, profile_type }"));try{const f=F({durationMs:i.duration_ms,profileType:i.profile_type}),h=await t.capture(f);return new Response(h,{headers:{"Content-Type":"application/gzip"},status:200})}catch(f){return m(f)}}},ce=({token:e})=>ne(e,Z());export{ne as buildProfileHandler,ce as createNodeProfileHandler};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/platform-node",
3
- "version": "1.0.0-alpha.128",
3
+ "version": "1.0.0-alpha.129",
4
4
  "description": "Node implementation of the Lunora platform contracts: ShardHost, SocketHost, ShardDirectory, ShardKvStore, SchedulerHost over better-sqlite3 and an in-process socket registry (dev/test target)",
5
5
  "keywords": [
6
6
  "better-sqlite3",
@@ -54,7 +54,8 @@
54
54
  "@lunora/workflow": "1.0.0-alpha.74",
55
55
  "@visulima/workflow": "3.0.0",
56
56
  "better-sqlite3": "^13.0.3",
57
- "cron-parser": "5.10.1"
57
+ "cron-parser": "5.10.1",
58
+ "pprof-format": "2.3.1"
58
59
  },
59
60
  "peerDependencies": {
60
61
  "@lunora/shard-engine": ">=1.0.0-alpha.45 <2.0.0-0"